1. 程式人生 > >Linux 網路程式設計——TCP 粘包及其解決方案

Linux 網路程式設計——TCP 粘包及其解決方案

  首先,我們回顧一下 TCP 和 UDP 的頭部資訊:

這裡寫圖片描述

圖1. TCP 頭部
這裡寫圖片描述

圖2. UDP 頭部

  我們知道,TCP 和 UDP 是 TCP/IP 協議族傳輸層中的兩個具有代表性的協議。其中,TCP 是面向連線的複雜的、可靠的位元組流傳輸協議,而 UDP 是面向無連線的簡單的、不可靠的資料報傳輸協議。
  “流”的概念就是指不間斷的資料結構,可以把它想象成你們家裡的自來水管道中的水流。什麼意思呢?舉個例子:TCP 傳送端應用程式傳送了10次100位元組的訊息,那麼,在接收端的應用程式接收到的可能是一個1000位元組的連續不間斷的資料。但如果是 UDP 埠傳送了一個100位元組的訊息,那麼 UDP 接收端就會以100位元組的長度來接收資料。正因為這樣,我們可以看到 TCP 頭部中並沒有長度資訊,而 UDP 頭部包含長度資訊。好了,現在你應該明白我們在建立套接字的時候,為什麼 type 的型別是 SOCK_STREAM

SOCK_DGRAM 了吧!
  
  正如文章標題所述,顯然,你已經知道 UDP 資料包存在明確邊界,是不存在粘包現象的,只有 TCP 才會出現粘包現象。那麼,接下來我們逐步分析 TCP 的流傳輸特性所帶來如粘包這樣的一些問題及其解決方案。

  按每次通訊後是否閉關連線,可以分為兩類情況:長連線和短連線。長連線——指的是客戶端和服務端先建立通訊連線,連線建立後不斷開,然後再進行報文傳送和接收。短連線——指的是客戶端和服務端每進行一次報文收發交易時才進行通訊連線,交易完畢後立即斷開連線,比如 http 協議。
  所以我們可以分析以下幾種情況:
  (1)如果利用 TCP 每次傳送資料,就與對方建立連線,然後雙方傳送完一段資料後,就關閉連線,這樣就不會出現粘包問題(因為只有一種資料結構)。
  (2)如果傳送資料無結構,如檔案傳輸,這樣傳送端只管傳送,接收端只管接收儲存就行,也不用考慮粘包。
  (3)如果雙方建立連線,需要在連線後一段時間內傳送多個不同結構的資料,這時候接收端收到就可能是一堆粘在一起的資料,這樣接收端應用程式就傻了,到底是要幹嘛?不知道,因為協議並沒有規定這麼詭異的資料。
  
  那麼,可以認為 TCP 粘包問題並不是對所有應用都造成困擾的,只是對那些長連線並且需要傳輸多種資料結構的應用造成影響。仔細分析會發現,除了粘包問題,其實還可能會出現多包、少包、半包、斷包等情況。
  
  針對這些問題,一般會有如下解決方法:
  (1)呼叫傳送函式之後都強制資料立即傳送(PUSH 指令)。
  (2)對於接收端引起的粘包,則可通過優化程式設計、精簡接收程序工作量、提高接收程序優先順序等措施,使其及時接收資料,從而儘量避免出現粘包現象。
  (3)新增一個固定的訊息頭,該訊息頭包含資料長度資訊,每次資料時先接收固定大小的訊息頭,再根據其攜帶的長度資訊接收訊息實體。也就是說,通過人為控制多次接收來避免粘包。
  (4)設定 TCP_NODELAY 選項,禁止 Nagle 演算法。
  (5)設定 SO_RCVBUF 和 SO_SNDBUF 選項,根據應用需求修改一個合適的接收、傳送緩衝區大小。
  (6)新增報文分隔標識,比如傳送報文是在末尾新增 '\n'

,同時接收端使用 recv() 函式接收報文,並且設定引數 flags 的值為 MSG_PEEK。注意:當 flags 引數的值設定為 MSG_PEEK 時,recv() 可以從 socket 快取中讀取資料,但是不會將快取中該部分資料清除,但如果使用 read() 函式直接讀取 socket 快取區中的內容,會清空快取區中的內容。假設兩段報文粘包,read() 會清空快取區中所有內容,從而導致後一段報文中的粘包的部分資料丟失。

  實際上,上述的幾種解決方法都有不足之處,並且不一定能夠完全避免 TCP 粘包問題。所以還是需要根據實際應用來進行應用場景和效能方面的衡量。