TCP协议——粘包与拆包

TCP的基础

TCP协议基础,传送门
TCP协议流量控制,传送门

1.1 什么是TCP粘包/拆包

TCP是个“流”协议,所谓流,就是没有界限的一串数据。大家可以想想河里的流水,是连成一片的,其间并没有分界线。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。

我们都知道TCP属于传输层的协议,传输层除了有TCP协议外还有UDP协议。那么UDP是否会发生粘包或拆包的现象呢?答案是不会。UDP是基于报文发送的,从UDP的帧结构可以看出,在UDP首部采用了16bit来指示UDP数据报文的长度,因此在应用层能很好的将不同的数据报文区分开,从而避免粘包和拆包的问题。而TCP是基于字节流的,虽然应用层和TCP传输层之间的数据交互是大小不等的数据块,但是TCP把这些数据块仅仅看成一连串无结构的字节流,没有边界;另外从TCP的帧结构也可以看出,在TCP的首部没有表示数据长度的字段,基于上面两点,在使用TCP传输数据时,才有粘包或者拆包现象发生的可能。

1.2 粘包、拆包表现形式

现在假设客户端向服务端连续发送了两个数据包,用packet1和packet2来表示,那么服务端收到的数据可以分为三种,现列举如下:

第一种情况,接收端正常收到两个数据包,即没有发生拆包和粘包的现象,此种情况不在本文的讨论范围内。

在这里插入图片描述

第二种情况,接收端只收到一个数据包,由于TCP是不会出现丢包的,所以这一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包。这种情况由于接收端不知道这两个数据包的界限,所以对于接收端来说很难处理。

在这里插入图片描述

第三种情况,这种情况有两种表现形式,如下图。接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包。这两种情况如果不加特殊处理,对于接收端同样是不好处理的。

在这里插入图片描述
在这里插入图片描述
1.3 粘包、拆包发生原因

发生TCP粘包或拆包有很多原因,现列出常见的几点,可能不全面,欢迎补充,

  • 1、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。

  • 2、待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。

  • 3、要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。

  • 4、接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。

1.4 粘包、拆包解决办法

通过以上分析,我们清楚了粘包或拆包发生的原因,那么如何解决这个问题呢?解决问题的关键在于如何给每个数据包添加边界信息,常用的方法有如下几个:

  • 1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。

  • 2、发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。

  • 3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。

样例程序

我将在程序中使用两种方法来解决粘包和拆包问题,固定数据包长度和添加长度首部,这两种方法各有优劣。固定数据包长度传输效率一般,尤其是在要发送的数据长度长短差别很大的时候效率会比较低,但是编程实现比较简单;添加长度首部虽然可以获得较高的传输效率,冗余信息少且固定,但是编程实现较为复杂。下面给出的样例程序是基于之前的文章《Java中BIO,NIO和AIO使用样例》中提到的NIO实例的,如果对NIO的使用还不是很熟悉,可以先了解一下Java中NIO编程。

固定数据包长度

这种处理方式的思路很简单,发送端在发送实际数据前先把数据封装为固定长度,然后在发送出去,接收端接收到数据后按照这个固定长度进行拆分即可。发送端程序如下:

// 发送端
String msg = "hello world " + number++;  
socketChannel.write(ByteBuffer.wrap(new FixLengthWrapper(msg).getBytes()));// 封装固定长度的工具类
public class FixLengthWrapper {public static final int MAX_LENGTH = 32;private byte[] data;public FixLengthWrapper(String msg) {ByteBuffer byteBuffer = ByteBuffer.allocate(MAX_LENGTH);byteBuffer.put(msg.getBytes());byte[] fillData = new byte[MAX_LENGTH - msg.length()];byteBuffer.put(fillData);data = byteBuffer.array();}public FixLengthWrapper(byte[] msg) {ByteBuffer byteBuffer = ByteBuffer.allocate(MAX_LENGTH);byteBuffer.put(msg);byte[] fillData = new byte[MAX_LENGTH - msg.length];byteBuffer.put(fillData);data = byteBuffer.array();}public byte[] getBytes() {return data;}public String toString() {StringBuilder sb = new StringBuilder();for (byte b : getBytes()) {sb.append(String.format("0x%02X ", b));}return sb.toString();}
}

可以看到客户端在发送数据前首先把数据封装为长度为32bytes的数据包,这个长度是根据目前实际数据包长度来规定的,这个长度必须要大于所有可能出现的数据包的长度,这样才不会出现把数据“截断”的情况。接收端程序如下:

private static void processByFixLength(SocketChannel socketChannel) throws IOException {  while (socketChannel.read(byteBuffer) > 0) {byteBuffer.flip();while (byteBuffer.remaining() >= FixLengthWrapper.MAX_LENGTH) {byte[] data = new byte[FixLengthWrapper.MAX_LENGTH];byteBuffer.get(data, 0, FixLengthWrapper.MAX_LENGTH);System.out.println(new String(data) + " <---> " + number++);}byteBuffer.compact();}
}

可以看出接收端的处理很简单,只需要每次读取固定的长度即可区分出来不同的数据包。

添加长度首部

这种方式的处理较上面提到的方式稍微复杂一点。在发送端需要给待发送的数据添加固定的首部,然后再发送出去,然后在接收端需要根据这个首部的长度信息进行数据包的组合或拆分,发送端程序如下:

// 发送端
String msg = "hello world " + number++;  
// add the head represent the data length
socketChannel.write(ByteBuffer.wrap(new PacketWrapper(msg).getBytes()));// 添加长度首部的工具类
public class PacketWrapper {private int length;private byte[] payload;public PacketWrapper(String payload) {this.payload = payload.getBytes();this.length = this.payload.length;}public PacketWrapper(byte[] payload) {this.payload = payload;this.length = this.payload.length;}public byte[] getBytes() {ByteBuffer byteBuffer = ByteBuffer.allocate(this.length + 4);byteBuffer.putInt(this.length);byteBuffer.put(payload);return byteBuffer.array();}public String toString() {StringBuilder sb = new StringBuilder();for (byte b : getBytes()) {sb.append(String.format("0x%02X ", b));}return sb.toString();}
}

从程序可以看到,发送端在发送数据前首先给待发送数据添加了代表长度的首部,首部长为4bytes(即int型长度),这样接收端在收到这个数据之后,首先需要读取首部,拿到实际数据长度,然后再继续读取实际长度的数据,即实现了组包和拆包的操作。程序如下:

private static void processByHead(SocketChannel socketChannel) throws IOException {while (socketChannel.read(byteBuffer) > 0) {// 保存bytebuffer状态int position = byteBuffer.position();int limit = byteBuffer.limit();byteBuffer.flip();// 判断数据长度是否够首部长度if (byteBuffer.remaining() < 4) {byteBuffer.position(position);byteBuffer.limit(limit);continue;}// 判断bytebuffer中剩余数据是否足够一个包int length = byteBuffer.getInt();if (byteBuffer.remaining() < length) {byteBuffer.position(position);byteBuffer.limit(limit);continue;}// 拿到实际数据包byte[] data = new byte[length];byteBuffer.get(data, 0, length);System.out.println(new String(data) + " <---> " + number++);byteBuffer.compact();}
}

关键信息已经在程序中做了注释,可以很明显的感觉到这种方法的处理难度相对于固定长度要大一些,不过这种方式可以获取更大的传输效率。

这里需要提醒各位同学一个问题,由于我在测试的时候采用的是一台机器连续发送数据来模拟高并发的场景,所以在测试的时候会发现服务器端收到的数据包的个数经常会小于包的序号,好像发生了丢包。但经过仔细分析可以发现,这种情况是因为TCP发送缓存溢出导致的丢包,也就是这个数据包根本没有发出来。也就是说,发送端发送数据过快,导致接收端缓存很快被填满,这个时候接收端会把通知窗口设置为0从而控制发送端的流量,这样新到的数据只能暂存在发送端的发送缓存中,当发送缓存溢出后,就出现了我上面提到的丢包,这个问题可以通过增大发送端缓存来缓解这个问题,

socketChannel.socket().setSendBufferSize(102400);  

当然这个话题不在本文的讨论范围,如果有兴趣的同学可以参阅《TCP/IP详解卷一》中的拥塞窗口一章。

关于源码说明,源码默认是把粘包和拆包处理这一部分注释掉了,分别位于NIOTcpServer和NIOTcpClient文件中,需要测试粘包和拆包处理程序的同学需要把这一段注释给去掉。

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

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

相关文章

B. Lynyrd Skynyrd(倍增 + 区间最小值)

B. Lynyrd Skynyrd&#xff08;segment tree redouble&#xff09; 给定一个长度为nnn的排列ppp&#xff0c;一个长度为mmm的数组aaa&#xff0c;有mmm次询问&#xff0c;每次询问给定l,rl, rl,r&#xff0c;问在数组aaa中是否存在一个子序列构成的串是ppp的循环位移串&#…

你的通勤时间都去哪了?

大家好&#xff0c;我是Z哥。今天我来唠叨一下。最近无意间看到一份报告&#xff0c;关于我们职场人士上下班通勤时间的。有时候想想也挺无奈的&#xff0c;我们越想去发达一些的城市打拼&#xff0c;反而越被通勤这种琐碎的事情给耽误更多的奋斗时间。但是没办法&#xff0c;在…

规模化敏捷必须SAFe

引子&#xff1a;规模化敏捷转型从来不是一件容易的事情。当只有1-2个敏捷团队进行协同的时候&#xff0c;计划和工作同步是可控的。团队和产品负责人互相聊一聊&#xff0c;基本就能搞清楚需要做什么&#xff0c;一个简单的SOS架构&#xff08;Scrum of Scrums&#xff09;就能…

http1.0 http1.1 http2 之间的区别

一、HTTP基础 1.1 HTTP定义 HTTP协议&#xff08;HyperTextTransferProtocol&#xff0c;超文本传输协议&#xff09;是用于从WWW服务器传输超文本到本地浏览器的传输协议。 1.2 HTTP发展史 1.3 HTTP1.0 早先1.0的HTTP版本&#xff0c;是一种无状态、无连接的应用层协议。 …

B. Alyona and a tree(dsu on tree + bit)

B. Alyona and a tree&#xff08;dsu on tree bit&#xff09; 给定一颗以111号节点为根的树&#xff0c;每个点有点权aia_iai​&#xff0c;边有边权&#xff0c;如果vvv控制了点uuu&#xff0c;当且仅当uuu是vvv的子树中的节点且dis(u,v)≤audis(u, v) \leq a_udis(u,v)≤…

HTTP get post put delte等

超文本传输协议&#xff08;HTTP, HyperText Transfer Protocol&#xff09;是一种无状态的协议&#xff0c;它位于OSI七层模型的传输层。HTTP客户端会根据需要构建合适的HTTP请求方法&#xff0c;而HTTP服务器会根据不同的HTTP请求方法做出不同的响应。 HTTP版本与HTTP请求方…

P1600 [NOIP2016 提高组] 天天爱跑步(线段树合并,lca)

P1600 [NOIP2016 提高组] 天天爱跑步 给定一颗有nnn个点的树&#xff0c;有mmm个人在树上移动&#xff0c;第iii个人从sis_isi​点&#xff0c;移动到tit_iti​点&#xff0c;且他们按照最短路移动&#xff0c;每秒移动一条边的距离&#xff0c; 点iii在wiw_iwi​时刻有一个观…

漫画:程序员一时单身一时爽,一直单身...有点惨

1妹子没吃早饭早啊敲哥&#xff0c;你在吃早饭啊嗯啊&#xff0c;你吃了吗&#xff1f;没呢&#xff0c;早上实在是太赶了&#xff0c;没来得及嗯...那我还是换个地方吃吧免得馋着你。。。2代码重要还是女朋友重要女朋友能哄好&#xff0c;代码能哄好吗&#xff1f;写代码是赚钱…

P3564 [POI2014]BAR-Salad Bar(ST表 + 二分)

P3564 [POI2014]BAR-Salad Bar 给定一个长度为nnn的数组&#xff0c;里面元素只有111跟−1-1−1&#xff0c;问选出一个长度为lenlenlen的区间使得&#xff0c;这个区间的前缀和时刻大于零&#xff0c;后缀和时刻大于零&#xff0c;输出最大长度lenlenlen&#xff0c; 考虑枚…

【招聘(深圳)】迈瑞招.NET 开发Leader和PM

应用开发管理&#xff08;开发leader&#xff09;工作职责&#xff1a;1.负责应用开发小组管理&#xff1b;2.协助项目经理制定项目计划和控制项目进度&#xff1b;3.评估用户需求&#xff0c;设计解决方案、系统功能&#xff0c;并带领开发小组进行交付&#xff1b;4.对开发过…

Java RMI,Socket,HttpClient

Java RMI Java远程方法调用&#xff08;Java Remote Method Invocation&#xff09; 是Java编程语言里&#xff0c;一种用于实现远程过程调用的应用程序编程接口。 它使客户机上运行的程序可以调用远程服务器上的对象。 远程方法调用特性使Java编程人员能够在网络环境中分布操…

E. Sign on Fence(整体二分 + 线段树维护区间最大连续 1 的个数)

E. Sign on Fence 给定一个长度为nnn的数组aaa&#xff0c;1≤ai≤1091 \leq a_i \leq 10 ^ 91≤ai​≤109&#xff0c;有mmm次询问&#xff0c;每次给定l,r,kl, r, kl,r,k&#xff0c;要我们在[l,r][l, r][l,r]区间内找到一个长度为kkk的区间&#xff0c;使得区间最小值最大&…

AutoMapper多个对象映射到一个Dto对象

一、定义源映射对象为了体现AutoMapper映射特性&#xff0c;在SocialAttribute中的Name属性没有定义在People中&#xff0c;People的Ear属性也不存在与SocialAttribute和PhysicalAttribute中。代码如下&#xff1a;二、注入AutoMapper例子中使用的IOC容器是Autofac&#xff0c;…

cookie 与 session

1 背景介绍 什么是会话&#xff1f; 用户打开一个浏览器, 点击多个超链接, 访问服务器多个web资源, 然后关闭浏览器, 整个过程称之为一个会话。我们知道&#xff0c;HTTP协议是一种"无状态"协议&#xff0c;客户浏览器与服务器建立连接&#xff0c;发出请求&#x…

Java写一个简单的静态文件的HTTP服务器(基于Socket)

** 一、实现思路 ** 1、使用 ServerSocket 监听某一端口&#xff0c;然后等待连接获取 Socket对象。 2、创建一个类 HttpServer 继承 java.lang.Thread 类&#xff0c;重写 run()方法&#xff0c;执行浏览器请求。 3、获得浏览器请求&#xff0c;解析资源文件路径。 4、读…

P2839 [国家集训队]middle(二分 套 主席树)

P2839 [国家集训队]middle 有一个长度为nnn的序列&#xff0c;有mmm次询问&#xff0c;每次询问a,b,c,da, b, c, da,b,c,d&#xff0c;为l∈[a,b],r∈[c,d]l \in [a, b], r \in [c, d]l∈[a,b],r∈[c,d]&#xff0c;[l,r][l, r][l,r]区间的中位数最大是多少&#xff0c;强制在…

asp.net core 使用HttpClientFactory Polly实现熔断降级

前言在NET Core2.1后也是增加更新了很多东西,当然HttpClientFactory更新中的一部分.虽然说HttpClient这个实现了disposable,但使用它的时候用using包装块的方式通常不是最好的选择。处理HttpClient,底层socket套接字不会立即释放。该HttpClient类是未多个请求重复使用而创建的。…

.NET生态系统概览

本文要点.NET 5 预计会在 2020 年发布&#xff0c;届时将统一所有.NET 平台。在进行特性开发时优先考虑性能。C#语言的发展直接推动了开发者的采用。开源社区让.NET 生态系统变得更好。2002 年&#xff0c;.NET 发布。在接下来的 12 年多时间里&#xff0c;.NET 开发社区以看似…

P1848 [USACO12OPEN]Bookshelf G(线段树优化 DP)

P1848 [USACO12OPEN]Bookshelf G 有nnn间物品&#xff0c;每个物品有两个属性Wi,HiW_i, H_iWi​,Hi​&#xff0c;宽度跟高度&#xff0c;要求把这nnn件物品划分成若干连续的组&#xff0c;每组内∑Wi≤L\sum\limits W_i \leq L∑Wi​≤L&#xff0c;并且要求最小化每组最大高…

Java 实现 FTP 服务

1、FTP简介 FTP 是File Transfer Protocol&#xff08;文件传输协议&#xff09;的英文简称&#xff0c;而中文简称为“文传协议”。用于Internet上的控制文件的双向传输。同时&#xff0c;它也是一个应用程序&#xff08;Application&#xff09;。基于不同的操作系统有不同的…