Socket編程實踐(5) --TCP粘包問題與解決

TCP粘包問題

由於TCP協議是基於字節流且無邊界的傳輸協議, 因此很有可能產生粘包問題, 問題描述如下


   對於Host A 發送的M1與M2兩個各10K的數據塊, Host B 接收數據的方式不確定, 有以下方式接收:

   先接收M1, 再接收M2(正確方式)

   先接收M2, 再接收M1(錯誤)

   一次性收到20k數據(錯誤)

   分兩次收到,第一次15k,第二次5k(錯誤)

   分兩次收到,第一次5k,第二次15k(錯誤)

   其他任何可能(錯誤)

 

粘包產生的原因 

   1、SQ_SNDBUF 套接字本身有緩衝區 (發送緩衝區、接受緩衝區)

   2、tcp傳送的端 mss大小限制

   3、鏈路層也有MTU大小限制,如果數據包大於>MTU要在IP層進行分片,導致消息分割。

   4、tcp的流量控制和擁塞控制,也可能導致粘包

   5、tcp延遲發送機制等

 

TCP與UDP關於粘包問題的對比

TCP

UDP

字節流

數據報

無邊界

有邊界

對等方的一次讀操作並不能保證完全把消息讀完

對方接收數據包的個數是不確定的

 

粘包解決方案(本質上是要在應用層維護消息與消息的邊界)

(1)定長包

   該方式並不實用: 如果所定義的長度過長, 則會浪費網絡帶寬, 而又如果定義的長度過短, 則一條消息又會拆分成爲多條, 僅在TCP的應用一層就增加了合併的開銷, 何況在其他層(因此我在博客中並未給出定長包的示例, 而是將之(一個不太完善的實現)與使用自定義報頭的示例放到了一起, 感興趣的讀者可以下載下來查看);

(2)包尾加\r\n(FTP使用方案)

   如果消息本身含有\r\n字符,則也分不清消息的邊界;

(3)報文長度+報文內容

(4)更復雜的應用層協議

 

readn / writen實現

Socket, 管道以及某些設備(特別是終端和網絡)有下列兩種性質:

   1)一次read操作所返回的數據可能少於所要求的數據,即使還沒到達文件尾端也可能這樣,但這不是一個錯誤,應當繼續讀該設備;

   2)一次write操作的返回值也可能少於指定輸入的字節數.這可能是由於某個因素造成的,如:內核緩衝區滿...但這也不是一個錯誤,應當繼續寫餘下的數據(通常,只有非阻塞描述符,或捕捉到一個信號時,才發生這種write的中途返回)

      在讀寫磁盤文件時從未見到過這種情況,除非是文件系統用完了空間,或者接近了配額限制,不能將所要求寫的數據全部寫出!

      通常,在讀/寫一個網絡設備,管道或終端時,需要考慮這些特性.於是,我們就有了下面的這兩個函數:readn和writen,功能分別是讀/寫指定的count字節數據,並處理返回值可能小於要求值的情況:

  1. /**實現: 
  2. 這兩個函數只是按需多次調用read和write系統調用直至讀/寫了count個數據 
  3. **/  
  4. /**返回值說明: 
  5.     == count: 說明正確返回, 已經真正讀取了count個字節 
  6.     == -1   : 讀取出錯返回 
  7.     <  count: 讀取到了末尾 
  8. **/  
  9. ssize_t readn(int fd, void *buf, size_t count)  
  10. {  
  11.     size_t nLeft = count;  
  12.     ssize_t nRead = 0;  
  13.     char *pBuf = (char *)buf;  
  14.     while (nLeft > 0)  
  15.     {  
  16.         if ((nRead = read(fd, pBuf, nLeft)) < 0)  
  17.         {  
  18.             //如果讀取操作是被信號打斷了, 則說明還可以繼續讀  
  19.             if (errno == EINTR)  
  20.                 continue;  
  21.             //否則就是其他錯誤  
  22.             else  
  23.                 return -1;  
  24.         }  
  25.         //讀取到末尾  
  26.         else if (nRead == 0)  
  27.             return count-nLeft;  
  28.   
  29.         //正常讀取  
  30.         nLeft -= nRead;  
  31.         pBuf += nRead;  
  32.     }  
  33.     return count;  
  34. }  
  1. /**返回值說明: 
  2.     == count: 說明正確返回, 已經真正寫入了count個字節 
  3.     == -1   : 寫入出錯返回 
  4. **/  
  5. ssize_t writen(int fd, const void *buf, size_t count)  
  6. {  
  7.     size_t nLeft = count;  
  8.     ssize_t nWritten = 0;  
  9.     char *pBuf = (char *)buf;  
  10.     while (nLeft > 0)  
  11.     {  
  12.         if ((nWritten = write(fd, pBuf, nLeft)) < 0)  
  13.         {  
  14.             //如果寫入操作是被信號打斷了, 則說明還可以繼續寫入  
  15.             if (errno == EINTR)  
  16.                 continue;  
  17.             //否則就是其他錯誤  
  18.             else  
  19.                 return -1;  
  20.         }  
  21.         //如果 ==0則說明是什麼也沒寫入, 可以繼續寫  
  22.         else if (nWritten == 0)  
  23.             continue;  
  24.   
  25.         //正常寫入  
  26.         nLeft -= nWritten;  
  27.         pBuf += nWritten;  
  28.     }  
  29.     return count;  
  30. }  

報文長度+報文內容實踐

   發報文時:前四個字節長度+報文內容一次性發送;

   收報文時:先讀前四個字節,求出報文內容長度;根據長度讀數據。

發送結構:

  1. struct Packet  
  2. {  
  3.     unsigned int    msgLen;     //數據部分的長度(網絡字節序)  
  4.     char            text[1024]; //報文的數據部分  
  5. };  
  1. //server端echo部分的改進代碼  
  2. void echo(int clientfd)  
  3. {  
  4.     struct Packet buf;  
  5.     int readBytes;  
  6.     //首先讀取首部  
  7.     while ((readBytes = readn(clientfd, &buf.msgLen, sizeof(buf.msgLen))) > 0)  
  8.     {  
  9.         //網絡字節序 -> 主機字節序  
  10.         int lenHost = ntohl(buf.msgLen);  
  11.         //然後讀取數據部分  
  12.         readBytes = readn(clientfd, buf.text, lenHost);  
  13.         if (readBytes == -1)  
  14.             err_exit("readn socket error");  
  15.         else if (readBytes != lenHost)  
  16.         {  
  17.             cerr << "client connect closed..." << endl;  
  18.             return ;  
  19.         }  
  20.         cout << buf.text;  
  21.   
  22.         //然後將其回寫回socket  
  23.         if (writen(clientfd, &buf, sizeof(buf.msgLen)+lenHost) == -1)  
  24.             err_exit("write socket error");  
  25.         memset(&buf, 0, sizeof(buf));  
  26.     }  
  27.     if (readBytes == -1)  
  28.         err_exit("read socket error");  
  29.     else if (readBytes != sizeof(buf.msgLen))  
  30.         cerr << "client connect closed..." << endl;  
  31. }  
  1. //client端發送與接收代碼  
  2. ...  
  3.     struct Packet buf;  
  4.     memset(&buf, 0, sizeof(buf));  
  5.     while (fgets(buf.text, sizeof(buf.text), stdin) != NULL)  
  6.     {  
  7.         /**寫入部分**/  
  8.         unsigned int lenHost = strlen(buf.text);  
  9.         buf.msgLen = htonl(lenHost);  
  10.         if (writen(sockfd, &buf, sizeof(buf.msgLen)+lenHost) == -1)  
  11.             err_exit("writen socket error");  
  12.   
  13.         /**讀取部分**/  
  14.         memset(&buf, 0, sizeof(buf));  
  15.         //首先讀取首部  
  16.         ssize_t readBytes = readn(sockfd, &buf.msgLen, sizeof(buf.msgLen));  
  17.         if (readBytes == -1)  
  18.             err_exit("read socket error");  
  19.         else if (readBytes != sizeof(buf.msgLen))  
  20.         {  
  21.             cerr << "server connect closed... \nexiting..." << endl;  
  22.             break;  
  23.         }  
  24.   
  25.         //然後讀取數據部分  
  26.         lenHost = ntohl(buf.msgLen);  
  27.         readBytes = readn(sockfd, buf.text, lenHost);  
  28.         if (readBytes == -1)  
  29.             err_exit("read socket error");  
  30.         else if (readBytes != lenHost)  
  31.         {  
  32.             cerr << "server connect closed... \nexiting..." << endl;  
  33.             break;  
  34.         }  
  35.         //將數據部分打印輸出  
  36.         cout << buf.text;  
  37.         memset(&buf, 0, sizeof(buf));  
  38.     }  
  39. ...  

完整實現代碼:

http://download.csdn.net/detail/hanqing280441589/8460557

 

按行讀取實踐

recv/send函數

  1. ssize_t recv(int sockfd, void *buf, size_t len, int flags);  
  2. ssize_t send(int sockfd, const void *buf, size_t len, int flags);  

與read相比,recv只能用於套接字文件描述符,而且多了一個flags

recv的flags參數常用取值:

MSG_OOB(帶外數據: 通過緊急指針發送的數據[需設置TCP頭部緊急指針位有效])

   This flag requests receipt of out-of-band data that would not be received  

in the normal data stream.  Some protocols place expedited data at the head of 

the normal data queue, and  thus  this flag cannot be used with such protocols.

MSG_PEEK(可以讀數據,但不從緩存區中讀走[僅僅是一瞥],利用此特點可以方便的實現按行讀取數據;一個一個字符的讀,多次調用系統調用read方法,效率不高)

   This  flag  causes the receive operation to return data from the beginning of 

the receive queue without removing that  data  from the queue.  Thus, a subsequent 

receive call will return the same data.

  1. /**示例: 通過MSG_PEEK封裝一個recv_peek函數(僅查看數據, 但不取走)**/  
  2. ssize_t recv_peek(int sockfd, void *buf, size_t len)  
  3. {  
  4.     while (true)  
  5.     {  
  6.         int ret = recv(sockfd, buf, len, MSG_PEEK);  
  7.         //如果recv是由於被信號打斷, 則需要繼續(continue)查看  
  8.         if (ret == -1 && errno == EINTR)  
  9.             continue;  
  10.         return ret;  
  11.     }  
  12. }  
  13.   
  14. /**使用recv_peek實現按行讀取readline(只能用於socket)**/  
  15. /** 返回值說明: 
  16.     == 0:   對端關閉 
  17.     == -1:  讀取出錯 
  18.     其他:    一行的字節數(包含'\n') 
  19. **/  
  20. ssize_t readline(int sockfd, void *buf, size_t maxline)  
  21. {  
  22.     int ret;  
  23.     int nRead = 0;  
  24.     int returnCount = 0;  
  25.     char *pBuf = (char *)buf;  
  26.     int nLeft = maxline;  
  27.     while (true)  
  28.     {  
  29.         ret = recv_peek(sockfd, pBuf, nLeft);  
  30.         //如果查看失敗或者對端關閉, 則直接返回  
  31.         if (ret <= 0)  
  32.             return ret;  
  33.         nRead = ret;  
  34.         for (int i = 0; i < nRead; ++i)  
  35.             //在當前查看的這段緩衝區中含有'\n', 則說明已經可以讀取一行了  
  36.             if (pBuf[i] == '\n')  
  37.             {  
  38.                 //則將緩衝區內容讀出  
  39.                 //注意是i+1: 將'\n'也讀出  
  40.                 ret = readn(sockfd, pBuf, i+1);  
  41.                 if (ret != i+1)  
  42.                     exit(EXIT_FAILURE);  
  43.                 return ret + returnCount;  
  44.             }  
  45.   
  46.         // 如果在查看的這段消息中沒有發現'\n', 則說明還不滿足一條消息,  
  47.         // 在將這段消息從緩衝中讀出之後, 還需要繼續查看  
  48.         ret = readn(sockfd, pBuf, nRead);;  
  49.         if (ret != nRead)  
  50.             exit(EXIT_FAILURE);  
  51.         pBuf += nRead;  
  52.         nLeft -= nRead;  
  53.         returnCount += nRead;  
  54.     }  
  55.     //如果程序能夠走到這裏, 則說明是出錯了  
  56.     return -1;  
  57. }  

readline實現思想:

   在readline函數中,我們先用recv_peek”偷窺” 一下現在緩衝區有多少個字符並讀取到pBuf,然後查看是否存在換行符'\n'。如果存在,則使用readn連同換行符一起讀取(作用相當於清空socket緩衝區); 如果不存在,也清空一下緩衝區, 且移動pBuf的位置,回到while循環開頭,再次窺看。注意,當我們調用readn讀取數據時,那部分緩衝區是會被清空的,因爲readn調用了read函數。還需注意一點是,如果第二次纔讀取到了'\n',則先用returnCount保存了第一次讀取的字符個數,然後返回的ret需加上原先的數據大小。

 

按行讀取echo代碼:

  1. void echo(int clientfd)  
  2. {  
  3.     char buf[512] = {0};  
  4.     int readBytes;  
  5.     while ((readBytes = readline(clientfd, buf, sizeof(buf))) > 0)  
  6.     {  
  7.         cout << buf;  
  8.         if (writen(clientfd, buf, readBytes) == -1)  
  9.             err_exit("writen error");  
  10.         memset(buf, 0, sizeof(buf));  
  11.     }  
  12.     if (readBytes == -1)  
  13.         err_exit("readline error");  
  14.     else if (readBytes == 0)  
  15.         cerr << "client connect closed..." << endl;  
  16. }  

client端讀取與發送代碼

  1. ...  
  2.     char buf[512] = {0};  
  3.     memset(buf, 0, sizeof(buf));  
  4.     while (fgets(buf, sizeof(buf), stdin) != NULL)  
  5.     {  
  6.         if (writen(sockfd, buf, strlen(buf)) == -1)  
  7.             err_exit("writen error");  
  8.         memset(buf, 0, sizeof(buf));  
  9.         int readBytes = readline(sockfd, buf, sizeof(buf));  
  10.         if (readBytes == -1)  
  11.             err_exit("readline error");  
  12.         else if (readBytes == 0)  
  13.         {  
  14.             cerr << "server connect closed..." << endl;  
  15.             break;  
  16.         }  
  17.         cout << buf;  
  18.         memset(buf, 0, sizeof(buf));  
  19.     }  
  20. ...  

完整代碼實現:

http://download.csdn.net/detail/hanqing280441589/8460883

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章