Netty应用之粘包拆包及编码解码

前言

为什么要把编码解码和粘包拆包放在一起呢?原因是粘包拆包的解决方案就是利用编码解码。
当 Netty 发送或者接受一个消息的时候,就将会发生一次数据转换。入站消息会被解码:从字节转换为另一种
格式(比如 java 对象);如果是出站消息,它会被编码成字节。

编码解码

这里简单说一些编码器解码器的接口

MessageToMessageEncoder对应MessageToMessageDecoder

MessageToByteEncoder对应ByteToMessageDecoder

上面一个是编码,一个是解码,继承该类后必须实现其方法,里面有参数msg(数据),out/in将对应的数据长度写进去

Netty应用之粘包拆包及编码解码
可以看到上面代码里面,MessageProtocol是一个实体类,里面有content和len两个属性,用来放长度和数据,入站的时候将长度写入
Netty应用之粘包拆包及编码解码
如下图,从in里面读取数据长度(这里的in,就是客户端中的out),获取长度,然后根据长度读取数据,这样一个个的数据包就分开了、什么是粘包,下面会说的。
这里用到ReplayingDecoder的原因是其可以附带一个返回值类型,其实这个ReplayingDecoder是ByteToMessageDecoder的子类
Netty应用之粘包拆包及编码解码
这里介绍一个常用的比较重要的解码器,ByteToMessageDecoder,因为一般用到tcp传输的,大部分传过来的都是byte字节码,
ByteToMessageDecoder是一个大类,下面还有很多其子类。

  1. FixedLengthFrameDecoder:定长协议解码器,我们可以指定固定的字节数算一个完整的报文
  2. LineBasedFrameDecoder:行分隔符解码器,遇到\n或者\r\n,则认为是一个完整的报文
  3. DelimiterBasedFrameDecoder:分隔符解码器,与LineBasedFrameDecoder类似,只不过分隔符可以自己指定
  4. LengthFieldBasedFrameDecoder:长度编码解码器,将报文划分为报文头/报文体,根据报文头中的Length字段确定报文体的长度,因此报文提的长度是可变的
  5. JsonObjectDecoder:json格式解码器,当检测到匹配数量的"{" 、”}”或”[””]”时,则认为是一个完整的json对象或者json数组。
    具体解析的话,可以看https://www.jianshu.com/p/75a0a79ba39e,文章比较长,但是讲的比较透彻

上面编码解码简单案例可以看这里https://github.com/Coderxiangyang/NettyExercise/tree/master/NettyCode

粘包的原因

  1. TCP 是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的 socket,因此,发送端为了将多个发给接收端的包,更有效的发给对方,使用了优化方法(Nagle 算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样做虽然提高了效率,但是接收端就难于分辨出完整的数据包了,因为面向流的通信是无消息保护边界的
    2、由于 TCP 无消息保护边界, 需要在接收端处理消息边界问题,也就是我们所说的粘包、拆包问题,图解:
    Netty应用之粘包拆包及编码解码

说明

设客户端分别发送了两个数据包 D1 和 D2 给服务端,由于服务端一次读取到字节数是不确定的,故可能存在以
下四种情况:

  1. 服务端分两次读取到了两个独立的数据包,分别是 D1 和 D2,没有粘包和拆包
  2. 服务端一次接受到了两个数据包,D1 和 D2 粘合在一起,称之为 TCP 粘包
  3. 服务端分两次读取到了数据包,第一次读取到了完整的 D1 包和 D2 包的部分内容,第二次读取到了 D2 包的剩余内容,这称之为 TCP 拆包
  4. 服务端分两次读取到了数据包,第一次读取到了 D1 包的部分内容 D1_1,第二次读取到了 D1 包的剩余部
    分内容 D1_2 和完整的 D2 包。

解决方案

其他几个解码器都比较简单,无非就是定长,分隔符,分行什么的。不再说,比较麻烦的也是最常用的是LengthFieldBasedFrameDecoder,长度编码解码器,将报文划分为报文头/报文体,根据报文头中的Length字段确定报文体的长度,因此报文提的长度是可变的,这个通常是设备将信息上传,除了数据,还有报文头,版本,id,操作类型等到数据,这里直接使用LengthFieldBasedFrameDecoder,里面有7个参数可以设置,

  1. byteOrder 字节排序顺序,LITTLE_ENDIAN:将低序字节存储在起始地址(低位编址),BIG_ENDIAN:将高序字节存储在起始地址(高位编址)
  2. maxFrameLength:指定了每个包所能传递的最大数据包大小;.
  3. lengthFieldOffset:指定了长度字段在字节码中的偏移量;
  4. lengthFieldLength:指定了长度字段所占用的字节长度;
  5. lengthAdjustment:对一些不仅包含有消息头和消息体的数据进行消息头的长度的调整,这样就可以只得到消息体的数据,这里的lengthAdjustment指定的就是消息头的长度;
  6. initialBytesToStrip:对于长度字段在消息头中间的情况,可以通过initialBytesToStrip忽略掉消息头以及长度字段占用的字节。
  7. failFast 为true的话,则当解码器到帧的长度将超过maxFrameLength时,将抛出TooLongFrameException,反之则读完整个帧再抛出。

如果不太理解这些参数的用法,特别是第5、6个,不好理解,可以参考这篇文章https://www.cnblogs.com/crazymakercircle/p/10294745.html

客户端编码,采用的是自定义的编码

Netty应用之粘包拆包及编码解码
服务端解码:使用LengthFieldBasedFrameDecoder
Netty应用之粘包拆包及编码解码