当前位置: 首页 > news >正文

Netty 编解码器学习记:从粘包拆包到自定义协议

Netty 编解码器学习记:从粘包拆包到自定义协议

这是一个小白的学习记录
边学边练,把踩过的坑都记下来


为啥要学编解码器?

学了线程模型,感觉 Netty 跑起来挺快的。但要想数据传输稳定,必须学会编解码器。

我一开始还纳闷,编解码器不就是序列化和反序列化吗?有啥好学的?后来才发现,Netty 的编解码器还真不简单,特别是粘包/拆包问题,不学还真搞不定。

一、编解码器是啥?

我理解的编解码器

编解码器就是把数据在网络传输格式和应用程序格式之间转换的工具。

  • 编码器(Encoder):把应用程序的数据转换成网络能传输的格式
  • 解码器(Decoder):把网络传输的格式转换成应用程序能理解的格式

二、常用编解码器

1. 字符串编解码器

这是最常用的,把字节数据转换成字符串。

ch.pipeline().addLast(newStringDecoder(StandardCharsets.UTF_8));ch.pipeline().addLast(newStringEncoder(StandardCharsets.UTF_8));

2. 基于长度的帧解码器

这个是解决粘包/拆包问题的神器。

LengthFieldBasedFrameDecoder
// 长度字段占 4 字节,位于消息开头LengthFieldBasedFrameDecoderdecoder=newLengthFieldBasedFrameDecoder(1024,// 最大帧长度0,// 长度字段偏移量4,// 长度字段长度0,// 长度调整值4// 跳过长度字段);
FixedLengthFrameDecoder

用于固定长度的消息。

// 每个消息固定 100 字节FixedLengthFrameDecoderdecoder=newFixedLengthFrameDecoder(100);
LineBasedFrameDecoder

用于基于换行符的消息。

// 最大行长度 1024 字节LineBasedFrameDecoderdecoder=newLineBasedFrameDecoder(1024);

3. 对象编解码器

用于传输 Java 对象。

ch.pipeline().addLast(newObjectDecoder(ClassResolvers.cacheDisabled(null)));ch.pipeline().addLast(newObjectEncoder());

4. Protobuf 编解码器

用于传输 Protobuf 对象,性能更好。

ch.pipeline().addLast(newProtobufDecoder(MyMessage.getDefaultInstance()));ch.pipeline().addLast(newProtobufEncoder());

三、粘包/拆包问题

什么是粘包/拆包?

TCP 是面向流的协议,数据会被分割或合并,导致:

  • 粘包:多个小数据包被合并成一个大数据包
  • 拆包:一个大数据包被分割成多个小数据包

我踩过的坑

刚开始写 Echo 服务器时,客户端发"Hello",服务端收到"HelloHelloHello",或者收到"Hel"、“lo”。我当时就懵了,这是啥情况?

后来查资料才明白,这就是粘包/拆包问题。

解决方案

1. 基于长度字段

在消息开头加一个长度字段,告诉接收方消息有多长。

// 编码器:写入长度和数据ch.pipeline().addLast(newMessageToByteEncoder<String>(){@Overrideprotectedvoidencode(ChannelHandlerContextctx,Stringmsg,ByteBufout)throwsException{byte[]data=msg.getBytes();out.writeInt(data.length);// 写入长度out.writeBytes(data);// 写入数据}});// 解码器:根据长度字段解析ch.pipeline().addLast(newLengthFieldBasedFrameDecoder(1024,0,4,0,4));
2. 基于分隔符

用特定的分隔符标记消息结束,比如换行符。

// 编码器:加换行符ch.pipeline().addLast(newMessageToByteEncoder<String>(){@Overrideprotectedvoidencode(ChannelHandlerContextctx,Stringmsg,ByteBufout)throwsException{out.writeBytes((msg+"\n").getBytes());}});// 解码器:按换行符分割ch.pipeline().addLast(newLineBasedFrameDecoder(1024));
3. 固定长度

每个消息长度固定,不够的填充。

// 编码器:填充空格ch.pipeline().addLast(newMessageToByteEncoder<String>(){@Overrideprotectedvoidencode(ChannelHandlerContextctx,Stringmsg,ByteBufout)throwsException{StringpaddedMsg=String.format("%-100s",msg);out.writeBytes(paddedMsg.getBytes());}});// 解码器:固定长度ch.pipeline().addLast(newFixedLengthFrameDecoder(100));

教训:粘包/拆包问题一定要处理,否则数据传输会乱套!

四、自定义编解码器

实现自定义 Decoder

publicclassCustomDecoderextendsByteToMessageDecoder{@Overrideprotectedvoiddecode(ChannelHandlerContextctx,ByteBufin,List<Object>out)throwsException{// 确保有足够的字节可读if(in.readableBytes()<4){return;// 不够,等更多数据}// 读取长度intlength=in.readInt();if(in.readableBytes()<length){in.resetReaderIndex();// 重置读索引return;}// 读取数据byte[]data=newbyte[length];in.readBytes(data);Stringmessage=newString(data);out.add(message);}}

实现自定义 Encoder

publicclassCustomEncoderextendsMessageToByteEncoder<String>{@Overrideprotectedvoidencode(ChannelHandlerContextctx,Stringmsg,ByteBufout)throwsException{byte[]data=msg.getBytes();out.writeInt(data.length);// 写入长度out.writeBytes(data);// 写入数据}}

五、实战:实现自定义协议

协议设计

我设计了一个简单的协议:

  • 魔数(4字节):0xCAFEBABE
  • 版本(1字节):1
  • 命令(1字节):1-登录,2-消息,3-退出
  • 长度(4字节):数据部分的长度
  • 数据(可变长度):具体业务数据

实现编解码器

解码器
publicclassCustomProtocolDecoderextendsByteToMessageDecoder{privatestaticfinalintMAGIC_NUMBER=0xCAFEBABE;privatestaticfinalintHEADER_LENGTH=4+1+1+4;// 魔数 + 版本 + 命令 + 长度@Overrideprotectedvoiddecode(ChannelHandlerContextctx,ByteBufin,List<Object>out)throwsException{// 检查是否有足够的字节if(in.readableBytes()<HEADER_LENGTH){return;}// 标记当前位置in.markReaderIndex();// 读取魔数intmagic=in.readInt();if(magic!=MAGIC_NUMBER){ctx.close();// 魔数不匹配,关闭连接return;}// 读取版本和命令byteversion=in.readByte();bytecommand=in.readByte();// 读取数据长度intlength=in.readInt();// 检查数据长度if(in.readableBytes()<length){in.resetReaderIndex();// 重置,等更多数据return;}// 读取数据byte[]data=newbyte[length];in.readBytes(data);// 构造消息CustomMessagemessage=newCustomMessage(version,command,data);out.add(message);}}
编码器
publicclassCustomProtocolEncoderextendsMessageToByteEncoder<CustomMessage>{privatestaticfinalintMAGIC_NUMBER=0xCAFEBABE;@Overrideprotectedvoidencode(ChannelHandlerContextctx,CustomMessagemsg,ByteBufout)throwsException{// 写入魔数out.writeInt(MAGIC_NUMBER);// 写入版本和命令out.writeByte(msg.getVersion());out.writeByte(msg.getCommand());// 写入数据长度out.writeInt(msg.getData().length);// 写入数据out.writeBytes(msg.getData());}}

使用编解码器

publicclassCustomProtocolServer{publicstaticvoidmain(String[]args)throwsException{EventLoopGroupbossGroup=newNioEventLoopGroup(1);EventLoopGroupworkerGroup=newNioEventLoopGroup();try{ServerBootstrapb=newServerBootstrap();b.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class).childHandler(newChannelInitializer<SocketChannel>(){@OverridepublicvoidinitChannel(SocketChannelch)throwsException{ch.pipeline().addLast(newCustomProtocolDecoder());ch.pipeline().addLast(newCustomProtocolEncoder());ch.pipeline().addLast(newCustomProtocolHandler());}});ChannelFuturef=b.bind(8080).sync();System.out.println("Server started on port 8080");f.channel().closeFuture().sync();}finally{bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}}

六、我踩过的坑

  1. 粘包/拆包:刚开始没处理,导致数据错乱
  2. 长度字段配置错误:LengthFieldBasedFrameDecoder 参数配置错了,导致解码失败
  3. 内存溢出:没设置最大帧长度,导致大消息撑爆内存
  4. 解码器顺序:编解码器顺序放错了,导致数据处理失败

七、编解码器最佳实践

  1. 选择合适的编解码器:根据协议类型选择
  2. 正确配置参数:特别是 LengthFieldBasedFrameDecoder 的参数
  3. 设置最大帧长度:防止内存溢出
  4. 处理边界情况:比如数据不完整、长度不合法
  5. 测试编解码器:多测试正常、边界、异常情况
  6. 避免耗时操作:编解码器里别做耗时的事

验证步骤

1. 测试粘包/拆包

// 客户端连续发送多条消息for(inti=0;i<10;i++){channel.writeAndFlush("Message "+i);}

预期结果:服务端收到 10 条完整的消息,没有粘在一起。

2. 测试自定义协议

// 发送登录命令CustomMessageloginMsg=newCustomMessage((byte)1,(byte)1,"user:admin,pwd:123".getBytes());channel.writeAndFlush(loginMsg);// 发送消息命令CustomMessagemsgMsg=newCustomMessage((byte)1,(byte)2,"Hello Netty".getBytes());channel.writeAndFlush(msgMsg);

预期结果:服务端正确解析出登录和消息命令。

总结

其实 Netty 的编解码器也没那么难,就是把数据在不同格式之间转换,还要处理粘包/拆包问题。我也是踩了几个坑才明白这些道理的。

现在我对编解码器有了点感觉,知道什么时候用什么编解码器,怎么处理粘包/拆包问题。但要真正掌握,还得继续练习。

肯定有理解不对的地方,欢迎大佬指正。

如果你也是新手,希望这篇笔记能帮到你。

http://www.jsqmd.com/news/669054/

相关文章:

  • JAVA语法合集之(六):活用数组
  • 2026年评价高的天津预应力混凝土屋面板品牌厂家推荐 - 品牌宣传支持者
  • 数据结构面试题避坑指南:别再被这些‘送分题’骗了(附详细解析)
  • 半马:机器人已超过人类
  • 终极指南:专业级AMD Ryzen调试工具SMUDebugTool深度解析与实战应用
  • 2026届必备的五大AI辅助论文助手解析与推荐
  • 项目实训(一)|中医智能诊疗系统后端基础架构搭建与环境配置
  • 2026年3月评价好的除铁器公司口碑推荐,电磁悬挂除铁器/全自动永磁悬挂除铁器/永磁筒磁选机/电磁铁,除铁器厂家有哪些 - 品牌推荐师
  • 协作的“语法”:多 Agent 系统的编排
  • 别只背课文了!用Python爬虫+AI工具,高效复习《新概念英语三》Lesson 16-20
  • 智能客服的终局:从关键词匹配到能够处理复杂售后的全能 Agent
  • python开发一款翻译工具
  • RAG流程详解
  • 2026年热门的室内安全体验馆稳定合作公司 - 品牌宣传支持者
  • SQL处理分组聚合中的数据一致性_使用事务保证
  • 不止于加载:在Cesium中优化ArcGIS WMTS地图服务的性能与视觉体验
  • 【AGI环境监测革命】:3大颠覆性应用、7类实时预警场景与2025碳中和落地路径
  • 从 0 到 1 构建销售 AI Agent Harness Engineering:线索生成、客户画像与转化预测实战
  • 别再为网络不通发愁了!手把手教你配置ARM与交换芯片的MAC直连模式
  • XML 与 CSS:构建现代网页的关键技术
  • Spark大数据分析实战【1.4】
  • Spring Boot项目里,别再用try-catch了!试试@ControllerAdvice+@ExceptionHandler搞定全局异常
  • ESP32开发环境搭建:手把手教你搞定Python依赖报错(ESP-IDF 4.x/5.x通用)
  • CentOS 7.9 保姆级教程:从零搭建IPFS私有节点,并配置WebUI可视化面板
  • 别再傻傻等编译了!手把手教你给Gradle配上本地+远程缓存,Android构建速度飞起
  • 从家庭路由器到云服务器:一次完整的Web请求,DNS、NAT和ICMP都扮演了什么角色?
  • 2026年热门的烟台沙滩赶海热门推荐 - 行业平台推荐
  • 从理论到实践:一维与二维水污染扩散模型的在线模拟与代码实现
  • AGI用户研究黄金三角模型(SITS2026首次发布|含实时仿真沙盒访问权限)
  • 别再只盯着协议了!手把手教你用示波器实测MIPI D-PHY的HS/LP模式切换波形