zoukankan      html  css  js  c++  java
  • Socket编程--TCP粘包问题

    • TCP是个流协议,它存在粘包问题
      •   产生粘包的原因是:
        •   TCP所传输的报文段有MSS的限制,如果套接字缓冲区的大小大于MSS,也会导致消息的分割发送。
        •   由于链路层最大发送单元MTU,在IP层会进行数据的分片。
        •   应用层调用write方法,将应用层的缓冲区中的数据拷贝到套接字的发送缓冲区。而发送缓冲区有一个SO_SNDBUF的限制,如果应用层的缓冲区数据大小大于套接字发送缓冲区的大小,则数据需要进行多次的发送。


    • 粘包问题的解决
      •   ①:发送定长包
      • 这里需要封装两个函数:
        • ssize_t readn(int fd, void *buf, size_t count)
          ssize_t writen(int fd, void *buf, size_t count)
      •   这两个函数的参数列表和返回值与readwrite一致。它们的作用的读取/写入count个字节后再返回。其实现如下:
        • ssize_t readn(int fd, void *buf, size_t count)
          {
                  int left = count ; //剩下的字节
                  char * ptr = (char*)buf ;
                  while(left>0)
                  {
                          int readBytes = read(fd,ptr,left);
                          if(readBytes< 0)//read函数小于0有两种情况:1中断 2出错
                          {
                                  if(errno == EINTR)//读被中断
                                  {
                                          continue;
                                  }
                                  return -1;
                          }
                          if(readBytes == 0)//读到了EOF
                          {
                                  //对方关闭呀
                                  printf("peer close
          ");
                                  return count - left;
                          }
                          left -= readBytes;
                          ptr += readBytes ;
                  }
                  return count ;
          }
          
          /*
          writen 函数
          写入count字节的数据
          */
          ssize_t writen(int fd, void *buf, size_t count)
          {
                  int left = count ;
                  char * ptr = (char *)buf;
                  while(left >0)
                  {
                          int writeBytes = write(fd,ptr,left);
                          if(writeBytes<0)
                          {
                                  if(errno == EINTR)
                                          continue;
                                  return -1;
                          }
                          else if(writeBytes == 0)
                                  continue;
                          left -= writeBytes;
                          ptr += writeBytes;
                  }
                  return count;
          }


          有了这两个函数之后,我们就可以使用定长包来发送数据了,我抽取其关键代码来讲诉:

          char readbuf[512];
          readn(conn,readbuf,sizeof(readbuf));  //每次读取512个字节

          同理的,写入的时候也写入512个字节

          char writebuf[512];
          fgets(writebuf,sizeof(writebuf),stdin);
          writen(conn,writebuf,sizeof(writebuf);
        • 每个消息都以固定的512字节(或其他数字,看你的应用层的缓冲区大小)来发送,以此区分每一个信息,这便是以固定长度解决粘包问题的思路。定长包解决方案的缺点在于会导致增加网络的负担,无论每次发送的有效数据是多大,都得按照定长的数据长度进行发送。


      •   ②:粘包解决方案二:使用结构体,显式说明数据部分的长度
        • 在这个方案中,我们需要定义一个‘struct packet’包结构,结构中指明数据部分的长度,用四个字节来表示。发送端的对等方接收报文时,先读取前四个字节,获取数据的长度,由长度来进行数据的读取。定义一个结构体
          struct packet
          {
                  unsigned int msgLen ;  //4个字节字段,说明数据部分的大小
                  char data[512] ;  //数据部分 
          }
        • 读写过程如下所示,这里抽取关键代码进行说明:
          //发送数据过程
              struct packet writebuf;
              memset(&writebuf,0,sizeof(writebuf));
              while(fgets(writebuf.data,sizeof(writebuf.data),stdin)!=NULL)
              {      
                      int n = strlen(writebuf.data);   //计算要发送的数据的字节数
                      writebuf.msgLen =htonl(n);    //将该字节数保存在msgLen字段,注意字节序的转换
                      writen(conn,&writebuf,4+n);   //发送数据,数据长度为4个字节的msgLen 加上data长度
                      memset(&writebuf,0,sizeof(writebuf)); 
              }
        • 下面是读取数据的过程,先读取msgLen字段,该字段指示了有效数据data的长度。依据该字段再读出data。
          memset(&readbuf,0,sizeof(readbuf));
            int ret = readn(conn,&readbuf.msgLen,4); //先读取四个字节,确定后续数据的长度
            if(ret == -1)
            {
                     err_exit("readn");
            }
            else if(ret == 0)
           {
                     printf("peer close
          ");
                     break;
          }
           int dataBytes = ntohl(readbuf.msgLen); //字节序的转换
           int readBytes = readn(conn,readbuf.data,dataBytes); //读取出后续的数据
           if(readBytes == 0)
           {
                   printf("peer close
          ");
                   break;
           }
           if(readBytes<0)
           {
                    err_exit("read");
          }
      •   ③:粘包解决方案三:按行读取
        •   ftp协议采用/r/n来识别一个消息的边界,我们在这里实现一个按行读取的功能,该功能能够按/n来识别消息的边界。这里介绍一个函数:
          ssize_t recv(int sockfd, void *buf, size_t len, int flags);
        • 与read函数相比,recv函数的区别在于两点:

          1. recv函数只能够用于套接口IO。
          2. recv函数含有flags参数,可以指定一些选项。

          recv函数的flags参数常用的选项是:

          1. MSG_OOB 接收带外数据,即通过紧急指针发送的数据
          2. MSG_PEEK 从缓冲区中读取数据,但并不从缓冲区中清除所读数据

          为了实现按行读取,我们需要使用recv函数的MSG_PEEK选项。PEEK的意思是"偷看",我们可以理解为窥视,看看socket的缓冲区内是否有某种内容,而清除缓冲区。

          /*
          * 封装了recv函数
            返回值说明:-1 读取出错 
          */
          ssize_t read_peek(int sockfd,void *buf ,size_t len)
          {
                  while(1)
                  {
                          //从缓冲区中读取,但不清除缓冲区
                          int ret = recv(sockfd,buf,len,MSG_PEEK);
                          if(ret == -1 && errno == EINTR)//文件读取中断
                                  continue;
                          return ret;
                  }
          }
          
          
          下面是按行读取的代码:
          
          /*
          *读取一行内容
          * 返回值说明:
                  == 0 :对端关闭
                  == -1 : 读取错误
                  其他:一行的字节数,包含
          
          * 
          **/
          ssize_t readLine(int sockfd ,void * buf ,size_t maxline)
          {
                  int ret ;
                  int nRead = 0;
                  int left = maxline ;
                  char * pbuf  = (char *) buf;
                  int count  = 0;
                  while(true)
                  {
                          //从socket缓冲区中读取指定长度的内容,但并不删除
                          ret = read_peek(sockfd,pbuf,left);
                          // ret = recv(sockfd , pbuf , left , MSG_PEEK);
                          if(ret<= 0)
                                  return ret;
                         nRead = ret ;
                          for(int i = 0 ;i< nRead ; ++i)
                          {
                                  if(pbuf[i]=='
          ') //探测到有
          
                                  {
                                          ret = readn (sockfd , pbuf, i+1);
                                          if(ret != i+1)
                                                  exit(EXIT_FAILURE);
                                          return ret + returnCount;
                                  }
                          }
                          //如果嗅探到没有
          
                          //那么先将这一段没有
          的读取出来
                          ret  = readn(sockfd , pbuf , nRead);
                          if(ret != nRead)
                                  exit(EXIT_FAILURE);
                          pbuf += nRead ;
                          left -= nRead ;
                          count += nRead;
                  }
                  return -1;
          }
  • 相关阅读:
    通配符
    Hibernate入门简介
    Java的参数传递是值传递?
    Java线程详解
    java基础总结
    谈谈对Spring IOC的理解
    Oracle SQL语句之常见优化方法总结--不定更新
    JVM 工作原理和流程
    Java中的String为什么是不可变的? -- String源码分析
    Spring AOP 简介
  • 原文地址:https://www.cnblogs.com/Kobe10/p/5770977.html
Copyright © 2011-2022 走看看