TCP数据粘包的处理
扫描二维码
随时随地手机看文章
1. 背锅侠 TCP
在前面介绍套接字通信的时候说到了 TCP 是传输层协议,它是一个面向连接的、安全的、流式传输协议。因为数据的传输是基于流的所以发送端和接收端每次处理的数据的量,处理数据的频率可以不是对等的,可以按照自身需求来进行决策。TCP 协议是优势非常明显,但是有时也会给我们造成困扰,正所谓:成也萧何败萧何。假设我们有如下需求:客户端和服务器之间要进行基于 TCP 的套接字通信根据上面的描述,服务器在接收数据的时候有如下几种情况:
- 通信过程中客户端会每次会不定期给服务器发送一个不定长度的有特定含义的字符串。
- 通信的服务器端每次都需要接收到客户端这个不定长度的字符串,并对其进行解析。
- 一次接收到了客户端发送过来的一个完整的数据包
- 一次接收到了客户端发送过来的 N 个数据包,由于每个包的长度不定,无法将各个数据包拆开
- 一次接收到了一个或者 N 个数据包 下一个数据包的一部分,还是很悲剧,无法将数据包拆开
- 一次收到了半个数据包,下一次接收数据的时候收到了剩下的一部分 下个数据包的一部分,更悲剧,头大了
- 另外,还有一些不可抗拒的因素:比如客户端和服务器端的网速不一样,发送和接收的数据量也会不一致
- 使用标准的应用层协议(比如:http、https)来封装要传输的不定长的数据包
- 在每条数据的尾部添加特殊字符,如果遇到特殊字符,代表当条数据接收完毕了
- 有缺陷:效率低,需要一个字节一个字节接收,接收一个字节判断一次,判断是不是那个特殊字符串
- 在发送数据块之前,在数据块最前边添加一个固定大小的数据头,这时候数据由两部分组成:数据头 数据块
- 数据头:存储当前数据包的总字节数,接收端先接收数据头,然后在根据数据头接收对应大小的字节
- 数据块:当前数据包的内容
2. 解决方案
如果使用 TCP 进行套接字通信,如果发送的数据包粘连到一起导致接收端无法解析,我们通常使用添加包头的方式轻松地解决掉这个问题。关于数据包的包头大小可以根据自己的实际需求进行设定,这里没有啥特殊需求,因此规定包头的固定大小为4个字节,用于存储当前数据块的总字节数。2.1 发送端
对于发送端来说,数据的发送分为 4 步:- 根据待发送的数据长度 N 动态申请一块固定大小的内存:N 4(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,