TCP粘包|拆包和解决方案

news/2024/4/18 9:34:14/文章来源:https://blog.csdn.net/qq_50652600/article/details/129225905

1 产生原因

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

  1. 因为TCP无消息保护边界,需要在接收端处理消息边界问题,也就是所说的粘包和拆包问题。如下:

2 演示代码(TCP产生粘包|拆包)

客户端向服务端发送10条消息,看服务端如何接受。

服务端Server:

package com.liubujun.tcp;import com.liubujun.netty.NettyServerHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;/*** @Author: liubujun* @Date: 2023/2/26 13:35*/public class MyServer {public static void main(String[] args) throws Exception {//1.创建2个线程组bossGroup和workerGroup//2  bossGroup只是处理连接请求,workerGroup真正的和客户端进行业务处理//3 两个都是无限循环NioEventLoopGroup bossGroup = new NioEventLoopGroup();NioEventLoopGroup workerGroup = new NioEventLoopGroup();try {//创建服务器端的启动对象,配置参数ServerBootstrap bootstrap = new ServerBootstrap();bootstrap.group(bossGroup,workerGroup)//设置两个线程组.channel(NioServerSocketChannel.class) //使用nioSocketChannel作为服务器的通道实现.childHandler(new MyServerInitializer());//给workerGroup的EventLoop对应的管道设置处理器//绑定一个端口并且同步,生成了一个ChannelFuture对象//启动服务器(并绑定端口)ChannelFuture cf = bootstrap.bind(7000).sync();//对关联通道进行监听cf.channel().closeFuture().sync();}finally {bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}
}

服务端Handle:

package com.liubujun.tcp;import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;import java.nio.charset.Charset;
import java.util.UUID;/*** @Author: liubujun* @Date: 2023/2/26 13:50*/public class MyServerHandle extends SimpleChannelInboundHandler<ByteBuf> {private int count;@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));//服务器回送数据给客户端,回送一个随机idByteBuf responseByteBuf = Unpooled.copiedBuffer(UUID.randomUUID().toString(), Charset.forName("utf-8"));ctx.writeAndFlush(responseByteBuf);}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {cause.printStackTrace();ctx.close();}
}

服务端Initializer

package com.liubujun.tcp;import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;/*** @Author: liubujun* @Date: 2023/2/26 13:49*/public class MyServerInitializer extends ChannelInitializer<SocketChannel> {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ChannelPipeline pipeline = ch.pipeline();pipeline.addLast(new MyServerHandle());}
}

客户端:

package com.liubujun.tcp;import com.liubujun.netty.NettyClientHandler;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;/*** @Author: liubujun* @Date: 2023/2/26 13:34*/public class MyClient {public static void main(String[] args) throws Exception{//客户端需要一个循环组EventLoopGroup group = new NioEventLoopGroup();try {//创建客户端的启动对象//注意客户端使用的是Bootstrap不是ServerBootstrapBootstrap bootstrap = new Bootstrap();bootstrap.group(group) //设置线程组.channel(NioSocketChannel.class) //设置客户端通道的实现类.handler(new MyClientInitializer());System.out.println("客户端 ok ...");//启动客户端连接服务器 ChannelFuture涉及到netty的异步模型ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 7000).sync();//给关闭通道进行监听channelFuture.channel().closeFuture().sync();}finally {group.shutdownGracefully();}}
}

客户端Handle:

package com.liubujun.tcp;import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;import java.nio.charset.Charset;/*** @Author: liubujun* @Date: 2023/2/26 13:40*/public class MyClientHandle extends SimpleChannelInboundHandler<ByteBuf> {private int count;@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {//使用客户端发送十条数据hello,serverfor (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();}
}

客户端Initializer

package com.liubujun.tcp;import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;/*** @Author: liubujun* @Date: 2023/2/26 13:37*/public class MyClientInitializer extends ChannelInitializer<SocketChannel> {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ChannelPipeline pipeline = ch.pipeline();pipeline.addLast(new MyClientHandle());}
}

启动服务端和客户端:

服务端控制台:一次全部接受

客户端控制台:只接受一次

发现进行了粘包

再次启动一个客户端:

服务端控制台:

客户端控制台:一次接受

发现进行了拆包。

3 TCP粘包|拆包解决方案

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

  1. 关键是解决服务器端每次读取数据长度的问题,这个问题解决,就不会出现服务器多读或少读数据的问题,从而避免TCP的拆包和粘包。

演示案例:

  1. 客户端发送5个Message对象,客户端每次发送一个message对象,服务器端就会每次接受一个message,分5次进行解码,每次读取到一个message,会回复一个message对象给客户端。

数据包:

//协议包
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;}
}

客户端代码:

public class MyClient {public static void main(String[] args) throws Exception{//客户端需要一个循环组EventLoopGroup group = new NioEventLoopGroup();try {//创建客户端的启动对象//注意客户端使用的是Bootstrap不是ServerBootstrapBootstrap bootstrap = new Bootstrap();bootstrap.group(group) //设置线程组.channel(NioSocketChannel.class) //设置客户端通道的实现类.handler(new MyClientInitializer());System.out.println("客户端 ok ...");//启动客户端连接服务器 ChannelFuture涉及到netty的异步模型ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 7000).sync();//给关闭通道进行监听channelFuture.channel().closeFuture().sync();}finally {group.shutdownGracefully();}}
}

客户端处理器:

public class MyClientHandle extends SimpleChannelInboundHandler<MessageProtocol> {private int count;@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {//使用客户端发送十条数据今天天气冷,来吃火锅for (int i = 0; i < 5; ++i) {String msg = "今天天气冷,来吃火锅";byte[] content = msg.getBytes(Charset.forName("utf-8"));int length = content.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();}
}

客户端初始化:

public class MyClientInitializer extends ChannelInitializer<SocketChannel> {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ChannelPipeline pipeline = ch.pipeline();pipeline.addLast(new MyMessageEncoder()); //加入编码器pipeline.addLast(new MyClientHandle());pipeline.addLast(new MyMessageDecoder());}
}

编码器:

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

解码器:

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

服务端:

public class MyServer {public static void main(String[] args) throws Exception {//1.创建2个线程组bossGroup和workerGroup//2  bossGroup只是处理连接请求,workerGroup真正的和客户端进行业务处理//3 两个都是无限循环NioEventLoopGroup bossGroup = new NioEventLoopGroup();NioEventLoopGroup workerGroup = new NioEventLoopGroup();try {//创建服务器端的启动对象,配置参数ServerBootstrap bootstrap = new ServerBootstrap();bootstrap.group(bossGroup,workerGroup)//设置两个线程组.channel(NioServerSocketChannel.class) //使用nioSocketChannel作为服务器的通道实现.childHandler(new MyServerInitializer());//给workerGroup的EventLoop对应的管道设置处理器//绑定一个端口并且同步,生成了一个ChannelFuture对象//启动服务器(并绑定端口)ChannelFuture cf = bootstrap.bind(7000).sync();//对关联通道进行监听cf.channel().closeFuture().sync();}finally {bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}
}

服务端处理器:

public class MyServerHandle extends SimpleChannelInboundHandler<MessageProtocol> {private int count;@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));//回复消息String responseContent = UUID.randomUUID().toString();int length = responseContent.getBytes("utf-8").length;byte[] bytes = responseContent.getBytes("utf-8");//构建一个协议包MessageProtocol messageProtocol = new MessageProtocol();messageProtocol.setLen(length);messageProtocol.setContent(bytes);ctx.writeAndFlush(messageProtocol);}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {cause.printStackTrace();ctx.close();}
}

服务端初始化:

public class MyServerInitializer extends ChannelInitializer<SocketChannel> {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ChannelPipeline pipeline = ch.pipeline();pipeline.addLast(new MyMessageDecoder()); //因为要接收消息加入解码器pipeline.addLast(new MyServerHandle());pipeline.addLast(new MyMessageEncoder());//因为要恢复消息加入编码器}
}

启动服务端和客户端:

服务端控制台输出:

客户端控制台输出:

可以发现,发送5条消息,服务端接收5次。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_74768.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【算法题】最大矩形面积,单调栈解法

力扣&#xff1a;84. 柱状图中最大的矩形 给定 n 个非负整数&#xff0c;用来表示柱状图中各个柱子的高度。每个柱子彼此相邻&#xff0c;且宽度为 1 。 求在该柱状图中&#xff0c;能够勾勒出来的矩形的最大面积。 题意很简单&#xff0c;翻译一下就是&#xff1a;求该图中…

预训练BERT

与PTB数据集相比&#xff0c;WikiText-2数据集保留了原来的标点符号、大小写和数字&#xff0c;并且比PTB数据集大了两倍多。 我们可以任意访问从WikiText-2语料库中的一对句子生成的预训练&#xff08;遮蔽语言模型和下一句预测&#xff09;样本。 原始的BERT有两个版本&…

BI的作用,体现在企业的哪些方面

对市场异常敏感的商业世界自然不会放过获取数字经济的机会&#xff0c;以国企和央企为首的众多企业开始进行数字化转型&#xff0c;通过信息化建设&#xff0c;部署商业智能BI来完成转型工作。 为什么会出现BI 有一点可能出乎很多人意料&#xff0c;虽然 BI 是因为信息化、数…

智能家居项目(六)之摄像头模块

目录 一、树莓派mipg-streamer实现监控功能调试 1、实现基本思路 2、安装摄像头模块 2.1、在安装sudo apt-get install libv4l-dev 的命令时报错 3、开启摄像头 以下内容是针对树莓派是stretch版本的修改办法&#xff1a; 一、树莓派mipg-streamer实现监控功能调试 1、…

spring boot maven打包jar包太大,怎么办?这个方法解决你的烦恼

在springboot maven项目中&#xff0c;有两种打包方式&#xff0c;一种是war包&#xff0c;一种是jar&#xff0c;今天我们讲一下jar的打包方式。但是在jar包打包只要我们发现&#xff0c;我们的项目jar太大了&#xff0c;每次上传到服务器的时候非常的慢&#xff0c;接下来我们…

大数据处理各组件概念及作用

一、数据采集&#xff1a; 1.1 Flume集群&#xff1a;数据采集工具&#xff0c;如写脚本将不同源端的数据采集后进行数据存储&#xff0c;或推送至Kafka等&#xff1b; 1.2 FTP集群&#xff1a;文件传输工具&#xff1b; 1.3 Kafka集群&#xff1a;消息队列&#xff0c;未避免…

高压放大器在应力波法套筒灌浆密实度检测研究中的应用

实验名称&#xff1a;高压放大器在应力波法套筒灌浆密实度检测研究中的应用研究方向&#xff1a;无损检测测试目的&#xff1a;钢筋套筒灌浆连接技术被广泛应用于装配式建筑节点连接中&#xff0c;但灌浆不密实将导致节点失效的风险。因此&#xff0c;施工中对套筒灌浆的密实度…

Spark 分析计算连续三周登录的用户数

前言&#xff1a;本文用到了窗口函数 range between&#xff0c;可以参考这篇博客进行了解——窗口函数rows between 、range between的使用 创建数据环境 在 MySQL 中创建数据测试表 log_data&#xff1a; create table if not exists log_data( log_id varchar(200) comm…

代码随想录【Day27】| 39. 组合总和、40. 组合总和 II、131. 分割回文串

39. 组合总和 题目链接 题目描述&#xff1a; 给定一个无重复元素的数组 candidates 和一个目标数 target &#xff0c;找出 candidates 中所有可以使数字和为 target 的组合。 candidates 中的数字可以无限制重复被选取。 说明&#xff1a; 所有数字&#xff08;包括 tar…

taobao.top.secret.bill.detail( 服务商的商家解密账单详情查询 )

&#xffe5;免费必须用户授权 服务商的商家解密账单详情查询&#xff0c;仅对90天内的账单提供SLA保障。 公共参数 请求地址: HTTP地址 http://gw.api.taobao.com/router/rest 公共请求参数: 公共响应参数: 请求参数 响应参数 点击获取key和secret 请求示例 TaobaoClient…

js 拖动--动态改变div的宽高大小

index.html 如下&#xff1a;&#xff08;可以新建一个index.html文件直接复制&#xff0c;打开运行&#xff09; <!DOCTYPE html> <html lang"en"> <head> <meta charset"UTF-8"> <meta http-equiv"X-UA-Compatible&qu…

Mybatis源码学习笔记(五)之Mybatis框架缓存机制原理解析

1 Mybatis框架的缓存模块 MyBatis 内置了一个强大的事务性查询缓存机制&#xff0c;它可以非常方便地配置和定制。Mybatis框架中的缓存分为一级缓存和二级缓存&#xff0c;三级缓存基本都要借助自定义缓存或第三方服务来进行实现。但本质上是一样的&#xff0c;都是借助Cache接…

【Python学习笔记】第十九节 Python 面向对象(一)

在现实世界中&#xff0c;随处可见的一种事物就是对象&#xff0c;对象是事物存在的实体&#xff0c;如学生、汽车等。人类解决问题的方式总是将复杂的事物简单化&#xff0c;于是就会思考这些对象都是由哪些部分组成的。通常都会将对象划分为两个部分&#xff0c;即静态部分与…

SSL证书对虚拟主机的用处有哪些?

虚拟主机是指在同一台服务器上&#xff0c;通过不同的域名或IP地址为多个网站提供服务的一种网络主机。而SSL证书则是一种数字证书&#xff0c;它用于加密网站与用户之间的通信&#xff0c;确保数据传输的安全性和完整性。在虚拟主机上&#xff0c;SSL证书有以下几个用处&#…

HiveSql一天一个小技巧:如何巧用分布函数percent_rank()求去掉最大最小值的平均薪水问题

0 问题描述参考链接(3条消息) HiveSql面试题12--如何分析去掉最大最小值的平均薪水&#xff08;字节跳动&#xff09;_莫叫石榴姐的博客-CSDN博客文中已经给出了三种解法&#xff0c;这里我们借助于此题&#xff0c;来研究如何用percent_rank()函数求解&#xff0c;简化解题思路…

深入理解C#的协变和逆变及其限制原因

阅读本文需要的一些前置知识&#xff1a; C#基本语法、C#的泛型使用、C#的运行过程 由于协变和逆变存在一些细节&#xff0c;在阅读时请注意“接口”和“类型”的差异&#xff0c;此外&#xff0c;文中有可能在不同的语境中将“结构体”和“值类型”混用&#xff0c;但表达的同…

深入浅出1588v2(PTP)里的时间同步原理

1.时间同步1.1 单步同步(OneStep)单步同步最为简单&#xff0c;master向slave发送一个sync的同步包&#xff0c;同步包里带有这条信息发送时master的当前时间t1&#xff0c;假如这条信息从master传输到slave需要的传输时间是D&#xff0c;那么slave收到信息时&#xff0c;maste…

BIM小技巧丨关于如何在Revit明细表中显示门窗面积

在明细表中显示门窗面积(以门明细表为例)在新建一个门明细表后&#xff0c;可以发现在Revit中不能直接使用明细表统计门窗面积。 这时&#xff0c;可以通过使用添加“计算值”的方式来处理&#xff0c;得到如下图所示&#xff0c;两种不同的面积统计结果&#xff1a; 除此之外&…

前端基础之CSS扫盲

文章目录一. CSS基本规范1. 基本语法格式2. 在HTML引入CSS3. 选择器分类二. CSS常用属性1. 文本属性2. 文本格式3. 背景属性4. 圆角矩形和圆5. 元素的显示模式6. CSS盒子模型7. 弹性布局光使用HTML来写一个前端页面的话其实只是写了一个大体的框架, 整体的页面并不工整美观, 而…

ledcode【用队列实现栈】

目录 题目描述&#xff1a; 解析题目 代码解析 1.封装一个队列 1.2封装带两个队列的结构体 1.3封装指向队列的结构体 1.4入栈函数实现 1.5出栈函数实现 1.6取栈顶数据 1.7判空函数实现 题目描述&#xff1a; 解析题目 这个题我是用c语言写的&#xff0c;所以队列的pu…