跳到主要内容

TCP 客户端

Doubao 中英对照 TCP Client

目录

Reactor Netty 提供了易于使用、易于配置的 TcpClient。它隐藏了创建 TCP 客户端所需的大部分 Netty 功能,并加入了 Reactive Streams 背压。

1. 连接与断开​

要将 TCP 客户端连接到给定端点,您必须创建并配置一个 TcpClient 实例。默认情况下,host 为 localhost,port 为 12012。以下示例展示了如何创建 TcpClient:

import reactor.netty.Connection;
import reactor.netty.tcp.TcpClient;

public class Application {

public static void main(String[] args) {
Connection connection =
TcpClient.create() 1
.connectNow(); 2

connection.onDispose()
.block();
}
}
  • 创建一个可用于配置的 TcpClient 实例。

  • 以阻塞方式连接客户端,并等待其完成初始化。

返回的 Connection 提供了简单的连接 API,其中包括 disposeNow(),它会以阻塞方式关闭客户端。

1.1. 主机与端口​

要连接到特定的 host 和 port,您可以对 TCP 客户端应用以下配置。以下示例展示了具体做法:

import reactor.netty.Connection;
import reactor.netty.tcp.TcpClient;

public class Application {

public static void main(String[] args) {
Connection connection =
TcpClient.create()
.host("example.com") 1
.port(80) 2
.connectNow();

connection.onDispose()
.block();
}
}
  • 配置 TCP 主机

  • 配置 TCP 端口

备注

端口也可以通过 PORT 环境变量来指定。

2. 立即初始化​

默认情况下,TcpClient 资源的初始化按需进行。这意味着 connect operation 会承担初始化和加载所需的额外时间:

  • 事件循环组

  • 主机名解析器

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

  • 安全相关的原生库(OpenSsl 的情形)

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

import reactor.core.publisher.Mono;
import reactor.netty.Connection;
import reactor.netty.tcp.TcpClient;

public class Application {

public static void main(String[] args) {
TcpClient tcpClient =
TcpClient.create()
.host("example.com")
.port(80)
.handle((inbound, outbound) -> outbound.sendString(Mono.just("hello")));

tcpClient.warmup() 1
.block();

Connection connection = tcpClient.connectNow(); 2

connection.onDispose()
.block();
}
}
  • 初始化并加载事件循环组、主机名解析器、原生传输库以及安全相关的原生库

  • 主机名解析在连接到远程对端时进行

3. 写入数据​

要向给定端点发送数据,您必须挂载一个 I/O 处理器。该 I/O 处理器可以访问 NettyOutbound,以便写入数据。

import reactor.core.publisher.Mono;
import reactor.netty.Connection;
import reactor.netty.tcp.TcpClient;

public class Application {

public static void main(String[] args) {
Connection connection =
TcpClient.create()
.host("example.com")
.port(80)
.handle((inbound, outbound) -> outbound.sendString(Mono.just("hello"))) 1
.connectNow();

connection.onDispose()
.block();
}
}
  • 向端点发送 hello 字符串。

当您需要对写入过程进行更多控制时,除了使用 I/O 处理器,您还可以使用 Connection#outbound。在 I/O 处理器中,当所提供的 Publisher 结束时(针对有限的 Publisher)连接会被关闭;而使用 Connection#outbound 时,您必须显式调用 Connection#dispose 来关闭连接。

import reactor.core.publisher.Mono;
import reactor.netty.Connection;
import reactor.netty.tcp.TcpClient;

public class Application {

public static void main(String[] args) {
Connection connection =
TcpClient.create()
.host("example.com")
.port(80)
.connectNow();

connection.outbound()
.sendString(Mono.just("hello 1")) 1
.then()
.subscribe();

connection.outbound()
.sendString(Mono.just("hello 2")) 2
.then()
.subscribe(null, null, connection::dispose); 3

connection.onDispose()
.block();
}
}
  • 向端点发送 hello 1 字符串。

  • 向端点发送 hello 2 字符串。

  • 消息发送到端点后关闭连接。

4. 消费数据​

要从给定端点接收数据,您必须挂载一个 I/O 处理器。该 I/O 处理器可以访问 NettyInbound,以便读取数据。以下示例展示了具体做法:

import reactor.netty.Connection;
import reactor.netty.tcp.TcpClient;

public class Application {

public static void main(String[] args) {
Connection connection =
TcpClient.create()
.host("example.com")
.port(80)
.handle((inbound, outbound) -> inbound.receive().then()) 1
.connectNow();

connection.onDispose()
.block();
}
}
  • 从给定端点接收数据

当您需要对读取过程进行更多控制时,除了使用 I/O 处理器,您还可以使用 Connection#inbound。在 I/O 处理器中,当所提供的 Publisher 结束时(针对有限的 Publisher)连接会被关闭;而使用 Connection#inbound 时,您必须显式调用 Connection#dispose 来关闭连接。

import reactor.netty.Connection;
import reactor.netty.tcp.TcpClient;

public class Application {

public static void main(String[] args) {
Connection connection =
TcpClient.create()
.host("example.com")
.port(80)
.connectNow();

connection.inbound()
.receive() 1
.then()
.subscribe();

connection.onDispose()
.block();
}
}
  • 从给定端点接收数据。

5. 生命周期回调​

提供以下生命周期回调,以便您扩展 TcpClient。

回调说明
doAfterResolve在远程地址成功解析后调用。
doOnChannelInit在初始化通道时调用。
doOnConnect在通道即将建立连接时调用。
doOnConnected在通道建立连接后调用。
doOnDisconnected在通道断开连接后调用。
doOnResolve在远程地址即将被解析时调用。
doOnResolveError在远程地址未能成功解析时调用。

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

import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.ReadTimeoutHandler;
import reactor.netty.Connection;
import reactor.netty.tcp.TcpClient;
import java.util.concurrent.TimeUnit;

public class Application {

public static void main(String[] args) {
Connection connection =
TcpClient.create()
.host("example.com")
.port(80)
.doOnConnected(conn ->
conn.addHandlerFirst(new ReadTimeoutHandler(10, TimeUnit.SECONDS))) 1
.doOnChannelInit((observer, channel, remoteAddress) ->
channel.pipeline()
.addFirst(new LoggingHandler("reactor.netty.examples"))) 2
.connectNow();

connection.onDispose()
.block();
}
}
  • 通道建立连接后,Netty 管道会追加 ReadTimeoutHandler。

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

6. TCP 级配置​

本节介绍您可以在 TCP 级别使用的三种配置:

6.1. 通道选项​

默认情况下,TCP 客户端配置了以下选项:

TcpClientConnect(ConnectionProvider provider) {
this.config = new TcpClientConfig(
provider,
Collections.singletonMap(ChannelOption.AUTO_READ, false),
() -> AddressUtils.createUnresolved(NetUtil.LOCALHOST.getHostAddress(), DEFAULT_PORT));
}

如果需要额外的选项或要更改当前选项,您可以应用以下配置:

import io.netty.channel.ChannelOption;
import reactor.netty.Connection;
import reactor.netty.tcp.TcpClient;

public class Application {

public static void main(String[] args) {
Connection connection =
TcpClient.create()
.host("example.com")
.port(80)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
.connectNow();

connection.onDispose()
.block();
}
}

您可以在以下链接中找到关于 Netty 通道选项的更多信息:

6.2. 线路日志记录器​

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

import reactor.netty.Connection;
import reactor.netty.tcp.TcpClient;

public class Application {

public static void main(String[] args) {
Connection connection =
TcpClient.create()
.wiretap(true) 1
.host("example.com")
.port(80)
.connectNow();

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

6.2.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.Connection;
import reactor.netty.tcp.TcpClient;
import reactor.netty.transport.logging.AdvancedByteBufFormat;

public class Application {

public static void main(String[] args) {
Connection connection =
TcpClient.create()
.wiretap("logger-name", LogLevel.DEBUG, AdvancedByteBufFormat.TEXTUAL) 1
.host("example.com")
.port(80)
.connectNow();

connection.onDispose()
.block();
}
}

6.3. 事件循环组​

默认情况下,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.Connection;
import reactor.netty.resources.LoopResources;
import reactor.netty.tcp.TcpClient;

public class Application {

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

Connection connection =
TcpClient.create()
.host("example.com")
.port(80)
.runOn(loop)
.connectNow();

connection.onDispose()
.block();
}
}

6.3.1. 释放事件循环组​

  • 如果您使用 Reactor Netty 提供的默认 事件循环组,请调用 HttpResources#disposeLoopsAndConnections/#disposeLoopsAndConnectionsLater 方法。
备注

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

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

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

7. 连接池​

默认情况下,TcpClient(TcpClient.create())使用一个共享的 ConnectionProvider。该 ConnectionProvider 被配置为为每个远程主机(远程主机指主机名及其关联端口号的组合)创建一个“固定”连接池,并具有以下配置:

  • 500 作为活动通道的最大数量

  • 1000 作为允许保持挂起状态的进一步通道获取尝试的最大数量

  • 其余配置均为默认值(请参阅下文的系统属性或构建器配置)

这意味着,只要池中创建并管理的通道少于 500 个,当有人尝试获取通道时,实现就会创建一个新通道。当池中的通道数达到最大值时,最多 1000 个新的通道获取尝试会被延迟(挂起),直到某个通道被关闭(从而腾出一个槽位,可以打开新连接),之后的更多尝试将以错误方式被拒绝。

备注

TcpClient 使用的连接绝不会被归还到连接池,而是被关闭。当连接关闭时,连接池中会释放一个槽位,从而在需要时可以打开新连接。此行为仅为 TcpClient 所特有,且是有意为之,因为只有用户/框架才知道实际协议是否支持连接复用。(这与 HttpClient 不同,后者的协议是已知的,Reactor Netty 可以在可行时将连接归还到连接池。)

/**
* Default max connections. Fallback to
* 2 * available number of processors (but with a minimum value of 16)
*/
public static final String POOL_MAX_CONNECTIONS = "reactor.netty.pool.maxConnections";
/**
* Default acquisition timeout (milliseconds) before error. If -1 will never wait to
* acquire before opening a new
* connection in an unbounded fashion. Fallback 45 seconds
*/
public static final String POOL_ACQUIRE_TIMEOUT = "reactor.netty.pool.acquireTimeout";

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

import reactor.netty.Connection;
import reactor.netty.resources.ConnectionProvider;
import reactor.netty.tcp.TcpClient;
import java.time.Duration;

public class Application {

public static void main(String[] args) {
ConnectionProvider provider =
ConnectionProvider.builder("fixed")
.maxConnections(50)
.pendingAcquireTimeout(Duration.ofSeconds(60)) 1
.build();

Connection connection =
TcpClient.create(provider)
.host("example.com")
.port(80)
.connectNow();

connection.onDispose()
.block();
}
}
  • 将挂起获取操作的最长时间配置为 60 秒。

以下列出了可用的配置:

配置名称说明
disposeInactivePoolsInBackground启用此选项后,会在后台定期检查各连接池,那些为空且在指定时间内未活动的连接池将可被释放。默认情况下,这种后台释放不活动连接池的行为是禁用的。
disposeTimeout调用 ConnectionProvider#dispose() 或 ConnectionProvider#disposeLater() 时,以此宽限期超时触发连接池的优雅关闭。此后,所有获取连接的调用都会快速失败并抛出异常。但在所提供的 Duration 内,挂起的获取请求仍有机会被满足。注意:无论是否订阅 ConnectionProvider#disposeLater() 返回的 Mono,拒绝新的获取请求与宽限期计时都会立即开始。后续调用会返回同一个 Mono,实际从第一次优雅关闭调用获取通知,并忽略随后提供的超时。默认情况下,不指定释放超时。
maxConnections在开始挂起之前连接的最大数量(每个连接池)。默认为可用处理器数量的 2 倍(但最小值为 16)。
metrics启用/禁用与 Micrometer 的内置集成。可以提供 ConnectionProvider.MeterRegistrar 以与其他指标系统集成。默认情况下,不启用指标。
pendingAcquireMaxCount保留在挂起队列中的额外连接获取尝试的最大数量。若指定为 -1,则挂起队列没有上限。默认为最大连接数的 2 倍。
pendingAcquireTimeout挂起获取必须在此时间内完成,否则抛出 TimeoutException(精度:毫秒)。若指定为 -1,则不应用此超时。默认:45 秒。

如果您需要禁用连接池,可以应用以下配置:

import reactor.netty.Connection;
import reactor.netty.tcp.TcpClient;

public class Application {

public static void main(String[] args) {
Connection connection =
TcpClient.newConnection()
.host("example.com")
.port(80)
.connectNow();

connection.onDispose()
.block();
}
}

7.1. 释放连接池​

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

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

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

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

7.2. 指标​

池化的 ConnectionProvider 支持与 Micrometer 的内置集成。它以 reactor.netty.connection.provider 为前缀暴露所有指标。

池化 ConnectionProvider 指标

指标名称类型说明
reactor.netty.connection.provider.total.connectionsGauge所有连接的数量,无论是活动的还是空闲的。请参阅 总连接数
reactor.netty.connection.provider.active.connectionsGauge已成功获取并正处于活动使用中的连接数量。请参阅 活动连接数
reactor.netty.connection.provider.max.connectionsGauge允许的活动连接最大数量。请参阅 最大连接数
reactor.netty.connection.provider.idle.connectionsGauge空闲连接的数量。请参阅 空闲连接数
reactor.netty.connection.provider.pending.connectionsGauge正在等待连接的请求数量。请参阅 挂起连接数
reactor.netty.connection.provider.pending.connections.timeTimer从连接池挂起获取连接所花费的时间。请参阅 挂起连接时间
reactor.netty.connection.provider.max.pending.connectionsGauge在等待就绪连接期间将排队的请求最大数量。请参阅 最大挂起连接数

以下示例启用了该集成:

import reactor.netty.Connection;
import reactor.netty.resources.ConnectionProvider;
import reactor.netty.tcp.TcpClient;

public class Application {

public static void main(String[] args) {
ConnectionProvider provider =
ConnectionProvider.builder("fixed")
.maxConnections(50)
.metrics(true) 1
.build();

Connection connection =
TcpClient.create(provider)
.host("example.com")
.port(80)
.connectNow();

connection.onDispose()
.block();
}
}
  • 启用与 Micrometer 的内置集成

8. SSL 与 TLS​

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

以下示例使用了 SslContextBuilder:

import reactor.netty.Connection;
import reactor.netty.tcp.TcpClient;
import reactor.netty.tcp.TcpSslContextSpec;

public class Application {

public static void main(String[] args) {
TcpSslContextSpec tcpSslContextSpec = TcpSslContextSpec.forClient();

Connection connection =
TcpClient.create()
.host("example.com")
.port(443)
.secure(spec -> spec.sslContext(tcpSslContextSpec))
.connectNow();

connection.onDispose()
.block();
}
}

8.1. 服务器名称指示​

默认情况下,TCP 客户端会将远程主机名作为 SNI 服务器名发送。当您需要更改此默认设置时,可以按如下方式配置 TCP 客户端:

import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import reactor.netty.Connection;
import reactor.netty.tcp.TcpClient;

import javax.net.ssl.SNIHostName;

public class Application {

public static void main(String[] args) throws Exception {
SslContext sslContext = SslContextBuilder.forClient().build();

Connection connection =
TcpClient.create()
.host("127.0.0.1")
.port(8080)
.secure(spec -> spec.sslContext(sslContext)
.serverNames(new SNIHostName("test.com")))
.connectNow();

connection.onDispose()
.block();
}
}

9. 代理支持​

Reactor Netty 支持 Netty 提供的代理功能,并提供了一种通过 ProxyProvider 构建器来指定 non proxy hosts 的方式。

无论使用 http 还是 https 方案,Netty 的 HTTP 代理支持始终使用 CONNECT 方法来与指定代理建立隧道。(更多信息:Netty enforce HTTP proxy to support HTTP CONNECT method)。有些代理在方案为 http 时可能不支持 CONNECT 方法,或者可能需要进行配置才能支持这种通信方式。有时这正是无法连接到代理的原因。请查阅代理文档,确认它是否支持或需要额外配置才能支持 CONNECT 方法。

以下示例使用了 ProxyProvider:

import reactor.netty.Connection;
import reactor.netty.transport.ProxyProvider;
import reactor.netty.tcp.TcpClient;

public class Application {

public static void main(String[] args) {
Connection connection =
TcpClient.create()
.host("example.com")
.port(80)
.proxy(spec -> spec.type(ProxyProvider.Proxy.SOCKS4)
.host("proxy")
.port(8080)
.nonProxyHosts("localhost")
.connectTimeoutMillis(20_000)) 1
.connectNow();

connection.onDispose()
.block();
}
}
  • 将连接建立超时配置为 20 秒。

10. 指标​

TCP 客户端支持与 Micrometer 的内置集成。它以 reactor.netty.tcp.client 为前缀暴露所有指标。

下表提供了 TCP 客户端指标的相关信息:

指标名称类型说明
reactor.netty.tcp.client.data.receivedDistributionSummary接收的数据量,以字节为单位。请参阅 接收数据
reactor.netty.tcp.client.data.sentDistributionSummary发送的数据量,以字节为单位。请参阅 发送数据
reactor.netty.tcp.client.errorsCounter发生的错误数量。请参阅 错误数
reactor.netty.tcp.client.tls.handshake.timeTimerTLS 握手所花费的时间。请参阅 TLS 握手时间
reactor.netty.tcp.client.connect.timeTimer连接到远程地址所花费的时间。请参阅 连接时间
reactor.netty.tcp.client.address.resolverTimer解析地址所花费的时间。请参阅 主机名解析时间

以下附加指标同样可用:

池化 ConnectionProvider 指标

指标名称类型说明
reactor.netty.connection.provider.total.connectionsGauge所有连接的数量,无论是活动的还是空闲的。请参阅 总连接数
reactor.netty.connection.provider.active.connectionsGauge已成功获取并正处于活动使用中的连接数量。请参阅 活动连接数
reactor.netty.connection.provider.max.connectionsGauge允许的活动连接最大数量。请参阅 最大连接数
reactor.netty.connection.provider.idle.connectionsGauge空闲连接的数量。请参阅 空闲连接数
reactor.netty.connection.provider.pending.connectionsGauge正在等待连接的请求数量。请参阅 挂起连接数
reactor.netty.connection.provider.pending.connections.timeTimer从连接池挂起获取连接所花费的时间。请参阅 挂起连接时间
reactor.netty.connection.provider.max.pending.connectionsGauge在等待就绪连接期间将排队的请求最大数量。请参阅 最大挂起连接数

ByteBufAllocator 指标

指标名称类型说明
reactor.netty.bytebuf.allocator.used.heap.memoryGauge堆缓冲区分配器预留的字节数。请参阅 已用堆内存
reactor.netty.bytebuf.allocator.used.direct.memoryGauge直接缓冲区分配器预留的字节数。请参阅 已用直接内存
reactor.netty.bytebuf.allocator.heap.arenasGauge堆 arena 的数量(使用 PooledByteBufAllocator 时)。请参阅 堆 Arena 数
reactor.netty.bytebuf.allocator.direct.arenasGauge直接 arena 的数量(使用 PooledByteBufAllocator 时)。请参阅 直接 Arena 数
reactor.netty.bytebuf.allocator.threadlocal.cachesGauge线程本地缓存的数量(使用 PooledByteBufAllocator 时)。请参阅 线程本地缓存数
reactor.netty.bytebuf.allocator.small.cache.sizeGauge小型缓存的大小(使用 PooledByteBufAllocator 时)。请参阅 小型缓存大小
reactor.netty.bytebuf.allocator.normal.cache.sizeGauge普通缓存的大小(使用 PooledByteBufAllocator 时)。请参阅 普通缓存大小
reactor.netty.bytebuf.allocator.chunk.sizeGaugearena 的块大小(使用 PooledByteBufAllocator 时)。请参阅 块大小
reactor.netty.bytebuf.allocator.active.heap.memoryGauge从堆缓冲区池分配且正在使用的缓冲区实际消耗的字节数(使用 PooledByteBufAllocator 时)。请参阅 活动堆内存
reactor.netty.bytebuf.allocator.active.direct.memoryGauge从直接缓冲区池分配且正在使用的缓冲区实际消耗的字节数(使用 PooledByteBufAllocator 时)。请参阅 活动直接内存

EventLoop 指标

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

以下示例启用了该集成:

import reactor.netty.Connection;
import reactor.netty.tcp.TcpClient;

public class Application {

public static void main(String[] args) {
Connection connection =
TcpClient.create()
.host("example.com")
.port(80)
.metrics(true) 1
.connectNow();

connection.onDispose()
.block();
}
}
  • 启用与 Micrometer 的内置集成

当需要将 TCP 客户端指标与 Micrometer 以外的系统集成,或者您想提供自己的与 Micrometer 的集成时,可以按如下方式提供自己的指标记录器:

import reactor.netty.Connection;
import reactor.netty.channel.ChannelMetricsRecorder;
import reactor.netty.tcp.TcpClient;

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

public class Application {

public static void main(String[] args) {
Connection connection =
TcpClient.create()
.host("example.com")
.port(80)
.metrics(true, CustomChannelMetricsRecorder::new) 1
.connectNow();

connection.onDispose()
.block();
}

11. 追踪​

TCP 客户端支持与 Micrometer Tracing 的内置集成。

下表提供了 TCP 客户端跨度的相关信息:

上下文名称说明
hostname resolution解析地址的相关信息及所花费时间。请参阅 主机名解析跨度。
connect连接到远程地址的相关信息及所花费时间。请参阅 连接跨度。
tls handshakeTLS 握手的相关信息及所花费时间。请参阅 TLS 握手跨度。

以下示例启用了该集成。此示例使用 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.BraveTracer;
import reactor.netty.Connection;
import reactor.netty.observability.ReactorNettyTracingObservationHandler;
import reactor.netty.tcp.TcpClient;
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

Connection connection =
TcpClient.create()
.host("example.com")
.port(80)
.metrics(true) 2
.connectNow();

connection.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());

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

  • 启用与 Micrometer 的内置集成。

在 Zipkin 中的结果如下所示:

TCP 客户端追踪

11.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.BraveTracer;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import reactor.netty.Connection;
import reactor.netty.observability.ReactorNettyTracingObservationHandler;
import reactor.netty.tcp.TcpClient;
import zipkin2.reporter.brave.AsyncZipkinSpanHandler;
import zipkin2.reporter.urlconnection.URLConnectionSender;

import java.net.SocketAddress;

import static reactor.netty.Metrics.OBSERVATION_REGISTRY;

public class Application {

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

Connection connection =
TcpClient.create()
.host("example.com")
.port(80)
.metrics(true) 2
.doOnChannelInit((observer, channel, address) -> channel.pipeline().addFirst(
"custom-channel-handler", CustomChannelOutboundHandler.INSTANCE)) 3
.connectNow();

connection.onDispose()
.block();
}

static final class CustomChannelOutboundHandler extends ChannelOutboundHandlerAdapter {

static final ChannelHandler INSTANCE = new CustomChannelOutboundHandler();

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

@Override
public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress, 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.connect(remoteAddress, localAddress, promise);
}
System.out.println("Current Observation: " + OBSERVATION_REGISTRY.getCurrentObservation());
}
}
  • 初始化 Brave、Zipkin 以及 Observation 注册表。

  • 启用与 Micrometer 的内置集成。

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

备注

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

12. Unix 域套接字​

当使用原生传输时,TCP 客户端在所有 Java 版本上都支持 Unix 域套接字(UDS);当使用 NIO 传输时,在 Java 17 及以上版本上支持。

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

import io.netty.channel.unix.DomainSocketAddress;
import reactor.netty.Connection;
import reactor.netty.tcp.TcpClient;

//import java.net.UnixDomainSocketAddress;

public class Application {

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

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

13. 主机名解析​

默认情况下,TcpClient 使用 Netty 的域名查找机制,该机制以异步方式解析域名。这是 JVM 内置阻塞解析器的一种替代方案。

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

import reactor.netty.Connection;
import reactor.netty.tcp.TcpClient;

import java.time.Duration;

public class Application {

public static void main(String[] args) {
Connection connection =
TcpClient.create()
.host("example.com")
.port(80)
.resolver(spec -> spec.queryTimeout(Duration.ofMillis(500))) 1
.connectNow();

connection.onDispose()
.block();
}
}
  • 此解析器执行的每次 DNS 查询的超时时间为 500ms。

以下列出了可用的配置。此外,TCP fallback 默认处于启用状态。

配置名称说明
bindAddressSupplier要绑定的本地地址的供应器。
cacheMaxTimeToLive缓存的 DNS 资源记录的最长存活时间(精度:秒)。如果 DNS 服务器返回的 DNS 资源记录的存活时间大于此最长存活时间,则此解析器忽略 DNS 服务器给出的存活时间,而使用此最长存活时间。默认为 Integer.MAX_VALUE。
cacheMinTimeToLive缓存的 DNS 资源记录的最短存活时间(精度:秒)。如果 DNS 服务器返回的 DNS 资源记录的存活时间小于此最短存活时间,则此解析器忽略 DNS 服务器给出的存活时间,而使用此最短存活时间。默认:0。
cacheNegativeTimeToLive失败 DNS 查询的缓存存活时间(精度:秒)。默认:0。
completeOncePreferredResolved启用此设置后,一旦首选地址类型的所有查询完成,解析器即发出通知。禁用此设置后,解析器会在所有可能地址类型均完成时才发出通知。此配置适用于 DnsNameResolver#resolveAll(String)。默认情况下,此设置为启用。
datagramChannelStrategy设置用于确定解析器如何使用 DatagramChannel 通过 UDP 协议发送查询的策略。默认为 DnsNameResolverChannelStrategy#ChannelPerResolver
disableOptionalRecord禁用自动包含一条可选记录,该记录用于向远程 DNS 服务器提示解析器每次响应可读取的数据量。默认情况下,此设置为启用。
disableRecursionDesired指定此解析器是否必须在发送 DNS 查询时设置递归期望(RD)标志。默认情况下,此设置为启用。
dnsAddressResolverGroupProvider设置一个自定义函数,用于根据给定的 DnsNameResolverBuilder 创建 DnsAddressResolverGroup
hostsFileEntriesResolver设置用于主机文件条目的自定义 HostsFileEntriesResolver。默认:DefaultHostsFileEntriesResolver。
maxPayloadSize设置数据报分组缓冲区的容量(以字节为单位)。默认:4096。
maxQueriesPerResolve设置解析主机名时允许发送的 DNS 查询最大数量。默认:16。
ndots设置在发起首次绝对查询之前名称中必须出现的点号数量。默认:-1(在 Unix 上从操作系统确定该值,否则使用值 1)。
queryTimeout设置此解析器执行的每次 DNS 查询的超时时间(精度:毫秒)。默认:5000。
resolveCache用于存储已解析 DNS 条目的缓存。
resolvedAddressTypes已解析地址的协议族列表。
retryTcpOnTimeout指定检测到超时时此解析器是否也回退到 TCP。默认情况下,仅当响应被标记为截断时解析器才会尝试使用 TCP。
roundRobinSelection启用一个 DnsNameResolver 的 AddressResolverGroup,当域名服务器提供多个目标地址时支持随机选择。请参阅 RoundRobinDnsAddressResolverGroup。默认:DnsAddressResolverGroup
runOn在给定的 LoopResources 上执行与 DNS 服务器的通信。默认使用客户端级别指定的 LoopResources。
searchDomains解析器的搜索域列表。默认情况下,有效的搜索域列表通过系统 DNS 搜索域来填充。
trace此解析器在解析失败时生成详细追踪信息所使用的特定日志记录器和日志级别。

有时,您可能想切换到 JVM 内置解析器。为此,可以按如下方式配置 TcpClient:

import io.netty.resolver.DefaultAddressResolverGroup;
import reactor.netty.Connection;
import reactor.netty.tcp.TcpClient;

public class Application {

public static void main(String[] args) {
Connection connection =
TcpClient.create()
.host("example.com")
.port(80)
.resolver(DefaultAddressResolverGroup.INSTANCE) 1
.connectNow();

connection.onDispose()
.block();
}
}
  • 设置 JVM 内置解析器。