Netty大白进阶教程
发布于2023-04-29 10:54:07,更新于2023-05-03 22:14:09,标签: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针对全局 |
短连接的做法,可以避免粘包,但拆包问题依然存在。
- 当发送方容量<接收方容量时,也就解决了粘包问题。因为我一个连接发完一条消息就结束了,所以只要接收方容量过大,就不会发生粘包。
- 当发送方容量>接收方容量时,拆包问题,仍然存在。