Netty大白进阶教程
发布于2023-04-29 10:54:07,更新于2025-04-04 16:11:01,标签:java netty 文章会持续修订,转载请注明来源地址:https://meethigher.top/blog部分基础概念,参考网络编程NIO
一、粘包与拆包
1.1 概念
粘包
发送方容量<接收方容量
现象,发送两条消息
abc
、def
,接收到一条消息abcdef
导致原因
- 应用层:接收方 ByteBuf 设置太大(Netty 默认 1024)
- 滑动窗口:假设发送方 256 bytes 表示一个完整报文,但由于接收方处理不及时且窗口大小足够大,这 256 bytes 字节就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文就会粘包
- Nagle 算法:会造成粘包
拆包
发送方容量>接收方容量
现象,发送一条消息
abcdef
,接收到两条消息abc
、def
导致原因
- 应用层:接收方 ByteBuf 小于实际发送数据量
- 滑动窗口:假设接收方的窗口只剩了 128 bytes,发送方的报文大小是 256 bytes,这时放不下了,只能先发送前 128 bytes,等待 ack 后才能发送剩余部分,这就造成了拆包
- MSS 限制:当发送的数据超过 MSS 限制后,会将数据切分发送,就会造成拆包
本质是因为 TCP 是流式协议,消息无边界
1.2 问题复现
写个demo,客户端每次向服务端发送16字节,但是服务端会直接收到了160字节,这种现象就是粘包
服务端代码
1 | public class HelloWorldServer { |
客户端代码
1 | public class HelloWorldClient { |
验证拆包,只需要在原有代码,添加SO_RCVBUF
参数
1 | ChannelFuture future = new ServerBootstrap() |
客户端保持不变,一次消息16个字节,但是收到4个字节。这时候就出现了拆包
现象。
1.2 产生原因
1.2.1 滑动窗口
TCP 以一个段(segment)为单位,每发送一个段就需要进行一次确认应答(ack)处理,但如果这么做,缺点是包的往返时间越长性能就越差。
为了解决此问题,引入了窗口概念,窗口大小即决定了无需等待应答而可以继续发送的数据最大值。
流量控制
窗口实际就起到一个缓冲区的作用
- 窗口内的数据才会允许被发送,当应答未到达前,窗口必须停止滑动
- 如果窗口内的数据 ack(确认应答) 回来了,窗口就可以向前滑动
- 发送方/接收方都会维护一个窗口,只有落在窗口内的数据才能允许发送/接收
同时,窗口又能起到流量控制的作用:发送方的发送频率不要过快,要让接收方来得及接收。
rwnd表示receiver window,表示接收窗口,TCP的窗口单位是字节。
解决死锁-持续计时器
死锁问题的呈现
- B给A发送零窗口通知,此时A就不再发送数据了。
- B给A发送非零窗口通知,然而这个通知在传输过程中丢失了。
- 这时就会出现死锁现象:A等B的非零窗口通知,B等A的发送消息。
那么TCP如何解决死锁呢?
TCP为每个连接设有一个持续计时器,只要收到对方的零窗口通知,就启动该持续计时器。
- 持续计时器到期,A向B获取现在的窗口值
- 若窗口仍然是零或者没收到,计时器仍然执行。
- 若窗口不是零,移除计时器,死锁问题解决。
1.2.2 Nagle算法
Nagle 算法
- 即使发送一个字节,也需要加入 tcp 头和 ip 头(两者通常均为20bytes),也就是总字节数会使用 41 bytes,非常不经济。因此为了提高网络利用率,tcp 希望尽可能发送足够大的数据,这就是 Nagle 算法产生的缘由
- 该算法是指发送端即使还有应该发送的数据,但如果这部分数据很少的话,则进行延迟发送
- 如果 SO_SNDBUF 的数据达到 MSS,则需要发送
- 如果 SO_SNDBUF 中含有 FIN(表示需要连接关闭)这时将剩余数据发送,再关闭
- 如果 TCP_NODELAY = true,则需要发送
- 如果已发送的数据都收到 ack 时,则需要发送
- 上述条件不满足,但发生超时(一般为 200ms)则需要发送
- 除上述情况,延迟发送
1.2.3 MSS限制
MSS(Maximum Segment Size)是TCP协议中一个重要的参数,它表示在一个TCP段(即TCP协议中的数据包)中所能承载的最大数据量。MSS的大小通常由网络中最小的MTU(Maximum Transmission Unit,即最大传输单元)和TCP头部的大小来决定,具体计算公式为MSS = MTU - TCP头部大小。
具体来说,当发送端发送一个TCP段时,它会根据自己的MSS大小将数据分割为多个TCP段进行传输。接收端在接收这些TCP段时,如果其MSS大小小于发送端的MSS大小,则接收端需要将接收到的TCP段重新分割为更小的TCP段进行传输,这会导致传输效率降低。
为了解决这个问题,TCP协议中提供了一种MSS限制机制。具体来说,当建立一个TCP连接时,双方会通过TCP选项交换各自的MSS大小。其实就是木桶短板理论,以小的为准,从而提高TCP连接的传输效率。
需要注意的是,MSS限制只会影响TCP连接的数据传输,而不会影响TCP连接的建立和关闭过程。
MSS 限制
链路层对一次能够发送的最大数据有限制,这个限制称之为 MTU(maximum transmission unit),不同的链路设备的 MTU 值也有所不同
- 以太网的 MTU 是 1500
- FDDI(光纤分布式数据接口)的 MTU 是 4352
- 本地回环地址的 MTU 是 65535(本地测试不走网卡)
MSS = MTU - TCP头部大小
- ipv4 tcp 头占用 20 bytes,ip 头占用 20 bytes,因此以太网 MSS 的值为 1500 - 40 = 1460
- TCP 在传递大量数据时,会按照 MSS 大小将数据进行分割发送
- MSS 的值在三次握手时通知对方自己 MSS 的值,然后在两者之间选择一个小值作为 MSS
1.3 解决方案
1.3.1 短连接
客户端发送消息后,接着关闭连接。
客户端代码修改如下
1 | public class HelloWorldClient { |
服务端代码修改option配置即可
1 | //option针对全局 |
短连接的做法,可以避免粘包,但拆包问题依然存在。
- 当发送方容量<接收方容量时,也就解决了粘包问题。因为我一个连接发完一条消息就结束了,所以只要接收方容量过大,就不会发生粘包。
- 当发送方容量>接收方容量时,拆包问题,仍然存在。
1.3.2 定长
定长解码器。源码参考io.netty.handler.codec.FixedLengthFrameDecoder
1.3.3 固定分隔符
使用\n
和\r\n
换行符进行消息分隔。源码参考io.netty.handler.codec.LineBasedFrameDecoder
1.3.4 预设长度
比如明确消息结构
1 | | 4 字节(消息长度) | 2 字节(消息类型) | 变长(消息体) | |
源码参考io.netty.handler.codec.LengthFieldBasedFrameDecoder
二、协议设计与解析
2.1 理解协议
协议的一个重要目的,就是为了解决数据传输过程中的粘包/半包问题。
以redis为例。
比如redis要执行set key value
。他对消息结构有如下要求
- 告诉redis内容数组的个数。比如
set key value
,数组个数就是3 - 依次发送每个数组值的长度,以及对应的值。比如3set、3key、5value
2.2 自定义协议
要点
- 魔数。用来在第一时间判定是否是无效数据包。比如Java字节码的前4个字节固定为十六进制的cafebabe
- 版本号。可以支持协议的升级。
- 序列化算法。消息正文到底采用哪种序列化和反序列化方式。比如json/protobuf/flatbuffers
- 指令类型。如登录、注册、单聊、群聊…等跟业务相关的。
- 请求序号。为了双工通信,提供异步能力,不需要等待请求1响应后,再发请求2。
- 正文长度
- 消息正文
三、优化
3.1 原理理解
TCP三次握手、四次挥手的基础知识,可以访问这里
3.2 参数优化
3.2.1 backlog
在 Linux 2.2 之前,backlog 大小包括了半连接队列、全连接队列总大小。
在 Linux 2.2 之后,分别用下面两个系统参数来控制。
- 半连接队列
- 通过
/proc/sys/net/ipv4/tcp_max_syn_backlog
指定。但是当syncookies
启用的情况下,这个设置就被忽略了,变成没有最大值限制。
- 通过
- 全连接队列
- 通过
/proc/sys/net/core/somaxconn
指定。在是使用 listen 函数时,Linux 内核会根据传入的 backlog 参数与系统参数,取两者最小值。 - 如果 accept queue 队列满了,server 将发送一个
Connection refused
的错误信息给 client。
- 通过
3.2.2 ulimit
一个进程最多可以打开的文件描述符(file descriptors)数量。
在 Linux 中,一切皆文件。每接收到一个 tcp 连接,就占用一个文件。
1.) 编辑/etc/sysctl.conf
,按照下述参数进行配置。修改后使用sysctl -p
生效, 使用sysctl -a
查看当前配置。
1 | # 操作系统可以打开的最大文件数 |
2.) 编辑/etc/security/limits.conf
,按照下述参数进行配置。重新打开会话会生效。
1 | # 修改用户进程可以打开的最大文件数 |
3.2.3 tcp_nodelay
参考上文中的 Nagle 算法:tcp 希望能够攒一批数据后进行发送,而不是来了数据立即就发送。
如果 tcp_nodelay 配置为 true,则表示数据立马发送。默认值是 false。
3.2.4 so_sndbuf & so_rcvbuf
即 sendbuffer 发送缓冲区和 recvbuffer 接收缓冲区。这个并不需要我们手动调整。