TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。
出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。
什么时候需要考虑粘包问题?
如果利用tcp每次发送数据,就与对方建立连接,然后双方发送完一段数据后,就关闭连接,这样就不会出现粘包问题(因为只有一种包结构,类似于http协议)。关闭连接主要是要双方都发送close连接(参考tcp关闭协议)。
如:A需要发送一段字符串给B,那么A与B建立连接,然后发送双方都默认好的协议字符如"hello give me sth abour yourself",然后B收到报文后,就将缓冲区数据接收,然后关闭连接,这样粘包问题不用考虑到,因为大家都知道是发送一段字符。
如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包。
如果双方建立连接,需要在连接后一段时间内发送不同结构数据,如连接后,有好几种结构:
1)“hellogive me sth abour yourself”
2)“Don’tgive me sth abour yourself”
那这样的话,如果发送方连续发送这个两个包出去,接收方一次接收可能会是"hellogive me sth abour yourselfDon’t give me sth abour yourself"这样接收方就傻了,到底是要干嘛?不知道,因为协议没有规定这么诡异的字符串,所以要处理把它分包,怎么分也需要双方组织一个比较好的包结构,所以一般可能会在头加一个数据长度之类的包,以确保接收。
粘包出现原因
简单得说,在流传输中出现,UDP不会出现粘包,因为它有消息边界(参考Windows网络编程)
发送端需要等缓冲区满才发送出去,造成粘包
接收方不及时接收缓冲区的包,造成多个包接收
具体点:
发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。
接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。
解决方案
粘包问题在实际的网络编程中是常见的,需要采取一些策略来解决或者减少其影响:
消息边界标记:在发送的消息中加入特定的消息边界标记(如换行符 \n),接收端根据消息边界标记来分割接收到的数据,从而识别出完整的消息。有缺陷: 效率低, 需要一个字节一个字节接收, 接收一个字节判断一次, 判断是不是那个特殊字符串
消息长度固定:发送端将每个消息的长度固定,接收端根据固定长度来分割接收到的数据,从而确保每个接收到的数据包含完整的消息。缺点:容易造成空间浪费
消息头部长度字段:发送端在每个消息前加入一个固定长度的消息头部,包含消息的长度信息,接收端根据头部长度字段来读取对应长度的消息数据。这时候数据由两部分组成:数据头+数据块,数据头:存储当前数据包的总字节数,接收端先接收数据头,然后在根据数据头接收对应大小的字节,数据块:当前数据包的内容
使用标准的应用层协议(比如:http、https)来封装要传输的不定长的数据包
解决方案具体实现
这里我们使用消息头+数据块的解决方案,如果使用TCP进行套接字通信,如果发送的数据包粘连到一起导致接收端无法解析,我们通常使用添加包头的方式轻松地解决掉这个问题。关于数据包的包头大小可以根据自己的实际需求进行设定,这里没有啥特殊需求,因此规定包头的固定大小为4个字节,用于存储当前数据块的总字节数。
发送端设计
对于发送端来说,数据的发送分为以下四步:
动态申请内存: 根据待发送的数据长度 N申请一块大小为 N+4 的内存,其中4个字节用于存储包头信息。
写入包头: 将待发送数据的总长度(N)写入申请的内存的前四个字节中,并将其转换为网络字节序(大端序)。
拷贝数据并发送: 将待发送的数据拷贝到包头后面的地址空间中,然后将整个数据包发送出去。这里需要确保数据包能够完整发送,因此可以设计一个发送函数,确保当前数据包中的数据全部发送完毕。
释放内存: 发送完毕后,释放申请的堆内存。
示例代码:
/*
函数描述: 发送指定的字节数
函数参数:
- 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) {
perror("send");
close(fd);
return -1; // 发送失败
} else if (len == 0) {
continue; // 发送未成功,继续尝试
}
buf += len; // 更新待发送数据的起始地址
count -= len; // 更新剩余待发送的数据字节数
}
return size; // 全部数据发送完毕,返回发送的总字节数
}
/*
函数描述: 发送带有数据头的数据包
函数参数:
- cfd: 通信的文件描述符(套接字)
- msg: 待发送的原始数据
- len: 待发送的原始数据的总字节数
函数返回值: 函数调用成功返回发送的字节数, 发送失败返回-1
*/
int sendMsg(int cfd, const char* msg, int len) {
if (msg == NULL || len <= 0 || cfd <= 0) {
return -1; // 参数无效
}
// 申请内存空间: 数据长度 + 包头4字节(存储数据长度)
char* data = (char*)malloc(len + 4);
if (data == NULL) {
perror("malloc");
return -1; // 内存申请失败
}
// 将数据长度转换为网络字节序(大端序)并存储在包头
int bigLen = htonl(len);
memcpy(data, &bigLen, 4);
// 将待发送的数据拷贝到包头后面
memcpy(data + 4, msg, len);
// 发送带有包头的数据包
int ret = writen(cfd, data, len + 4);
// 释放申请的内存
free(data);
return ret; // 返回发送的字节数
}
接收端设计
在接收端,需要确保每次接收到的都是完整的数据包,避免粘包问题。以下是具体的步骤和代码实现:
接收4字节的包头,并将其从网络字节序转换为主机字节序,得到即将要接收的数据的总长度。
根据总长度申请固定大小的堆内存,用于存储待接收的数据。
根据数据块长度接收固定数量的数据并保存到申请的堆内存中。
处理接收的数据。
释放存储数据的堆内存。
示例代码:
/*
函数描述: 接收指定的字节数
函数参数:
- 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) {
perror("recv");
return -1; // 接收失败
} else if (len == 0) {
return size - count; // 对方关闭连接,返回已接收的字节数
}
pt += len; // 更新缓冲区指针
count -= len; // 更新剩余需要接收的字节数
}
return size; // 返回实际接收的字节数
}
/*
函数描述: 接收带数据头的数据包
函数参数:
- cfd: 通信的文件描述符(套接字)
- msg: 一级指针的地址,函数内部会给这个指针分配内存,用于存储待接收的数据,这块内存需要使用者释放
函数返回值: 函数调用成功返回接收的字节数, 接收失败返回-1
*/
int recvMsg(int cfd, char** msg) {
// 接收数据头(4个字节)
int len = 0;
if (readn(cfd, (char*)&len, 4) != 4) {
return -1; // 接收数据头失败
}
// 将数据头从网络字节序转换为主机字节序,得到数据长度
len = ntohl(len);
printf("数据块大小: %d\n", len);
// 根据读出的长度分配内存,多分配1个字节用于存储字符串结束符'\0'
char* buf = (char*)malloc(len + 1);
if (buf == NULL) {
perror("malloc");
return -1; // 内存分配失败
}
// 接收数据
int ret = readn(cfd, buf, len);
if (ret != len) {
close(cfd);
free(buf);
return -1; // 接收数据失败
}
buf[len] = '\0'; // 添加字符串结束符
*msg = buf;
return ret; // 返回接收的字节数
}