流协议与粘包
这么说吧,TCP在传输数据的时候,是不区分边界的(数据和数据之间没有边界),因为是基于字节流,字节流是由字节组成,不包含边界数据的连续流。所以数据对TCP来说就是一大堆没有结构区别的字节块。那意味着什么?意味着TCP并不能对多个数据的整体的信息进行区分(打个比方:就像是你说一堆话没有标点符号全部连在一起,别人很可能弄错)或者对单个整体信息的错误区分(比如你要发送的整块数据被分成了好几块发送,然后这些数据在传输过程中可能由于网络原因,有的大数据块中被分片后的部分片段到了,可能由于接收端缓冲区满了,开始读取,而它们又没有边界之分,这时候就解释错误了)。那样就会使得我们要传输的信息可能粘连在一起,而被计算机解释错误。
而UDP是基于消息的传输服务,传输的是数据报,是有边界的。对于基于消息的协议来说,能保证对等方一次读操作返回的是一条消息,所以不会产生粘包问题。
粘包问题产生的原因
(1)发送方原因
我们知道,TCP默认会使用Nagle算法。而Nagle算法主要做两件事:1)只有上一个分组得到确认,才会发送下一个分组;2)收集多个小分组,在一个确认到来时一起发送。
所以,正是Nagle算法造成了发送方有可能造成粘包现象。
(2)接收方原因
TCP接收到分组时,并不会立刻送至应用层处理,或者说,应用层并不一定会立即处理;实际上,TCP将收到的分组保存至接收缓存里,然后应用程序主动从缓存里读收到的分组。这样一来,如果TCP接收分组的速度大于应用程序读分组的速度,多个包就会被存至缓存,应用程序读时,就会读到多个首尾相接粘到一起的包。
1. 应用程序写入数据的字节大小大于套接字发送缓冲区的大小
2. 进行MSS大小的TCP分段。MSS是最大报文段长度的缩写。MSS是TCP报文段中的数据字段的最大长度。数据字段加上TCP首部才等于整个的TCP报文段。所以MSS并不是TCP报文段的最大长度,而是:MSS=TCP报文段长度-TCP首部长度
3. 以太网的payload大于MTU进行IP分片。MTU指:一种通信协议的某一层上面所能通过的最大数据包大小。如果IP层有一个数据包要传,而且数据的长度比链路层的MTU大,那么IP层就会进行分片,把数据包分成若干片,让每一片都不超过MTU。注意,IP分片可以发生在原始发送端主机上,也可以发生在中间路由器上。
粘包解决方案
本质上是要在应用层维护消息与消息的边界
1.定长包 (对等方接收的时候是以定长的方式接收)
2.在包的尾部加上\r,\n等字符(ftp使用这种策略,如果包的内容中也包含\r,\n,这时候需要用转义字符 \ 处理)
3.包头加上包体长度(接收时先接收包头根据包头计算出包体的长度来接收包体)
4.更复杂的应用层协议
实现方式
首先第一种定长包:
每一个发送的包的长度都定好,无论你一个消息包多大,到我们发送区都划成定长发送,假设我们这个定长为1024(一般编程设定可以取46~1500字节之间,低于46发送时会为你填充至46字节,超过1500传输过程中会由以太网为你分片)。
有几种可能:
1.包长小于1024,消息包最后面的那部分都用空白字节补全,凑齐1024字节这个长度,发送。
2.包长大于1024,这时包被分成多个部分,假设是2049字节,那么分成3个段,编号1:0~1023,编号2:1024~2047,编号3:(2048+填充空白位1023个字节)。最后发送的字节就是3072字节的数据。
由于这样的数据包都是定长度,不会出现像上面那样的一次发送多个具有不同数据结构的消息,也就意味这接受的数据包不能无脑地组合在一起被解释,因为单个数据包都是独立的数据结构。
最大缺陷就是它并不能发挥TCP协议的高效性,而且极大地浪费了网络流量,很多无效数据在网络上传输,不推荐使用
read,write是系统自己实现的库函数;
readn和writen是自己实现的函数。readn,接收确切数目的读操作,writen发送确切数目的写操作
小知识点
ssize_t是有符号整型,在32位机器上等同与int,在64位机器上等同与long int,有没有注意到,它和long数据类型有啥区别?其实就是一样的。size_t 就是无符号型的ssize_t,也就是unsigned long/ unsigned int (在32位下)。
readn函数
//封装read函数,接收确定字节数的读操作
ssize_t readn(int fd,void *buf,size_t count)
{
//剩余字节数
size_t nleft=count;
//已接收字节数
ssize_t nread;
char *bufp=(char*)buf;
//既然不能够保证一次读操作能够返回的字节数是多少,所以需要循环进行接收
while(nleft>0)
{
if((nread=read(fd,bufp,nleft))<0)
{
//被信号中断的情况,这种情况不算出错,所以contiue
if(errno==EINTR)
continue;
return -1;
}
//nread==0意味着对等方关闭了
else if(nread==0)
return count-nleft;
//指针进行偏移
bufp+=nread;
nleft-=nread;
}
return count;
}
writen函数
//封装write函数
ssize_t writen(int fd,void *buf,size_t count)
{
//剩余要发送的字节数
size_t nleft=count;
ssize_t nwritten;
char *bufp=(char*)buf;
//不能保证一次发送就能将所有数据发送成功
while(nleft>0)
{
if((nwritten=write(fd,bufp,nleft))<0)
{
if(errno==EINTR)
continue;
return -1;
}
else if(nwritten==0)
continue;
bufp+=nwritten;
nleft-=nwritten;
}
return count;
}
第二种在包的尾部加上\r,\n等字符
我们常用的ftp服务器就是这样的设计方式,在区分包内的\r和\n等字符时候使用转义字符\解决,但是这种限制了只能做某种特殊的服务,因为我们必须保证结束符的唯一性,在通常的数据传输中,由于用户的输入是不能限定的,什么样的字符都有,所以不能随便应用。
第三种:包头加上包体长度
这是我们应用得最多的一种传输的方式,保证了TCP的高效性,而且解决了粘包问题。
下面来模拟一下这个传输方式:其实就是封装一个发送和接受的函数,然后在接受数据的时候先对接受的包头数据分析,求出包头告诉我们的实际数据的长度,再进行二次接受,那就是我们要的数据
使用此机制完善客户端、服务端
服务端
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
}while(0)
struct packet
{
int len;
char buf[1024];
};
//封装read函数,接收确定字节数的读操作
ssize_t readn(int fd,void *buf,size_t count)
{
//剩余字节数
size_t nleft=count;
//已接收字节数
ssize_t nread;
char *bufp=(char*)buf;
//既然不能够保证一次读操作能够返回的字节数是多少,所以需要循环进行接收
while(nleft>0)
{
if((nread=read(fd,bufp,nleft))<0)
{
//被信号中断的情况,这种情况不算出错,所以contiue
if(errno==EINTR)
continue;
return -1;
}
//nread==0意味着对等方关闭了
else if(nread==0)
return count-nleft;
bufp+=nread;
nleft-=nread;
}
return count;
}
//封装write函数
ssize_t writen(int fd,void *buf,size_t count)
{
//剩余要发送的字节数
size_t nleft=count;
ssize_t nwritten;
char *bufp=(char*)buf;
//不能保证一次发送就能将所有数据发送成功
while(nleft>0)
{
if((nwritten=write(fd,bufp,nleft))<0)
{
if(errno==EINTR)
continue;
return -1;
}
else if(nwritten==0)
continue;
bufp+=nwritten;
nleft-=nwritten;
}
return count;
}
void do_service(int conn)
{
struct packet recvbuf;
int n;
while(1)
{
memset(&recvbuf,0,sizeof(recvbuf));
//接收的时候先接受4个字节,头部长度,也就是包头部分
int ret=readn(conn,&recvbuf.len,4);
if(ret==-1)
ERR_EXIT("read");
else if(ret<4)
{
printf("client close\n");
break;
}
n=ntohl(recvbuf.len);
//接收包体
ret=readn(conn,recvbuf.buf,n);
if(ret==-1)
ERR_EXIT("read");
else if(ret<n)
{
printf("client close\n");
break;
}
fputs(recvbuf.buf,stdout);
writen(conn,&recvbuf,4+n);
}
}
int main(void)
{
int listenfd;
if((listenfd=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)
/*if((listenfd=socket(PF_INET,SOCK_STREAM,0))<0) */
ERR_EXIT("socket");
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(5188);
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
/*servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");*/
/*inet_aton("127.0.0.1,&servaddr.sin_addr");*/
int on=1;
if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)
ERR_EXIT("setsockopt");
if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
ERR_EXIT("bind");
if(listen(listenfd,SOMAXCONN)<0)
ERR_EXIT("listen");
struct sockaddr_in peeraddr;
socklen_t peerlen=sizeof(peeraddr);
int conn;
pid_t pid;
while(1)
{
if((conn=accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen))<0)
ERR_EXIT("accept");
printf("ip=%s port=%d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
pid=fork();
if(pid==-1)
ERR_EXIT("fork");
if(pid==0)
{
close(listenfd);
do_service(conn);
exit(EXIT_SUCCESS);
}
else
close(conn);//不会向客户端发送FIN段,仅仅只是将套接字的引用计数减一
}
return 0;
}
客户端
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
}while(0)
/*
自己定义包的结构,因为定长包的实现中,如果实际的数据包长度只有几个字节,
但定长包是1024字节,这样就会增加网络的负担
*/
struct packet
{
int len;//包头,包头存放的是包体实际的长度
char buf[1024];//包体
};
ssize_t readn(int fd,void *buf,size_t count)
{
size_t nleft=count;
ssize_t nread;
char *bufp=(char*)buf;
while(nleft>0)
{
if((nread=read(fd,bufp,nleft))<0)
{
if(errno==EINTR)
continue;
return -1;
}
else if(nread==0)
return count-nleft;
bufp+=nread;
nleft-=nread;
}
return count;
}
ssize_t writen(int fd,void *buf,size_t count)
{
size_t nleft=count;
ssize_t nwritten;
char *bufp=(char*)buf;
while(nleft>0)
{
if((nwritten=write(fd,bufp,nleft))<0)
{
if(errno==EINTR)
continue;
return -1;
}
else if(nwritten==0)
continue;
bufp+=nwritten;
nleft-=nwritten;
}
return count;
}
int main(void)
{
int sock;
if((sock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)
/*if((listenfd=socket(PF_INET,SOCK_STREAM,0))<0) */
ERR_EXIT("socket");
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(5188);
servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");
/*inet_aton("127.0.0.1,&servaddr.sin_addr");*/
if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
ERR_EXIT("connect");
struct packet sendbuf;
struct packet recvbuf;
memset(&sendbuf,0,sizeof(sendbuf));
memset(&recvbuf,0,sizeof(recvbuf));
int n;
while(fgets(sendbuf.buf,sizeof(sendbuf.buf),stdin)!=NULL)
{
//n是包的长度
n=strlen(sendbuf.buf);
sendbuf.len=htonl(n);
writen(sock,&sendbuf,4+n);
int ret=readn(sock,&recvbuf.len,4);
if(ret==-1)
ERR_EXIT("read");
else if(ret<4)
{
printf("client close\n");
break;
}
n=ntohl(recvbuf.len);
ret=readn(sock,recvbuf.buf,n);
if(ret==-1)
ERR_EXIT("read");
else if(ret<n)
{
printf("client close\n");
break;
}
fputs(recvbuf.buf,stdout);
memset(&sendbuf,0,sizeof(sendbuf));
memset(&recvbuf,0,sizeof(recvbuf));
}
close(sock);
return 0;
}