【Netty源码解读和权威指南】第08篇:LengthFieldBasedFrameDecoder——Netty最强帧解码器全攻略 上一篇【第07篇】TCP粘包/拆包问题全解——Netty如何理直气壮地解决这一难题下一篇【第09篇】Netty编解码框架实战——Protobuf/JSON/自定义协议全覆盖摘要如果您要设计一个自定义的TCP协议大概率会采用消息头消息体的格式——消息头中的某个字段表示消息体的长度。Netty专为这种协议设计了LengthFieldBasedFrameDecoder它是Netty中最通用、最强的帧解码器。但很多开发者被它的六个构造参数搞得头秃maxFrameLength、lengthFieldOffset、lengthFieldLength、lengthAdjustment、initialBytesToStrip、failFast。本文用大量图解和实战代码把每个参数的含义和配置方法讲得明明白白并给出五种典型报文格式的完整配置案例。一、为什么需要LengthFieldBasedFrameDecoder1.1 消息头消息体协议的通用性【典型自定义协议格式】 --------------------------------------- | 消息头固定长度 | 消息体变长 | --------------------------------------- | 魔数(4) | 版本(1) | 长度(4) | 消息内容 | ---------------------------------------这种格式的优势✅ 通用几乎所有自定义TCP协议都采用✅ 高效不需要分隔符没有转义问题✅ 完整消息头可以携带元数据消息类型、压缩方式等1.2 没有LengthFieldBasedFrameDecoder时的痛苦// ❌ 自己实现消息头消息体解析复杂且容易出错publicclassMyDecoderextendsByteToMessageDecoder{Overrideprotectedvoiddecode(ChannelHandlerContextctx,ByteBufin,ListObjectout){// 1. 检查是否有足够的字节读取消息头if(in.readableBytes()8){// 假设消息头8字节return;// 数据不够等待更多数据}// 2. 标记当前位置in.markReaderIndex();// 3. 跳过魔数和版本假设前5字节是魔数版本in.skipBytes(5);// 4. 读取长度字段intlengthin.readInt();// 假设长度字段是4字节int// 5. 检查是否有完整的消息体if(in.readableBytes()length){in.resetReaderIndex();// 回滚return;}// 6. 读取消息体ByteBufbodyin.readBytes(length);out.add(body);}}痛点自己实现容易出错忘记回滚、长度字段位置搞错、大消息攻击等而LengthFieldBasedFrameDecoder已经帮您处理了所有边界情况。二、六大参数详解——逐个搞懂LengthFieldBasedFrameDecoder有六个核心参数理解它们是使用这个解码器的关键。【LengthFieldBasedFrameDecoder参数图解】 接收到的字节流示例 --------------------------------------- | 魔数(4) | 版本(1) | 长度(4) | 消息体 | --------------------------------------- |← 0x12345678 →|← 0x01 →|← 0x0000000C →|← HelloWorld →| --------------------------------------- | | | | | v v v v v offset0 skip 5 lengthField lengthField body Offset5 Length4 content2.1 maxFrameLength——最大帧长度// 参数1maxFrameLength最大帧长度// 含义允许的最大消息长度字节超过此长度会抛出TooLongFrameException// 默认值无构造时必须指定// 建议值根据您的协议设计通常设置为1024*10241MB或更大newLengthFieldBasedFrameDecoder(1024*1024,// maxFrameLength 1MB...// 其他参数);安全建议一定要设置合理的maxFrameLength防止客户端发送超大消息导致服务端内存耗尽DoS攻击。2.2 lengthFieldOffset——长度字段的偏移量// 参数2lengthFieldOffset长度字段的偏移量// 含义从消息开始到第几个字节是长度字段// 示例如果消息头前5个字节是魔数(4)版本(1)第6-9字节是长度字段// 那么 lengthFieldOffset 5newLengthFieldBasedFrameDecoder(1024*1024,5,// lengthFieldOffset 5跳过前5个字节...// 其他参数);2.3 lengthFieldLength——长度字段的字节数// 参数3lengthFieldLength长度字段占用的字节数// 含义长度字段本身占几个字节1/2/4/8// 常见值// - 1长度范围是0-255适合小消息// - 2长度范围是0-65535适合中等消息// - 4长度范围是0-2G最常用int类型// - 8长度范围是0-9EB巨大消息long类型newLengthFieldBasedFrameDecoder(1024*1024,5,// lengthFieldOffset4,// lengthFieldLength 4长度字段是4字节int...// 其他参数);2.4 lengthAdjustment——长度修正值// 参数4lengthAdjustment长度修正值// 含义消息体长度 长度字段的值 lengthAdjustment// 使用场景如果长度字段的值不包括消息头需要加上消息头长度// 示例1长度字段的值 消息体长度不需要修正// 长度字段12消息体HelloWorld12字节→ lengthAdjustment0// 示例2长度字段的值 整个消息长度包括消息头// 长度字段17消息头5字节消息体12字节 → lengthAdjustment-5newLengthFieldBasedFrameDecoder(1024*1024,5,// lengthFieldOffset4,// lengthFieldLength0,// lengthAdjustment 0长度字段的值就是消息体长度...// 其他参数);2.5 initialBytesToStrip——跳过字节数// 参数5initialBytesToStrip解码后跳过的前几个字节// 含义解码后从完整帧中跳过前几个字节通常跳过消息头只保留消息体// 示例如果希望Handler只收到消息体不含消息头则设置为消息头长度newLengthFieldBasedFrameDecoder(1024*1024,5,// lengthFieldOffset4,// lengthFieldLength0,// lengthAdjustment9// initialBytesToStrip 9跳过魔数4版本1长度4 9字节的消息头);2.6 failFast——是否快速失败// 参数6failFast是否快速失败// 含义如果设置为true一旦检测到消息超过maxFrameLength立刻抛出TooLongFrameException// 如果设置为false会等到整个消息接收完才抛出异常// 默认值true推荐newLengthFieldBasedFrameDecoder(1024*1024,5,// lengthFieldOffset4,// lengthFieldLength0,// lengthAdjustment9,// initialBytesToStriptrue// failFast true快速失败);三、五种典型报文格式配置案例案例1长度字段在消息最前面最简单【报文格式】 ------------------ | 长度(2) | 消息体 | ------------------ | 0x000C | HelloWorld | ------------------// 配置// lengthFieldOffset 0 长度字段从开头开始// lengthFieldLength 2 长度字段占2字节// lengthAdjustment 0 长度字段的值就是消息体长度// initialBytesToStrip 0 不跳过任何字节Handler收到长度消息体newLengthFieldBasedFrameDecoder(1024*1024,0,2,0,0);案例2长度字段在消息头中间常用【报文格式】 -------------------------------- | 魔数(4) | 版本(1) | 长度(4) | 消息体 | -------------------------------- | 0x1234 | 0x01 | 0x000C | HelloWorld | --------------------------------// 配置// lengthFieldOffset 5 跳过魔数4版本1 5字节// lengthFieldLength 4 长度字段占4字节// lengthAdjustment 0 长度字段的值就是消息体长度// initialBytesToStrip 9 跳过魔数4版本1长度4 9字节只保留消息体newLengthFieldBasedFrameDecoder(1024*1024,5,4,0,9);案例3长度字段表示整个消息长度包括消息头【报文格式】 -------------------------------- | 魔数(4) | 版本(1) | 长度(4) | 消息体 | -------------------------------- | 固定值 | 固定值 | 0x0011 | HelloWorld | -------------------------------- 长度字段17包括消息头9字节消息体12字节// 配置// lengthFieldOffset 5// lengthFieldLength 4// lengthAdjustment -9 长度字段的值 - 9 消息体长度所以修正值为-9// initialBytesToStrip 9 跳过消息头newLengthFieldBasedFrameDecoder(1024*1024,5,4,-9,9);案例4长度字段不包括消息头但希望保留消息头【需求】Handler收到的消息包含消息头消息体不跳过消息头// 配置// lengthFieldOffset 5// lengthFieldLength 4// lengthAdjustment 0// initialBytesToStrip 0 不跳过任何字节Handler收到完整消息newLengthFieldBasedFrameDecoder(1024*1024,5,4,0,0);案例5超长消息长度字段用8字节long// 如果消息可能超过2GB虽然很少见用8字节long表示长度newLengthFieldBasedFrameDecoder(1024*1024*1024,// maxFrameLength 1GB0,// lengthFieldOffset8,// lengthFieldLength 8long类型0,// lengthAdjustment8// initialBytesToStrip 8跳过长度字段);四、LengthFieldPrepender——编码器配套使用有解码器自然要有编码器。LengthFieldPrepender是LengthFieldBasedFrameDecoder的配套编码器它在发送消息时自动在消息体前面加上长度字段。4.1 使用方法// 服务端和客户端的Pipeline中都要配置pipeline.addLast(newLengthFieldBasedFrameDecoder(1024*1024,5,4,0,9));pipeline.addLast(newLengthFieldPrepender(4));// ✅ 编码器在消息体前加4字节长度字段pipeline.addLast(newMyMessageDecoder());// 将ByteBuf解码为消息对象pipeline.addLast(newBusinessHandler());// 业务Handler4.2 LengthFieldPrepender的参数// 构造方法publicLengthFieldPrepender(intlengthFieldLength,// 长度字段占用的字节数1/2/4/8booleanincludeLengthFieldInLength// 长度字段的值是否包括自身长度);// 示例newLengthFieldPrepender(4,false);// 长度字段占4字节// 长度字段的值 消息体长度不包括长度字段自身// 如果消息体是HelloWorld12字节则发送// [0x00 0x00 0x00 0x0C] [0x48 0x65 0x6C 0x6C 0x6F 0x57 0x6F 0x72 0x6C 0x64]// 长度字段12 消息体HelloWorld五、完整实战——自定义协议通信5.1 协议设计【自定义协议格式】 -------------------------------------------------- | 魔数(4B) | 版本(1B) | 类型(1B) | 长度(4B) | 消息体(NB) | -------------------------------------------------- | 0x12345678 | 0x01 | 0x01 | 0x0000000C | HelloWorld | -------------------------------------------------- 字段说明 - 魔数固定值0x12345678用于快速识别协议 - 版本协议版本号当前为1 - 类型消息类型1请求2响应 - 长度消息体长度不包括消息头 - 消息体实际消息内容5.2 服务端配置// ServerBootstrap配置ServerBootstrapbnewServerBootstrap();b.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class).childHandler(newChannelInitializerSocketChannel(){OverrideprotectedvoidinitChannel(SocketChannelch){ChannelPipelinepch.pipeline();// 1. 帧解码器解决粘包/拆包// 参数maxFrameLength1MB, lengthFieldOffset6, lengthFieldLength4,// lengthAdjustment0, initialBytesToStrip10// 说明跳过魔数4版本1类型16字节长度字段占4字节// 解码后跳过整个消息头10字节只保留消息体p.addLast(newLengthFieldBasedFrameDecoder(1024*1024,6,4,0,10));// 2. 帧编码器发送时在消息体前加长度字段p.addLast(newLengthFieldPrepender(4));// 3. 消息解码器将ByteBuf解码为消息对象p.addLast(newMyMessageDecoder());// 4. 业务Handlerp.addLast(newMyBusinessHandler());}});5.3 消息对象定义// 消息对象publicclassMyMessage{privateintmagic;// 魔数privatebyteversion;// 版本privatebytetype;// 类型privateintlength;// 长度privatebyte[]body;// 消息体// getters setters...}5.4 消息解码器// 将完整帧已去掉消息头解码为消息对象publicclassMyMessageDecoderextendsByteToMessageDecoder{Overrideprotectedvoiddecode(ChannelHandlerContextctx,ByteBufin,ListObjectout){if(in.readableBytes()10){return;// 消息头不完整}intmagicin.readInt();byteversionin.readByte();bytetypein.readByte();intlengthin.readInt();if(in.readableBytes()length){in.resetReaderIndex();return;}byte[]bodynewbyte[length];in.readBytes(body);MyMessagemsgnewMyMessage();msg.setMagic(magic);msg.setVersion(version);msg.setType(type);msg.setLength(length);msg.setBody(body);out.add(msg);}}六、常见错误与排查错误1解码后消息不完整现象Handler收到的消息缺少消息头或消息体原因initialBytesToStrip配置错误解决根据实际需求调整initialBytesToStrip——如果希望Handler收到完整消息含消息头设为0如果只希望收到消息体设为消息头长度。错误2TooLongFrameException现象io.netty.handler.codec.TooLongFrameException: Adjusted frame length exceeds ...原因接收到的消息超过了maxFrameLength解决增大maxFrameLength或检查发送方是否发送了超长消息可能是攻击。错误3长度字段值不对现象解码后的消息体长度与预期不符原因lengthAdjustment配置错误或发送方填充长度字段时计算错误解决打印十六进制报文检查长度字段的值是否符合预期。总结LengthFieldBasedFrameDecoder是Netty最强帧解码器适用于所有消息头消息体格式的协议六大参数maxFrameLength最大帧长度、lengthFieldOffset长度字段偏移、lengthFieldLength长度字段字节数、lengthAdjustment长度修正、initialBytesToStrip跳过字节数、failFast快速失败配套编码器LengthFieldPrepender在发送时自动添加长度字段生产建议一定要设置合理的maxFrameLength防止DoS攻击下一步学习Netty的编解码框架第009篇——如何把字节流转换为Java对象上一篇【第07篇】TCP粘包/拆包问题全解——Netty如何理直气壮地解决这一难题下一篇【第09篇】Netty编解码框架实战——Protobuf/JSON/自定义协议全覆盖