tcp中粘包的产生以及处理

流协议与粘包

这么说吧,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;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值