TCP数据粘包的处理

TCP数据粘包的处理

  • 背锅侠TCP
  • 解决方案
    • 2.1 发送端
    • 2.2 接收端

背锅侠TCP

在前面介绍套接字通信的时候说到了TCP是传输层协议,它是一个面向连接的、安全的、流式传输协议。因为数据的传输是基于流的所以发送端和接收端每次处理的数据的量,处理数据的频率可以不是对等的,可以按照自身需求来进行决策。

TCP协议是优势非常明显,但是有时也会给我们造成困扰,正所谓:成也萧何败萧何。假设我们有如下需求:

客户端和服务器之间要进行基于TCP的套接字通信

  • 通信过程中客户端会每次会不定期给服务器发送一个不定长度的有特定含义的字符串。
  • 通信的服务器端每次都需要接收到客户端这个不定长度的字符串,并对其进行解析

根据上面的描述,服务器在接收数据的时候有如下几种情况:

  • 一次接收到了客户端发送过来的一个完整的数据包
  • 一次接收到了客户端发送过来的N个数据包,由于每个包的长度不定,无法将各个数据包拆开
  • 一次接收到了一个或者N个数据包 + 下一个数据包的一部分,还是很悲剧,无法将数据包拆开
  • 一次收到了半个数据包,下一次接收数据的时候收到了剩下的一部分+下个数据包的一部分,更悲剧,头大了
  • 另外,还有一些不可抗拒的因素:比如客户端和服务器端的网速不一样,发送和接收的数据量也会不一致

对于以上描述的现象很多时候我们将其称之为TCP的粘包问题但是这种叫法不太对的,本身TCP就是面向连接的流式传输协议,特性如此,我们却说是TCP这个协议出了问题,这只能说是使用者的无知。多个数据包粘连到一起无法拆分是我们的需求过于复杂造成的,是程序猿的问题而不是协议的问题,TCP协议表示这锅它不想背。

现在问题来了,服务器端如果想保证每次都能接收到客户端发送过来的这个不定长度的数据包,程序猿应该如何解决这个问题呢?下面给大家提供几种解决方案:

  1. 使用标准的应用层协议(比如:http、https)来封装要传输的不定长的数据包
  2. 在每条数据的尾部添加特殊字符, 如果遇到特殊字符, 代表当条数据接收完毕了
    • 有缺陷: 效率低, 需要一个字节一个字节接收, 接收一个字节判断一次, 判断是不是那个特殊字符串
  3. 在发送数据块之前, 在数据块最前边添加一个固定大小的数据头, 这时候数据由两部分组成:数据头+数据块
    • 数据头:存储当前数据包的总字节数,接收端先接收数据头,然后在根据数据头接收对应大小的字节
    • 数据块:当前数据包的内容

解决方案

如果使用TCP进行套接字通信,如果发送的数据包粘连到一起导致接收端无法解析,我们通常使用添加包头的方式轻松地解决掉这个问题。关于数据包的包头大小可以根据自己的实际需求进行设定,这里没有啥特殊需求,因此规定包头的固定大小为4个字节,用于存储当前数据块的总字节数。

在这里插入图片描述

2.1 发送端

对于发送端来说,数据的发送分为4步:

  1. 根据待发送的数据长度N动态申请一块固定大小的内存:N+4(4是包头占用的字节数)
  2. 将待发送数据的总长度写入申请的内存的前四个字节中,此处需要将其转换为网络字节序(大端)
  3. 待发送的数据拷贝到包头后边的地址空间中,将完整的数据包发送出去(字符串没有字节序问题)
  4. 释放申请的堆内存。

由于发送端每次都需要将这个数据包完整的发送出去,因此可以设计一个发送函数,如果当前数据包中的数据没有发送完就让它一直发送,处理代码如下:

/*
函数描述: 发送指定的字节数
函数参数:- fd: 通信的文件描述符(套接字)- msg: 待发送的原始数据- size: 待发送的原始数据的总字节数
函数返回值: 函数调用成功返回发送的字节数, 发送失败返回-1
*/
int writen(int fd, const char* msg, int size)
{const char* buf = msg;int count = size;while (count > 0){int len = send(fd, buf, count, 0);if (len == -1){close(fd);return -1;}else if (len == 0){continue;}buf += len;count -= len;}return size;
}

有了这个功能函数之后就可以发送带有包头的数据块了,具体处理动作如下:

/*
函数描述: 发送带有数据头的数据包
函数参数:- cfd: 通信的文件描述符(套接字)- msg: 待发送的原始数据- len: 待发送的原始数据的总字节数
函数返回值: 函数调用成功返回发送的字节数, 发送失败返回-1
*/
int sendMsg(int cfd, char* msg, int len)
{if(msg == NULL || len <= 0 || cfd <=0){return -1;}// 申请内存空间: 数据长度 + 包头4字节(存储数据长度)char* data = (char*)malloc(len+4);int bigLen = htonl(len);memcpy(data, &bigLen, 4);memcpy(data+4, msg, len);// 发送数据int ret = writen(cfd, data, len+4);// 释放内存free(data);return ret;
}

关于数据的发送最后再次强调:字符串没有字节序问题,但是数据头不是字符串是整形,因此需要从主机字节序转换为网络字节序再发送。

完整的放在一起如下

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>/*
函数描述: 发送指定的字节数
函数参数:- fd: 通信的文件描述符(套接字)- msg: 待发送的原始数据- size: 待发送的原始数据的总字节数
函数返回值: 函数调用成功返回发送的字节数, 发送失败返回-1
msg是要发送的字符串指针,size是要发送的字符串的长度
再次while循环的时候,已经发送了len长度,指针后移len长度,发送的字符串长度也减len
*/
int writen(int fd, const char* msg, int size)
{const char* buf = msg;int count = size;while (count > 0){int len = send(fd, buf, count, 0);if (len == -1)   // 表示发送出错,关闭文件描述符并返回-1。{close(fd);return -1;}else if (len == 0)  // 表示没有发送任何数据{continue;}buf += len;count -= len;}return size;
}/*
函数描述: 发送带有数据头的数据包
函数参数:- cfd: 通信的文件描述符(套接字)- msg: 待发送的原始数据- len: 待发送的原始数据的总字节数
函数返回值: 函数调用成功返回发送的字节数, 发送失败返回-1
*/
int sendMsg(int cfd, char* msg, int len)
{if(msg == NULL || len <= 0 || cfd <=0){return -1;}// 申请内存空间: 数据长度 + 包头4字节(存储数据长度)char* data = (char*)malloc(len+4);int bigLen = htonl(len);memcpy(data, &bigLen, 4);memcpy(data+4, msg, len);// 发送数据int ret = writen(cfd, data, len+4);// 释放内存free(data);return ret;
}

2.2 接收端

了解了套接字的发送端如何发送数据,接收端的处理步骤也就清晰了,具体过程如下:

  1. 首先接收4字节数据,并将其从网络字节序转换为主机字节序,这样就得到了即将要接收的数据的总长度
  2. 根据得到的长度申请固定大小的堆内存,用于存储待接收的数据
  3. 根据得到的数据块长度接收固定数目的数据保存到申请的堆内存中
  4. 处理接收的数据
  5. 释放存储数据的堆内存

从数据包头解析出要接收的数据长度之后,还需要将这个数据块完整的接收到本地才能进行后续的数据处理,因此需要编写一个接收数据的功能函数,保证能够得到一个完整的数据包数据,处理函数实现如下:

/*
函数描述: 接收指定的字节数
函数参数:- fd: 通信的文件描述符(套接字)- buf: 存储待接收数据的内存的起始地址- size: 指定要接收的字节数
函数返回值: 函数调用成功返回发送的字节数, 发送失败返回-1
*/
int readn(int fd, char* buf, int size)
{char* pt = buf;int count = size;while (count > 0){int len = recv(fd, pt, count, 0);if (len == -1){return -1;}else if (len == 0){return size - count;}pt += len;count -= len;}return size;
}

这个函数搞定之后,就可以轻松地接收带包头的数据块了,接收函数实现如下:

/*
函数描述: 接收带数据头的数据包
函数参数:- cfd: 通信的文件描述符(套接字)- msg: 一级指针的地址,函数内部会给这个指针分配内存,用于存储待接收的数据,这块内存需要使用者释放
函数返回值: 函数调用成功返回接收的字节数, 发送失败返回-1
*/
int recvMsg(int cfd, char** msg)
{// 接收数据// 1. 读数据头int len = 0;readn(cfd, (char*)&len, 4);len = ntohl(len);printf("数据块大小: %d\n", len);// 根据读出的长度分配内存,+1 -> 这个字节存储\0char *buf = (char*)malloc(len+1);int ret = readn(cfd, buf, len);if(ret != len){close(cfd);free(buf);return -1;}buf[len] = '\0';*msg = buf;return ret;
}

这样,在进行套接字通信的时候通过调用封装的sendMsg()和recvMsg()就可以发送和接收带数据头的数据包了,而且完美地解决了粘包的问题。

完整的放在一起如下

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>/*
函数描述: 接收指定的字节数
函数参数:- fd: 通信的文件描述符(套接字)- buf: 存储待接收数据的内存的起始地址- size: 指定要接收的字节数
函数返回值: 函数调用成功返回发送的字节数, 发送失败返回-1
*/
int readn(int fd, char* buf, int size)
{char* pt = buf;int count = size;while (count > 0){int len = recv(fd, pt, count, 0);if (len == -1)  // -1:接收数据失败了{return -1;}else if (len == 0)  //等于0:对方断开了连接{return size - count;}pt += len;count -= len;}return size;
}/*
函数描述: 接收带数据头的数据包
函数参数:- cfd: 通信的文件描述符(套接字)- msg: 一级指针的地址,函数内部会给这个指针分配内存,用于存储待接收的数据,这块内存需要使用者释放
函数返回值: 函数调用成功返回接收的字节数, 发送失败返回-1
*/
int recvMsg(int cfd, char** msg)
{// 接收数据// 1. 读数据头int len = 0;readn(cfd, (char*)&len, 4);len = ntohl(len);printf("数据块大小: %d\n", len);// 根据读出的长度分配内存,+1 -> 这个字节存储\0char *buf = (char*)malloc(len+1);int ret = readn(cfd, buf, len);if(ret != len){close(cfd);free(buf);return -1;}buf[len] = '\0';*msg = buf;return ret;
}

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

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

相关文章

周周爱学习之Redis重点总结

redis重点总结 在正常的业务流程中&#xff0c;用户发送请求&#xff0c;然后到缓存中查询数据。如果缓存中不存在数据的话&#xff0c;就会去数据库查询数据。数据库中有的话&#xff0c;就会更新缓存然后返回数据&#xff0c;数据库中也没有的话就会给用户返回一个空。 1.缓…

Cephadm部署使用rgw对象网关(s3cmd和Java)

文章目录 前提重要概念部署rgw使用对象存储&#xff08;s3cmd&#xff09;创建radosgw user安装s3cmd配置s3cmds3cmd使用 使用对象存储&#xff08;Java代码&#xff09;引入依赖编码测试 参考 前提 RGW的部署基于以下集群&#xff0c;集群信息如下&#xff1a; 重要概念 区域…

网页设计中增强现实的兴起

目录 了解增强现实 增强现实的历史背景 AR 和网页设计的交叉点 AR 在网页设计中的优势 增强参与度和互动性 个性化的用户体验 竞争优势和品牌差异化 AR 在网页设计中的用例 结论 近年来&#xff0c;增强现实已成为一股变革力量&#xff0c;重塑了我们与数字领域互动的方式。它被…

科研绘图配色方案

科研绘图配色方案 在撰写论文的时候&#xff0c;美观&#xff0c;大气&#xff0c;上档次的图表能够很好地给自己的论文加分。但是在绘制图表的时候往往会面临色彩搭配的问题&#xff0c;选择合适的色彩搭配能够有效地展示自己的方法&#xff0c;但是色彩搭配选择不当的话往往会…

coding创建远程分支。并拉取远程新分支+推送代码

进入coding ----项目----代码仓库---点击 下拉之后查看全部----创建分支 创建分支之后执行下面命令 git branch -a // 查看所有分支 这个时候发现自己创建的分支没有显示这是因为自己在远程创建了分支但是本地还没有分支 执行 git fetch命令 用于从远程仓库获取最新的提交…

《深入理解计算机系统》学习笔记 - 第四课 - 浮点数

Floating Point 浮点数 文章目录 Floating Point 浮点数分数二进制示例能代表的数浮点数的表示方式浮点数编码规格化值规格化值编码示例 非规格化的值特殊值 示例IEEE 编码的一些特殊属性四舍五入&#xff0c;相加&#xff0c;相乘四舍五入四舍五入的模式二进制数的四舍五入 浮…

2024 年甘肃省职业院校技能大赛中职组 电子与信息类“网络安全”赛项竞赛样题-A

2024 年甘肃省职业院校技能大赛中职组 电子与信息类“网络安全”赛项竞赛样题-A 目录 2024 年甘肃省职业院校技能大赛中职组 电子与信息类“网络安全”赛项竞赛样题-A 需要环境或者解析可以私信 &#xff08;二&#xff09;A 模块基础设施设置/安全加固&#xff08;200 分&…

golang学习笔记——sync.Pool

文章目录 sync.Pool示例sync.Pool数据结构TCP连接池总结参考资料 sync.Pool示例 代码 sync.Pool对外提供的New、Get和Put方法。 var buffers sync.Pool{New: func() interface{} { return new(bytes.Buffer)}, }func GetBuffer() *bytes.Buffer {return buffers.Get().(*byt…

怎么查看mysql Connector/J实现的JDBC规范的版本号

打开mysql-connector-j的jar包&#xff0c;例如mysql-connector-j-8.2.0.jar&#xff0c;在Jar包的META-INF目录下面有个MANIFEST.MF文件&#xff0c;打开该文件&#xff1a; 文件内容中Specification-Version: 4.2这一项&#xff0c;就代表实现的JDBC规范的版本号&#xff0c…

忘记PDF密码了,怎么办?

PDF文件有两种密码&#xff0c;一个打开密码、一个限制编辑密码&#xff0c;因为PDF文件设置了密码&#xff0c;那么打开、编辑PDF文件就会受到限制。忘记了PDF密码该如何解密&#xff1f; PDF和office一样&#xff0c;可以对文件进行加密&#xff0c;但是没有提供恢复密码的功…

Leetcode刷题笔记题解(C++):25. K 个一组翻转链表

思路&#xff1a;利用栈的特性&#xff0c;K个节点压入栈中依次弹出组成新的链表&#xff0c;不够K个节点则保持不变 /*** struct ListNode {* int val;* struct ListNode *next;* ListNode(int x) : val(x), next(nullptr) {}* };*/ #include <stack> class Solution { …

上位机与PLC:ModbusTCP通讯之数据类型转换

前请提要: 从PLC读取的数值,不管是读正负整数还是正负浮点数,读取过来后都会变成UInt16,也就是Ushort类型 一、ushort(UInt16)转成 Int32 源代码方法: //ushort类型转Int32类型的方法private int ushortToInt32(ushort[] date, int start){//先进行判断,长度是否正确…

[VSCode] Java开发环境配置

文章目录 1 VSCode & Java 安装1.1 安装 VSCode1.2 安装 JDK 2 环境变量配置3 在 VSCode 中安装 Java 扩展4 运行测试 1 VSCode & Java 安装 1.1 安装 VSCode Visual Studio Code 官方下载 地址&#xff1a; https://code.visualstudio.com/详细安装步骤这里不做赘…

全面高压化与全面超快充,破解新能源汽车的时代难题

是什么让新能源车主感到疲惫与焦虑&#xff1f;是什么阻挡更多消费者选择新能源汽车&#xff1f;我们在身边进行一个简单的调查就会发现&#xff0c;问题的答案非常一致&#xff1a;充电。 充电难&#xff0c;充电慢的难题&#xff0c;始终是困扰新能源汽车产业发展&#xff0c…

【Flutter】vs2022上开发flutter

在vs上开发flutter&#xff0c;结果扩展仓库上没办法找到Dart&#xff0c;Flutter。 在 这 搜索Dart时也无法找到插件。 最后发现是安装工具出错了 安装了 开发需要的是

CentOS7 部署PostgreSQL

参考文档&#xff1a;https://www.postgresql.org/download/linux/redhat/ 1. 配置yum源 yum install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm2. 安装PostgreSQL13 yum install -y postgresql13-server3…

C++包管理利器CPM

C包管理利器CPM 一、介绍 CPM.cmake is a cross-platform CMake script that adds dependency management capabilities to CMake. It’s built as a thin wrapper around CMake’s FetchContent module that adds version control, caching, a simple API and more. CPM.cma…

minio的k8s的单机部署

minio的k8s的单机部署 apiVersion: apps/v1 kind: Deployment metadata:name: minionamespace: itshare spec:replicas: 1selector:matchLabels:app: miniotemplate:metadata:labels:app: miniospec:containers:- name: minioimage: minio/minio:RELEASE.2022-10-15T19-57-03Z…

MySQL - 表达式With as 语句的使用及练习

目录 8.1 WITH AS 的含义 8.2 WITH AS语法的基本结构如下&#xff1a; 8.3 练习题1 8.4 牛客练习题 8.1 WITH AS 的含义 WITH AS 语法是MySQL中的一种临时结果集&#xff0c;它可以在SELECT、INSERT、UPDATE或DELETE语句中使用。通过使用WITH AS语句&#xff0c;可以将一个查…

DAPP开发【10】express.js的使用

Express.js 是一种流行、轻量级的开源 Web 应用程序框架&#xff0c;用于开发基于 Node.js 的服务器端 Web 应用程序。它提供了强大的功能集&#xff0c;适用于 Web 和移动应用程序。Express.js 旨在支持单页、多页和混合式 Web 应用程序的开发。Express.js 提供了广泛的功能&a…