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;//buf指向的是msg的首地址
int count=size;//剩余的长度没有被发送出去的字节数
while(count>0)
{
//不停的进行数据发送
int len=send(fd,buf,count,0);//send成功返回发送出去的字节数,否则失败返回-1,fd为向哪一个文件描述符里面发送
//buf是发送的数据
//count是数据的长度
if(len==-1)
{
//发送失败
return -1;
}
else if(len==0)
{
//一个字节都没有发送出去
continue;//再发送一次
}
else
{
buf+=len;//buf这个指针往后移动,后面buf就全部发送了出去
count-=len;//count为剩余的字节数,变成0的话就发送完成了
}
}
return size;//发送成功
}
//这个加包头的操作就是这样了,其他客户端该怎么发还是怎么发送
//发送数据
//len为数据的大小
//msg为发送的数据
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));//先动态申请一些内存,+4是为了存数据头
int biglen=htonl(len);//把要发送的数据的长度先转化成网络字节序
memcpy(data,&biglen,4);//把biglen的浅4个字节拷贝到data里面
//把我们需要的数据也拷贝到这一个内存里面去
memcpy(data+4,msg,len);
//数据拷贝完之后就要发送数据了
int ret=writen(cfd,data,len+4);//+4是因为要加上这个数据的包头,把data传过去发送,连通它的头
if(ret==-1)
{
close(cfd);//函数调用失败,把文件描述符关掉
}
//发送完之后再把内存给释放掉
free(data);
}
接收端
- 首先先接收4个字节(包头,记录了接收的数据的长度),并将它从网络字节序转化为主机字节序,这样就可以获得这些数据的总长度了
- 根据得到的数据块长度申请固定大小的堆内存,用于存储待接收的信息
- 处理接收的数据
- 释放存储数据的堆内存
//接收端
//接收指定字节个数
int readn(int fd,char* buf,int size)//buf里面就是我们要把数据读取到的地方
{
//我们需要往buf这个内存地址里面写数据了,所以不能加const
//我们需要记录还需要读取多少个字节,以及读取到的位置
char* pt=buf;
int count=size;//我们剩余要接收的字节数
while(count>0)
{
int len=recv(fd,pt,count,0);//pt我们需要读取的地址,count就是我们需要读取的字节数,len就是实际读取到的长度
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)//这里的msg是一个输出型参数
{
//我们需要先把数据头给读出来,看它的数据是有多少的数据
int len=0;
readn(fd,(char*)&len,4);//我们把数据读取到len里面
//现在还是网络字节序,我们需要将它转化为主机字节序
len=ntohl(len);
cout<<"要接收到的数据块的长度为"<<len<<endl;
//根据我们读取到的长度len(有效数据的大小)来分配长度
char* data=(char*)malloc(sizeof(len+1));//+1是‘\0’,字符串结束的标志
//再去调用这个函数
int length=readn(fd,data,len);//我们要接收的数据长度是len
if(length==len)
{
cout<<"读取成功"<<endl;
}
else
{
//接收数据失败了
cout<<"接收数据失败了"<<endl;
close(fd);
free(data);//因为接收失败了,所以这块内存就没有意义了
return -1;
}
data[len]='\0';
*msg=data;
return length;
}