使用 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 次,日志无报错

yc.png

开发期间遇到的主要 BUG

  • 请求丢失,主 Reactor 分发后 从 Reactor 接收不到
  • 部署于 CentOS 后发现程序启动后接收不到请求,日志没有任何信息

排查 & 原因 & 解决方案