使用 Java NIO 手动实现 http 服务器、部署运行
本文迁移至作者 CSDN 账号
本文的完整实例、代码、相关解释、帮助说明:【github-baka】
本文内容仅为该实例的简要介绍、总结
主要技术
- 多路复用,主从 Reactor 请求分发
- Java NIO 非阻塞监听
- 模拟 Controller, Session, Cookie
设计思想
- 练习使用 Java NIO, 反射
- 模拟 Web 框架的部分功能
功能
- 提供简易的 Json 格式 HTTP 请求、响应框架
- 默认支持静态文件服务
- 支持全局配置文件、全局日志
- 支持 Cookie Session 机制
- 模拟的 @Controller, @Mapping 注解
使用示例
Controller 代码
@Controller
public class DefaultController {
// 日志对象
private static final Logger LOGGER = Logger.getLogger(DefaultController.class);
static String staticPath;
@Mapping(method = "GET", url = "/")
public static File mappingIndex() {
return new File(staticPath + "index.html");
}
@Mapping(method = "*", url = "/.+")
public static File mappingStaticFile(HttpRequest request) {
String targetFileName = request.getURL().substring(1);
return new File(staticPath + targetFileName);
}
}
上述代码可以访问 简单部署效果 进行查看,示例服务已关停,如需要可使用源代码自行测试
实现
NIO: 使用 NIO 旨在使用尽可能少的线程处理多个连接请求,为避免传统 BIO 中每请求一个线程的资源消耗,使用多路复用方式,信道(Channel)、选择器(Selector)配合,当请求来的时候,通知选择器,获取指定信道中的请求,从而实现在一个线程中处理多个请求的分发,而且规避了效率略低的轮询方式。
主从 Reactor:为发挥多核处理器的优势,采用多线程处理请求,主 Reactor 仅处理请求监听、分发,从 Reacotr 仅处理请求读取、响应。
// 主 Reactor 核心处理逻辑
if (selector.select() <= 0) continue; // 阻塞,直到至少一个 key 可用
Set<SelectionKey> readyKeys = selector.selectedKeys(); // 就绪操作集
var iterator = readyKeys.iterator();
while (iterator.hasNext()) {
SelectionKey nowKey = iterator.next();
iterator.remove();
if (nowKey.isValid() && nowKey.isAcceptable()) {
SocketChannel clientRequest = channel.accept(); // 接受请求
clientRequest.configureBlocking(false);
// 分发给从 Reactor 处理
if (nowSubReactorIndex >= subReactorCount) nowSubReactorIndex = 0;
SubReactor target = subReactors[nowSubReactorIndex++];
target.dispatch(clientRequest);
}
}
// 从 Reactor 核心处理逻辑
if (key.isReadable()) {
ByteBuffer buffer = ByteBuffer.allocate(Application.BUFFER_SIZE);
SocketChannel clientInfoChannel = (SocketChannel) key.channel();
int readCount = clientInfoChannel.read(buffer);
... 处理请求读取
clientInfoChannel.register(selector, SelectionKey.OP_WRITE, request); // 注册为可写
} else if (key.isWritable()) {
SocketChannel clientInfoChannel = (SocketChannel) key.channel();
... 处理请求响应
clientInfoChannel.close();
key.cancel();
}
模拟 @Controller、@Mapping 注解功能:扫描注解了 @Controller 的类,使用反射对其中 @Mapping 注解的方法进行处理,处理 url 采用正则表达式方式,将这些方法注册到全局的责任链中,请求时,迭代责任链,将请求交给第一个接受请求的方法处理。
压测结果
单核 CentOS 处理 100 并发请求 10 次,日志无报错

开发期间遇到的主要 BUG
- 请求丢失,主 Reactor 分发后 从 Reactor 接收不到
- 部署于 CentOS 后发现程序启动后接收不到请求,日志没有任何信息