Netty学习(八):TCP的粘包拆包与解决方案
创始人
2025-05-29 10:38:36
0次

一、什么是粘包和拆包

TCP是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发给接收端的包,更有效的发给对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样做虽然提高了效率,但是接收端就难于分辨出完整的数据包了,因为面向流的通信是无消息保护边界的。

由于TCP无消息保护边界,需要在接收端处理消息边界问题,这也就是我们所说的粘包、拆包问题。

二、拆包粘包示意图

假设客户端分别发送了两个数据包Dl和D2给服务端,由于服务端一次读取到字节数是不确定的,故可能存在以下四种情况:

  1. 服务端分两次读取到了两个独立的数据包,分别是D1和 D2,没有粘包和拆包

  1. 服务端一次接受到了两个数据包,D1和D2粘合在一起,称之为TCP粘包

  1. 服务端分两次读取到了数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这称之为TCP拆包

  1. 服务端分两次读取到了数据包,第一次读取到了D1包的部分内容Dl_1,第二次读取到了D1包的剩余部分内容Dl_2和完整的D2包

三、拆包粘包现象实例

在编写Netty程序时,如果没有做处理,默认会发生粘包拆包问题。

我们在客户端中发送10条“hello,server”,看看服务端会收到什么。

3.1 客户端handler

public class MyClientHandler extends SimpleChannelInboundHandler {private int count;@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {//使用客户端发送10条数据 hello,server 编号for(int i= 0; i< 10; ++i) {ByteBuf buffer = Unpooled.copiedBuffer("hello,server " + i, Charset.forName("utf-8"));ctx.writeAndFlush(buffer);}}@Overrideprotected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {byte[] buffer = new byte[msg.readableBytes()];msg.readBytes(buffer);String message = new String(buffer, Charset.forName("utf-8"));System.out.println("客户端接收到消息=" + message);System.out.println("客户端接收消息数量=" + (++this.count));}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {cause.printStackTrace();ctx.close();}
}

3.2 服务端handler

public class MyServerHandler extends SimpleChannelInboundHandler{private int count;@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {//cause.printStackTrace();ctx.close();}@Overrideprotected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {byte[] buffer = new byte[msg.readableBytes()];msg.readBytes(buffer);//将buffer转成字符串String message = new String(buffer, Charset.forName("utf-8"));System.out.println("服务器接收到数据 " + message);System.out.println("服务器接收到消息量=" + (++this.count));//服务器回送数据给客户端, 回送一个随机id ,ByteBuf responseByteBuf = Unpooled.copiedBuffer(UUID.randomUUID().toString() + " ", Charset.forName("utf-8"));ctx.writeAndFlush(responseByteBuf);}
}

3.3 发送结果(每次启动结果都有可能不同)

客户端:发送了一条消息:

客户端接收到消息=301f5819-059a-4ac2-b7f8-18688b3d88f6 5ecf1160-0609-4f24-aa1a-69b3d291c018 b0436ae3-c1cd-483e-94e3-b0b7cfbdc713 88995985-342e-4ab8-b742-2128bb4e63db 
客户端接收消息数量=1

服务端:

服务器接收到数据 hello,server 0hello,server 1hello,server 2hello,server 3hello,server 4hello,server 5hello,server 6hello,server 7hello,server 8hello,server 9
服务器接收到消息量=1
服务器接收到数据 hello,server 0
服务器接收到消息量=1
服务器接收到数据 hello,server 1
服务器接收到消息量=2
服务器接收到数据 hello,server 2hello,server 3hello,server 4hello,server 5hello,server 6
服务器接收到消息量=3
服务器接收到数据 hello,server 7hello,server 8hello,server 9
服务器接收到消息量=4

可以看到服务端将这条消息拆成了4次来接收。

四、拆包粘包解决方案

4.1 解决方案

解决问题的关键在于要解决服务器端每次读取数据长度的问题。这样就不会出现服务器多读或少读数据的问题,从而避免的TCP粘包、拆包。

因此我们使用自定义协议+编解码器来解决。

自定义协议即是规定一次读取的长度等,这样的协议需要一套自己的编解码器。将这两个部分实现就可以解决粘包拆包问题了。

在这里有必要介绍一下Protocol。Protobuf是Google 发布的开源项目,全称Google Protocol Buffers,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或RPC数据交换格式,能够跨平台,跨语言,有高性能、高可靠性,解决了许多Java自带编解码器的弊端。(参考文档: https://developers.google.com/protocol-buffers/docs/proto)

4.2 案例描述

客户端每次发送一个Message对象,共发送5个Message 对象,

服务器端每次接收一个Message,分5次进行解码,每读取到一个Message,会回复一个Message对象给客户端。

如果不做处理的话,这五个message可能就会被拆开或者粘在一起,我们做这个案例就是为了避免这个粘包拆包的问题。

4.3 协议

//协议包
public class MessageProtocol {private int len; //关键private byte[] content;public int getLen() {return len;}public void setLen(int len) {this.len = len;}public byte[] getContent() {return content;}public void setContent(byte[] content) {this.content = content;}
}

4.4 客户端handler

public class MyClientHandler extends SimpleChannelInboundHandler {private int count;@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {//使用客户端发送10条数据 "今天天气冷,吃火锅" 编号for(int i = 0; i< 5; i++) {String mes = "今天天气冷,吃火锅";byte[] content = mes.getBytes(Charset.forName("utf-8"));int length = mes.getBytes(Charset.forName("utf-8")).length;//创建协议包对象MessageProtocol messageProtocol = new MessageProtocol();messageProtocol.setLen(length);messageProtocol.setContent(content);ctx.writeAndFlush(messageProtocol);}}//    @Overrideprotected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) throws Exception {int len = msg.getLen();byte[] content = msg.getContent();System.out.println("客户端接收到消息如下");System.out.println("长度=" + len);System.out.println("内容=" + new String(content, Charset.forName("utf-8")));System.out.println("客户端接收消息数量=" + (++this.count));}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {System.out.println("异常消息=" + cause.getMessage());ctx.close();}
}

4.5 服务器handler

public class MyServerHandler extends SimpleChannelInboundHandler{private int count;@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {//cause.printStackTrace();ctx.close();}@Overrideprotected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) throws Exception {//接收到数据,并处理int len = msg.getLen();byte[] content = msg.getContent();System.out.println();System.out.println();System.out.println();System.out.println("服务器接收到信息如下");System.out.println("长度=" + len);System.out.println("内容=" + new String(content, Charset.forName("utf-8")));System.out.println("服务器接收到消息包数量=" + (++this.count));//回复消息String responseContent = UUID.randomUUID().toString();int responseLen = responseContent.getBytes("utf-8").length;byte[]  responseContent2 = responseContent.getBytes("utf-8");//构建一个协议包MessageProtocol messageProtocol = new MessageProtocol();messageProtocol.setLen(responseLen);messageProtocol.setContent(responseContent2);ctx.writeAndFlush(messageProtocol);}
}

4.6 编码器

public class MyMessageEncoder extends MessageToByteEncoder {@Overrideprotected void encode(ChannelHandlerContext ctx, MessageProtocol msg, ByteBuf out) throws Exception {System.out.println("MyMessageEncoder encode 方法被调用");out.writeInt(msg.getLen());out.writeBytes(msg.getContent());}
}

4.7 解码器

public class MyMessageDecoder extends ReplayingDecoder {@Overrideprotected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception {System.out.println("MyMessageDecoder decode 被调用");//需要将得到二进制字节码-> MessageProtocol 数据包(对象)int length = in.readInt();byte[] content = new byte[length];in.readBytes(content);//封装成 MessageProtocol 对象,放入 out, 传递下一个handler业务处理MessageProtocol messageProtocol = new MessageProtocol();messageProtocol.setLen(length);messageProtocol.setContent(content);out.add(messageProtocol);}
}

4.8 运行结果

客户端:

MyMessageEncoder encode 方法被调用
MyMessageEncoder encode 方法被调用
MyMessageEncoder encode 方法被调用
MyMessageEncoder encode 方法被调用
MyMessageEncoder encode 方法被调用
MyMessageDecoder decode 被调用
客户端接收到消息如下
长度=36
内容=b980df87-3c1d-4ebd-923d-583d6c08377a
客户端接收消息数量=1
MyMessageDecoder decode 被调用
客户端接收到消息如下
长度=36
内容=34c572fc-c5fc-4763-aa86-9f0d6ebbce51
客户端接收消息数量=2
MyMessageDecoder decode 被调用
客户端接收到消息如下
长度=36
内容=2bdc5cd2-a7de-4e1d-80c2-8e654458f6e8
客户端接收消息数量=3
MyMessageDecoder decode 被调用
客户端接收到消息如下
长度=36
内容=8fa37455-cafa-420f-8f18-7ec39ccdb07f
客户端接收消息数量=4
MyMessageDecoder decode 被调用
客户端接收到消息如下
长度=36
内容=2fca698d-581e-49c9-ab96-d47b3c52a1bd
客户端接收消息数量=5

服务端:

MyMessageEncoder encode 方法被调用
MyMessageEncoder encode 方法被调用
MyMessageEncoder encode 方法被调用
MyMessageEncoder encode 方法被调用
MyMessageEncoder encode 方法被调用
MyMessageDecoder decode 被调用
客户端接收到消息如下
长度=36
内容=b980df87-3c1d-4ebd-923d-583d6c08377a
客户端接收消息数量=1
MyMessageDecoder decode 被调用
客户端接收到消息如下
长度=36
内容=34c572fc-c5fc-4763-aa86-9f0d6ebbce51
客户端接收消息数量=2
MyMessageDecoder decode 被调用
客户端接收到消息如下
长度=36
内容=2bdc5cd2-a7de-4e1d-80c2-8e654458f6e8
客户端接收消息数量=3
MyMessageDecoder decode 被调用
客户端接收到消息如下
长度=36
内容=8fa37455-cafa-420f-8f18-7ec39ccdb07f
客户端接收消息数量=4
MyMessageDecoder decode 被调用
客户端接收到消息如下
长度=36
内容=2fca698d-581e-49c9-ab96-d47b3c52a1bd
客户端接收消息数量=5

可以看到,服务端完整地接收了五条消息,并在接收到每条message后给客户端返回信息了。说明我们解决了粘包拆包问题。

相关内容

热门资讯

在大吉订购APP订购交易亏损被...   近期,不少投资者反映在“大吉订购App”上投资白银、铂金等商品遭遇严重亏损。该平台宣称提供现货订...
铂银壹号APP商品订购交易亏损...   许多中小投资者误以为网上的“商品现货订购”是合规合法的理财渠道,殊不知这背后隐藏着巨大的合规风险...
兴鑫淘金APP贵金属订购交易亏...   白银、贵金属或其他商品类投资,除国家批准的正规期货交易场所及持牌期货公司外,市面上大量第三方交易...
铂银掘金APP无资质的违规平台...   铂银掘金等平台的投资者,往往是被“注册送现金”“充值返现券”“首单免亏”等广告话术吸引,进而下载...
八方淘金APP订购交易不合规,...   “八方淘金APP”“铂银掘金APP”“铂银壹号APP”等软件被指以“现货订购”为名,实际采用高杠...
在指尖订购APP订购商品亏损被...   指尖订购APP这类平台,表面是“订购交易”,实则是披着电商外衣的非法期货陷阱。绝大多数投资者被蒙...
在犀贝易购APP做商品订购交易...   市面上打着“现货投资”“贵金属交易”旗号的APP并不少,但真正合规、受监管的平台并不多。犀贝易购...
易达淘金APP虚假商品订购交易...   易达淘金APP存在严重的虚假宣传与违规交易行为。该平台打着“商品订购”的旗号,实则通过不合规的交...
大浪淘金APP白银订购亏损被骗...   大浪淘金APP最隐蔽的套路,在于其完全封闭的内盘交易系统。表面上,它宣称与国际贵金属市场接轨;实...
一品好购APP上订购白银出现亏...   一品好购APP原名“一品订购”,运营方为上海宝智未来信息技术有限公司。近期不少投资者反映在该平台...
九州订购APP贵金属订购投资骗...   九州订购APP正通过抖.音、快手、今日头条等平台大量投放投资广告,用“5元起投”“充值返现”“0...
坚锅淘金APP交易软件并不正规...   当前,市面上大量所谓的“现货商城订购”交易软件,早已偏离了正常的实物商品买卖,演变为高杠杆、高手...
斗金订购APP贵金属期货交易骗...   斗金订购APP的投资者往往被铺天盖地的广告宣传所诱导——注册即送现金、充值返现券等看似诱人的福利...
警惕!云启购APP违规经营,披...   "5元起投、充值返现、稳赚不赔"——这些诱人的广告语背后,可能是一个精心设计的金融陷阱。  近期...
量子淘金APP贵金属投资不合规...   "4元起投""充值返券""月入过万"——这些极具诱惑力的广告语在短视频平台、社交软件上铺天盖地。...
鑫汇智投APP贵金属商品订购骗...   鑫汇智投APP非法期货投资骗局,受害者在投资过程中根本不懂交易,也不去研究和验证这个平台是否正规...
矅光智策APP平台违规经营非法...   矅光智策APP对外宣称是“商城订购”“现货交易”,但查询其经营范围,并未显示具备开展现货期货交易...
黄金珠宝怎么选?内侧刻字都代表... 潮新闻客户端 记者 马焱 通讯员 任倩影 9月8日下午,“云游杭州 乐购金秋”——“浙城·浙放心”活...
原创 冰... 最近全球国债市场正在上演魔幻一幕: 美债、日债、欧债遭遇猛烈抛售,收益率冲到几十年新高,美国财政部都...
日韩股市开盘表现分化 上证报中国证券网讯 9月9日,日韩股市开盘表现分化,韩国综合指数开盘上涨0.26%,报6972.87...