上一篇【第06篇】Netty第一个实战——Echo服务器的完整实现与源码解析下一篇【第08篇】LengthFieldBasedFrameDecoder——Netty最强帧解码器全攻略摘要如果您用Netty写过TCP服务器大概率遇到过这样的诡异现象客户端明明发了3条消息服务端却只收到1条粘包或者客户端发了一条长消息服务端却收到了2条半拆包。这不是Bug而是TCP的天性——TCP是流协议没有消息边界。本文详解TCP粘包/拆包产生的三大原因四种经典解决策略以及Netty内置的三种帧解码器的使用方法。读完这篇您将彻底解决这一网络编程的经典难题。一、TCP粘包/拆包——网络编程的第一坑1.1 什么是粘包/拆包【粘包/拆包现象图解】 客户端发送 消息1: Hello 消息2: World 消息3: ! 服务端可能收到的情况 情况1正常收到3条独立消息 ✅ 情况2粘包收到1条消息 HelloWorld! ❌3条粘在一起 情况3拆包收到2条消息 HelloW 和 orld! ❌第2条被拆开 情况4既粘又拆收到2条消息 HelloWorl 和 d!1.2 TCP为什么会有粘包/拆包问题TCP是流协议Stream Protocol数据是像水流一样连续的字节流没有消息边界。【TCP发送和接收的数据流】 发送端应用层 TCP发送缓冲区 网络IP包 TCP接收缓冲区 接收端应用层 │ │ │ │ │ │ write(Hello) │ │ │ │ │──────┐ │ │ │ │ │ │ │ │ │ │ │ write(World) │ │ │ │ │──────┤ │ │ │ │ │ │ │ │ │ │ │ write(!) │ │ │ │ │──────┘ │ │ │ │ │ │ │ │ │ v v v v v HelloWorld! HelloWorld! 可能被拆成多个IP包发送 接收端读到的也是这个流根本原因TCP只保证字节流的有序、可靠传输不保证每次读到的刚好是一条完整的应用层消息。1.3 粘包/拆包产生的三大原因原因说明发生场景发送方Nagle算法发送方把多个小包合并成一个大包发送发送频繁小消息时接收方TCP缓冲区接收方一次read可能读到多个消息消息短、发送快时IP层MTU限制一个TCP段太大IP层会拆成多个IP包消息超过MSS最大报文段时二、四种经典解决策略既然TCP不保证消息边界那只能靠应用层协议设计来解决。业界有四种经典策略【四种解决TCP粘包/拆包的策路】 方案1固定长度消息 └── 每条消息长度固定如100字节 优点简单 缺点浪费带宽短消息也要占固定长度 方案2分隔符 └── 用特殊分隔符标记消息结束如换行符\n 优点简单人类可读 缺点消息内容不能包含分隔符 方案3消息头消息体✅ 最常用 └── 消息头中包含消息体长度 格式| 消息头固定长度 | 消息体变长 | 优点通用高效 缺点需要设计协议 方案4特定字符集长度 └── 如JSON可以以{开头、}结尾来判断完整性 优点无需额外长度字段 缺点解析复杂性能差三、Netty的解决方案——帧解码器FrameDecoderNetty提供了多种帧解码器FrameDecoder它们在Pipeline中作为Inbound Handler把字节流拆分成完整的应用层消息。【Netty解决粘包/拆包的架构】 ByteBuf字节流可能有粘包/拆包 │ v ┌───────────────────────────────┐ │ FrameDecoder帧解码器 │ ← 解决粘包/拆包 │ Netty内置多种实现 │ └─────────────┬───────────────┘ │ 输出完整的消息对象 v ┌───────────────────────────────┐ │ 业务Handler │ │ 处理完整的消息 │ └───────────────────────────────┘四、LineBasedFrameDecoder——按换行符分割LineBasedFrameDecoder是最简单的帧解码器它按换行符\n或\r\n分割消息。4.1 使用方法// 在ChannelInitializer中配置pipeline.addLast(newLineBasedFrameDecoder(1024));// 最大行长度1024字节pipeline.addLast(newStringDecoder());// 将ByteBuf解码为Stringpipeline.addLast(newBusinessHandler());// 业务Handler4.2 工作原理【LineBasedFrameDecoder工作原理】 接收到的字节流 Hello\nWorld\nNice to meet you!\nSome more data │ v LineBasedFrameDecoder扫描换行符 │ v 输出3条完整消息 1. Hello 2. World 3. Nice to meet you! Some more data 还不完整暂存起来等待后续数据4.3 完整示例// 服务端使用LineBasedFrameDecoder解决粘包publicclassLineBasedServer{publicstaticvoidmain(String[]args)throwsException{EventLoopGroupbossGroupnewNioEventLoopGroup(1);EventLoopGroupworkerGroupnewNioEventLoopGroup();try{ServerBootstrapbnewServerBootstrap();b.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class).childHandler(newChannelInitializerSocketChannel(){OverrideprotectedvoidinitChannel(SocketChannelch){ChannelPipelinepch.pipeline();// ✅ 关键添加LineBasedFrameDecoderp.addLast(newLineBasedFrameDecoder(1024));// 将ByteBuf解码为Stringp.addLast(newStringDecoder());// 业务Handlerp.addLast(newLineBasedServerHandler());}});ChannelFuturefb.bind(8080).sync();System.out.println(LineBased服务器启动监听8080...);f.channel().closeFuture().sync();}finally{bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}}classLineBasedServerHandlerextendsChannelInboundHandlerAdapter{OverridepublicvoidchannelRead(ChannelHandlerContextctx,Objectmsg){Stringmessage(String)msg;System.out.println(收到完整消息message);ctx.writeAndFlush(收到message\n);}}客户端测试用telnet$ telnet localhost8080Hello World Nice to meet you!服务端将正确收到3条独立消息不会被粘包。五、DelimiterBasedFrameDecoder——自定义分隔符如果您的协议不是用换行符分割而是用其他分隔符如|、#、$$$等可以用DelimiterBasedFrameDecoder。5.1 使用方法// 定义分隔符例如用 $$$ 作为消息结束标记ByteBufdelimiterUnpooled.copiedBuffer($$$.getBytes());pipeline.addLast(newDelimiterBasedFrameDecoder(1024,delimiter));pipeline.addLast(newStringDecoder());pipeline.addLast(newBusinessHandler());5.2 多个分隔符// 支持多个分隔符先匹配到的生效ByteBufdelimiter1Unpooled.copiedBuffer(\n.getBytes());ByteBufdelimiter2Unpooled.copiedBuffer($$$.getBytes());pipeline.addLast(newDelimiterBasedFrameDecoder(1024,delimiter1,delimiter2));六、FixedLengthFrameDecoder——定长消息如果您的协议每条消息长度固定可以用FixedLengthFrameDecoder。6.1 使用方法// 每条消息固定20字节pipeline.addLast(newFixedLengthFrameDecoder(20));pipeline.addLast(newStringDecoder());pipeline.addLast(newBusinessHandler());6.2 适用场景场景是否适合FixedLengthFrameDecoder固定格式报文如银行卡号固定16位✅ 适合变长消息❌ 不适合浪费带宽或消息被截断二进制协议⚠️ 需确保每条消息长度完全相同七、三种解码器的对比与选型【Netty三种内置帧解码器对比】 解码器 消息格式 优点 缺点 适用场景 ──────────────────────────────────────────────────────────────────────────────────── LineBasedFrameDecoder 按\n或\r\n分割 简单人类可读 不能包含\n 文本协议如HTTP请求行 DelimiterBasedFrameDecoder 按自定义分隔符分割 灵活可自定义 分隔符不能出现在 自定义文本协议 消息内容中 FixedLengthFrameDecoder 固定长度 高效无额外开销 浪费带宽 固定格式二进制协议最重要的建议生产环境中最推荐用消息头消息体协议第008篇详解因为它最通用、最高效。上面三种解码器更适合简单的文本协议或学习场景。八、没有现成解码器怎么办——自定义FrameDecoder如果您的协议比较复杂例如消息头消息体格式上面三种解码器都不合适可以自定义FrameDecoder。8.1 继承ByteToMessageDecoder/** * 自定义帧解码器消息格式为 [长度(4字节)][消息内容] */publicclassMyFrameDecoderextendsByteToMessageDecoder{Overrideprotectedvoiddecode(ChannelHandlerContextctx,ByteBufin,ListObjectout){// 1. 检查是否有足够的字节读取消息长度需要4字节if(in.readableBytes()4){return;// 数据不够等待更多数据}// 2. 标记当前读取位置如果消息体不完整要回滚in.markReaderIndex();// 3. 读取消息长度前4字节intlengthin.readInt();// 4. 检查是否有完整的消息体if(in.readableBytes()length){// 消息体不完整回滚读取位置等待更多数据in.resetReaderIndex();return;}// 5. 读取完整的消息体ByteBufbodyin.readBytes(length);out.add(body);// 交给下一个Handler}}8.2 在Pipeline中使用pipeline.addLast(newMyFrameDecoder());// 自定义帧解码器pipeline.addLast(newMyMessageDecoder());// 将ByteBuf解码为消息对象pipeline.addLast(newBusinessHandler());// 业务Handler九、常见错误与排查错误1TooLongFrameException现象io.netty.handler.codec.TooLongFrameException: Frame length exceeds ...原因接收到的消息超过了帧解码器设置的最大长度解决增大帧解码器的最大长度参数或检查发送方是否发送了超长消息错误2收到不完整的消息现象Handler中收到的消息总是缺斤短两原因没有配置帧解码器或者帧解码器配置错误解决确保在业务Handler之前配置了正确的帧解码器错误3分隔符冲突现象消息内容中包含分隔符导致消息被错误拆分原因协议设计问题分隔符出现在消息内容中解决改用消息头消息体协议或对消息内容做转义处理总结TCP粘包/拆包的根本原因TCP是流协议没有消息边界四大解决策略固定长度、分隔符、消息头消息体、特定字符集Netty内置三种帧解码器LineBasedFrameDecoder换行分割、DelimiterBasedFrameDecoder自定义分隔符、FixedLengthFrameDecoder定长生产推荐用消息头消息体协议 LengthFieldBasedFrameDecoder第008篇详解自定义解码器继承ByteToMessageDecoder在decode()方法中实现解析逻辑下一步深入学习LengthFieldBasedFrameDecoder——Netty最强、最通用的帧解码器第08篇上一篇【第06篇】Netty第一个实战——Echo服务器的完整实现与源码解析下一篇【第08篇】LengthFieldBasedFrameDecoder——Netty最强帧解码器全攻略