您现在的位置是:首页 > 学术指导
Netty: 何为拆包/粘包及Netty中对应的解决方案
研思启迪坊
2025-07-24【学术指导】279人已围观
简介拆包/粘包是在TCP协议中不得不提及的一个概念,也是必须解决的一个问题。本文由浅入深,带你揭开拆包/粘包的面纱,总结了常规的解决方案,并利用Netty的编解码器,在开发过程中,如何编写代码解决该问题,并在最后列举了Netty中开箱即用的解码器。希望通过本文,你不仅是弄清理论,更重要的是可以在项目中,...
拆包/粘包是在TCP协议中不得不提及的一个概念,也是必须解决的一个问题。本文由浅入深,带你揭开拆包/粘包的面纱,总结了常规的解决方案,并利用Netty的编解码器,在开发过程中,如何编写代码解决该问题,并在最后列举了Netty中开箱即用的解码器。希望通过本文,你不仅是弄清理论,更重要的是可以在项目中,根据需要定义协议并完成编解码。
为什么有拆包/粘包TCP是面向流,没有边界,而操作系统在发送TCP数据时,会通过缓冲区来进行优化,例如缓冲区为1024个字节大小。
如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送,这就形成了粘包问题。
如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是拆包。
为什么UDP没有粘包?
粘包拆包问题在数据链路层、网络层以及传输层都有可能发生。日常的网络应用开发大都在传输层进行,由于UDP有消息保护边界,不会发生粘包拆包问题,因此粘包拆包问题只发生在TCP协议中。
在客户端和服务端通信的过程中,服务端一次读到的数据大小是不确定的。如图所示,拆包/粘包可能会出现以下五种情况:
服务端恰巧读到了两个完整的数据包A和B,没有出现拆包/粘包问题;
服务端接收到A和B粘在一起的数据包,服务端需要解析出A和B;
服务端收到完整的A和B的一部分数据包B-1,服务端需要解析出完整的A,并等待读取完整的B数据包;
服务端接收到A的一部分数据包A-1,此时需要等待接收到完整的A数据包;
数据包A较大,服务端需要多次才可以接收完数据包A。
拆包/粘包解决方案-通信协议由于拆包/粘包问题的存在,数据接收方很难界定数据包的边界在哪里,很难识别出一个完整的数据包。所以需要提供一种机制来识别数据包的界限,这也是解决拆包/粘包的唯一方法:定义应用层的通信协议。
这里先看一下主流协议的解决方案:
(1)消息长度固定
每个数据报文都需要一个固定的长度。当接收方累计读取到固定长度的报文后,就认为已经获得一个完整的消息。当发送方的数据小于固定长度时,则需要空位补齐。
+----+------+------+---+----+|AB|CDEF|GHIJ|K|LM|+----+------+------+---+----+
假设我们的固定长度为4字节,那么如上所示的5条数据一共需要发送4个报文:
+------+------+------+------+|ABCD|EFGH|IJKL|M000|+------+------+------+------+
消息定长法使用非常简单,但是缺点也非常明显,无法很好设定固定长度的值,如果长度太大会造成字节浪费,长度太小又会影响消息传输,所以在一般情况下消息定长法不会被采用。
(2)特定分隔符
既然接收方无法区分消息的边界,那么我们可以在每次发送报文的尾部加上特定分隔符,接收方就可以根据特殊分隔符进行消息拆分。以下报文根据特定分隔符\n按行解析,即可得到AB、CDEF、GHIJ、K、LM五条原始报文。
+-------------------------+|AB\nCDEF\nGHIJ\nK\nLM\n|+-------------------------+
由于在发送报文时尾部需要添加特定分隔符,所以对于分隔符的选择一定要避免和消息体中字符相同,以免冲突。否则可能出现错误的消息拆分。比较推荐的做法是将消息进行编码,例如base64编码,然后可以选择64个编码字符之外的字符作为特定分隔符。特定分隔符法在消息协议足够简单的场景下比较高效,例如大名鼎鼎的Redis在通信过程中采用的就是换行分隔符。
(3)消息长度+消息内容
消息头消息体+--------+----------+|Length|Content|+--------+----------+
消息长度+消息内容是项目开发中最常用的一种协议,如上展示了该协议的基本格式。消息头中存放消息的总长度,例如使用4字节的int值记录消息的长度,消息体实际的二进制的字节数据。接收方在解析数据时,首先读取消息头的长度字段Len,然后紧接着读取长度为Len的字节数据,该数据即判定为一个完整的数据报文。依然以上述提到的原始字节数据为例,使用该协议进行编码后的结果如下所示:
+-----+-------+-------+----+-----+|2AB|4CDEF|4GHIJ|1K|2LM|+-----+-------+-------+----+-----+
消息长度+消息内容的使用方式非常灵活,且不会存在消息定长法和特定分隔符法的明显缺陷。当然在消息头中不仅只限于存放消息的长度,而且可以自定义其他必要的扩展字段,例如消息版本、算法类型等。
自定义协议通信目前市面上已经有不少通用的协议,例如HTTP、HTTPS、JSON-RPC、FTP、IMAP、Protobuf等。通用协议兼容性好,易于维护,各种异构系统之间可以实现无缝对接。如果在满足业务场景以及性能需求的前提下,推荐采用通用协议的方案。相比通用协议,自定义协议主要有以下优点。
极致性能:通用的通信协议考虑了很多兼容性的因素,必然在性能方面有所损失。
扩展性:自定义的协议相比通用协议更好扩展,可以更好地满足自己的业务需求。
安全性:通用协议是公开的,很多漏洞已经很多被黑客攻破。自定义协议更加安全,因为黑客需要先破解你的协议内容。
那么如何设计自定义的通信协议呢?结合实战经验我们一起看下一个完备的网络协议需要具备哪些基本要素。
(1)魔数
魔数是通信双方协商的一个暗号,通常采用固定的几个字节表示。魔数的作用是防止任何人随便向服务器的端口上发送数据。服务端在接收到数据时会解析出前几个固定字节的魔数,然后做正确性比对。如果和约定的魔数不匹配,则认为是非法数据,可以直接关闭连接或者采取其他措施以增强系统的安全防护。魔数的思想在压缩算法、JavaClass文件等场景中都有所体现,例如Class文件开头就存储了魔数0xCAFEBABE,在加载Class文件时首先会验证魔数的正确性。
(2)协议版本号
随着业务需求的变化,协议可能需要对结构或字段进行改动,不同版本的协议对应的解析方法也是不同的。所以在生产级项目中强烈建议预留协议版本号这个字段。
(3)序列化算法
序列化算法字段表示数据发送方应该采用何种方法将请求的对象转化为二进制,以及如何再将二进制转化为对象,如JSON、Hessian、Java自带序列化等。
(4)报文类型
在不同的业务场景中,报文可能存在不同的类型。例如在RPC框架中有请求、响应、心跳等类型的报文,在IM即时通信的场景中有登陆、创建群聊、发送消息、接收消息、退出群聊等类型的报文。
(5)长度域字段
长度域字段代表请求数据的长度,接收方根据长度域字段获取一个完整的报文。
(6)请求数据
请求数据通常为序列化之后得到的二进制流,每种请求数据的内容是不一样的。
(7)状态
状态字段用于标识请求是否正常。一般由被调用方设置。例如一次RPC调用失败,状态字段可被服务提供方设置为异常状态。
(8)保留字段
保留字段是可选项,为了应对协议升级的可能性,可以预留若干字节的保留字段,以备不时之需。
基于以上协议基本要素习,可以得到一个较为通用的协议示例:
+---------------------------------------------------------------+|魔数2byte|协议版本号1byte|序列化算法1byte|报文类型1byte|+---------------------------------------------------------------+|状态1byte|保留字段4byte|数据长度4byte|+---------------------------------------------------------------+|数据内容(长度不定)|+---------------------------------------------------------------+Netty如何实现自定义通信协议
在Netty中如何实现自定义的通信协议呢?其实Netty作为一个非常优秀的网络通信框架,已经为我们提供了非常丰富的编解码抽象基类,帮助我们更方便地基于这些抽象基类扩展实现自定义协议。
首先来看下Netty中编解码器是如何分类的。
Netty常用编码器类型:
MessageToByteEncoder对象编码成字节流;
MessageToMessageEncoder一种消息类型编码成另外一种消息类型。
Netty常用解码器类型:
ByteToMessageDecoder/ReplayingDecoder将字节流解码为消息对象;
MessageToMessageDecoder将一种消息类型解码为另外一种消息类型。
编解码器可以分为一次解码器和二次解码器,一次解码器用于解决TCP拆包/粘包问题,按协议解析后得到的字节数据。如果你需要对解析后的字节数据做对象模型的转换,这时候便需要用到二次解码器,同理编码器的过程是反过来的。
一次编解码器:MessageToByteEncoder/ByteToMessageDecoder。
二次编解码器:MessageToMessageEncoder/MessageToMessageDecoder。
接下来,针对前面的自定义通信协议,通过代码展示如何利用Netty的编解码框架实现该协议的解码器。
在实现协议编码器之前,首先需要清楚一个问题:如何判断ByteBuf是否存在完整的报文?最常用的做法就是通过读取消息长度dataLength进行判断。如果ByteBuf的可读数据长度小于dataLength,说明ByteBuf还不够获取一个完整的报文。在该协议前面的消息头部分包含了魔数、协议版本号、数据长度等固定字段,共14个字节。固定字段长度和数据长度可以作为我们判断消息完整性的依据,具体编码器实现逻辑示例如下:
/*+---------------------------------------------------------------+|魔数2byte|协议版本号1byte|序列化算法1byte|报文类型1byte|+---------------------------------------------------------------+|状态1byte|保留字段4byte|数据长度4byte|+---------------------------------------------------------------+|数据内容(长度不定)|+---------------------------------------------------------------+*/@Overridepublicfinalvoiddecode(ChannelHandlerContextctx,ByteBufin,ListObjectout){//判断ByteBuf可读取字节if(()14){return;}();//标记ByteBuf读指针位置(2);//跳过魔数(1);//跳过协议版本号byteserializeType=();(1);//跳过报文类型(1);//跳过状态字段(4);//跳过保留字段intdataLength=();//数据内容为达到定义的数据长度(即包不完整)-解决拆包if(()dataLength){();//重置ByteBuf读指针位置return;}//按照定义的长度读取长度-解决粘包byte[]data=newbyte[dataLength];(data);SerializeServiceserializeService=getSerializeServiceByType(serializeType);Objectobj=(data);if(obj!=null){(obj);}}Netty支持哪些常用的解码器?在Netty的框架中,也提供了很多开箱即用的解码器,这些解码器基本覆盖了TCP拆包/粘包的通用解决方案。接下来,对照TCP拆包/粘包的主流解决方案,梳理一下Netty对应的编码器类。
(1)固定长度解码器FixedLengthFrameDecoder
固定长度解码器FixedLengthFrameDecoder非常简单,直接通过构造函数设置固定长度的大小frameLength,无论接收方一次获取多大的数据,都会严格按照frameLength进行解码。如果累积读取到长度大小为frameLength的消息,那么解码器认为已经获取到了一个完整的消息。如果消息长度小于frameLength,FixedLengthFrameDecoder解码器会一直等后续数据包的到达,直至获得完整的消息。
通过一个例子感受一下使用Netty实现固定长度解码是多么简单。
publicclassEchoServer{publicvoidstartEchoServer(intport)throwsException{EventLoopGroupbossGroup=newNioEventLoopGroup();EventLoopGroupworkerGroup=newNioEventLoopGroup();try{ServerBootstrapb=newServerBootstrap();(bossGroup,workerGroup).channel().childHandler(newChannelInitializerSocketChannel(){@OverridepublicvoidinitChannel(SocketChannelch){().addLast(newFixedLengthFrameDecoder(10));().addLast(newEchoServerHandler());}});ChannelFuturef=(port).sync();().closeFuture().sync();}finally{();();}}publicstaticvoidmain(String[]args)throwsException{newEchoServer().startEchoServer(8088);}}@SharablepublicclassEchoServerHandlerextsChannelInboundHandlerAdapter{@OverridepublicvoidchannelRead(ChannelHandlerContextctx,Objectmsg){("Receiveclient:["+((ByteBuf)msg).toString(_8)+"]");}}在上述服务端的代码中使用了固定10字节的解码器,并在解码之后通过EchoServerHandler打印结果。启动服务端之后,通过telnet命令像服务端发送数据,观察代码输出的结果。
客户端输入:
telnetlocalhost8088Trying::1'^]'.1234567890123456789012
服务端输出:
Receiveclient:[1234567890]Receiveclient:[12345678]
(2)特殊分隔符解码器DelimiterBasedFrameDecoder
使用特殊分隔符解码器DelimiterBasedFrameDecoder之前我们需要了解以下几个属性的作用。
delimiters
delimiters指定特殊分隔符,通过写入ByteBuf作为参数传入。delimiters的类型是ByteBuf数组,所以我们可以同时指定多个分隔符,但是最终会选择长度最短的分隔符进行消息拆分。
例如接收方收到的数据为:
+--------------+|ABC\nDEF\r\n|+--------------+
如果指定的多个分隔符为\n和\r\n,DelimiterBasedFrameDecoder会退化成使用LineBasedFrameDecoder进行解析,那么会解码出两个消息。
+-----+-----+|ABC|DEF|+-----+-----+
如果指定的特定分隔符只有\r\n,那么只会解码出一个消息:
+----------+|ABC\nDEF|+----------+
maxLength
maxLength是报文最大长度的限制。如果超过maxLength还没有检测到指定分隔符,将会抛出TooLongFrameException。可以说maxLength是对程序在极端情况下的一种保护措施。
failFast
failFast与maxLength需要搭配使用,通过设置failFast可以控制抛出TooLongFrameException的时机,可以说Netty在细节上考虑得面面俱到。如果failFast=true,那么在超出maxLength会立即抛出TooLongFrameException,不再继续进行解码。如果failFast=false,那么会等到解码出一个完整的消息后才会抛出TooLongFrameException。
stripDelimiter
stripDelimiter的作用是判断解码后得到的消息是否去除分隔符。如果stripDelimiter=false,特定分隔符为\n,那么上述数据包解码出的结果为:
+-------+---------+|ABC\n|DEF\r\n|+-------+---------+
下面还是结合代码示例学习DelimiterBasedFrameDecoder的用法,依然以固定编码器小节中使用的代码为基础稍做改动,引入特殊分隔符解码器DelimiterBasedFrameDecoder:
(bossGroup,workerGroup).channel().childHandler(newChannelInitializerSocketChannel(){@OverridepublicvoidinitChannel(SocketChannelch){ByteBufdelimiter=("".getBytes());().addLast(newDelimiterBasedFrameDecoder(10,true,true,delimiter));().addLast(newEchoServerHandler());}});依然通过telnet模拟客户端发送数据,观察代码输出的结果,可以发现由于maxLength设置的只有10,所以在解析到第三个消息时抛出异常。
客户端输入:
telnetlocalhost8088Trying::1'^]'.helloworld1234567890ab
服务端输出:
Receiveclient:[hello]Receiveclient:[world]九月25,20208:46:01下午警告:AnexceptionCaught()eventwasfired,:framelengthexceeds10:13-(:302)(:268)(:218)
(3)长度域解码器LengthFieldBasedFrameDecoder
长度域解码器LengthFieldBasedFrameDecoder是解决TCP拆包/粘包问题最常用的解码器。它基本上可以覆盖大部分基于长度拆包场景,开源消息中间件RocketMQ就是使用LengthFieldBasedFrameDecoder进行解码的。LengthFieldBasedFrameDecoder相比FixedLengthFrameDecoder和DelimiterBasedFrameDecoder要复杂一些,接下来我们就一起学习下这个强大的解码器。
首先,同样先了解LengthFieldBasedFrameDecoder中的几个重要属性,这里我主要把它们分为两个部分:长度域解码器特有属性以及与其他解码器(如特定分隔符解码器)的相似的属性。
长度域解码器特有属性。
//长度字段的偏移量,也就是存放长度数据的起始位置privatefinalintlengthFieldOffset;//长度字段所占用的字节数privatefinalintlengthFieldLength;/**消息长度的修正值**在很多较为复杂一些的协议设计中,长度域不仅仅包含消息的长度,而且包含其他的数据,如版本号、数据类型、数据状态等,那么这时候我们需要使用lengthAdjustment进行修正**lengthAdjustment=包体的长度值-长度域的值**/privatefinalintlengthAdjustment;//解码后需要跳过的初始字节数,也就是消息内容字段的起始位置privatefinalintinitialBytesToStrip;//长度字段结束的偏移量,lengthFieldOffset=lengthFieldOffset+lengthFieldLengthprivatefinalintlengthFieldOffset;
与固定长度解码器和特定分隔符解码器相似的属性。
privatefinalintmaxFrameLength;//报文最大限制长度privatefinalbooleanfailFast;//是否立即抛出TooLongFrameException,与maxFrameLength搭配使用privatebooleandiscardingTooLongFrame;//是否处于丢弃模式privatelongtooLongFrameLength;//需要丢弃的字节数privatelongbytesToDiscard;//累计丢弃的字节数
下面我们结合具体的示例来解释下每种参数的组合,其实在NettyLengthFieldBasedFrameDecoder源码的注释中已经描述得非常详细,一共给出了7个场景示例,理解了这些示例基本上可以真正掌握LengthFieldBasedFrameDecoder的参数用法。
示例1:典型的基于消息长度+消息内容的解码。
BEFOREDECODE(14bytes)AFTERDECODE(14bytes)+--------+----------------++--------+----------------+|Length|ActualContent|-----|Length|ActualContent||0x000C|"HELLO,WORLD"||0x000C|"HELLO,WORLD"|+--------+----------------++--------+----------------+
上述协议是最基本的格式,报文只包含消息长度Length和消息内容Content字段,其中Length为16进制表示,共占用2字节,Length的值0x000C代表Content占用12字节。该协议对应的解码器参数组合如下:
lengthFieldOffset=0,因为Length字段就在报文的开始位置。
lengthFieldLength=2,协议设计的固定长度。
lengthAdjustment=0,Length字段只包含消息长度,不需要做任何修正。
initialBytesToStrip=0,解码后内容依然是Length+Content,不需要跳过任何初始字节。
示例2:解码结果需要截断。
BEFOREDECODE(14bytes)AFTERDECODE(12bytes)+--------+----------------++----------------+|Length|ActualContent|-----|ActualContent||0x000C|"HELLO,WORLD"||"HELLO,WORLD"|+--------+----------------++----------------+
示例2和示例1的区别在于解码后的结果只包含消息内容,其他的部分是不变的。该协议对应的解码器参数组合如下:
lengthFieldOffset=0,因为Length字段就在报文的开始位置。
lengthFieldLength=2,协议设计的固定长度。
lengthAdjustment=0,Length字段只包含消息长度,不需要做任何修正。
initialBytesToStrip=2,跳过Length字段的字节长度,解码后ByteBuf中只包含Content字段。
示例3:长度字段包含消息长度和消息内容所占的字节。
BEFOREDECODE(14bytes)AFTERDECODE(14bytes)+--------+----------------++--------+----------------+|Length|ActualContent|-----|Length|ActualContent||0x000E|"HELLO,WORLD"||0x000E|"HELLO,WORLD"|+--------+----------------++--------+----------------+
与前两个示例不同的是,示例3的Length字段包含Length字段自身的固定长度以及Content字段所占用的字节数,Length的值为0x000E(2+12=14字节),在Length字段值(14字节)的基础上做lengthAdjustment(-2)的修正,才能得到真实的Content字段长度,所以对应的解码器参数组合如下:
lengthFieldOffset=0,因为Length字段就在报文的开始位置。
lengthFieldLength=2,协议设计的固定长度。
lengthAdjustment=-2,长度字段为14字节,需要减2才是拆包所需要的长度。
initialBytesToStrip=0,解码后内容依然是Length+Content,不需要跳过任何初始字节。
示例4:基于长度字段偏移的解码。
BEFOREDECODE(17bytes)AFTERDECODE(17bytes)+----------+----------+----------------++----------+----------+----------------+|Header1|Length|ActualContent|-----|Header1|Length|ActualContent||0xCAFE|0x00000C|"HELLO,WORLD"||0xCAFE|0x00000C|"HELLO,WORLD"|+----------+----------+----------------++----------+----------+----------------+
示例4中Length字段不再是报文的起始位置,Length字段的值为0x00000C,表示Content字段占用12字节,该协议对应的解码器参数组合如下:
lengthFieldOffset=2,需要跳过Header1所占用的2字节,才是Length的起始位置。
lengthFieldLength=3,协议设计的固定长度。
lengthAdjustment=0,Length字段只包含消息长度,不需要做任何修正。
initialBytesToStrip=0,解码后内容依然是完整的报文,不需要跳过任何初始字节。
示例5:长度字段与内容字段不再相邻。
BEFOREDECODE(17bytes)AFTERDECODE(17bytes)+----------+----------+----------------++----------+----------+----------------+|Length|Header1|ActualContent|-----|Length|Header1|ActualContent||0x00000C|0xCAFE|"HELLO,WORLD"||0x00000C|0xCAFE|"HELLO,WORLD"|+----------+----------+----------------++----------+----------+----------------+
示例5中的Length字段之后是Header1,Length与Content字段不再相邻。Length字段所表示的内容略过了Header1字段,所以也需要通过lengthAdjustment修正才能得到Header+Content的内容。示例5所对应的解码器参数组合如下:
lengthFieldOffset=0,因为Length字段就在报文的开始位置。
lengthFieldLength=3,协议设计的固定长度。
lengthAdjustment=2,由于Header+Content一共占用2+12=14字节,所以Length字段值(12字节)加上lengthAdjustment(2字节)才能得到Header+Content的内容(14字节)。
initialBytesToStrip=0,解码后内容依然是完整的报文,不需要跳过任何初始字节。
示例6:基于长度偏移和长度修正的解码。
BEFOREDECODE(16bytes)AFTERDECODE(13bytes)+------+--------+------+----------------++------+----------------+|HDR1|Length|HDR2|ActualContent|-----|HDR2|ActualContent||0xCA|0x000C|0xFE|"HELLO,WORLD"||0xFE|"HELLO,WORLD"|+------+--------+------+----------------++------+----------------+
示例6中Length字段前后分为别HDR1和HDR2字段,各占用1字节,所以既需要做长度字段的偏移,也需要做lengthAdjustment修正,具体修正的过程与示例5类似。对应的解码器参数组合如下:
lengthFieldOffset=1,需要跳过HDR1所占用的1字节,才是Length的起始位置。
lengthFieldLength=2,协议设计的固定长度。
lengthAdjustment=1,由于HDR2+Content一共占用1+12=13字节,所以Length字段值(12字节)加上lengthAdjustment(1)才能得到HDR2+Content的内容(13字节)。
initialBytesToStrip=3,解码后跳过HDR1和Length字段,共占用3字节。
示例7:长度字段包含除Content外的多个其他字段。
BEFOREDECODE(16bytes)AFTERDECODE(13bytes)+------+--------+------+----------------++------+----------------+|HDR1|Length|HDR2|ActualContent|-----|HDR2|ActualContent||0xCA|0x0010|0xFE|"HELLO,WORLD"||0xFE|"HELLO,WORLD"|+------+--------+------+----------------++------+----------------+
示例7与示例6的区别在于Length字段记录了整个报文的长度,包含Length自身所占字节、HDR1、HDR2以及Content字段的长度,解码器需要知道如何进行lengthAdjustment调整,才能得到HDR2和Content的内容。所以我们可以采用如下的解码器参数组合:
lengthFieldOffset=1,需要跳过HDR1所占用的1字节,才是Length的起始位置。
lengthFieldLength=2,协议设计的固定长度。
lengthAdjustment=-3,Length字段值(16字节)需要减去HDR1(1字节)和Length自身所占字节长度(2字节)才能得到HDR2和Content的内容(1+12=13字节)。
initialBytesToStrip=3,解码后跳过HDR1和Length字段,共占用3字节。
以上7种示例涵盖了LengthFieldBasedFrameDecoder大部分的使用场景,你是否学会了呢?最后,大家可以尝试利用LengthFieldBasedFrameDecoder,完成我们前文自定义的协议的解码。(在看后续代码之前,最好先自我尝试一下[加油])
通信协议:
/***+---------------------------------------------------------------+*|魔数2byte|协议版本号1byte|序列化算法1byte|报文类型1byte|*+---------------------------------------------------------------+*|状态1byte|保留字段4byte|数据长度4byte|*+---------------------------------------------------------------+*|数据内容(长度不定)|*+---------------------------------------------------------------+*/publicclassRequestParam{/***魔数2byte*/privateshortmagic;/***协议版本号*/privatebyteversion;/***序列化算法*/privatebytealgorithm;/***报文类型*/privatebytetype;/***状态*/privatebytestatus;/***保留字段*/privateintremark;/***请求消息内容长度*/privateintlength;/***请求消息体*/privateStringbody;}解码器:
publicclassRequestDecoderextsByteToMessageDecoder{@Overrideprotectedvoiddecode(ChannelHandlerContextctx,ByteBufin,ListObjectout)throwsException{//注意在读的过程中,readIndex的指针也在移动shortmagic=();byteversion=();bytealgorithm=();bytetype=();bytestatus=();intremark=();intlength=();Stringbody=null;if(length0){ByteBufbuf=(length);byte[]req=newbyte[()];(req);body=newString(req,"UTF-8");}RequestParamrequestParam=newRequestParam(magic,version,algorithm,type,status,remark,length,body);(requestParam);}}消息处理:
publicclassRequestHandlerextsChannelInboundHandlerAdapter{@OverridepublicvoidchannelRead(ChannelHandlerContextctx,Objectmsg)throwsException{if(msginstanceofRequestParam){RequestParamrequestParam=(RequestParam)msg;switch(()){case1:("thisisbeatmessagereceivedmsg:="+());break;case2:("thisisbusinessmessagereceivedmsg:="+());break;default:("errormessage"+());break;}}else{("error");}}}启动服务:
publicstaticvoidmain(String[]args)throwsInterruptedException{EventLoopGroupbossGroup=newNioEventLoopGroup(1);EventLoopGroupworkerGroup=newNioEventLoopGroup();try{ServerBootstrapb=newServerBootstrap();(bossGroup,workerGroup).channel().childOption(_NODELAY,true).childHandler(newChannelInitializerSocketChannel(){@OverrideprotectedvoidinitChannel(SocketChannelch)throwsException{().addLast(newLengthFieldBasedFrameDecoder(65534,10,4));().addLast(newRequestDecoder());().addLast(newRequestHandler());}});ChannelFuturechannelFuture=(8088).sync();().closeFuture().sync();}finally{();();}}总结本文对拆包/粘包的产生原因作了分析,并给出通用解决方案及Netty中的实现。希望朋友们在项目开发中能够学以致用。
很赞哦!(185)