用Netty快速落地一个客户端程序
# 写在文章开头
上一篇我们讲了Netty服务端的实现,这篇我们基于Netty快速落地一个客户端程序,让你对于Netty网络编程有着更进一步的了解和掌握。

Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili 。
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

# 基于Netty快速落地一个高性能客户端
# 基本思路与实现
我们希望实现一个高性能事件驱动的客户端,因为客户端无需处理新连接接入,所以我们只需要一个EventLoopGroup即可,这里笔者选择NioEventLoopGroup毕竟Netty强大就体现在NIO,所以我们就没有设置为BIO。然后添加对应的客户端读写处理器ClientHandler处理当前客户端channel轮询到的读写事件即可:

于是我们给出下面这样一段基于引导类配置客户端程序的代码段,可以看到因为客户端无需处理接入连接,所以就没有配置主从reactor,对应的引导类我们就采用Bootstrap :
//1. 客户端引导类配置
Bootstrap bootstrap = new Bootstrap();
// 2. 执行客户端任务执行先测测
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
// 指定线程组
bootstrap.group(workerGroup)
//指定NioSocketChannel
.channel(NioSocketChannel.class)
// 客户端读写数据处理器
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel channel) throws Exception {
//添加客户端channel事件处理器,StringEncoder对消息进行编码,FirstClientHandler输出收到的结果
channel.pipeline().addLast(new StringEncoder())
.addLast(new FirstClientHandler());
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
同时为了支持因为网络不稳定导致客户端初次连接失败,我们会在连接后设置异步监听,根据返回结果进行断线重连,我们做法是每一次重连失败都基于上次超时等待时间2倍的延迟等待,一旦超过3次则跑错不再重连:
private static Channel connect(Bootstrap bootstrap, String host, int port, int retry) {
//connect会返回一个异步的future,所以我们可以通过添加监听查看连接结果
return bootstrap.connect(host, port)
.addListener(future -> {
if (future.isSuccess()) {
System.out.println("连接成功!");
} else if (retry == 0) {
System.err.println("重试次数已用完,放弃连接!");
} else {
// 第几次重连
int order = (MAX_RETRY - retry) + 1;
// 定时任务下次执行重连的时间,时间*2
int delay = 1 << order;
System.err.println(new Date() + ": 连接失败,第" + order + "次重连……");
//基于递归算法,提交一个延迟任务下一次重连
bootstrap.config()
.group()
.schedule(() -> connect(bootstrap, host, port, retry - 1),
delay, TimeUnit.SECONDS);
}
}).channel();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 客户端常见参数详解
除此之外Netty还给我们提供了attr进行channel的属性设定,如下所示,我们希望指明客户端的名称,就可以通过该函数设置其名称:
// attr() 方法 用于给NioSocketChannel指定一个Map 按需从其中取值
bootstrap.attr(AttributeKey.newInstance("clientName"), "nettyClient");
2
对于该方法,我们可以步入其内部实现查看可以看到attr方法本质就是将数据存到attrs这个LinkedHashMap中,同时为了保证线程安全它该方法内部还用当前客户端的引导类的this进行上锁保证线程安全:
private final Map<AttributeKey<?>, Object> attrs = new LinkedHashMap<AttributeKey<?>, Object>();
public <T> B attr(AttributeKey<T> key, T value) {
//key的空校验
if (key == null) {
throw new NullPointerException("key");
}
//value设置为空时会上对象锁移除这个键值对
if (value == null) {
synchronized (attrs) {
attrs.remove(key);
}
} else {
//上对象锁设置键值对
synchronized (attrs) {
attrs.put(key, value);
}
}
return (B) this;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
同时Nety也提供了option设置一些系统级别TCP习惯参数:
// option() 方法用于指定一些TCP参数 CONNECT_TIMEOUT_MILLIS 指定连接超时的时间
bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
// SO_KEEPALIVE 表示是否开启TCP心跳机制
bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
// TCP_NODELAY 表示是否开启Nagle算法
bootstrap.option(ChannelOption.TCP_NODELAY, true);
2
3
4
5
6
7
# 关于Nagle算法
上文我们提到了一个Nagle算法,这里笔者也简单介绍一下,我们都知道每次应用层发送的数据传输时都需要打上tcp header(20 bytes)和ip header(20 bytes)等信息,为了避免因为一个小数据传输而消耗40bytes的流量显然是很不划算的做法。
于是提出了Nagle算法,该算法对应伪代码如下,即没有收到ack确认前,只有传输的数据缓冲区大小大于mss才能发送,由此保证利用带宽,避免各种高频小数据传输的开销。
如下图所示,我们客户端要发送1462字节数据,大于mss所以拆出1460字节发送,按照Nagle算法,只有收到ACK或者超时在进行发送,所以在没收到ACK且没超时期间,客户端又攒了18bytes的数据,服务端发送ACK确认之后,客户端再将20bytes数据一次性发送出去,由此避免了一些没必要的流量消耗:

对应笔者给出一段Nagle的伪代码:
if there is new data to send then
if the window size ≥ MSS and available data is ≥ MSS then
send complete MSS segment now
else
if there is unconfirmed data still in the pipe then
enqueue data in the buffer until an acknowledge is received
else
send data immediately
end if
end if
end if
2
3
4
5
6
7
8
9
10
11
而这种算法也不是没有缺点,对于实时性要求高的场景就不太友好,就比如redis服务端是明确禁止该算法的,因为redis的理念就是快速接收指令并处理。
# 小结
自此我们基于Netty客户端案例了解Netty更多的使用方式与核心理念,希望对你有帮助。
我是 sharkchili ,CSDN Java 领域博客专家,开源项目—JavaGuide contributor,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili 。 因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

# 参考
再多来点 TCP 吧:Delay ACK 和 Nagle 算法:https://www.kawabangga.com/posts/5845 (opens new window)
Nagle算法与Ack延迟确认:https://juejin.cn/post/7152076663449190436 (opens new window)
《跟着闪电侠学Netty》