跳到主要内容

HTTP 服务器

Doubao 中英对照 HTTP Server

目录

Reactor Netty 提供了易于使用、易于配置的 HttpServer 类。它屏蔽了创建 HTTP 服务器所需的大部分 Netty 功能,并加入了 Reactive Streams 背压。

1. 启动与停止​

要启动 HTTP 服务器,您必须创建并配置一个 HttpServer 实例。默认情况下,host 被配置为任意本地地址,当调用 bind 操作时,系统会选用一个临时端口。以下示例展示了如何创建 HttpServer 实例:

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

public class Application {

public static void main(String[] args) {
DisposableServer server =
HttpServer.create() 1
.bindNow(); 2

server.onDispose()
.block();
}
}
  • 创建一个可供配置的 HttpServer 实例。

  • 以阻塞方式启动服务器,并等待其完成初始化。

返回的 DisposableServer 提供了简单的服务器 API,其中包括 disposeNow(),它以阻塞方式关闭服务器。

1.1. 主机与端口​

要在指定的 host 和 port 上提供服务,您可以对 HTTP 服务器应用以下配置:

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

public class Application {

public static void main(String[] args) {
DisposableServer server =
HttpServer.create()
.host("localhost") 1
.port(8080) 2
.bindNow();

server.onDispose()
.block();
}
}
  • 配置 HTTP 服务器主机

  • 配置 HTTP 服务器端口

要在多个地址上提供服务,在配置好 HttpServer 之后,您可以多次绑定它以获得各自独立的 DisposableServer。所有创建的服务器都会共享诸如 LoopResources 之类的资源,因为它们在底层使用同一个配置实例。

import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

public class MultiAddressApplication {
public static void main(String[] args) {
HttpServer httpServer = HttpServer.create();
DisposableServer server1 = httpServer
.host("localhost") 1
.port(8080) 2
.bindNow();

DisposableServer server2 = httpServer
.host("0.0.0.0") 3
.port(8081) 4
.bindNow();

Mono.when(server1.onDispose(), server2.onDispose())
.block();
}
}
  • 配置第一个 HTTP 服务器主机

  • 配置第一个 HTTP 服务器端口

  • 配置第二个 HTTP 服务器主机

  • 配置第二个 HTTP 服务器端口

2. 预先初始化​

默认情况下,HttpServer 资源是按需初始化的。这意味着 bind operation 会承担初始化与加载所需的额外时间:

  • 事件循环组

  • 原生传输库(当使用原生传输时)

  • 用于安全的原生库(在使用 OpenSsl 的情况下)

当您需要预加载这些资源时,可以按如下方式配置 HttpServer:

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

public class Application {

public static void main(String[] args) {
HttpServer httpServer =
HttpServer.create()
.handle((request, response) -> request.receive().then());

httpServer.warmup() 1
.block();

DisposableServer server = httpServer.bindNow();

server.onDispose()
.block();
}
}
  • 初始化并加载事件循环组、原生传输库以及用于安全的原生库

3. HTTP 路由​

为 HTTP 服务器定义路由,需要配置所提供的 HttpServerRoutes 构建器。以下示例展示了如何操作:

import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

public class Application {

public static void main(String[] args) {
DisposableServer server =
HttpServer.create()
.route(routes ->
routes.get("/hello", 1
(request, response) -> response.sendString(Mono.just("Hello World!")))
.post("/echo", 2
(request, response) -> response.send(request.receive().retain()))
.get("/path/{param}", 3
(request, response) -> response.sendString(Mono.just(request.param("param"))))
.ws("/ws", 4
(wsInbound, wsOutbound) -> wsOutbound.send(wsInbound.receive().retain())))
.bindNow();

server.onDispose()
.block();
}
}
  • 处理发往 /hello 的 GET 请求,并返回 Hello World!

  • 处理发往 /echo 的 POST 请求,并将收到的请求体作为响应返回。

  • 处理发往 /path/{param} 的 GET 请求,并返回路径参数的值。

  • 处理发往 /ws 的 WebSocket 请求,并将收到的入站数据作为出站数据返回。

备注

服务器路由是唯一的,只会调用按声明顺序匹配到的第一个路由。

备注

如果配置了基于 HTTP/2 的 WebSocket,请确保启用了 HTTP/2 初始设置 projectreactor.io/docs/netty/1.3.0-RC1/api/reactor/netty/http/Http2SettingsSpec.Builder.html#connectProtocolEnabled-boolean-。

3.1. SSE​

以下代码展示了如何配置 HTTP 服务器以提供 Server-Sent Events:

import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;
import reactor.netty.http.server.HttpServerRequest;
import reactor.netty.http.server.HttpServerResponse;

import java.io.ByteArrayOutputStream;
import java.nio.charset.Charset;
import java.time.Duration;
import java.util.function.BiFunction;

public class Application {

public static void main(String[] args) {
DisposableServer server =
HttpServer.create()
.route(routes -> routes.get("/sse", serveSse()))
.bindNow();

server.onDispose()
.block();
}

/**
* Prepares SSE response.
* The "Content-Type" is "text/event-stream".
* The flushing strategy is "flush after every element" emitted by the provided Publisher.
*/
private static BiFunction<HttpServerRequest, HttpServerResponse, Publisher<Void>> serveSse() {
Flux<Long> flux = Flux.interval(Duration.ofSeconds(10));
return (request, response) ->
response.sse()
.send(flux.map(Application::toByteBuf), b -> true);
}

/**
* Transforms the Object to ByteBuf following the expected SSE format.
*/
private static ByteBuf toByteBuf(Object any) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
out.write("data: ".getBytes(Charset.defaultCharset()));
MAPPER.writeValue(out, any);
out.write("\n\n".getBytes(Charset.defaultCharset()));
}
catch (Exception e) {
throw new RuntimeException(e);
}
return ByteBufAllocator.DEFAULT
.buffer()
.writeBytes(out.toByteArray());
}

private static final ObjectMapper MAPPER = new ObjectMapper();
}

3.2. 静态资源​

以下代码展示了如何配置 HTTP 服务器以提供静态资源:

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

import java.net.URISyntaxException;
import java.nio.file.Path;
import java.nio.file.Paths;

public class Application {

public static void main(String[] args) throws URISyntaxException {
Path file = Paths.get(Application.class.getResource("/logback.xml").toURI());
DisposableServer server =
HttpServer.create()
.route(routes -> routes.file("/index.html", file))
.bindNow();

server.onDispose()
.block();
}
}

4. 写入数据​

要向已连接的客户端发送数据,您必须通过 handle(…​) 或 route(…​) 附加一个 I/O 处理器。该 I/O 处理器可访问 HttpServerResponse,以便写入数据。以下示例使用了 handle(…​) 方法:

import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

public class Application {

public static void main(String[] args) {
DisposableServer server =
HttpServer.create()
.handle((request, response) -> response.sendString(Mono.just("hello"))) 1
.bindNow();

server.onDispose()
.block();
}
}
  • 向已连接的客户端发送 hello 字符串

4.1. 添加请求头与其他元数据​

当您向已连接的客户端发送数据时,可能还需要发送额外的请求头、Cookie、状态码及其他元数据。您可以通过 HttpServerResponse 提供这些额外的元数据。以下示例展示了如何操作:

import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpResponseStatus;
import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

public class Application {

public static void main(String[] args) {
DisposableServer server =
HttpServer.create()
.route(routes ->
routes.get("/hello",
(request, response) ->
response.status(HttpResponseStatus.OK)
.header(HttpHeaderNames.CONTENT_LENGTH, "12")
.sendString(Mono.just("Hello World!"))))
.bindNow();

server.onDispose()
.block();
}
}

4.2. 压缩​

您可以根据请求头 Accept-Encoding,配置 HTTP 服务器发送压缩后的响应。

Reactor Netty 提供了三种不同的出站数据压缩策略:

  • compress(boolean):根据所提供的布尔值启用(true)或禁用(false)压缩。

  • compress(int):当响应大小超过给定值(字节)时执行压缩。

  • compress(BiPredicate<HttpServerRequest, HttpServerResponse>):当谓词返回 true 时执行压缩。

  • compressOptions(HttpCompressionOption…​ compressionOptions):指定 GZip、Deflate 和 ZSTD 的压缩选项。

    备注

    GZip 压缩选项

    • compression level:仅允许 0 到 9 的范围。(默认值:6)

    • window bits:仅允许 0 到 9 的范围。(默认值:15)

    • memory level:仅允许 1 到 9 的范围。(默认值:8)

    Deflate 压缩选项

    • compression level:仅允许 0 到 9 的范围。(默认值:6)

    • window bits:仅允许 0 到 9 的范围。(默认值:15)

    • memory level:仅允许 1 到 9 的范围。(默认值:8)

    ZSTD 压缩选项

    • compression level:仅允许 -131072 到 9 的范围。(默认值:3)

    • block size:仅允许正数。(默认值:65536,即 64KB)

    • max encode size:仅允许正数。(默认值:33554432,即 32MB)

以下示例使用 compress 方法(设为 true)来启用压缩:

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

import java.net.URISyntaxException;
import java.nio.file.Path;
import java.nio.file.Paths;

public class Application {

public static void main(String[] args) throws URISyntaxException {
Path file = Paths.get(Application.class.getResource("/logback.xml").toURI());
DisposableServer server =
HttpServer.create()
.compress(true)
.route(routes -> routes.file("/index.html", file))
.bindNow();

server.onDispose()
.block();
}
}

5. 读取数据​

要从已连接的客户端接收数据,您必须通过 handle(…​) 或 route(…​) 附加一个 I/O 处理器。该 I/O 处理器可访问 HttpServerRequest,以便读取数据。

以下示例使用了 handle(…​) 方法:

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

public class Application {

public static void main(String[] args) {
DisposableServer server =
HttpServer.create()
.handle((request, response) -> request.receive().then()) 1
.bindNow();

server.onDispose()
.block();
}
}
  • 从已连接的客户端接收数据

5.1. 读取请求头、URI 参数与其他元数据​

当您从已连接的客户端接收数据时,可能需要检查请求头、参数及其他元数据。您可以通过 HttpServerRequest 获取这些额外的元数据。以下示例展示了如何操作:

import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

public class Application {

public static void main(String[] args) {
DisposableServer server =
HttpServer.create()
.route(routes ->
routes.get("/{param}",
(request, response) -> {
if (request.requestHeaders().contains("Some-Header")) {
return response.sendString(Mono.just(request.param("param")));
}
return response.sendNotFound();
}))
.bindNow();

server.onDispose()
.block();
}
}

5.2. 读取 POST 表单或 Multipart 数据​

当您从已连接的客户端接收数据时,可能希望访问 POST form(application/x-www-form-urlencoded)或 multipart(multipart/form-data)数据。您可以通过 HttpServerRequest 获取这些数据。

import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

public class Application {

public static void main(String[] args) {
DisposableServer server =
HttpServer.create()
.route(routes ->
routes.post("/multipart", (request, response) -> response.sendString(
request.receiveForm() 1
.flatMap(data -> Mono.just('[' + data.getName() + ']')))))
.bindNow();

server.onDispose()
.block();
}
}
  • 接收 POST 表单/multipart 数据。

当您需要更改默认设置时,可以配置 HttpServer,也可以为每个请求提供单独的配置:

import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

public class Application {

public static void main(String[] args) {
DisposableServer server =
HttpServer.create()
.httpFormDecoder(builder -> builder.maxInMemorySize(0)) 1
.route(routes ->
routes.post("/multipart", (request, response) -> response.sendString(
request.receiveForm(builder -> builder.maxInMemorySize(256)) 2
.flatMap(data -> Mono.just('[' + data.getName() + ']')))))
.bindNow();

server.onDispose()
.block();
}
}
  • 在 HttpServer 上配置,指定数据仅存储在磁盘上。

  • 按请求配置,指定当数据大小超过指定大小时,将数据存储到磁盘上。

以下清单展示了可用的配置:

配置名称说明
baseDirectory配置在磁盘上存储数据的目录。默认为生成的临时目录。
charset配置数据的 Charset。默认为 StandardCharsets#UTF_8。
maxInMemorySize配置每条数据的最大内存大小;即当数据大小大于 maxInMemorySize 时写入磁盘,否则保存在内存中。设为 -1 时全部内容保存在内存中。设为 0 时全部内容保存在磁盘上。默认为 16kb。
maxSize配置每条数据的最大大小。达到该限制时会抛出异常。设为 -1 表示不限制。默认为 -1(无限制)。
scheduler配置在解码阶段用于卸载磁盘操作的调度器。默认为 Schedulers#boundedElastic()
streaming设为 true 时,数据直接从解析后的输入缓冲区流中流式传输,意味着既不存储在内存中也不存储在文件中。设为 false 时,各部分由内存和/或文件存储支撑。默认为 false。注意:启用流式传输后,所提供的数据可能并非处于完整状态,即必须检查 HttpData#isCompleted()。另请注意,启用此属性会使 maxInMemorySize、baseDirectory 和 scheduler 实际上失效。

5.2.1. 获取远程(客户端)地址​

除了可以从请求中获取的元数据之外,您还可以获取 host(服务器) 地址、remote(客户端) 地址以及 scheme。根据所选的工厂方法,您可以直接从通道获取这些信息,也可以通过 Forwarded 或 X-Forwarded-* HTTP 请求头获取。以下示例展示了如何操作:

import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

public class Application {

public static void main(String[] args) {
DisposableServer server =
HttpServer.create()
.forwarded(true) 1
.route(routes ->
routes.get("/clientip",
(request, response) ->
response.sendString(Mono.just(request.remoteAddress() 2
.getHostString()))))
.bindNow();

server.onDispose()
.block();
}
}
  • 指定在可能时从 Forwarded 和 X-Forwarded-* HTTP 请求头获取连接相关信息。

  • 返回远程(客户端)对端的地址。

也可以自定义 Forwarded 或 X-Forwarded-* 请求头处理器的行为。以下示例展示了如何操作:

import java.net.InetSocketAddress;

import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;
import reactor.netty.transport.AddressUtils;

public class CustomForwardedHeaderHandlerApplication {

public static void main(String[] args) {
DisposableServer server =
HttpServer.create()
.forwarded((connectionInfo, request) -> { 1
String hostHeader = request.headers().get("X-Forwarded-Host");
if (hostHeader != null) {
String[] hosts = hostHeader.split(",", 2);
InetSocketAddress hostAddress = AddressUtils.createUnresolved(
hosts[hosts.length - 1].trim(),
connectionInfo.getHostAddress().getPort());
connectionInfo = connectionInfo.withHostAddress(hostAddress);
}
return connectionInfo;
})
.route(routes ->
routes.get("/clientip",
(request, response) ->
response.sendString(Mono.just(request.remoteAddress() 2
.getHostString()))))
.bindNow();

server.onDispose()
.block();
}
}
  • 添加自定义请求头处理器。

  • 返回远程(客户端)对端的地址。

5.3. HTTP 请求解码器​

默认情况下,Netty 对入站请求配置了一些限制,例如:

  • 初始行的最大长度

  • 所有请求头的最大长度

  • 内容或每个数据块的最大长度

有关更多信息,请参阅 HttpRequestDecoder 与 HttpServerUpgradeHandler

默认情况下,HTTP 服务器配置了以下设置:

public static final int DEFAULT_MAX_INITIAL_LINE_LENGTH             = 4096;
public static final int DEFAULT_MAX_HEADER_SIZE = 8192;
/**
* Default max chunk size.
*
* @deprecated as of 1.1.0. This will be removed in 2.0.0 as Netty 5 does not support this configuration.
*/
@Deprecated
public static final int DEFAULT_MAX_CHUNK_SIZE = 8192;
public static final boolean DEFAULT_VALIDATE_HEADERS = true;
public static final int DEFAULT_INITIAL_BUFFER_SIZE = 128;
public static final boolean DEFAULT_ALLOW_DUPLICATE_CONTENT_LENGTHS = false;
public static final boolean DEFAULT_ALLOW_PARTIAL_CHUNKS = true;
/**
* The maximum length of the content of the HTTP/2.0 clear-text upgrade request.
* By default, the server will reject an upgrade request with non-empty content,
* because the upgrade request is most likely a GET request.
*/
public static final int DEFAULT_H2C_MAX_CONTENT_LENGTH = 0;

当您需要更改这些默认设置时,可以按如下方式配置 HTTP 服务器:

import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

public class Application {

public static void main(String[] args) {
DisposableServer server =
HttpServer.create()
.httpRequestDecoder(spec -> spec.maxHeaderSize(16384)) 1
.handle((request, response) -> response.sendString(Mono.just("hello")))
.bindNow();

server.onDispose()
.block();
}
}

6. 生命周期回调​

提供了以下生命周期回调,供您扩展 HttpServer:

回调说明
doOnBind在服务器通道即将绑定时调用。
doOnBound在服务器通道已绑定时调用。
doOnChannelInit在初始化通道时调用。
doOnConnection在远程客户端连接时调用
doOnUnbound在服务器通道解绑时调用。

以下示例使用了 doOnConnection 和 doOnChannelInit 回调:

import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.ReadTimeoutHandler;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;
import java.util.concurrent.TimeUnit;

public class Application {

public static void main(String[] args) {
DisposableServer server =
HttpServer.create()
.doOnConnection(conn ->
conn.addHandlerFirst(new ReadTimeoutHandler(10, TimeUnit.SECONDS))) 1
.doOnChannelInit((observer, channel, remoteAddress) ->
channel.pipeline()
.addFirst(new LoggingHandler("reactor.netty.examples"))) 2
.bindNow();

server.onDispose()
.block();
}
}
  • 当远程客户端连接时,Netty 管道会加入 ReadTimeoutHandler。

  • 在初始化通道时,Netty 管道会加入 LoggingHandler。

7. TCP 层配置​

当您需要更改 TCP 层配置时,可以使用以下代码片段来扩展默认的 TCP 服务器配置:

import io.netty.channel.ChannelOption;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

public class Application {

public static void main(String[] args) {
DisposableServer server =
HttpServer.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
.bindNow();

server.onDispose()
.block();
}
}

有关 TCP 层配置的更多详细信息,请参阅 TCP Server。

7.1. 线路日志​

当需要检查各对端之间的流量时,Reactor Netty 提供了线路日志。默认情况下线路日志是禁用的。要启用它,您必须将日志记录器 reactor.netty.http.server.HttpServer 的级别设为 DEBUG,并应用以下配置:

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

public class Application {

public static void main(String[] args) {
DisposableServer server =
HttpServer.create()
.wiretap(true) 1
.bindNow();

server.onDispose()
.block();
}
}
  • 启用线路日志

7.1.1. 线路日志格式化器​

Reactor Netty 支持 3 种不同的格式化器:

/**
* When wire logging is enabled with this format, both events and content will be logged.
* The content will be in hex format.
* <p>Examples:</p>
* <pre>
* {@code
* reactor.netty.http.HttpTests - [d5230a14, L:/0:0:0:0:0:0:0:1:60267 - R:/0:0:0:0:0:0:0:1:60269] REGISTERED
* reactor.netty.http.HttpTests - [d5230a14, L:/0:0:0:0:0:0:0:1:60267 - R:/0:0:0:0:0:0:0:1:60269] ACTIVE
* reactor.netty.http.HttpTests - [d5230a14, L:/0:0:0:0:0:0:0:1:60267 - R:/0:0:0:0:0:0:0:1:60269] READ: 145B
* +-------------------------------------------------+
* | 0 1 2 3 4 5 6 7 8 9 a b c d e f |
* +--------+-------------------------------------------------+----------------+
* |00000000| 50 4f 53 54 20 2f 74 65 73 74 2f 57 6f 72 6c 64 |POST /test/World|
* |00000010| 20 48 54 54 50 2f 31 2e 31 0d 0a 43 6f 6e 74 65 | HTTP/1.1..Conte|
* |00000020| 6e 74 2d 54 79 70 65 3a 20 74 65 78 74 2f 70 6c |nt-Type: text/pl|
* |00000030| 61 69 6e 0d 0a 75 73 65 72 2d 61 67 65 6e 74 3a |ain..user-agent:|
* |00000040| 20 52 65 61 63 74 6f 72 4e 65 74 74 79 2f 64 65 | ReactorNetty/de|
* ...
* reactor.netty.http.HttpTests - [d5230a14, L:/0:0:0:0:0:0:0:1:60267 - R:/0:0:0:0:0:0:0:1:60269] WRITE: 38B
* +-------------------------------------------------+
* | 0 1 2 3 4 5 6 7 8 9 a b c d e f |
* +--------+-------------------------------------------------+----------------+
* |00000000| 48 54 54 50 2f 31 2e 31 20 32 30 30 20 4f 4b 0d |HTTP/1.1 200 OK.|
* |00000010| 0a 63 6f 6e 74 65 6e 74 2d 6c 65 6e 67 74 68 3a |.content-length:|
* |00000020| 20 30 0d 0a 0d 0a | 0.... |
* +--------+-------------------------------------------------+----------------+
* }
* </pre>
*/
/**
* When wire logging is enabled with this format, only the events will be logged.
* <p>Examples:</p>
* <pre>
* {@code
* reactor.netty.http.HttpTests - [230d3686, L:/0:0:0:0:0:0:0:1:60241 - R:/0:0:0:0:0:0:0:1:60245] REGISTERED
* reactor.netty.http.HttpTests - [230d3686, L:/0:0:0:0:0:0:0:1:60241 - R:/0:0:0:0:0:0:0:1:60245] ACTIVE
* reactor.netty.http.HttpTests - [230d3686, L:/0:0:0:0:0:0:0:1:60241 - R:/0:0:0:0:0:0:0:1:60245] READ: 145B
* reactor.netty.http.HttpTests - [230d3686, L:/0:0:0:0:0:0:0:1:60241 - R:/0:0:0:0:0:0:0:1:60245] WRITE: 38B
* }
* </pre>
*/
/**
* When wire logging is enabled with this format, both events and content will be logged.
* The content will be in plain text format.
* <p>Examples:</p>
* <pre>
* {@code
* reactor.netty.http.HttpTests - [02c3db6c, L:/0:0:0:0:0:0:0:1:60317 - R:/0:0:0:0:0:0:0:1:60319] REGISTERED
* reactor.netty.http.HttpTests - [02c3db6c, L:/0:0:0:0:0:0:0:1:60317 - R:/0:0:0:0:0:0:0:1:60319] ACTIVE
* reactor.netty.http.HttpTests - [02c3db6c, L:/0:0:0:0:0:0:0:1:60317 - R:/0:0:0:0:0:0:0:1:60319] READ: 145B POST /test/World HTTP/1.1
* Content-Type: text/plain
* user-agent: ReactorNetty/dev
* ...
* reactor.netty.http.HttpTests - [02c3db6c, L:/0:0:0:0:0:0:0:1:60317 - R:/0:0:0:0:0:0:0:1:60319] WRITE: 38B HTTP/1.1 200 OK
* content-length: 0
* }
* </pre>
*/

当您需要更改默认格式化器时,可以按如下方式配置:

import io.netty.handler.logging.LogLevel;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;
import reactor.netty.transport.logging.AdvancedByteBufFormat;

public class Application {

public static void main(String[] args) {
DisposableServer server =
HttpServer.create()
.wiretap("logger-name", LogLevel.DEBUG, AdvancedByteBufFormat.TEXTUAL) 1
.bindNow();

server.onDispose()
.block();
}
}

7.2. 事件循环组​

默认情况下,Reactor Netty 使用一个“事件循环组”,其中工作线程的数量等于初始化时运行时可用的处理器数量(但最少为 4)。该“事件循环组”在同一个 JVM 中的所有服务器与客户端之间共享。当您需要不同的配置时,可以使用 LoopResources 的 #create 方法之一。

以下清单展示了事件循环组的默认配置:

/**
* Default worker thread count, fallback to available processor
* (but with a minimum value of 4).
*/
public static final String IO_WORKER_COUNT = "reactor.netty.ioWorkerCount";
/**
* Default selector thread count, fallback to -1 (no selector thread)
* <p><strong>Note:</strong> In most use cases using a worker thread also as a selector thread works well.
* A possible use case for specifying a separate selector thread might be when the worker threads are too busy
* and connections cannot be accepted fast enough.
* <p><strong>Note:</strong> Although more than 1 can be configured as a selector thread count, in reality
* only 1 thread will be used as a selector thread.
*/
public static final String IO_SELECT_COUNT = "reactor.netty.ioSelectCount";
/**
* Default worker thread count for UDP, fallback to available processor
* (but with a minimum value of 4).
*/
public static final String UDP_IO_THREAD_COUNT = "reactor.netty.udp.ioThreadCount";
/**
* Default quiet period that guarantees that the disposal of the underlying LoopResources
* will not happen, fallback to 2 seconds.
*/
public static final String SHUTDOWN_QUIET_PERIOD = "reactor.netty.ioShutdownQuietPeriod";
/**
* Default maximum amount of time to wait until the disposal of the underlying LoopResources
* regardless if a task was submitted during the quiet period, fallback to 15 seconds.
*/
public static final String SHUTDOWN_TIMEOUT = "reactor.netty.ioShutdownTimeout";

/**
* Default value whether the native transport (epoll, io_uring, kqueue) will be preferred,
* fallback it will be preferred when available.
* <p><strong>Note:</strong> On {@code Linux}, {@code Epoll} will be preferred by default.
* If {@code IO_Uring} needs to be configured, a dependency to {@code io.netty:netty-transport-native-io_uring}
* has to be added.
*/
public static final String NATIVE = "reactor.netty.native";

如果您需要更改这些设置,可以应用以下配置:

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

public class Application {

public static void main(String[] args) {
LoopResources loop = LoopResources.create("event-loop", 1, 4, true);

DisposableServer server =
HttpServer.create()
.runOn(loop)
.bindNow();

server.onDispose()
.block();
}
}

7.2.1. 释放事件循环组​

  • 如果您使用 Reactor Netty 提供的默认 Event Loop Group,请调用 HttpResources 的 #disposeLoopsAndConnections / #disposeLoopsAndConnectionsLater 方法。
备注

释放 HttpResources 意味着正在使用它的每个服务器/客户端都将无法再使用它!

  • 如果您使用自定义 LoopResources,请调用 LoopResources 的 #dispose / #disposeLater 方法。
备注

释放自定义 LoopResources 意味着配置为使用它的每个服务器/客户端都将无法再使用它!

8. SSL 与 TLS​

当您需要 SSL 或 TLS 时,可以应用下例所示的配置。默认情况下,如果 OpenSSL 可用,则使用 SslProvider.OPENSSL 作为提供方;否则使用 SslProvider.JDK。您可以通过 SslContextBuilder 或设置 -Dio.netty.handler.ssl.noOpenSsl=true 来切换提供方。

以下示例使用了 SslContextBuilder:

import reactor.netty.DisposableServer;
import reactor.netty.http.Http11SslContextSpec;
import reactor.netty.http.server.HttpServer;
import java.io.File;

public class Application {

public static void main(String[] args) {
File cert = new File("certificate.crt");
File key = new File("private.key");

Http11SslContextSpec http11SslContextSpec = Http11SslContextSpec.forServer(cert, key);

DisposableServer server =
HttpServer.create()
.secure(spec -> spec.sslContext(http11SslContextSpec))
.bindNow();

server.onDispose()
.block();
}
}

8.1. 服务器名称指示​

您可以为 HTTP 服务器配置多个映射到特定域名的 SslContext。配置 SNI 映射时,可以使用精确域名,也可以使用包含通配符的域名。

以下示例使用了一个包含通配符的域名:

import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

import java.io.File;

public class Application {

public static void main(String[] args) throws Exception {
File defaultCert = new File("default_certificate.crt");
File defaultKey = new File("default_private.key");

File testDomainCert = new File("default_certificate.crt");
File testDomainKey = new File("default_private.key");

SslContext defaultSslContext = SslContextBuilder.forServer(defaultCert, defaultKey).build();
SslContext testDomainSslContext = SslContextBuilder.forServer(testDomainCert, testDomainKey).build();

DisposableServer server =
HttpServer.create()
.secure(spec -> spec.sslContext(defaultSslContext)
.addSniMapping("*.test.com",
testDomainSpec -> testDomainSpec.sslContext(testDomainSslContext)))
.bindNow();

server.onDispose()
.block();
}
}

9. HTTP 日志​

您可以通过编程方式或配置来启用 HTTP 访问日志或错误日志。默认情况下是禁用的。

9.1. 访问日志​

您可以使用 -Dreactor.netty.http.server.accessLogEnabled=true 通过配置来启用 HTTP 访问日志。

您可以使用以下配置(适用于 Logback 或类似日志框架)来获得独立的 HTTP 访问日志文件:

<appender name="accessLog" class="ch.qos.logback.core.FileAppender">
<file>access_log.log</file>
<encoder>
<pattern>%msg%n</pattern>
</encoder>
</appender>
<appender name="async" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="accessLog" />
</appender>

<logger name="reactor.netty.http.server.AccessLog" level="INFO" additivity="false">
<appender-ref ref="async"/>
</logger>

以下示例通过编程方式启用它:

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

public class Application {

public static void main(String[] args) {
DisposableServer server =
HttpServer.create()
.accessLog(true)
.bindNow();

server.onDispose()
.block();
}
}

调用此方法的优先级高于系统属性配置。

默认情况下,日志格式为 Common Log Format,但您可以通过参数指定自定义格式,如下例所示:

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

public class CustomLogAccessFormatApplication {

public static void main(String[] args) {
DisposableServer server =
HttpServer.create()
.accessLog(true, x -> AccessLog.create("method={}, uri={}", x.method(), x.uri()))
.bindNow();

server.onDispose()
.block();
}
}

您还可以使用 AccessLogFactory#createFilter 方法过滤 HTTP 访问日志,如下例所示:

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

public class FilterLogAccessApplication {

public static void main(String[] args) {
DisposableServer server =
HttpServer.create()
.accessLog(true, AccessLogFactory.createFilter(p -> !String.valueOf(p.uri()).startsWith("/health/")))
.bindNow();

server.onDispose()
.block();
}
}

请注意,此方法也可以接受自定义格式参数,如下例所示:

import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;
import reactor.netty.http.server.logging.AccessLog;
import reactor.netty.http.server.logging.AccessLogFactory;

public class CustomFormatAndFilterAccessLogApplication {

public static void main(String[] args) {
DisposableServer server =
HttpServer.create()
.accessLog(true, AccessLogFactory.createFilter(p -> !String.valueOf(p.uri()).startsWith("/health/"), 1
x -> AccessLog.create("method={}, uri={}", x.method(), x.uri()))) 2
.bindNow();

server.onDispose()
.block();
}
}
  • 指定要使用的过滤谓词

  • 指定要应用的自定义格式

9.2. 错误日志​

您可以使用 -Dreactor.netty.http.server.errorLogEnabled=true 通过配置来启用 HTTP 错误日志。

您可以使用以下配置(适用于 Logback 或类似日志框架)来获得独立的 HTTP 错误日志文件:

<appender name="errorLog" class="ch.qos.logback.core.FileAppender">
<file>error_log.log</file>
<encoder>
<pattern>%msg%n</pattern>
</encoder>
</appender>
<appender name="async" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="errorLog" />
</appender>

<logger name="reactor.netty.http.server.ErrorLog" level="ERROR" additivity="false">
<appender-ref ref="async"/>
</logger>

以下示例通过编程方式启用它:

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

public class Application {

public static void main(String[] args) {
DisposableServer server =
HttpServer.create()
.errorLog(true)
.bindNow();

server.onDispose()
.block();
}
}

调用此方法的优先级高于系统属性配置。

默认情况下,日志格式为 [{datetime}] [pid {PID}] [client {remote address}] {exception message},但您可以通过参数指定自定义格式,如下例所示:

import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;
import reactor.netty.http.server.logging.error.ErrorLog;

public class CustomLogErrorFormatApplication {

public static void main(String[] args) {
DisposableServer server =
HttpServer.create()
.errorLog(true, x -> ErrorLog.create("method={}, uri={}", x.httpServerInfos().method(), x.httpServerInfos().uri()))
.bindNow();

server.onDispose()
.block();
}
}

您还可以使用 ErrorLogFactory#createFilter 方法过滤 HTTP 错误日志,如下例所示:

import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;
import reactor.netty.http.server.logging.error.ErrorLogFactory;

public class FilterLogErrorApplication {

public static void main(String[] args) {
DisposableServer server =
HttpServer.create()
.errorLog(true, ErrorLogFactory.createFilter(p -> p.cause() instanceof RuntimeException))
.bindNow();

server.onDispose()
.block();
}
}

请注意,此方法也可以接受自定义格式参数,如下例所示:

import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;
import reactor.netty.http.server.logging.error.ErrorLog;
import reactor.netty.http.server.logging.error.ErrorLogFactory;

public class CustomFormatAndFilterErrorLogApplication {

public static void main(String[] args) {
DisposableServer server =
HttpServer.create()
.errorLog(true, ErrorLogFactory.createFilter(p -> p.cause() instanceof RuntimeException, 1
x -> ErrorLog.create("method={}, uri={}", x.httpServerInfos().method(), x.httpServerInfos().uri()))) 2
.bindNow();

server.onDispose()
.block();
}
}
  • 指定要使用的过滤谓词

  • 指定要应用的自定义格式

10. HTTP/2​

默认情况下,HTTP 服务器支持 HTTP/1.1。如果您需要 HTTP/2,可以通过配置获得。除了协议配置之外,如果您需要 H2 而不需要 H2C(明文),还必须配置 SSL。

备注

由于 JDK8 并不“开箱即用”地支持应用层协议协商(ALPN)(尽管部分厂商已将 ALPN 移植回 JDK8),您需要额外依赖一个支持它的原生库——例如 netty-tcnative-boringssl-static。

以下清单给出了一个简单的 H2 示例:

import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.Http2SslContextSpec;
import reactor.netty.http.HttpProtocol;
import reactor.netty.http.server.HttpServer;
import java.io.File;

public class H2Application {

public static void main(String[] args) {
File cert = new File("certificate.crt");
File key = new File("private.key");

Http2SslContextSpec http2SslContextSpec = Http2SslContextSpec.forServer(cert, key);

DisposableServer server =
HttpServer.create()
.port(8080)
.protocol(HttpProtocol.H2) 1
.secure(spec -> spec.sslContext(http2SslContextSpec)) 2
.handle((request, response) -> response.sendString(Mono.just("hello")))
.bindNow();

server.onDispose()
.block();
}
}
  • 配置服务器仅支持 HTTP/2

  • 配置 SSL

现在应用程序应表现为:

$ curl --http2 https://localhost:8080 -i
HTTP/2 200

hello

以下清单给出了一个简单的 H2C 示例:

import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.HttpProtocol;
import reactor.netty.http.server.HttpServer;

public class H2CApplication {

public static void main(String[] args) {
DisposableServer server =
HttpServer.create()
.port(8080)
.protocol(HttpProtocol.H2C)
.handle((request, response) -> response.sendString(Mono.just("hello")))
.bindNow();

server.onDispose()
.block();
}
}

现在应用程序应表现为:

$ curl --http2-prior-knowledge http://localhost:8080 -i
HTTP/2 200

hello

10.1. 协议选择​

/**
* The default supported HTTP protocol by HttpServer and HttpClient.
*/
HTTP11,

/**
* HTTP/2.0 support with TLS
* <p>If used along with HTTP/1.1 protocol, HTTP/2.0 will be the preferred protocol.
* While negotiating the application level protocol, HTTP/2.0 or HTTP/1.1 can be chosen.
* <p>If used without HTTP/1.1 protocol, HTTP/2.0 will always be offered as a protocol
* for communication with no fallback to HTTP/1.1.
*/
H2,

/**
* HTTP/2.0 support with clear-text.
* <p>If used along with HTTP/1.1 protocol, will support H2C "upgrade":
* Request or consume requests as HTTP/1.1 first, looking for HTTP/2.0 headers
* and {@literal Connection: Upgrade}. A server will typically reply a successful
* 101 status if upgrade is successful or a fallback HTTP/1.1 response. When
* successful the client will start sending HTTP/2.0 traffic.
* <p>If used without HTTP/1.1 protocol, will support H2C "prior-knowledge": Doesn't
* require {@literal Connection: Upgrade} handshake between a client and server but
* fallback to HTTP/1.1 will not be supported.
*/
H2C,

/**
* HTTP/3.0 support.
* @since 1.2.0
*/
HTTP3
}

11. HTTP/3​

默认情况下,HTTP 服务器支持 HTTP/1.1。如果您需要 HTTP/3,可以通过配置获得。除了协议配置之外,您还需要添加对 io.netty:netty-codec-native-quic 的依赖。

以下清单给出了一个简单的 HTTP3 示例:

import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.Http3SslContextSpec;
import reactor.netty.http.HttpProtocol;
import reactor.netty.http.server.HttpServer;

import java.io.File;
import java.time.Duration;

public class Application {

public static void main(String[] args) throws Exception {
File certChainFile = new File("certificate chain file");
File keyFile = new File("private key file");

Http3SslContextSpec serverCtx = Http3SslContextSpec.forServer(keyFile, null, certChainFile);

DisposableServer server =
HttpServer.create()
.port(8080)
.protocol(HttpProtocol.HTTP3) 1
.secure(spec -> spec.sslContext(serverCtx)) 2
.http3Settings(spec -> spec.idleTimeout(Duration.ofSeconds(5)) 3
.maxData(10000000)
.maxStreamDataBidirectionalLocal(1000000)
.maxStreamDataBidirectionalRemote(1000000)
.maxStreamsBidirectional(100))
.handle((request, response) -> response.header("server", "reactor-netty")
.sendString(Mono.just("hello")))
.bindNow();

server.onDispose()
.block();
}
}
  • 配置服务器仅支持 HTTP/3

  • 配置 SSL

  • 配置 HTTP/3 设置

现在应用程序应表现为:

$ curl --http3 https://localhost:8080 -i
HTTP/3 200
server: reactor-netty
content-length: 5

hello

12. 指标​

HTTP 服务器支持与 Micrometer 的内置集成。它以 reactor.netty.http.server 为前缀暴露所有指标。

下表提供了 HTTP 服务器指标的相关信息:

指标名称类型说明
reactor.netty.http.server.streams.activeGauge活动的 HTTP/2 流数量。请参阅 Streams Active
reactor.netty.http.server.connections.activeGauge当前正在处理请求的 http 连接数。请参阅 Connections Active
reactor.netty.http.server.connections.totalGauge已打开的全部连接数。请参阅 Connections Total
reactor.netty.http.server.data.receivedDistributionSummary接收的数据量(字节)。请参阅 Data Received
reactor.netty.http.server.data.sentDistributionSummary发送的数据量(字节)。请参阅 Data Sent
reactor.netty.http.server.errorsCounter发生的错误数量。请参阅 Errors Count
reactor.netty.http.server.data.received.timeTimer消费入站数据所花费的时间。请参阅 Http Server Data Received Time
reactor.netty.http.server.data.sent.timeTimer发送出站数据所花费的时间。请参阅 Http Server Data Sent Time
reactor.netty.http.server.response.timeTimer请求/响应的总时间。请参阅 Http Server Response Time

还有以下附加指标可用:

ByteBufAllocator 指标

指标名称类型说明
reactor.netty.bytebuf.allocator.used.heap.memoryGauge堆缓冲区分配器预留的字节数。请参阅 Used Heap Memory
reactor.netty.bytebuf.allocator.used.direct.memoryGauge直接缓冲区分配器预留的字节数。请参阅 Used Direct Memory
reactor.netty.bytebuf.allocator.heap.arenasGauge堆 arena 数量(当使用 PooledByteBufAllocator 时)。请参阅 Heap Arenas
reactor.netty.bytebuf.allocator.direct.arenasGauge直接 arena 数量(当使用 PooledByteBufAllocator 时)。请参阅 Direct Arenas
reactor.netty.bytebuf.allocator.threadlocal.cachesGauge线程本地缓存数量(当使用 PooledByteBufAllocator 时)。请参阅 Thread Local Caches
reactor.netty.bytebuf.allocator.small.cache.sizeGauge小型缓存的大小(当使用 PooledByteBufAllocator 时)。请参阅 Small Cache Size
reactor.netty.bytebuf.allocator.normal.cache.sizeGauge普通缓存的大小(当使用 PooledByteBufAllocator 时)。请参阅 Normal Cache Size
reactor.netty.bytebuf.allocator.chunk.sizeGaugearena 的块大小(当使用 PooledByteBufAllocator 时)。请参阅 Chunk Size
reactor.netty.bytebuf.allocator.active.heap.memoryGauge从堆缓冲池分配的在用缓冲区实际消耗的字节数(当使用 PooledByteBufAllocator 时)。请参阅 Active Heap Memory
reactor.netty.bytebuf.allocator.active.direct.memoryGauge从直接缓冲池分配的在用缓冲区实际消耗的字节数(当使用 PooledByteBufAllocator 时)。请参阅 Active Direct Memory

EventLoop 指标

指标名称类型说明
reactor.netty.eventloop.pending.tasksGauge事件循环上待处理的任务数量。请参阅 Pending Tasks

以下示例启用了该集成:

import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.config.MeterFilter;
import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

public class Application {

public static void main(String[] args) {
Metrics.globalRegistry 1
.config()
.meterFilter(MeterFilter.maximumAllowableTags("reactor.netty.http.server", "URI", 100, MeterFilter.deny()));

DisposableServer server =
HttpServer.create()
.metrics(true, s -> {
if (s.startsWith("/stream/")) { 2
return "/stream/{n}";
}
else if (s.startsWith("/bytes/")) {
return "/bytes/{n}";
}
return s;
}) 3
.route(r ->
r.get("/stream/{n}",
(req, res) -> res.sendString(Mono.just(req.param("n"))))
.get("/bytes/{n}",
(req, res) -> res.sendString(Mono.just(req.param("n")))))
.bindNow();

server.onDispose()
.block();
}
}
  • 对带有 URI 标签的计量器应用上限

  • 在可能时使用模板化 URI 作为 URI 标签的值

  • 启用与 Micrometer 的内置集成

备注

为避免启用指标带来的内存与 CPU 开销,重要的是在可能时将真实 URI 转换为模板化 URI。若不转换为类似模板的形式,每个不同的 URI 都会产生一个不同的标签,从而占用大量指标内存。

备注

务必为带有 URI 标签的计量器应用上限。当真实 URI 无法模板化时,对计量器数量配置上限会有所帮助。有关更多信息,请参阅 maximumAllowableTags。

当需要将 HTTP 服务器指标集成到 Micrometer 之外的系统,或您希望自行提供与 Micrometer 的集成时,可以按如下方式提供自己的指标记录器:

import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;
import reactor.netty.http.server.HttpServerMetricsRecorder;

import java.net.SocketAddress;
import java.time.Duration;

public class Application {

public static void main(String[] args) {
DisposableServer server =
HttpServer.create()
.metrics(true, CustomHttpServerMetricsRecorder::new) 1
.route(r ->
r.get("/stream/{n}",
(req, res) -> res.sendString(Mono.just(req.param("n"))))
.get("/bytes/{n}",
(req, res) -> res.sendString(Mono.just(req.param("n")))))
.bindNow();

server.onDispose()
.block();
}

13. 追踪​

HTTP 服务器支持与 Micrometer Tracing 的内置集成。

下表提供了 HTTP 服务器跨度的相关信息:

上下文名称说明
<HTTP METHOD>_<URI>请求的信息与总时间。请参阅 Http Server Response Span。

以下示例启用了该集成。本示例使用 Brave,并将信息上报到 Zipkin。有关 OpenTelemetry 的设置,请参阅 Micrometer Tracing 文档。

import brave.Tracing;
import brave.propagation.StrictCurrentTraceContext;
import brave.sampler.Sampler;
import io.micrometer.tracing.CurrentTraceContext;
import io.micrometer.tracing.Tracer;
import io.micrometer.tracing.brave.bridge.BraveBaggageManager;
import io.micrometer.tracing.brave.bridge.BraveCurrentTraceContext;
import io.micrometer.tracing.brave.bridge.BravePropagator;
import io.micrometer.tracing.brave.bridge.BraveTracer;
import io.micrometer.tracing.propagation.Propagator;
import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.observability.ReactorNettyPropagatingReceiverTracingObservationHandler;
import reactor.netty.http.server.HttpServer;
import zipkin2.reporter.brave.AsyncZipkinSpanHandler;
import zipkin2.reporter.urlconnection.URLConnectionSender;

import static reactor.netty.Metrics.OBSERVATION_REGISTRY;

public class Application {

public static void main(String[] args) {
init(); 1

DisposableServer server =
HttpServer.create()
.metrics(true, s -> {
if (s.startsWith("/stream/")) { 2
return "/stream/{n}";
}
return s;
}) 3
.route(r -> r.get("/stream/{n}",
(req, res) -> res.sendString(Mono.just(req.param("n")))))
.bindNow();

server.onDispose()
.block();
}

/**
* This setup is based on
* <a href="https://micrometer.io/docs/tracing#_micrometer_tracing_brave_setup">Micrometer Tracing Brave Setup</a>.
*/
static void init() {
AsyncZipkinSpanHandler spanHandler = AsyncZipkinSpanHandler
.create(URLConnectionSender.create("http://localhost:9411/api/v2/spans"));

StrictCurrentTraceContext braveCurrentTraceContext = StrictCurrentTraceContext.create();

CurrentTraceContext bridgeContext = new BraveCurrentTraceContext(braveCurrentTraceContext);

Tracing tracing =
Tracing.newBuilder()
.currentTraceContext(braveCurrentTraceContext)
.supportsJoin(false)
.traceId128Bit(true)
.sampler(Sampler.ALWAYS_SAMPLE)
.addSpanHandler(spanHandler)
.localServiceName("reactor-netty-examples")
.build();

brave.Tracer braveTracer = tracing.tracer();

Tracer tracer = new BraveTracer(braveTracer, bridgeContext, new BraveBaggageManager());

Propagator propagator = new BravePropagator(tracing);

OBSERVATION_REGISTRY.observationConfig()
.observationHandler(new ReactorNettyPropagatingReceiverTracingObservationHandler(tracer, propagator));
}
}
  • 初始化 Brave、Zipkin 以及 Observation 注册表。

  • 在可能时使用模板化 URI 作为 URI 标签的值。

  • 启用与 Micrometer 的内置集成。

Zipkin 中的结果如下所示:

http server tracing

13.1. 获取当前观测​

Micrometer 项目提供了一个库,用于在不同类型的上下文机制(如 ThreadLocal、Reactor Context 等)之间协助进行上下文传播。

以下示例展示了如何在自定义 ChannelHandler 中使用该库:

import brave.Tracing;
import brave.propagation.StrictCurrentTraceContext;
import brave.sampler.Sampler;
import io.micrometer.context.ContextSnapshot;
import io.micrometer.context.ContextSnapshotFactory;
import io.micrometer.tracing.CurrentTraceContext;
import io.micrometer.tracing.Tracer;
import io.micrometer.tracing.brave.bridge.BraveBaggageManager;
import io.micrometer.tracing.brave.bridge.BraveCurrentTraceContext;
import io.micrometer.tracing.brave.bridge.BravePropagator;
import io.micrometer.tracing.brave.bridge.BraveTracer;
import io.micrometer.tracing.propagation.Propagator;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.observability.ReactorNettyPropagatingReceiverTracingObservationHandler;
import reactor.netty.http.server.HttpServer;
import zipkin2.reporter.brave.AsyncZipkinSpanHandler;
import zipkin2.reporter.urlconnection.URLConnectionSender;

import static reactor.netty.Metrics.OBSERVATION_REGISTRY;

public class Application {

public static void main(String[] args) {
init(); 1

DisposableServer server =
HttpServer.create()
.metrics(true, s -> {
if (s.startsWith("/stream/")) { 2
return "/stream/{n}";
}
return s;
}) 3
.doOnConnection(conn -> conn.addHandlerLast(CustomChannelOutboundHandler.INSTANCE)) 4
.route(r -> r.get("/stream/{n}",
(req, res) -> res.sendString(Mono.just(req.param("n")))))
.bindNow();

server.onDispose()
.block();
}

static final class CustomChannelOutboundHandler extends ChannelOutboundHandlerAdapter {

static final ChannelHandler INSTANCE = new CustomChannelOutboundHandler();

@Override
public boolean isSharable() {
return true;
}

@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
try (ContextSnapshot.Scope scope = ContextSnapshotFactory.builder().build().setThreadLocalsFrom(ctx.channel())) {
System.out.println("Current Observation in Scope: " + OBSERVATION_REGISTRY.getCurrentObservation());
//"FutureReturnValueIgnored" this is deliberate
ctx.write(msg, promise);
}
System.out.println("Current Observation: " + OBSERVATION_REGISTRY.getCurrentObservation());
}
}
  • 初始化 Brave、Zipkin 以及 Observation 注册表。

  • 在可能时使用模板化 URI 作为 URI 标签的值。

  • 启用与 Micrometer 的内置集成。

  • 使用上下文传播库的自定义 ChannelHandler。本示例仅重写了 ChannelOutboundHandlerAdapter#write;如有需要,其余方法也可使用相同逻辑。此外,本示例会为给定 Channel 中存在值的所有 ThreadLocal 值进行设置;如需其他行为,请查阅上下文传播库 API。例如,您可能只想设置部分 ThreadLocal 值。

备注

当您在某个框架内启用 Reactor Netty 追踪时,可能需要让 Reactor Netty 使用该框架创建的 ObservationRegistry。为此,您需要调用 reactor.netty.Metrics#observationRegistry。您可能还需要使用该框架提供的 API 来配置 Reactor Netty 的 ObservationHandlers。

14. Unix 域套接字​

当使用原生传输时,HTTP 服务器在所有 Java 版本上都支持 Unix 域套接字(UDS);当使用 NIO 传输时,则在 Java 17 及以上版本上支持。

以下示例展示了如何使用 UDS 支持:

import io.netty.channel.unix.DomainSocketAddress;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

//import java.net.UnixDomainSocketAddress;

public class Application {

public static void main(String[] args) {
DisposableServer server =
HttpServer.create()
// The configuration below is available only when Epoll/KQueue transport is used
.bindAddress(() -> new DomainSocketAddress("/tmp/test.sock")) 1
// The configuration below is available only when NIO transport is used with Java 17+
//.bindAddress(() -> UnixDomainSocketAddress.of("/tmp/test.sock"))
.bindNow();

server.onDispose()
.block();
}
}
  • 指定要使用的 DomainSocketAddress

15. 超时配置​

本节介绍可在 HttpServer 中使用的各种超时配置选项。配置恰当的超时可以改善或解决通信过程中的问题。这些配置选项可分组如下:

15.1. 请求超时​

以下清单列出了所有可用的请求超时配置选项。

  • readTimeout - 读取给定请求内容时,每次网络级读取操作之间的最大时间(精度:ms)

  • requestTimeout - 读取给定请求内容的最大时间(精度:ms)。

备注

配置读取/请求超时始终是一种良好实践。

要自定义默认设置,可以按如下方式配置 HttpServer:

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

import java.time.Duration;

public class Application {

public static void main(String[] args) {
DisposableServer server =
HttpServer.create()
.readTimeout(Duration.ofSeconds(5)) 1
.requestTimeout(Duration.ofSeconds(30)) 2
.handle((request, response) -> request.receive().then())
.bindNow();

server.onDispose()
.block();
}
}
  • 将读取超时配置为 5 秒。

  • 将请求超时配置为 30 秒。

15.2. 连接超时​

以下清单列出了所有可用的连接超时配置选项。

  • idleTimeout - 此连接保持打开并等待 HTTP 请求的最大时间(精度:ms)。一旦达到该超时,连接即被关闭。默认情况下未指定 idleTimeout,这表示不超时(即无限),意味着只有当某一对方显式决定关闭时连接才会被关闭。
备注

配置空闲超时始终是一种良好实践。

要自定义默认设置,可以按如下方式配置 HttpServer:

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

import java.time.Duration;

public class Application {

public static void main(String[] args) {
DisposableServer server =
HttpServer.create()
.idleTimeout(Duration.ofSeconds(1)) 1
.bindNow();

server.onDispose()
.block();
}
}
  • 将默认空闲超时配置为 1 秒。

15.3. SSL/TLS 超时​

HttpServer 支持 Netty 提供的 SSL/TLS 功能。

以下列表描述了可用的超时配置选项:

  • handshakeTimeout - 使用此选项配置 SSL 握手超时(精度:ms)。默认值:10s。
备注

当预计会出现缓慢的网络连接时,应考虑增大 SSL 握手超时。

  • closeNotifyFlushTimeout - 使用此选项配置 SSL close_notify 刷写超时(精度:ms)。默认值:3s。

  • closeNotifyReadTimeout - 使用此选项配置 SSL close_notify 读取超时(精度:ms)。默认值:0s。

要自定义默认设置,可以按如下方式配置 HttpServer:

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

import java.io.File;
import java.time.Duration;

public class Application {

public static void main(String[] args) {
File cert = new File("certificate.crt");
File key = new File("private.key");

Http11SslContextSpec http11SslContextSpec = Http11SslContextSpec.forServer(cert, key);

DisposableServer server =
HttpServer.create()
.secure(spec -> spec.sslContext(http11SslContextSpec)
.handshakeTimeout(Duration.ofSeconds(30)) 1
.closeNotifyFlushTimeout(Duration.ofSeconds(10)) 2
.closeNotifyReadTimeout(Duration.ofSeconds(10))) 3
.bindNow();

server.onDispose()
.block();
}
}
  • 将 SSL 握手超时配置为 30 秒。

  • 将 SSL close_notify 刷写超时配置为 10 秒。

  • 将 SSL close_notify 读取超时配置为 10 秒。