netty-半包&粘包
what?
在Netty发送和读取数据时,场所实际是ByteBuf缓冲区。 每一次发送就是向通道写入一个ByteBuf. 发送数据时先填好ByteBuf, 然后通过通道发送出去。 接收端每一次读取通过Handler业务处理的入站方法,从通道读到一个ByteBuf.
最为理想的情况下,发送端每发送一个ByteBuf缓冲区,接收端就能收到一个ByteBuf, 并且发送端和接收端的ByteBuf能够一模一样。
但在实际的情况下,可能存在三种类型的输出:
(1)读到一个完整的客户端输入ByteBuf
(2)读到多个客户端的ByteBuf输入,但是”粘“在了一起
(3)读到部分ByteBuf的内容,并且有乱码。
对于第一种,我们叫 全包; 第二种,叫 粘包; 第三种,叫半包。 为了简单起见,也可以将”粘包“问题看成特殊的”半包“。 可以把粘包和半包统称为半包问题。 粘包和半包指的都是不正常的 ByteBuf缓冲区接收。如下图所示
why?
底层网络是以二进制字节报文的形式来传输数据的。 读数据的过程大致为: 当IO可读时,Netty会从底层网络将二进制数据读到ByteBuf缓冲区中,再交给Netty程序转成Java POJO对象。 写数据的过程大致为:将一个Java类型的数据转换成底层能够传输的二进制ByteBuf缓冲数据。
在发送端Netty的应用层进程缓冲区,程序以ByteBuf为单位来发送数据,但是到了底层操作系统内核缓冲区,底层会按照协议的规范对数据包进行二次拼装,拼装成传输层TCP层的协议报文,再进行发送。 在接收端接收到传输层的二进制包后,首先保存在内核缓冲区,Netty读取ByteBuf时才复制到进程缓冲区。 那么问题就来了
(1)每次读取底层缓冲的数据容量是有限制的,当TCP底层缓冲的数据包比较大时,会将一个底层包分成多次ByteBuf进行复制,进而造成进程缓冲区读到的是半包。
(2)当TCP底层缓冲的数据包比较小时,一次复制的却不止一个内核缓冲区包,进行造成进程缓冲区读到的是粘包。
下图说明了内核缓冲区和进程缓冲区的关系。
how ?
解决思路,在接收端,Netty程序需要根据自定义协议,将读到的进程缓冲区ByteBuf, 在应用层进行二次拼装,重新组装我们应用层的数据包。这个过程也成为分包或者 拆包。
在Netty中,有两种分包方法。
(1)自定义解码器分包器。 基于ByteToMessageDecoder或者ReplayingDecoder,定义自己的进程缓冲区分包器。
(2)使用内置的解码器,使用LengthFileldBasedFrameDecoder自定义分隔符数据包解码器,进行正确分包。
当然前提是写入的数据都是有一定规则的,这样可以在接收端进行解码。
参考:
1 《Netty,redis,zookeeper高并发实践》