TCP是一个面向连接的,可靠的,安全的流式协议
什么是粘包
粘包是指的是数据和数据之间没有没有明确的分界线,导致不能够正确的传输数据(只有TCP会粘包 UDP 永远不会粘包),粘包问题只针对于一切字节流的协议
TCP也可以称为流式协议,UDP称为数据报式协议
对于流式协议:发送端可以1K1K的发送数据,接收端可以2k2k的提取数据,也可以3K4K的提取数据,所以对于接收端应用程序中看到的数据就是一个整体,“数据流”,一条消息里面有多少字节应用程序是看不见的,所以TCP协议面向字节流,就会出现粘包问题,而UDP这种面向消息的协议,每个UDP段都是一条消息,接收方必须以消息为单位进行提取数据,不能一次提取任意字节的数据
所谓的粘包问题就是接收方不知道消息和消息之间的边界,不知道一次提取多少个字节导致的
粘包问题出现的具体原因
应用程序无法直接操作硬件,应用程序想要操作数据必须要将数据交给操作系统,OS会为应用提供数据传输的服务,所以OS不会立刻把数据发出去,会为应用程序提供一个缓冲区,存在临时的数据, 发送方:
当应用程序调用send函数时候,应用程序会将数据从应用程序拷贝到操作系统缓存里面,再由OS从缓冲区里面读数据,把数据发出去
接收方:
对方计算机收到的数据也是OS先收到的,至于应用程序如何处理这些数据,OS不知道,所以同样需要将数据先存储到OS 的缓冲区里面,当应用程序调用recv的时候,实际上是将OS缓冲区里面的数据拷贝到应用程序的过程
粘包问题的解决
服务端如果想要保证每次都能接收到客户端发来的不定长度的数据包,程序员应该如何来解决这个问题呢?
- 使用应用层协议(http,https)来封装要传输的不定长的数据包
- 再每个数据的后面添加一些特殊字符,如果遇到特殊字符,说明这条数据接收完毕了
- 每接收一个字符就要对这些字符进行判定,判定是不是特殊的字符串,效率很低
- 在发送数据快之前,在数据块之前添加一个固定大小的包头:数据头+数据块
- 数据头:存储当前数据包的总字节数,接收端先接收数据头,然后再根据数据头接收对应的大小
- 数据块:当前数据包的内容
解决方案
如果使用TCP进行套接字通信,如果发送的数据包连在了一块,导致接收端无法解析,我们通常使用添加包头的方式来轻松解决这个问题,包头的大小为4个字节(一个int类型),存储当前数据块的总字节数
发送端
- 根据发送的数据长度N****动态申请一个固定大小的内存:N+4(4是包头占用的字节数)
- 将待发送的数据的总长度写入申请的内存的前4个字节(memcpy前4个字节),此处应该先将其转化为网络字节序(大端),再写入
- 将待发送的数据拷贝到包头后面的地址空间中,将完整的数据包发送出去(字符串没有字节序问题)
- 用一个函数来进行发送,把所有的字节全部发送出去
- 释放申请的堆内存空间
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)
{
return -1;
}
else if(len==0)
{
continue;
}
else
{
buf+=len;
count-=len;
}
}
return size;
}
int sendmsg(int cfd,const char* msg,int len)
{
if(cfd<0||msg==nullptr||len<=0)
{
exit(1);
}
char * data=(char*)malloc(sizeof(len+4));
int biglen=htonl(len);
memcpy(data,&biglen,4);
memcpy(data+4,msg,len);
int ret=writen(cfd,data,len+4);
if(ret==-1)
{
close(cfd);
}
free(data);
}
接收端
- 首先先接收4个字节(包头,记录了接收的数据的长度),并将它从网络字节序转化为主机字节序,这样就可以获得这些数据的总长度了
- 根据得到的数据块长度申请固定大小的堆内存,用于存储待接收的信息
- 处理接收的数据
- 释放存储数据的堆内存
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;
}
else
{
pt+=len;
count-=len;
}
}
return size;
}
int recvmsg(int fd,char** msg)
{
int len=0;
readn(fd,(char*)&len,4);
len=ntohl(len);
cout<<"要接收到的数据块的长度为"<<len<<endl;
char* data=(char*)malloc(sizeof(len+1));
int length=readn(fd,data,len);
if(length==len)
{
cout<<"读取成功"<<endl;
}
else
{
cout<<"接收数据失败了"<<endl;
close(fd);
free(data);
return -1;
}
data[len]='\0';
*msg=data;
return length;
}
|