TCP通訊中的粘包問題

TCP通訊中的粘包問題 編程

尹德位 2015 西安 緩存

 

關鍵詞 TCP 網絡通訊 粘包 Linux C/S 網絡

  粘包問題概述 併發

  粘包迴避設計 socket

 

第一章  粘包問題概述 性能


1.1  描述背景 大數據

採用TCP協議進行網絡數據傳送的軟件設計中,廣泛存在粘包問題。這主要是因爲現代操做系統的網絡傳輸機制所產生的。咱們知道,網絡通訊採用的套接字(socket)技術,其實現實際是由系統內核提供一片連續緩存(流緩衝)來實現應用層程序與網卡接口之間的中轉功能。多個數據包被連續存儲於連續的緩存中,在對數據包進行讀取時因爲沒法肯定發生方的發送邊界,而採用某一估測值大小來進行數據讀出,若雙方的size不一致時就會使數據包的邊界發生錯位,致使讀出錯誤的數據分包,進而曲解原始數據含義。 spa

 

1.2  粘包的概念 操作系統

粘包問題的本質就是數據讀取邊界錯誤所致,經過下圖能夠形象地理解其現象。 設計


如圖1所示,當前的socket緩存中已經有6個數據分組到達,其大小如圖中數字。而應用程序在對數據進行收取時(如圖2),採用了300字節的要求去讀取,則會誤將pkg1pkg2一塊兒收走當作一個包來處理。而實際上,極可能pkg1是一個文本文件的內容,而pkg2則多是一個音頻內容,這風馬牛不相及的兩個數據包卻被揉進一個包進行處理,顯然有失穩當。嚴重時可能由於丟了pkg2而致使軟件陷入異常分支產生烏龍事件。

所以,粘包問題必須引發全部軟件設計者(項目經理)的高度重視

那麼,或許會有讀者發問,爲什麼不讓接收程序按照100字節來讀取呢?我想若是您瞭解一些TCP編程的話就不會有這樣的問題。網絡通訊程序中,數據包一般是不能肯定大小的,尤爲在軟件設計階段沒法真的作到肯定爲一個固定值。好比聊天軟件客戶端若採用TCP傳輸一個用戶名和密碼到服務端進行驗證登錄,我想這個數據包不過是幾十字節,至多幾百字節便可發送完畢,而有時候要傳輸一個很大的視頻文件,即便分包發送也應該一個包在幾千字節吧。(聽說,某國電信平臺的MW中見到過一次發送1.5萬字節的電話數據)這種狀況下,發送數據的分包大小沒法固定,接收端也就沒法固定。因此通常採用一個較爲合理的預估值進行輪詢接收。(網卡的MTU都是1500字節,所以這個預估值通常爲MTU1~3倍)。

相信讀者對粘包問題應該有了初步認識了。

 

第二章  粘包迴避設計


2.0  閒扯

做者在此提出三種可解之法,這都是從軟件設計的角度去考慮的,固然代碼實現也是能夠驗證沒問題的。下面一一爲讀者解開其謎底。

讀者在別的文獻中還能看到一種叫作【短鏈接】的方法,根據經驗不建議採用此法,開銷太大得不償失。故而本文對該方案不作解釋。


2.1  設計方案一:定長髮送

在進行數據發送時採用固定長度的設計,也就是不管多大數據發送都分包爲固定長度(爲便於描述,此處定長爲記爲LEN),也就是發送端在發送數據時都以LEN爲長度進行分包。這樣接收方都以固定的LEN進行接收,如此一來發送和接收就能一一對應了。分包的時候不必定能完整的剛好分紅多個完整的LEN的包,最後一個包通常都會小於LEN,這時候最後一個包能夠在不足的部分填充空白字節。

固然,這種方法會有缺陷。1.最後一個包的不足長度被填充爲空白部分,也即無效字節序。那麼接收方可能難以辨別這無效的部分,它自己就是爲了補位的,並沒有實際含義。這就爲接收端處理其含義帶來了麻煩。固然也有解決辦法,能夠經過增添標誌位的方法來彌補,即在每個數據包的最前面增長一個定長的報頭,而後將該數據包的末尾標記一併發送。接收方根據這個標記確認無效字節序列,從而實現數據的完整接收。2.在發送包長度隨機分佈的狀況下,會形成帶寬浪費。好比發送長度可能爲 1,100,1000,4000字節等等,則都須要按照定長最大值即4000來發送,數據包小於4000字節的其餘包也會被填充至4000,形成網絡負載的無效浪費。

綜上,此方案適在發送數據包長度較爲穩定(趨於某一固定值)的狀況下有較好的效果。

 

2.2  設計方案二:尾部標記序列

在每一個要發送的數據包的尾部設置一個特殊的字節序列,此序列帶有特殊含義,跟字符串的結束符標識」\0」同樣的含義,用來標示這個數據包的末尾,接收方可對接收的數據進行分析,經過尾部序列確認數據包的邊界。

這種方法的缺陷較爲明顯:1.接收方須要對數據進行分析,甄別尾部序列。2.尾部序列的肯定自己是一個問題。什麼樣的序列能夠向」\0」同樣來作一個結束符呢?這個序列必須是不具有一般任何人類或者程序可識別的帶含義的數據序列,就像「\0」是一個無效字符串內容,於是能夠做爲字符串的結束標記。那普通的網絡通訊中,這個序列是什麼呢?我想一時間很難找到恰當的答案。

 

2.3  設計方案三:頭部標記分步接收

這個方法是做者有限學識裏最好的辦法了。它既不損失效率,還完美解決了任何大小的數據包的邊界問題。

這個方法的實現是這樣的,定義一個用戶報頭,在報頭中註明每次發送的數據包大小。接收方每次接收時先以報頭的size進行數據讀取,這必然只能讀到一個報頭的數據,從報頭中獲得該數據包的數據大小,而後再按照此大小進行再次讀取,就能讀到數據的內容了。這樣一來,每一個數據包發送時都封裝一個報頭,而後接收方分兩次接收一個包,第一次接收報頭,根據報頭大小第二次才接收數據內容。(此處的data[0]的本質是一個指針,指向數據的正文部分,也能夠是一篇連續數據區的起始位置。所以能夠設計成data[user_size],這樣的話。)

下面經過一個圖來展示設計思想。


 

由圖看出,數據發送多了封裝報頭的動做;接收方將每一個包的接收拆分紅了兩次。

這方案看似精妙,實則也有缺陷:1.報頭雖小,但每一個包都須要多封裝sizeof(_data_head)的數據,積累效應也不可徹底忽略。2.接收方的接收動做分紅了兩次,也就是進行數據讀取的操做被增長了一倍,而數據讀取操做的recv或者read都是系統調用,這對內核而言的開銷是一個不能徹底忽略的影響,對程序而言性能影響可忽略(系統調用的速度很是快)。

優勢:避免了程序設計的複雜性,其有效性便於驗證,對軟件設計的穩定性要求來講更容易達標。綜上,方案三乃上上策!

 

熱愛技術和努力的人們 2015/11/29 凌晨

    <<END>>

相關文章
相關標籤/搜索