Netty 粘包/拆包应用案例及解决方案分析
熟悉TCP变成的可以知道,无论是客户端还是服务端,但我们读取或者发送消息的时候,都需要考虑TCP底层粘包/拆包机制,下面我们先看一下TCP 粘包/拆包和基础知识,然后模拟一个没有考虑TCP粘包/拆包导致功能异常的案例,最后,通过正确的例程来谈谈Netty是如何实现的。
主要内容:
-
TCP粘包/拆包的基础知识
-
没考虑TCP粘包/拆包的问题案例
-
使用Netty解决读半包问题
1、TCP粘包/拆包
TCP是个“流“协议,所谓流,就是没有界限的一串数据。TCP底层并不知道上层业务逻辑,它会根据TCP缓冲区的实际情况进行包的拆分,所以在业务上认为,一个完整的包可能会被拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包/拆包的问题。
2、TCP粘包/拆包发生的原因
问题产生的原因有三个:如下
-
应用程序write写入的字节大小大于套接口发送缓冲区大小;
-
进行MSS大小的分段;
-
以太网帧的payload大于MTU进行IP分片;
备注:mtu是网络传输最大报文包。mss是网络传输数据最大值。
3、粘包问题的解决策略
由于底层TCP无法理解上层业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,可以归纳如下:
-
消息定长,例如每个报文的大小长度200字节,如果不够,不空格;
-
在包尾增加回车换行符,例如FTP协议;
-
将消息分为消息头和消息体,消息头包含表示消息总长度的字段,通常设计思路为消息头的第一个字段使用int32来表示消息的总长度;
-
更复杂的设计协议;
介绍完了TCP粘包/拆包的基础知识后,我们看一下Netty是如何解决半包问题的,是如何使用Netty的半包解码器来解决TCP粘包/拆包问题。
4、未考虑TCP粘包/拆包问题出现的功能异常
TimeServer的改造(可以查看上一篇文章中的netty客户端-服务端的实现):
每读到一条消息后,就计数一次,然后发送应答消息给服务端。
TimeClient端的改造:
运行结果(服务端接收指令):
The time server receive order : QUERY TIME ORDER
此处省略57行。。。。。。。
QUERY TIME ORD ; the counter is :1
The time server receive order :
此处省略43行。。。。。。。
QUERY TIME ORDER ; the counter is :2
运行结果(客户端接收响应):
Now is : BAD ORDER
BAD ORDER
; the counter is : 1
原因分析:服务端运行结果表明它只接收到两条消息,第一条包含57条“QUERY TIME ORDER”指令,第二天包含了43条指令,总数100条,我们期望的也是100条,但是计数只有两条,所有发生TCP粘包,按照设计初衷,客户端应该收到100响应,但实际上只收到了1条,不难理解,客户端也发生了粘包,一条应答消息中包含两条“BAD ORDER”指令的消息。
5、通过LineBasedFrameDecoder解决TCP粘包问题
为了解决TCP粘包/拆包导致的半包读写问题,Netty默认提供了多种编解码器用于处理半包,这是其他NIO框架和JDK原生的NIO API不能匹敌的。
直接上代码
TimeServer:
在原来的TimeServerHandler之前增加了两个解码器:LineBasedFrameDecoder、StringDecoder
TimeServerHandler:
TimeClient:
TimeClientHandler:
支持TCP粘包的运行结果:
服务端:
The time server receive order : QUERY TIME ORDER ; the counter is :1
此处省略92条。。。。。。
The time server receive order : QUERY TIME ORDER ; the counter is :100
客户端:
Now is : Tue Aug 21 15:15:21 CST 2018 ; the counter is : 1
此处省略92条。。。。。。
Now is : Tue Aug 21 15:15:21 CST 2018 ; the counter is : 100
6、LineBasedFrameDecoder、StringDecoder原来分析
LineBasedFrameDecoder的工作原理是它依次遍历ByteBuf中的可读字节,判断是否有“\n“或者“\r\n”,如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。它是以换行符为结束标记的解码器,
StringDecoder非常简单,就是将接收到的对象转换成字符串,然后继续调用后面的Handler,
总结:LineBasedFrameDecoder + StringDecoder组合就是按行切换的文本解码器,它被设计用来支持TCP的粘包、拆包。
疑问:
1、如果发送的消息不是以换行符结束的怎么办?
2、靠消息头中的长度字段来分包的怎么办?
这样的话是否需要自己写半包解码器,答案是否定的,Netty 提供了多种支持 TCP粘包、拆包的解码器,用来满足需求,下面的文章中会详细介绍《分隔符解码器》《定长解码器》,因为它在项目中使用非常广泛,所以单独去分享这一知识点。