掌秋使 手游攻略 新游动态 tcp粘包/拆包的原因,tcp粘包和拆包的处理

tcp粘包/拆包的原因,tcp粘包和拆包的处理

时间:2025 08 30 07:18:25 来源:头条 浏览:0

TCP数据包粘贴和拆包的基本概述

1、TCP是面向连接、面向流的,提供可靠的服务。由于发送方和接收方(客户端和服务器)都需要一对套接字,因此在发送方会使用一种优化技术(Nagle 算法)来更有效地向接收方发送多个数据包。少量数据被合并成更大的数据块,然后丰富。虽然这提高了效率,但面向流的通信缺乏消息保护边界,使得接收方很难区分完整的数据包。

2. TCP没有消息保护边界,因此消息边界问题必须在接收方处理。这就是所谓的粘包和拆包问题。

3、TCP报文粘包拆包示意图

TCP贴包拆包示意图

假设客户端分别向服务器发送两个数据包D1和D2。由于服务器一次读取多少字节是不确定的,所以有四种可能的情况:

服务器两次读取两个独立的数据包,即D1和D2,不进行粘包或拆包。

服务器一次收到两个数据包,D1和D2合并。这称为TCP 粘性数据包。

服务器读取数据包两次。第一次读取整个D1包和部分D2包,第二次读取D2包的剩余内容。这称为TCP 拆包。

服务器读取数据包两次,第一次读取部分D1数据包D1_1,第二次读取D1数据包D1_2的其余部分和整个D2数据包。

TCP数据包粘包和拆包现象示例

1、编写Netty程序时,如果不做任何处理,就会出现卡住、解压的问题。

代码演示

服务器代码

/** * Server*/public class MyServer { @SuppressWarnings('all') public static void main(String[] args) { NioEventLoopGroup BossGroup=new NioEventLoopGroup(1); NioEventLoopGroup workerGroup=new NioEventLoopGroup(); try { ServerBootstrap serverBootstrap=new ServerBootstrap(); serverBootstrap.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class) //自定义初始化class.childHandler(new MyServerInitializer()); ChannelFuture channelFuture=serverBootstrap.bind(7000).sync ( ); channelFuture .channel().closeFuture().sync(); } catch (Exception e) { e.printStackTrace(); }finally { BossGroup.shutdownGraceful();workerGroup.shutdownGraceful(); } }}MyServerInitializer 初始化只需设置代码即可您班级中的业务处理处理程序

public class MyServerInitializer extends ChannelInitializer { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline Pipeline=ch.pipeline(); Pipeline.addLast(new MyServerHandler()); }} 服务端业务处理MyServerHandler 这里定义了它有到过。 Message Received Count,打印客户端发送的消息并观察服务器接收消息的次数。这是不确定的,每次操作的结果可能会有所不同。服务器每次收到消息时都会进行回复。接收消息消息发送给客户端

public class MyServerHandler extends SimpleChannelInboundHandler { /** * 服务器接收消息的次数。每个客户端与服务器的连接都是一个独立的通道,所以不同的客户端没有影响*/private int count; @Override protected void channelRead0( ChannelHandlerContext ctx, ByteBuf msg) throws Exception { byte[]buffer=new byte[msg.readBytes( )]; 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); } @Override public voidExceptionCaught(ChannelHandlerContext ctx, Throwable Cause) 抛出异常{ Cause.printStackTrace(); } ctx.close(); }} 客户端代码

公共类MyClient { @SuppressWarnings('all') 公共静态void main(String[] args) { NioEventLoopGroup group=new NioEventLoopGroup(); try { Bootstrap bootstrap=new Bootstrap(); bootstrap.group(group).channel(NioSocketChannel .class) //自定义初始化class.handler(new MyClientInitializer()); ChannelFuture channelFuture=bootstrap.connect('localhost', 7000).sync(); channelFuture.channel().closeFuture().sync (); } catch (Exception e) { e.printStackTrace(); }finally { group.shutdownGraceful(); } }}客户端初始化类MyClientInitializer

public class MyClientInitializer extends ChannelInitializer { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline Pipeline=ch.pipeline(); Pipeline.addLast(new MyClientHandler()); }} 客户端业务处理类MyClientHandler,这是客户端demo一旦建立连接,就会向服务器发送10 次hello。它还显示客户端如何从服务器接收消息。消息分几批接收。

public class MyClientHandler extends SimpleChannelInboundHandler { /** * 客户端收到消息的次数。每个客户端和服务端的连接都是一个独立的通道,所以不同的客户端没有影响*/private int count; @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { //10 使用client 发送数据for hello,servernumber for(int i=0;i10;i++){ ByteBufbuffer=Unpooled.copiedBuffer('hello,server ' + i, Charset .forName('utf-8')); ctx.writeAndFlush (buffer); } } @Override protected void channelRead0( ChannelHandlerContext ctx, ByteBuf msg) 抛出异常{ byte[]buffer=new byte[msg.readBytes()] ; msg.readBytes(buffer); String message=new String(buffer , Charset.forName('utf-8')) ; System.out.println('客户端收到的消息='+message); System.out.println('客户端收到的消息数='+(++this.count)); } @Override public voidExceptionCaught(ChannelHandlerContext ctx , Throwable Cause) throws Exception { Cause.printStackTrace(); ctx.close(); }}演示代码。先启动服务器,然后多次启动客户端并观察看服务器的输出。

可以看到,客户端第一次连接服务器时,服务器只收到了客户端发送的10次数据。

服务器收到数据hello,服务器0hello,服务器1hello,服务器2hello,服务器3hello,服务器4hello,服务器5hello,服务器6hello,服务器7hello,服务器8hello,服务器9。服务器接收消息数=1。客户端连接服务端,此时我们看到服务端收到了客户端发送的10次消息,分为5次,第一次收到了客户端发送的一次数据,第二次收到了客户端发送的数据客户端发送了一次,我们可以看到已经收到了数据,我们已经收到了两次客户端发送的数据,第四次和第五次收到了客户端发送的数据,一共三次。

服务器收到数据你好,服务器0。服务器接收到的消息数=1。服务器收到数据hello,服务器1 hello,服务器2。服务器收到的消息数=2。服务器是数据hello、服务器3 hello、服务器4 hello、服务器5。服务器接收到的消息数=3 服务器接收到的消息数=3 服务器接收到的数据hello、服务器6 hello、服务器7 hello、服务器8 服务器接收到的消息数=4 服务器接收到的数据hello , 服务器9 服务器收到的消息数量=5 现在我们已经展示了每次客户端向服务器发送消息时,服务器都会向客户端发送回一个$$ 分隔的随机数,让我们看看客户端再次。客户端一次性接收服务器发送的号码。

客户端发送消息=e8a93f08-32e6-4af2-a344-56c00c409d8f$$c8c363b5-d25e-4007-b7be-1d50d86acb7f$$2572c3c9-76ed-491f-a3a5-acec6938ce7d$$b4927691-0 已收到。 745-42cd-abbc-06a35cf5b2da$ $6 fdf8259-3bd4-4d6a-ae33-ea8b72124df0$$客户端收到的消息数=1TCP数据包粘包和拆包解决方案

1.使用自定义协议+编解码器解决

2.重要的是解决服务器每次读取数据长度的问题,一旦解决了这个问题,服务器读取数据多或少的问题就消失了,避免了TCP数据包的堆叠和拆包。

我们来看一个具体的例子

1、客户端必须发送5个Message对象,客户端每次发送1个Message对象。

2. 服务端每接收到一条消息,都会解码五次,每次读取消息都会返回一个消息对象给客户端。

打开粘袋

代码演示

首先,我们定义自己的协议包类MessageProtocol。这仅定义了两个字段。一是内容的长度,二是消息的内容。

/** * 自定义协议包*/@Datapublic class MessageProtocol { /** * 长度,这是关键*/private int len; private byte[] content;} 接下来定义服务器MyServer。服务器接收消息。消息在发送之前必须进行解码和编码。消息载体就是上面定义的协议包类。

public class MyServer { @SuppressWarnings('all') public static void main(String[] args) { NioEventLoopGroup BossGroup=new NioEventLoopGroup(1); NioEventLoopGroup workerGroup=new NioEventLoopGroup(); 尝试{ ServerBootstrap serverBootstrap=new ServerBootstrap(); serverBootstrap .group(bossGroup,workerGroup).channel(NioServerSocketChannel.class) //自定义初始化class.childHandler(new MyServerInitializer()); ChannelFuture channelFuture=serverBootstrap.bind(7000).sync(); channelFuture.channel( ).closeFuture ( ).sync(); } catch (Exception e) { e.printStackTrace(); }finally { BossGroup.shutdownGraceful();workerGroup.shutdownGraceful(); } }} 服务端初始化类的内容为,配置服务器。 -side 编解码器及其业务逻辑处理器MyServerInitializer

public class MyServerInitializer extends ChannelInitializer { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline Pipeline=ch.pipeline(); //添加自定义解码器Pipeline.addLast(new MyMessageDecoder()); //自定义编码Pipeline.addLast ( new MyMessageEncoder()); //业务逻辑处理器Pipeline.addLast(new MyServerHandler()); }}编码器代码MyMessageEncoder.在发送消息之前,对其进行编码并发送消息的长度和内容。

public class MyMessageEncoder extends MessageToByteEncoder { @Override protected void encode(ChannelHandlerContext ctx, MessageProtocol msg, ByteBuf out) throws Exception { System.out.println('MyMessageEncoder 编码方法被调用'); out.writeInt(msg.getLen( )); } out.writeBytes(msg.getContent()); }} 解码器代码MyMessageDecoder,对消息进行解码,读取消息长度和内容,封装在消息协议包中,传递给下一个处理程序。

public class MyMessageDecoder extends ReplayingDecoder { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { System.out.println('MyMessageDecoder解码被调用'); //获取二进制字节码MessageProtocol数据必须是Package( object ) int length=in.readInt(); byte[] content=new byte[length]; in.readBytes(content); //封装在一个MessageProtocol对象中,并inout MessageProtocol的业务处理,然后传递给下一个处理程序做。 messageProtocol=new MessageProtocol(); messageProtocol.setLen(length); messageProtocol.setContent(content); out.add(messageProtocol); }} 服务器的业务逻辑处理器代码读取客户端发送的消息并安装协议包。并发送回客户端

public class MyServerHandler extends SimpleChannelInboundHandler { /** * 服务器接收消息的次数。每个客户端与服务端的连接都是一个独立的通道,所以不同的客户端没有影响*/private int count; @Override protected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) throws Exception { //接收数据处理int len=msg .getLen(); byte[] content=msg.getContent(); System.out.println('服务器收到的信息: Length='+len+'; Content='+new String(content,Charset.forName(' utf-8'))+'; 收到的消息包数量='+(++this.count)); //向客户端回复消息byte[] responseContent=UUID.randomUUID( ).toString().getBytes(' utf-8'); int length=responseContent.length; //构建协议包MessageProtocol messageProtocol=new MessageProtocol ( ); messageProtocol.setLen(length); messageProtocol.setContent(responseContent) ; ctx.writeAndFlush(messageProtocol); } @ Override public voidExceptionCaught(ChannelHandlerContext ctx, Throwable Cause) throws Exception { Cause.printStackTrace(); ctx.close( )客户端代码在接收消息时和发送消息之前对消息进行解码。必须对其进行编码。消息载体就是上面定义的协议包类。

公共类MyClient { @SuppressWarnings('all') 公共静态void main(String[] args) { NioEventLoopGroup group=new NioEventLoopGroup(); try { Bootstrap bootstrap=new Bootstrap(); bootstrap.group(group).channel(NioSocketChannel .class) //自定义初始化class.handler(new MyClientInitializer()); ChannelFuture channelFuture=bootstrap.connect('localhost', 7000).sync(); channelFuture.channel().closeFuture().sync (); } catch (Exception e) { e.printStackTrace(); }finally { group.shutdownGraceful(); } }}客户端初始化类MyClientInitializer,配置编解码器和客户端业务逻辑处理器

public class MyClientInitializer extends ChannelInitializer { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline Pipeline=ch.pipeline(); //添加自定义编码器Pipeline.addLast(new MyMessageEncoder()); //添加自定义解码器AddPipeline .addLast(new MyMessageDecoder()); //客户端业务逻辑处理器Pipeline.addLast(new MyClientHandler()); }}客户端业务逻辑处理器MyClientHandler 与服务器建立连接,持续提供服务。客户端发送5。构建的协议包消息

public class MyClientHandler extends SimpleChannelInboundHandler { /** * 客户端收到消息的次数。每个客户端和服务器之间的连接是一个独立的通道,所以不同的客户端没有影响*/private int count; @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { //使用客户端发送5 个数据项for ``It's今天冷,我们吃火锅吧'' for(int i=0;i5;i++){ String message='今天冷,我们吃火锅吧'; byte[] content=message.getBytes(Charset.forName( 'utf-8')); int length=message.getBytes(Charset.forName('utf-8')).length; //创建协议包对象MessageProtocol messageProtocol=new MessageProtocol (); messageProtocol.setLen(length) ; messageProtocol.setContent(content); ctx.writeAndFlush(messageProtocol); } } @Override protected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) 抛出异常{ int len=msg.getLen (); byte[] content=msg. getContent(); System.out.println('客户端收到消息:length='+len+', content='+new String(content,Charset.forName('utf-8'))+', 收到消息数='+(++this.count)); } @Override public voidExceptionCaught(ChannelHandlerContext ctx, Throwable Cause) throws Exception { Cause.printStackTrace(); ctx.close(); }} 演示代码。首先启动服务器,然后多次启动客户端并观察,首先检查服务器的输出。

MyMessageDecoder解码被调用的服务器收到以下信息: length=27; content=今天冷了,我去吃火锅;收到的消息包数量=1 MyMessageEncoder的encode方法被调用MyMessageDecoder解码被调用服务器收到以下信息: length=27; content=今天冷了,吃火锅吧;收到的消息包数量=2 调用MyMessageEncoder 的编码方法,调用MyMessageDecoder 解码被叫服务器收到如下信息: Length=27; content=It's今天冷,吃火锅吧;收到的消息包数量=3 MyMessageEncoder 的编码方式调用MyMessageDecoder 解码被调用的服务器收到如下信息: 接收信息: 长度=27; 内容=今天冷,我去吃饭hotpot; 接收到的消息包数=4 MyMessageEncoder 的编码方式称为MyMessageDecoderdecode 被调用的服务器收到如下信息: Length=27; Content=今天冷了,吃个火锅吧; 接收到的消息包数=5 可以看到什么时候调用MyMessageEncoder 编码方法并建立连接。客户端连续向服务器发送5个协议包消息,服务器Bian首先对消息进行解码,读取整个消息,然后对消息进行编码,然后将其发送回客户端。可以看到服务器收到了5个协议包消息。协议报文按顺序完成,没有发生卡顿现象。打包和拆包现象,我们看一下客户端输出

MyMessageEncoder 编码方法调用MyMessageEncoder 编码方法MyMessageEncoder 编码方法调用MyMessageEncoder 编码方法调用MyMessageDecoder 解码客户端收到消息: length=36, content=83d7f5b6-1c40- 4437-b3fe-5940552c933d ,收到消息数=1 MyMessageDecoder 解码调用客户端收到一条消息:长度=36,内容=0a9fd95a-1ca5-4ae6-9596-8066fa21bd63,接收到的消息数=2 MyMessageDecoder 解码被调用被叫客户端收到一条消息:长度=36,内容=146d26dc-0045-48ab-abe8 -18f196151d76,收到的消息数=3MyMessageDecoder 解码被叫客户端收到一条消息:长度=36,内容=7995d62a-27ef-4860-bbf5-12334fa2fc23,收到的消息数=4MyMessageDecoder 被调用客户端并解码。客户端收到消息:长度=36,内容=13275e5f-3483-4fbd-af91-30876a7cebea,收到消息数=5。客户端一旦建立连接,就会连续发送五个协议报文。发送到服务器。消息在发送之前进行编码。然后它接收服务器返回的消息。解码器在接收之前被调用。这里我们也可以看到客户端每次都完美的接收到了服务器发送的消息。服务器发送后,客户端发送5次后完成,接收5次后,粘贴解包没有问题,粘贴解包演示到此结束。

标题:tcp粘包/拆包的原因,tcp粘包和拆包的处理
链接:https://www.zhangqiushi.com/news/xydt/4721.html
版权:文章转载自网络,如有侵权,请联系删除!
资讯推荐
更多
湖北电视台生活频道如何培养孩子的学习兴趣直播回放在哪看?直播视频回放地址入口

湖北电视台生活频道如何培养孩子的学习兴趣直播回放在哪看?直播视频回放地址入口[多图],湖北电视台生活频道

2025-08-10
阴阳师4月22日更新内容:帝释天上线技能调整,红莲华冕活动来袭

阴阳师4月22日更新内容:帝释天上线技能调整,红莲华冕活动来袭[多图],阴阳师4月22日更新的内容有哪些?版本更新

2025-08-10
四川电视台经济频道如何培养孩子的学习习惯与方法直播在哪看?直播视频回放地址

四川电视台经济频道如何培养孩子的学习习惯与方法直播在哪看?直播视频回放地址[多图],2021四川电视台经济频

2025-08-10
小森生活金币不够用怎么办?金币没了不够用解决方法

小森生活金币不够用怎么办?金币没了不够用解决方法[多图],小森生活金币突然就不够用的情况很多人都有,金币没

2025-08-10