基于Reactor架构的http服务器及其开发框架
概述
这是一个用原生 Java NIO 包实现的 http服务器,在此基础上,我们还提供了如下功能,使它更像一个开发框架!
Interceptor
Controller
视图解析
Ioc 容器
该项目没有使用 jdk 之外的第三方包。
喜欢这个项目吗?给作者一颗星表达你的支持!
快速上手
本代码包含了一个实例,通过这个示例你可以了解到具体的内容。
使用配置文件进行Ioc注入。
构建拦截器。
用Controller实现业务逻辑处理。
项目结构概述:
+ src
|___+ com
|___ ... ... 这里是框架和服务器代码
|___+ test
|___+ controller
|___MyController.java
|___+ interceptor
|___MyInterceptor1.java
|___MyInterceptor2.java
|___+ unit
|___ServerInit.java
|___+ iocxml
|___ bean.xml
|___config.property
|___+ static
|___+ html
|___index.html
|___+ css
|___mycss.css
|___+ javascript
|___myjs.js
配置文件
以下是 config.property 的内容:
port: 18080
controller-base-package: test/controller
interceptor-base-pacakge: test/interceptor
interceptors: MyInterceptor1, MyInterceptor2
static: src/static
ioc-xml-path: src/test/iocxml/bean.xml
use-main-sub-reactor: true
sub-reactor-count: 1
参数说明:
port: 服务器运行的端口
controller-base-package: Controller 所在的包,包中放置控制器,所谓控制器,就是负责业务逻辑处理的部分。
interceptor-base-package: Interceptor 所在的包,包中放置拦截器,所谓拦截器,就是拦截请求,并对其进行校验,决定是否让它进入业务逻辑处理的 java 类。
interceptors: 用逗号分割,指定了全部用于请求过滤的拦截器,分先后顺序,前面的拦截器将最先执行。
static: 静态资源存放根目录。
ioc-xml-path: Ioc容器功能的配置文件。
use-main-sub-reactor: 是否使用主从reactor模型,如果 false,使用的是单 reactor 模型。
sub-reactor-count: 从 reactor(netty 的 worker)数量。
启动应用程序
ServerInit.java 中包含了 main 方法。
package tutorial;
import com.galaxyzeta.server.reactor.WebApplicationContext;
public class MyApplication {
public static void main(String[] args) {
WebApplicationContext.run("src/tutorial/config.property");
}
}
控制器
MyController.java 包含了业务逻辑控制器的写法。
package test.controller;
import com.galaxyzeta.annotation.RequestMapping;
import com.galaxyzeta.http.HttpRequest;
import com.galaxyzeta.http.HttpResponse;
import com.galaxyzeta.server.reactor.Controller;
import com.galaxyzeta.util.Logger;
import com.galaxyzeta.util.LoggerFactory;
import com.galaxyzeta.util.ResponseFactory;
public class MyController implements Controller {
private static Logger LOG = LoggerFactory.getLogger(MyController.class);
@RequestMapping(method = "GET", url = "/debug")
public Object debugGet(HttpRequest req, HttpResponse resp) {
LOG.DEBUG("GET /debug invoked OK");
return "html/index.html";
}
@RequestMapping(method = "GET", url = "/json")
public Object debugPost(HttpRequest req, HttpResponse resp) {
LOG.DEBUG("POST /debug invoked OK");
HttpResponse myResponse = ResponseFactory.getSuccess();
resp.setResponseBody("Hello this is a json view object!");
// resp.setResponseBody("hello this is a response body");
return myResponse;
}
}
目前控制器支持的内容:
GET/POST/PUT/DELETE 方法的解析。
执行指定 url 相应的业务逻辑。
返回 视图字符串 或者 HttpResponse
简单的解释一下这个控制器:这个控制器包含两个方法:
一个处理 GET /debug 请求的方法,方法被invoke时,返回 html/index.html 这个视图对象。
一个处理 POST /debug 请求的方法,方法在运行时返回 myResponse 对象视图。
一些重要说明:
控制器必须放在配置文件指定的包下,否则不会被检测到。
作为控制器的方法必须附带 @RequestMapping(method = "xxx", url = "xxx")。
项目对业务逻辑方法的限制:参数必须有 HttpReqeust 和 HttpResponse。
如果 controller 方法返回 null,参数 resp 将被作为 Response 写回给浏览器。
如果 controller 方法返回视图物体,参数 resp 不会起到作用。
拦截器
拦截器用于对请求进行过滤,同样的我们编写两个简单的拦截器 MyInterceptor1.java, MyInterceptor2.java
package test.interceptor;
import com.galaxyzeta.http.HttpRequest;
import com.galaxyzeta.http.HttpResponse;
import com.galaxyzeta.server.reactor.Interceptor;
import com.galaxyzeta.util.Logger;
import com.galaxyzeta.util.LoggerFactory;
public class MyInterceptor1 implements Interceptor {
private static Logger LOG = LoggerFactory.getLogger(MyInterceptor1.class);
public boolean intercept(HttpRequest req, HttpResponse resp) {
LOG.DEBUG("请求正在经过 [拦截器1] 的过滤");
return true;
}
}
这两个拦截器什么都没干,只是输出日志,并返回 true。
返回 true 的含义是放行,请求将前往下一个拦截器接受检查。当然你可以对 request 进行检查,禁止某些 request 通过拦截器,只需返回 false 即可。
根据配置文件决定拦截器执行顺序。MyInterceptor1 首先作用,而 MyInterceptor2 将在请求通过前置拦截器的检查后发挥作用。
一些重要说明:
拦截器必须放在指定的包下。
拦截器的方法规范:必须是 public boolean intercept(HttpRequest req, HttpResponse resp) 不能有任何变动。
若你的某个拦截器返回 false ,控制器方法不会被执行,参数 resp 将作为最终 response 被写回浏览器。
运行Demo
回到 TestAPI.java 运行 Demo,得到以下结果:
[INFO] WebApplicationContext -- 配置文件解析完毕
[INFO] WebApplicationContext -- 业务逻辑处理器 解析成功
[INFO] WebApplicationContext -- 拦截器 test.interceptor.MyInterceptor1 注册成功
[INFO] WebApplicationContext -- 拦截器 test.interceptor.MyInterceptor2 注册成功
[INFO] ReactorServer -- 正在启动... ...
[INFO] ReactorServer -- 已在端口 8080 上启动服务.
此时服务已经启动,打开浏览器,输入 localhost:8080/debug,可以立即得到一个 html 页面:
(以下是浏览器显示的内容)
Hello html
Test JS
This should be colored cyan.
按下按钮可以弹出一个 alert ,下面的文字是青色,则说明项目已经运行成功。
项目架构
下面简单的描述以下 Reactor Server 和 开发框架的工作过程。
ReactorServer 运作流程
我以主从 Reactor 为例,讲解项目启动的过程:
WebApplication.runApplication 启动应用程序上下文。
WebApplication 启动过程中,读取 config.property 和 bean.xml ,使用反射特性初始化 Ioc 容器,配置拦截器,控制器,最后调用 MainSubReactorServer 的 run 方法开启服务器。
MainSubReactorServer 初始化,创建若干 SubReactor 线程并开始运行。
此时浏览器发来一个请求。主线程实际上是 Acceptor,通过多路复用器发现连接事件后,Acceptor 调用 accept 接收连接,并通过 round-robin 选择一个 subReactor 负责处理 SocketChannel。
subReactor 将 SocketChannel 和它的 Selector 绑定,随即注册 Read 事件。
Handler 是业务逻辑处理的核心。每个 Handler 对应一个 SocketChannel 的业务逻辑处理流程,通过调用 execute 方法,可以使得 Handler 进行相应事件的处理。Handler 内部分别维护了一个 request response viewObject对象,它们会在整个处理流程中被反复使用。
此时浏览器发来一些数据。SubReactor 检测到 Read 事件可用。调用 execute,Handler 发现这是 Read 事件,调用 Read 处理流程。
在 Read 处理流程中,分为以下部分:
从 SocketChannel 读取数据,解析请求。
调用拦截器组的拦截方法,对请求进行拦截。
若通过拦截,检测 request 对应的 Controller。
调用对应 Controller ,过程中可以操作 response 对象,可以返回 viewObject
Read 事件处理完毕后注册 Write 事件。
Write 事件被 SubReactor 的 selector 检测到,subReactor 调用 Handler 的 execute 方法使得 Handler 进行处理。
Write 流程主要是视图解析。视图解析包含以下内容:
若 viewObject 是字符串,将它作为静态资源处理。
若 viewObject 是 HttpResponse ,将它作为相应直接返回。
若请求解析失败,返回一个 404 的响应。
浏览器收到响应,展示结果。此时 SocketChannel 关闭。
Ioc 框架的设计
IocContainer 调用 init() 方法,开始初始化 Ioc 容器。
以下是 IocContainer 类的概要:
public class IocContainer {
private final ConcurrentHashMap registry = new ConcurrentHashMap<>();
private final ArrayList beanPostProcessors = new ArrayList<>();
private String xmlPath;
private final Object singletonLock = new Object();
...
}
首先读取 xml。将 bean 配置文件写入 BeanDefinition。读取 xml 文件用到了 dom4j 的支持。这些 BeanDefinition 将被放入 IocContainer 的 registry 变量中。
以下是 beanDefinition 类的概要:
public class BeanDefinition {
private String name;
private String classname;
private String initMethod;
private ArrayList prop = new ArrayList<>();
private Object bean;
...
}
注册 bean 后置处理器。通过 by-type 寻找 registry 中类型为 BeanPostProcessor 的 bean 定义,然后写入 IocContainer 的 beanPostProcessors 列表。
创建 bean。遍历 registry,依次创建 bean。若发现此 bean 有需要注入的基本类型依赖,则直接创建基本类型变量并注入。若发现此 bean 有 ref 型注入,则转向创建 ref 指向的 bean,然后再注入。
调用注册的 beanPostProcessor,对所有 bean 进行初始化。
至此 IocContainer 初始化完成。
IocContainer 现在支持循环依赖,借鉴了 Spring 处理这一问题的思想和状态机的方法。
更新记录
修改线程模型,抛弃使用线程池处理。
增加 JasonConverter ,提供对 Object 对象的解析支持。
提供了包扫描的递归支持。
压力测试数据
JMeter 压测,测试通过接口调用 controller 业务逻辑,获取静态页面数据。测试时使用 8 SubReactor。模拟 1000 线程同时获取。
最终测试结果:253ms 平均响应速度,728ms 90%线,124ms 中位数响应速度。
To-do
编写一个像 mybatis 那样的,基于 jdbc 的持久层框架。
对于 Ioc 的注入,提供基于注解的支持。