之前我们有个netty5的demo(参加netty5自定义私有协议实例),里面有个NettyMessageDecoder类针对拆包的解码有问题,我们修复下这个bug:
package com.wlf.netty.nettyapi.msgpack; import com.wlf.netty.nettyapi.constant.Delimiter; import com.wlf.netty.nettyapi.javabean.Header; import com.wlf.netty.nettyapi.javabean.NettyMessage; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageDecoder; import java.util.List; public class NettyMessageDecoder extends ByteToMessageDecoder { /** * 消息体字节大小:分割符字段4字节+长度字段4字节+请求类型字典1字节+预留字段1字节=10字节 */ private static final int HEAD_LENGTH = 10; @Override protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception { while (true) { // 标记字节流开始位置 byteBuf.markReaderIndex(); // 若读取到分割标识,说明读取当前字节流开始位置了 if (byteBuf.readInt() == Delimiter.DELIMITER) { break; } // 重置读索引为0 byteBuf.resetReaderIndex(); // 长度校验,字节流长度至少10字节,小于10字节则等待下一次字节流过来 if (byteBuf.readableBytes() < HEAD_LENGTH) { byteBuf.resetReaderIndex(); return; } } // 2、获取data的字节流长度 int dataLength = byteBuf.readInt(); // 校验数据包是否全部发送过来,总字节流长度(此处读取的是除去delimiter和length之后的总长度)- // type和reserved两个字节=data的字节流长度 int totalLength = byteBuf.readableBytes(); if ((totalLength - 2) < dataLength) { // 长度校验,字节流长度少于数据包长度,说明数据包拆包了,等待下一次字节流过来 byteBuf.resetReaderIndex(); return; } // 3、请求类型 byte type = byteBuf.readByte(); // 4、预留字段 byte reserved = byteBuf.readByte(); // 5、数据包内容 byte[] data = null; if (dataLength > 0) { data = new byte[dataLength]; byteBuf.readBytes(data); } NettyMessage nettyMessage = new NettyMessage(); Header header = new Header(); header.setDelimiter(Delimiter.DELIMITER); header.setLength(dataLength); header.setType(type); header.setReserved(reserved); nettyMessage.setHeader(header); nettyMessage.setData(data); list.add(nettyMessage); // 回收已读字节 byteBuf.discardReadBytes(); } }
我们的改动很小,只不过将原来的读索引改为标记索引,然后在拆包时退出方法前重置读索引,这样下次数据包过来,我们的读索引依然从0开始,delimiter的标记就可以读出来,而不会陷入死循环了。
ByteBuf是ByteBuffer的进化版,ByteBuffer(参见ByteBuffer使用实例)才一个索引,读写模式需要通过flip来转换,而ByteBuf有两个索引,readerIndex读索引和writerIndex写索引,读写转换无缝连接,青出于蓝而胜于蓝:
+-------------------+------------------+------------------+
| discardable bytes | readable bytes | writable bytes |
| | (CONTENT) | |
+-------------------+------------------+------------------+
| | | |
0 <= readerIndex <= writerIndex <= capacity
既然有两个索引,那么标记mask、重置reset必然也是两两对应,上面的代码中我们只需要用到读标记和读重置。
详细过程看下debug,我们的大包里有3939116个字节,去掉10个字节的header,剩下小包是3939106:
第一次读1024,我们可以看到读索引是8(delimiter+length=8),写索引就是1024:
第二次再读1024,代码已经执行reset重置读索引了,所以读索引由8改为0,写索引累增到2048:
第三次再读1024,写索引继续累增到3072:
最后一次发1024,写索引已经到达3939116,大包传结束了: