1.1 Reactor Netty 最小 HTTP 服务器

从零搭建 Reactor Netty 最小 HTTP 服务器:HttpServer.create 配置链、端口绑定与 handle 回调、LoopResources 线程池配置、优雅关闭与 DisposableServer 生命周期管理。附可直接 javac 编译运行的完整示例。

最小可运行服务器

最简单的 Reactor Netty HTTP 服务器只需三行:

import reactor.netty.http.server.HttpServer;

public class MinimalServer {
    public static void main(String[] args) {
        HttpServer.create()
            .port(8080)
            .handle((request, response) ->
                response.sendString(
                    reactor.core.publisher.Mono.just("Hello, Reactor Netty")
                )
            )
            .bindNow();

        System.out.println("Server started on http://localhost:8080");
        System.out.println("Press Enter to stop...");
        try { System.in.read(); } catch (Exception ignored) {}
    }
}

编译并运行:

javac --release 25 MinimalServer.java
java MinimalServer
# 另开终端测试
curl http://localhost:8080
# Hello, Reactor Netty

HttpServer.create() 返回一个配置对象,.port()、.runOn()、.handle() 等方法每次返回新的实例(内部复制旧配置),原对象保持不变。因此可以安全地从一个基础配置派生多个变体,也不会出现两处并发配置互相覆盖。.bindNow() 阻塞当前线程直到服务器绑定成功,并返回一个 DisposableServer 引用,可通过 .dispose() 关闭。

handle 回调的签名

BiFunction<HttpServerRequest, HttpServerResponse, Publisher<Void>> handler
参数类型说明
requestHttpServerRequest请求头、URI、查询参数、请求体(receive())
responseHttpServerResponse状态码、响应头、响应体(send()/sendString())
返回值Publisher<Void>响应完成的信号,必须发出 onComplete 或 onError

关键点:返回值是 Publisher<Void>,不是 String 或字节数组。你要把数据包装成 Mono/Flux,Reactor Netty 负责把它写给 TCP 连接。

读取请求信息

HttpServer.create()
    .port(8080)
    .handle((req, res) -> {
        String method = req.method().name();           // GET/POST/...
        String path = req.path();                      // /users/123
        String name = req.param("name");               // 查询参数 ?name=abc
        req.requestHeaders().entries().forEach(e ->
            System.out.println(e.getKey() + ": " + e.getValue())
        );
        return res.status(200)
            .sendString(Mono.just("Method: " + method + ", Path: " + path));
    })
    .bindNow();

注意 req.param("name") 只返回第一个值;req.params() 返回的是 Map<String, String>,每个 key 也只保留一个值,并没有多值重载。要拿到 ?tag=java&tag=reactive 的全部值,用 Netty 的 QueryStringDecoder 解析原始 URI:

List<String> tags = new QueryStringDecoder(req.uri())
    .parameters().get("tag");   // [java, reactive]

EventLoop 线程数

bindNow() 只是在调用线程上等待端口绑定完成,并不会让服务器「只跑一个线程」。未显式配置时,服务器使用全局默认的 LoopResources,EventLoop 线程数由 LoopResources.DEFAULT_IO_WORKER_COUNT 决定,即 max(可用 CPU 核数, 4)。在 8 核机器上就是 8 个 EventLoop 线程,handle() 回调会分散到这些线程上执行。

需要自定义线程名前缀、数量或守护属性时才显式传入:

import reactor.netty.resources.LoopResources;

HttpServer.create()
    .port(8080)
    .runOn(LoopResources.create("eventd", 4, true))  // 4 个 EventLoop 线程
    .handle((req, res) -> ...)
    .bindNow();

LoopResources.create(String prefix, int workerCount, boolean daemon):

  • prefix:线程名前缀,便于线程 dump 识别
  • workerCount:EventLoop 数量;默认是 max(cores, 4),不是 cores × 2。调大不是免费的,连接压力大时应以压测结果决定
  • daemon:true 时不会阻止 JVM 退出(开发时推荐)

不写代码也能调整这个默认值:reactor.netty.ioWorkerCount 系统属性会被 LoopResources 的静态初始化读取,优先级高于上面的默认计算。一个 EventLoop 会被多个连接共享,所以线程数不等于并发连接数;真正限制并发的是每条连接上的处理速度,以及是否有人在 EventLoop 上阻塞。

优雅关闭

生产环境需要捕获 SIGTERM 并优雅释放端口:

import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

public class GracefulServer {
    public static void main(String[] args) {
        DisposableServer server = HttpServer.create()
            .port(8080)
            .handle((req, res) -> res.sendString(Mono.just("OK")))
            .bindNow();

        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("Shutting down...");
            server.disposeNow(java.time.Duration.ofSeconds(5));
        }));

        server.onDispose().block();  // 阻塞直到 dispose()
    }
}

disposeNow(Duration) 等待的是底层 channel 关闭完成,最长等指定时长;不传参数时默认 Duration.ofSeconds(3),不是无限等待。它不会先停止接收新连接、再排空在途请求——那属于应用层的 graceful draining。急着释放端口可以用 disposeNow();要求「不中断正在处理的请求」时,应在业务层先摘掉流量、等待在途请求的 Mono 完成,再调用 dispose。

线程模型速查

main 线程
    └─ bindNow() 阻塞
        └─ EventLoop 线程池(N 个)
            ├─ 接收 TCP 连接
            ├─ 读取 HTTP 请求
            ├─ 执行 handle() 回调
            └─ 写入 HTTP 响应

handle() 回调运行在 EventLoop 线程上。如果处理逻辑包含阻塞 I/O(如 JDBC 查询),要把阻塞调用本身包进 Mono.fromCallable(...).subscribeOn(Schedulers.boundedElastic())(见第 1.2 节),而不是给 res.sendString(...) 加 subscribeOn——后者不会把已经在 EventLoop 上执行的阻塞代码挪走。否则 EventLoop 线程被阻塞,整个服务器停止接收新连接。

常见错误

错误现象修复
忘记返回值curl 无响应,连接挂起return res.sendString(...) 不可省略
在 handle 中阻塞只有一个请求能处理使用 subscribeOn(boundedElastic) 或改写为响应式
端口被占用BindException: Address already in useserver.dispose() 或换端口
未调用 bindNow()启动后 curl 连接被拒绝必须 .bindNow() 才实际监听端口

小结

  • HttpServer.create() 返回配置对象,链式方法返回新实例,原对象不变
  • handle() 是核心回调,接收 (HttpServerRequest, HttpServerResponse) 返回 Publisher<Void>
  • bindNow() 阻塞启动,disposeNow() 关闭 channel(默认等 3 秒,不负责排空在途请求)
  • 默认 EventLoop 数为 max(cores, 4),需要自定义时用 LoopResources
  • 永远不要阻塞 EventLoop 线程

下一节 1.2 请求响应管线 :URI 路由、请求体读取、JSON 编码解码、中间件模式。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 15.3 全书总结与学习路径回顾
  2. 15.2 响应式流处理的演进趋势
  3. 15.1 引擎选型指南