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\n");
                                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\n");
                   break;
        }
         int dataBytes = ntohl(readbuf.msgLen); //字节序的转换
         int readBytes = readn(conn,readbuf.data,dataBytes); //读取出后续的数据
         if(readBytes == 0)
         {
                 printf("peer close\n");
                 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 : 读取错误
                其他:一行的字节数,包含\n
        *
        **/
        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]==‘\n‘) //探测到有\n
                                {
                                        ret = readn (sockfd , pbuf, i+1);
                                        if(ret != i+1)
                                                exit(EXIT_FAILURE);
                                        return ret + returnCount;
                                }
                        }
                        //如果嗅探到没有\n
                        //那么先将这一段没有\n的读取出来
                        ret  = readn(sockfd , pbuf , nRead);
                        if(ret != nRead)
                                exit(EXIT_FAILURE);
                        pbuf += nRead ;
                        left -= nRead ;
                        count += nRead;
                }
                return -1;
        }
时间: 2024-10-22 15:23:15

Socket编程--TCP粘包问题的相关文章

python/socket编程之粘包

python/socket编程之粘包 粘包: 只有TCP有尿包现象,UDP永远不会粘包. 首先需要掌握一个socket收发消息的原理 发送端可以是1k,1k的发送数据而接受端的应用程序可以2k,2k的提取数据,当然也有可能是3k或者多k提取数据,也就是说,应用程序是不可见的,因此TCP协议是面来那个流的协议,这也是容易出现粘包的原因而UDP是面向笑死的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任一字节的数据,这一点和TCP是很同的.怎样定义消息呢?认为对方一次

java socket编程解决粘包和丢包问题

##socket 丢包粘包解决方式 采用固定头部长度(一般为4个字节),包头保存的是包体的长度 header+body 包头+包体 思路是:先读出一个包头,得到包体的长度,解析出包体 public class SocketServer { public static void main(String args[]) { ServerSocket serverSocket; try { serverSocket = new ServerSocket(); serverSocket.bind(new

Socket编程实践(5) --TCP粘包问题与解决

TCP粘包问题 因为TCP协议是基于字节流且无边界的传输协议, 因此非常有可能产生粘包问题, 问题描写叙述例如以下 watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvempmMjgwNDQxNTg5/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" /> 对于Host A 发送的M1与M2两个各10K的数据块, Host B 接收数据的方式不确定, 有以下

SOCKET TCP 粘包及半包问题

大家在使用SOCKET通信编程的时候,一般会采用UDP和TCP两种方式:TCP因为它没有包的概念,它只有流的概念,并且因为发送或接收缓冲区大小的设置问题,会产生粘包及半包的现象. 场景: 服务端向连续发送三个"HelloWorld"(三次消息无间隔),那么客户端接收到的情况会有以下三种: 1)HelloWorld HelloWorld HelloWorld (客户端接收三次) 2)HelloWorldHelloWor ldHelloWorld (客户端接收两次) 3)HelloWorl

tcp粘包问题(封包)

tcp粘包分析     http://blog.csdn.net/zhangxinrun/article/details/6721495 解决TCP网络传输“粘包”问题(经典)       http://blog.csdn.net/zhangxinrun/article/details/6721508 粘包出现原因:在流传输中出现,UDP不会出现粘包,因为它有消息边界(参考Windows 网络编程)1 发送端需要等缓冲区满才发送出去,造成粘包2 接收方不及时接收缓冲区的包,造成多个包接收 解决办

TCP粘包问题分析和解决(全)

TCP通信粘包问题分析和解决(全) 在socket网络程序中,TCP和UDP分别是面向连接和非面向连接的.因此TCP的socket编程,收发两端(客户端和服务器端)都要有成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小.数据量小的数据,合并成一个大的数据块,然后进行封包.这样,接收端,就难于分辨出来了,必须提供科学的拆包机制. 对于UDP,不会使用块的合并优化算法,这样,实际上目前认为,是由于UDP支持的是一对多的模式,

netty 解决TCP粘包与拆包问题(二)

TCP以流的方式进行数据传输,上层应用协议为了对消息的区分,采用了以下几种方法. 1.消息固定长度 2.第一篇讲的回车换行符形式 3.以特殊字符作为消息结束符的形式 4.通过消息头中定义长度字段来标识消息的总长度 一.采用指定分割符解决粘包与拆包问题 服务端 1 package com.ming.netty.nio.stickpack; 2 3 4 5 import java.net.InetSocketAddress; 6 7 import io.netty.bootstrap.ServerB

Netty学习之TCP粘包/拆包

一.TCP粘包/拆包问题说明,如图 二.未考虑TCP粘包导致功能异常案例 按照设计初衷,服务端应该收到100条查询时间指令的请求查询,客户端应该打印100次服务端的系统时间 1.服务端类 package com.phei.netty.s2016042302; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitial

TCP粘包, UDP丢包, nagle算法

一.TCP粘包 1. 什么时候考虑粘包 如果利用tcp每次发送数据,就与对方建立连接,然后双方发送完一段数据后,就关闭连接,这样就不会出现粘包问题(因为只有一种包结构,类似于http协议,UDP不会出现粘包现象).关闭连接主要要双方都发送close连接(参考tcp关闭协议).如:A需要发送一段字符串给B,那么A与B建立连接,然后发送双方都默认好的协议字符如"hello give me sth abour yourself",然后B收到报文后,就将缓冲区数据接收,然后关闭连接,这样粘包问