《LINUX网络编程》TCP粘包问题的处理
TCP协议概述
TCP是传输层的协议,它是一个面向连接,安全的,流式传输协议。
- 面向连接:TCP在传输数据之前,在两个设备之间建立连接。这种连接确保了数据包的可靠和有序传递。
- 可靠性:TCP通过使用确认和重传来确保数据的可靠传递。当发送方发送一个数据包时,它期望接收方发送确认。如果在一定时间内未收到确认,发送方将重新发送数据包。
- 流量控制:TCP实现了流量控制机制,防止快速发送方压倒较慢的接收方。通过滑动窗口的过程,TCP动态调整可以发送的未确认数据包的数量。
- 有序数据传递:TCP保证数据包的有序传递。如果发送多个数据包,接收方将按照相同的顺序接收和重新组装它们。
- 错误检测:TCP使用校验和来检测传输数据中的错误。校验和在发送方计算,在接收方验证,以确保数据的完整性。
- 全双工通信:TCP支持全双工通信,允许数据在设备之间的双向传输(双向通信)。
- 端口号:TCP使用端口号来标识运行在设备上的特定进程或服务。这使得多个应用程序可以同时使用TCP,通过端口号来区分它们之间的区别。
因为数据传输基于流的所以发送端和接收端每次处理的数量,处理数据的频率是不对等的,传递的数据是没有消息边界的。
TCP通信问题
如果客户端和服务器端要进行基于TCP的套接字通信
- 通信过程中客户端每次会不定期给服务器发送一个不定长度的有特定含义的字符串
- 通信的服务器端每次都需要接收客户端这个不定长度的字符串并对其进行解析
根据上面的描述,我们可能会遇到以下几种情况:
-
一次接收客户端发出的完整的包
-
一次接收多个数据包,长度不定,无法拆开
-
一次接收到数据包+下一个数据包的一部分
-
其他因素导致接收和发送的数据速度不一样
对于上述问题我们称之为TCP的粘包问题。
问题分析
此类问题有以下几种解决方案
- 使用标准的应用层协议来封装不定长度的数据包
- 在每条数据的尾部添加特殊字符,如果遇到特殊字符,代表当条数据接收完毕
- 在发送数据块之前,在数据块最前边添加一个固定大小的数据头,这时候数据由俩个部分组成:数据头+数据块
分析实现
这里我们采用第三种方法来解决这个问题,即在数据前面添加数据头
客户端思路
在数据块前添加头部字符,指定数据块的大小
- 首先封装自己的write函数(往后移)
- 使用send函数发送数据的返回值查看该数据的字节大小,发生错误返回-1
- 封装自己的发送数据函数sendMsg函数
- 首先检查错误情况,包括fd<0|msg==NULL|len<=
- 开辟地址空间,len+4
- 字节序转换htonl(数据头需要转换)
- 字符串复制memcpy
- 调用writen函数,返回成功发送的字节数,释放内存
发送端每次都需要将数据包的长度完整的发送出去,因此设计一个发送函数,如果数据没有完整发送举一直让他发送,代码如下:
/*函数条用成功返回发送的字节数,发送失败返回-1*/
int writen(int fd,const char *msg,int size)
{
const char *buf=msg;
int cnt=size;
while(cnt>0)
{
int len=send(fd,buf,cnt,0);//发送字节数大小
if(len==-1)//发生错误
{
close(fd);
return -1;
}else if(len==0)
{
continue;
}
buf+=len;
cnt-=len;//计算剩余待发送量
}
return size;//返回成功发送的字节数
}
再封装一个发送带有包头数据块的函数
int sendMsg(int cfd,const char *msg,int len)
{
if(cfd<0||msg==NULL||len<=0)
{
return -1;
}
//开辟地址空间
char *data=(char *)malloc(len+4);
//字节序转换
int biglen=htonl(len);
//字符串复制
memcpy(data,&biglen,4);//将数据头先复制到data中
memcpy(data+4,msg,len);//将后续字符串复制到data中
//发送数据
int ret=writen(cfd,data,len+4);
//释放内存空间
free(data);
return ret;//返回成功发送的字节数
}
服务器端思路
首先接收4字节数据
根据得到的长度申请固定长度的堆内存,用于存储待接收的数据
根据得到的数据长度接收固定数目的数据保存在申请的堆内存中
处理接收的数据
释放存储数据的堆内存
从数据包头解析出数据长度后还要对数据进行后续的处理,因此封装一个接收数据的函数
int readn(int fd,char *buf,int size)
{
char *pt=buf;
int cnt=size;
while(cnt>0)
{
int len=recv(fd,pt,cnt,0);
if(len==-1)
{
return -1;
}else if(len==0)//表示连接结束
{
return size-cnt;
}
pt+=len;
cnt-=len;
}
return size-cnt;//返回发送的字节数
}
接收包头的函数如下:
int recvMsg(int cfd,char** msg)//接受带数据头的数据包
{
//接收数据头
int len=0;
readn(cfd,(char *)&len,4);
len=ntohl(len);
// printf("数据块大小为%d\n",len);
char *buf=(char *)malloc(len+1);//留出存储'\0'的位置
int ret=readn(cfd,buf,len);
/*if(ret!=len)
{
printf("数据接收失败\n");
}else if(ret==0){
printf("对方断开连接\n");
close(cfd);
}*/
buf[len]='\0';
*msg=buf;
return ret;//返回接收的字节数
}