TCP粘包原因分析以及解决方案
一、TCP粘包简介
使用TCP
协议进行数据传输时,多个数据包被连续存储于缓存中,在对数据包进行读取时由于无法确定发送方的发送边界,而采用某一估测值大小来进行数据读取,使得发送方发送的若干个数据包到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。

从上图中可以看出粘包主要分为两种情况:
多个完整的数据包粘在一起
一个完整的数据包被拆分成两部分与其它完整包粘在一起
TCP粘包问题会影响到有数据结构的数据包,会导致数据包解析出现问题.
二、TCP粘包原因分析
TCP
协议是面向连接的、可靠的、基于字节流的传输层通信协议出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。
粘包的根本原因
原因 | 说明 |
---|---|
TCP是字节流协议 | 数据像水流一样没有固定边界,应用层需自行划分。 |
Nagle算法 | 默认开启,会合并多个小数据包发送(减少网络拥塞)。 |
缓冲区机制 | 发送缓冲区积累到一定大小或超时才会发送;接收缓冲区可能一次性读取多个包。 |
发送方引起的粘包是由TCP协议本身造成的
TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP
会根据优化算法
(Nagle)
把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。

发送方一次性发送的数据大于
MTU
,则会发生拆包,将字节流进行切片分成多个包进行发送

接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象
这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。
三、TCP解决方案
方式一 : 使用定长数据包,每次必须要读取固定长度的数据,(适用于数据长度固定的情景)
方式二:使用数据长度+数据的方式,先接收数据长度,再根据长度接收数据,(适用于数据长读度不固定的情景)
三, TCP粘包使用方式二解决实现
发送时,将数据长度+数据
作为数据包的内容,整体发送:
客户端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
int main(int argc,char* argv[])
{
// 创建socket套接字的
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1)
{
perror("socket");
exit(EXIT_FAILURE);
}
// 与服务器进行连接
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port=htons(atoi(argv[2]));
inet_aton(argv[1],&(addr.sin_addr));
int ret = connect(sockfd,(const struct sockaddr*)&addr,sizeof(struct
sockaddr_in));
if(ret==-1 )
{
perror("connect");
close(sockfd);
exit(EXIT_FAILURE);
}
// 向服务器端发送数据
char buf[]={"hello tcp!"};
for(int i=0;i<1000;i++)
{
int ret;
// 1、计算数据长度
int length = strlen(buf);
// 2、开辟空间
char* p_buf=(char*)malloc(length+4);// 使用整型来保存数据长度
// 3、对指定的空间赋值
memcpy(p_buf,&length,4);
memcpy(p_buf+4,buf,length);
//4、发送数据
ret = send(sockfd,p_buf,length+4,0);
if(ret == -1)
{
perror("send failed");
exit(EXIT_FAILURE);
}
}
close(sockfd);
return 0;
}
服务器端:
接收时,先接收数据长度,再根据数据长度固定接收数据:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <strings.h>
#include <arpa/inet.h>
#include <unistd.h>
#define BACKLOG 20
int main(int argc,char* argv[])
{
if(argc !=3)
{
fprintf(stderr,"%s ip port.\n",argv[0]);
exit(EXIT_FAILURE);
}
// 创建socket
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1)
{
perror("socket failed.");
exit(EXIT_FAILURE);
}
// 绑定ip+端口号
struct sockaddr_in addr;
int addrlen = sizeof(struct sockaddr_in);
bzero(&addr,addrlen);
addr.sin_family = AF_INET;
addr.sin_port=htons(atoi(argv[2]));
inet_aton(argv[1],&(addr.sin_addr));
int ret = bind(sockfd,(const struct sockaddr*)&addr,addrlen);
if(ret == -1)
{
perror("bind failed");
exit(EXIT_FAILURE);
}
// 建立监听队列
ret = listen(sockfd,BACKLOG);
if(ret == -1)
{
perror("listen failed");
exit(EXIT_FAILURE);
}
//建立连接
struct sockaddr_in client_addr;
int client_addrlen = sizeof(struct sockaddr_in);
int cfd = accept(sockfd,(struct
sockaddr*)&client_addr,&client_addrlen);
if(cfd == -1)
{
perror("accept failed.");
exit(EXIT_FAILURE);
}
// 打印客户端的信息
ssize_t rbytes = 0;
char buf[1024]={0};
while(1)
{
int length = 0;
int total_received = 0;//已经接收的字节数
// 1、先接收数据长度
rbytes = recv(cfd,&length,4,0);
if(rbytes == -1)
{
perror("recv data length failed");
exit(EXIT_FAILURE);
}
memset(buf,0,sizeof(buf));
// 接收客户端的消息(注意:使用新的文件标识符)
while(1)
{
printf("length=%d\n",length);
rbytes = recv(cfd,buf+total_received,lengthtotal_received,0);
if(rbytes==-1)
{
perror("server recv failed.");
exit(EXIT_FAILURE);
}
else if(rbytes == 0)
{
fprintf(stderr,"stream socket shutdown.\n");
exit(EXIT_FAILURE);
}
else
{
total_received += rbytes;
if(total_received == length)//接收完成
{
printf("recv message: %s\n",buf);
break;
}
}
}
// 休眠一秒,发送数据比接收数据快
sleep(1);
}
return 0;
}
运行结果:
