之前学习Netty的时候学到自定义编解码器这一部分后就没再继续学习,同时对于这部分知识学习不深入。一直有个误区:自定义编码以及解码服务器就能够解决TCP作为流式协议传输(无消息边界)导致的粘包、半包问题。实则上面这句话有非常大的问题,今日再学习了粘包、半包以及自定义编解码器的关系,纠正已有部分错误认识。
之前写过一篇Netty中的粘包、半包以及另一篇自定义编解码器的文章:
Netty中半包粘包的产生与处理:短连接、固定长度、固定分隔符、预设长度;redis、http协议举例;网络数据的发送和接收过程_discarded inbound message pooledslicedbytebuf-CSDN博客
自定义协议编解码:Codec(encode\decode)及Sharble注解详解_自定义解码-CSDN博客
借此机会对上面内容进行补充以及纠正。
粘包现象以及产生原因
现象:发送 abc def,接收 abcdef。
原因:应用层、滑动窗口、Nagle算法。
应用层:接收方 ByteBuf 设置太大(Netty 默认 1024)
滑动窗口:假设发送方 256 bytes 表示一个完整报文,但由于接收方处理不及时且窗口大小足够大,这 256 bytes 字节就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文就会粘包
Nagle 算法:会造成粘包
Nagle算法:也称为延迟确认算法(Delayed Acknowledgment Algorithm)。
主要目的:减少因频繁发送小数据包而造成的网络拥塞。在TCP连接中,如果应用程序发送了一系列小的数据包,这些数据包可能会在网络中引起额外的开销。Nagle算法通过以下机制来优化数据传输:
实现方式:
数据合并:当应用程序连续发送小的数据包时,Nagle算法会将这些数据包合并为一个较大的数据包,然后一起发送。这样可以减少发送的数据包数量,从而减少网络拥塞。
拥塞窗口:TCP使用拥塞窗口(Congestion Window)来控制发送数据的速率。Nagle算法会根据拥塞窗口的大小来决定是否延迟发送数据包。
确认等待:当TCP接收到数据包时,它会发送一个确认(ACK)给发送方。Nagle算法会延迟发送这个确认,直到有足够的数据来填充一个最大大小的TCP段,或者直到一个特定的时间间隔过去。
-
即使发送一个字节,也需要加入 tcp 头和 ip 头,也就是总字节数会使用 41 bytes,非常不经济。因此为了提高网络利用率,tcp 希望尽可能发送足够大的数据,这就是 Nagle 算法产生的缘由
-
该算法是指发送端即使还有应该发送的数据,但如果这部分数据很少的话,则进行延迟发送
-
如果 SO_SNDBUF 的数据达到 MSS,则需要发送
-
如果 SO_SNDBUF 中含有 FIN(表示需要连接关闭)这时将剩余数据发送,再关闭
-
如果 TCP_NODELAY = true,则需要发送
-
已发送的数据都收到 ack 时,则需要发送
-
上述条件不满足,但发生超时(一般为 200ms)则需要发送
-
除上述情况,延迟发送
-
半包现象以及产生原因
现象:发送 abcdef,接收 abc def
原因:应用层、滑动窗口、MSS限制。
应用层:接收方 ByteBuf 小于实际发送数据量
滑动窗口:假设接收方的窗口只剩了 128 bytes,发送方的报文大小是 256 bytes,这时放不下了,只能先发送前 128 bytes,等待 ack 后才能发送剩余部分,这就造成了半包
MSS 限制:当发送的数据超过 MSS 限制后,会将数据切分发送,就会造成半包
MSS和MTU的关系:
MTU(maximum transmission unit):链路层对一次能够发送的最大数据有限制,这个限制称之为MTU,不同的链路设备的MTU值也不相同。
MSS(maximum segment size)最大段长度:它是MTU刨去tcp头和ip头后剩余能够作为数据传输的字节数。
-
ipv4 tcp 头占用 20 bytes,ip 头占用 20 bytes,因此以太网 MSS 的值为 1500 - 40 = 1460
-
TCP 在传递大量数据时,会按照 MSS 大小将数据进行分割发送
-
MSS 的值在三次握手时通知对方自己 MSS 的值,然后在两者之间选择一个小值作为 MSS
粘包、半包解决方案
- 短连接:能够很好解决粘包,对于半包解决不是很友好(当需要发送字节数超过能够发送最大字节数时就会产生半包)。
- 固定长度:如果发送数据达不到预设长度,需要填充特定字符,浪费带宽。
- 固定分隔符:对于消息中含有固定分隔符的消息不友好,无法识别这个分隔符是消息体还是消息边界。
- 预设长度:每一条消息包含head以及body,head中包含body的长度。
具体实现方案参考上面博客链接。
自定义编解码器和粘包、半包的关系
自定义编解码器就是上面提到的第四种实现方案(head[length] body),貌似感觉非常正确不存在问题。
对于粘包,自定义解码器能够很好处理,此时接收到的消息总量肯定大于需要解析的消息长度,此时不会产生问题。
对于半包,如果此时发送方一条消息由于某种原因划分为两次发送,此时第一次发送后接收到接收到消息后,首先解析出消息头中的长度,直接使用解析到的长度提取对应的字节数,然而此时并不能提取到对应长度的字节数,此时就会反序列化失败。并不能处理此种情况的半包。
测试代码:
下面红色方框中框住的内容即为通过Netty的slice将一条encode后的消息(Message->ByteBuf)划分为两个ByteBuf。如果此时没有上面方框框住的帧解码器,直接调用一次slice后获取到s1,就对此ByteBuf执行channel.writeInbound(s1),由于消息体中没有长度的字节,此时解码失败。
如果添加上第一个红色方框选中的 LengthFieldBasedFrameDecoder,按照上述方式执行两次slice后依次对于两个ByteBuf执行channel.writeInbound(),此时第一个执行成功后并不会立即解码,而是在第二个channel.writeInbound()执行成功后再解码。
LengthFieldBasedFrameDecoder:是Netty网络编程的一部分,属于解码器(Decode)的一部分,主要作用将接收到的字节流划分为独立的帧(frames),对于处理基于长度的协议(例如某些二进制协议或自定义文本协议)非常有用。
添加此组件后,当需要解码的消息长度没有达到指定长度时不会解码,会暂存在缓冲区中,继续等待消息的接收,当达到指定长度后才会将获取指定Length对应的字节数进行Decode。
LengthFieldBasedFrameDecoder详解
-
基于长度字段的帧解码:这个解码器假设每个消息(或帧)的开始部分包含一个或多个字节,这些字节指示了随后消息体的长度。例如,一个消息可能以一个32位整数开始,表示后续数据的长度。
-
自动帧拆分:
LengthFieldBasedFrameDecoder
会自动从接收到的字节流中拆分出完整的帧。它根据长度字段的值来确定帧的边界,并将完整的帧传递给下一个处理环节。 -
处理粘包和半包问题:在网络编程中,经常会遇到粘包(多个帧粘连在一起)和半包(一个帧被分成多次接收)的问题。
LengthFieldBasedFrameDecoder
通过长度字段来正确地拆分帧,从而解决了这些问题。、 -
可配置的长度字段:开发者可以指定长度字段的偏移量、长度,以及是否包含长度字段本身的长度。这使得它能够适应不同的协议设计。
-
灵活的解码策略:除了基本的长度字段解码,
LengthFieldBasedFrameDecoder
还支持一些高级特性,如调整最大帧长度、处理过长的帧等。