TCP 粘包問題及解決方案

1、TCP粘包問題

TCP是一個基於字節流的傳輸服務,"流"意味着TCP所傳輸的數據是沒有邊界的。這不一樣於UDP提供基於消息的傳輸服務,其傳輸的數據是有邊界的。TCP的發送方沒法保證對等方每次接收到的是一個完整的數據包。主機A向主機B發送兩個數據包,主機B的接收狀況多是以下幾種狀況:算法

產生粘包問題的緣由有如下幾個:網絡

一、應用層調用write方法,將應用層的緩衝區中的數據拷貝到套接字的發送緩衝區。而發送緩衝區有一個SO_SNDBUF的限制,若是應用層的緩衝區數據大小大於套接字發送緩衝區的大小,則數據須要進行屢次的發送。
二、TCP所傳輸的報文段有MSS的限制,若是套接字緩衝區的大小大於MSS,也會致使消息的分割發送。
三、因爲鏈路層最大發送單元MTU,在IP層會進行數據的分片。
這些狀況都會致使一個完整的應用層數據被分割成屢次發送,致使接收對等方不是按完整數據包的方式來接收數據。socket

四、發送端須要等緩衝區滿才發送出去,形成粘包(由Nagle算法形成的發送端的粘包)
五、接收方不及時接收緩衝區的包,形成多個包接收(接收端接收不及時形成的接收端粘包)函數

2、粘包的問題的解決

粘包問題的最本質緣由在與接收對等方沒法分辨消息與消息之間的邊界在哪。咱們經過使用某種方案給出邊界,例如:spa

一、發送定長包。若是每一個消息的大小都是同樣的,那麼在接收對等方只要累計接收數據,直到數據等於一個定長的數值就將它做爲一個消息。這裏須要封裝兩個函數:指針

ssize_t readn(int fd, void *buf, size_t count)
ssize_t writen(int fd, void *buf, size_t count)

這兩個函數的參數列表和返回值與read、write一致,做用是讀取/寫入count個字節後再返回。實現以下:code

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個字節:get

char writebuf[512];
fgets(writebuf,sizeof(writebuf),stdin);
writen(conn,writebuf,sizeof(writebuf);

每一個消息都以固定的512字節(或其餘數字,看你的應用層的緩衝區大小)來發送,以此區分每個信息,這即是以固定長度解決粘包問題的思路。定長包解決方案的缺點在於會致使增長網絡的負擔,不管每次發送的有效數據是多大,都得按照定長的數據長度進行發送。it


二、包尾加上\r\n標記。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;
}

 

三、包頭加上包體長度。包頭是定長的4個字節,說明了包體的長度。接收對等方先接收包體長度,依據包體長度來接收包體。

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");
}

 

四、使用更加複雜的應用層協議。

相關文章
相關標籤/搜索