最小可运行服务器
最简单的 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
| 参数 | 类型 | 说明 |
|---|---|---|
request | HttpServerRequest | 请求头、URI、查询参数、请求体(receive()) |
response | HttpServerResponse | 状态码、响应头、响应体(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 use | server.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 编码解码、中间件模式。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。