netty的粘包和拆包是很重要的一部分,粘包分如图中的几种情况
假设客户端分别发送了两个数据包,D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下情况:
服务端分别收到了D1和D2,没有粘包和拆包
服务端一次性收到了D1和D2,称为TCP粘包
服务端两次读取到了两个数据包,第一次读到了D1的完整部分和D2的部分数据,第二次读到了D2的剩余部分。 这称为TCP拆包
服务端两次读取到两个数据包,第一次是D1的部分,第二次是D1的剩余部分和D2的完整部分
也有可能D1和D2非常大,期间发生多次拆包。
netty有很多自带的拆包封装类,比如LineBasedFrameDecoder等,但是最多的还是根据自己的帧格式自定义协议来进行拆包,我们拿GB/T18657.1—2002的6.2.4条FT1.2异步式传输帧格式来举例。
格式如图
应对这种比较难的帧格式就不能用自带的拆包类,而要根据帧格式自定义解码器。
先判断枕头,再略过两字节(长度)再判断帧中间标识,再读帧尾标识。
具体代码如下
@Slf4j
public class GateWayDecoder extends ByteToMessageDecoder{
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
//bytebuf gc出错怎么解决
log.info("收到报文");
if(byteBuf == null){
log.info("收到的报文为空");
return;
}
//判断是否满足字节最小数目
if(byteBuf.readableBytes() < ProtocolConstant.FRAME_SIZE_MIN){
log.info("报文不够最小数目");
return;
}
int beginReader;
while (true) {
// 获取包头开始的index
beginReader = byteBuf.readerIndex();
// 标记包头开始的index
byteBuf.markReaderIndex();
// 读到了协议的开始标志,结束while循环
if (byteBuf.readByte() == ProtocolConstant.FRAME_HEAD_FLAG ) {
byte[] lengthBytes1 = new byte[2];
lengthBytes1[0] = byteBuf.readByte();
lengthBytes1[1] = byteBuf.readByte();
int length1 = FrameCommUtil.bytes2Int(lengthBytes1,0, lengthBytes1.length);//读取长度
if(byteBuf.readableBytes() < length1 + 3){//报文不够最小数目
log.info("报文不够最小数目3");
byteBuf.resetReaderIndex();
return;
}
if(byteBuf.readByte() == ProtocolConstant.FRAME_MIDDLE_FLAG) {//如果跳过长度字节后一个为标志位
byteBuf.readBytes(length1);//跳过数据区
byteBuf.readByte();//跳过校验和
if(byteBuf.readByte() == ProtocolConstant.FRAME_TAIL_FLAG){
byteBuf.resetReaderIndex();//还原到包头位置
byteBuf.readByte();
break;
}
}
}
// 未读到包头,略过一个字节
// 每次略过,一个字节,去读取,包头信息的开始标记
byteBuf.resetReaderIndex();
byteBuf.readByte();
// 当略过,一个字节之后,
// 数据包的长度,又变得不满足
// 此时,应该结束。等待后面的数据到达
if (byteBuf.readableBytes() < ProtocolConstant.FRAME_SIZE_MIN) {
log.info("剩余字节数小于帧最小满足数1");
return;
}
}
log.info("O Length:" + (byteBuf.readableBytes() + 1));
//读取消息长度 两字节
byte[] lengthBytes = new byte[2];
lengthBytes[0] = byteBuf.readByte();
lengthBytes[1] = byteBuf.readByte();
int length = FrameCommUtil.bytes2Int(lengthBytes,0, lengthBytes.length);
if (byteBuf.readableBytes() < length + 3) {//如果剩下可读的信息长度小于帧所需要的长度 +3的意思是校验和和尾帧标识和中间标识的字节数
// 还原读指针
byteBuf.readerIndex(beginReader);//还原到帧头开始位置
log.info("剩余字节数小于帧最小满足数2");
return;
}
byte middle = byteBuf.readByte();//读取中间标志位
byte[] dataBytes = new byte[length];//创造一个长度为length的容纳数据的数组
try {
byteBuf.readBytes(dataBytes);//将数据写入数组
}catch (Exception e){
e.printStackTrace();
return;
}
byte check = byteBuf.readByte();//读取校验和
byte tail = byteBuf.readByte();//读取尾部标识
ByteBuffer frameBytes = ByteBuffer.allocate(6 + length);//6+length的意思是数据字节总数+标志位+校验和
frameBytes.put(ProtocolConstant.FRAME_HEAD_FLAG);//放入头部标识
frameBytes.put(FrameCommUtil.int2Bytes(length), 0, ProtocolConstant.FRAME_LENGTH_BYTESIZE);//放入长度
frameBytes.put(ProtocolConstant.FRAME_MIDDLE_FLAG);//放入中间标识
frameBytes.put(dataBytes);//放入数据
frameBytes.put(check);//放入校验和
frameBytes.put(ProtocolConstant.FRAME_TAIL_FLAG);//放入尾部标识
byte[] bytesData = frameBytes.array();//转换为数组
log.info("A Length:" + bytesData.length);
Frame frame = FrameUtil.parse(bytesData);//转换为帧实体类
if (frame != null){
list.add(frame);//交给vastlistener解析
}
System.gc();
}
}