SOCK_STREAM & SOCK_DGRAM


從UDP數據報長度說起

UDP屬於網絡模型中的傳輸層。下面我們由下至上一步一步來看:

理論上,IP協議允許的最大IP數據包(packet)為2^16=65535(IP包總長為16位):

但是!以太網(Ethernet)數據幀的長度必須在46-1500字節之間,這是由以太網的物理特性決定的。這個1500字節被稱為鏈路層的MTU(最大傳輸單元)。但這並不是指鏈路層的長度被限制在1500字節,其實這個MTU指的是鏈路層的數據區,而不包括鏈路層的首部和尾部的18個字節。所以,1500字節就是網絡層IP數據報的長度限制。

因為IP數據報的首部為20字節,所以IP數據報的數據區長度最大為1480字節。而這個1480字節就是用來放TCP傳來的TCP報文段或UDP傳來的UDP數據報的。

又因為UDP數據報的首部8字節,所以UDP數據報的數據區最大長度為1472字節。1472字節就是一個UDP數據報可以使用的字節數。

那么問題來了,應用層傳輸的數據,超過1472字節怎么辦?

這也就是說IP數據報大於1500字節,大於MTU。這個時候發送方IP層就需要分片(fragmentation)——把數據報分成若干片,使每一片都小於MTU。而接收方IP層則需要進行數據報的重組。這樣就實現了分片和重組過程對運輸層(TCP/UDP)是透明的——TCP/UDP協議無需關注數據是否過長。

盡管IP分片過程看起來透明的,但對於TCP協議來說,但有一個缺陷:即使只丟失一片數據也要重新傳整個數據報。why?因為IP層本身沒有超時重傳機制——由更高層(TCP)來負責超時和重傳。當來自TCP報文段的某一片丟失后,TCP在超時后會重發整個TCP報文段,該報文段對應於一份IP數據報(而不是一個分片),沒有辦法只重傳數據報中的一個數據分片。

而UDP——協議本身不管理數據的可靠性,也沒有重傳機制。由於UDP的特性,當某一片數據傳送中丟失時。接收方將無法重組報文,故而導致整個UDP報文的丟棄。因此,在局域網環境下,一般建議將UDP數據包控制在1472byte以下為宜。進行Internet編程時則不同——因為Internet上的路由器可能會將MTU設為不同的值。如果我們假定MTU為1500來發送數據的,而途經的某個網絡的MTU值小於1500字節,那么系統將會使用一系列的機制來調整MTU值,使數據報能夠順利到達目的地,這樣就會做許多不必要的操作。鑒於Internet上的標准MTU值為576字節,建議在進行Internet的UDP編程時,將UDP的數據長度控件在576-8-20=548字節以內。

MSS(Maxitum Segment Size)

事實上,采用TCP協議進行數據傳輸是不會造成IP分片的,因為一旦TCP數據過大,超過了MSS,傳輸層就會對TCP包進行分段。由於MSS一般小於MTU,IP層對於TCP的分段數據就不用再分片了。

為了達到最佳的傳輸效能,TCP協議在建立連接的時候通常要協商雙方的MSS值,這個值TCP協議在實現的時候往往用MTU值代替(需要減去IP數據包包頭的大小20Bytes和TCP數據段的包頭20Bytes)所以往往MSS為1460。通訊雙方會根據雙方提供的MSS值得最小值確定為這次連接的最大MSS值。

那么如何分段呢?其實TCP無所謂分段,因為每個TCP數據報在組成前其大小就已經被MSS限制了,所以TCP數據報的長度是不可能大於MSS的。

總結:報文在長度較大時會發生分段 or 分片。分段發生在傳輸層的TCP協議,分片發生在網絡層的IP協議。


關於內核緩沖區

SOCK_DGRAM 類型的 socket 特點:

  1. 只有一個接收緩沖區,而不存在發送緩沖區。數據的發送是直接進行的,而不管對端是否能夠正常接收,也不管對端接收緩沖區是否充滿。
  2. 由於UDP是沒有流量控制的,發送端可以很容易地就淹沒接收者(更慢),導致接收方的UDP丟棄數據報。
  3. 另外需要注意的是,UDP的報文並不保證順序,所以接收緩沖區里的報文需要手動排序(如有必要);
  4. 好消息是,UDP並不需要手動拆包——面向數據報的報文,並不會在緩沖區中自動合並。

對於TCP socket 而言:

  1. 內核中都有一個發送緩沖區和一個接收緩沖區——TCP的全雙工工作模式以及TCP的滑動窗口就是依賴這兩個獨立的buufer以及buffer的填充狀態。
  2. 對端發送過來數據,內核會存入接收緩沖區。緩沖區數據會一直保留直到應用層 read() 取走數據,在此過程中,TCP/IP協議棧繼續執行,不斷的將新的報文數據填充到接收緩沖區后面,直到填滿為止。
  3. 接下來發生的動作是:通知對端TCP協議中的窗口關閉。這個便是滑動窗口的實現,用以保證TCP套接口接收緩沖區不會溢出,從而保證了TCP是可靠傳輸。因為對方不允許發出超過所通告窗口大小的數據。 這就是TCP的流量控制,如果對方無視窗口大小而發出了超過窗口大小的數據,則接收方TCP將丟棄它。

關於TCP的設計策略和問題

為何TCP采用字節流協議

其實,這種不同是由TCP和UDP的特性決定的。TCP是面向連接的,也就是說,在連接持續的過程中,socket中收到的數據都是由同一台主機發出的(劫持什么的不考慮),因此,只要保證數據是有序的到達就行了,至於每次讀取多少數據自己看着辦。 (問:那為啥不直接由協議指定封包,把這個事兒辦了呢?)
而UDP是無連接的協議,也就是說,只要知道接收端的IP和端口,且網絡是可達的,任何主機都可以向接收端發送數據。這時候,如果一次能讀取超過一個報文的數據,則會亂套。比如,主機A向發送了報文P1,主機B發送了報文P2,如果能夠讀取超過一個報文的數據,那么就會將P1和P2的數據合並在了一起,這樣的數據是沒有意義的。

粘包,封包與拆包

由於TCP報文是面向流的,沒有任何的邊界記錄,故而在 read() 時,將根據參數(buffer_length)一並讀取所有報文,必須通過手動解包;

"粘包"可發生在發送端也可發生在接收端:

  • 由Nagle算法造成的發送端的粘包:Nagle算法是一種改善網絡傳輸效率的算法。簡單的說,,當我們提交一段數據給TCP發送時,TCP並不立刻發送此段數據,而是等待一小段時間,看看在等待期間是否還有要發送的數據,若有則會一次把這兩段數據發送出去。
  • 接收端接收不及時造成的接收端粘包:TCP會把接收到的數據存在自己的緩沖區中,然后通知應用層取數據。當應用層由於某些原因不能及時的把TCP的數據取出來,就會造成TCP緩沖區中存放了多段數據。

封包就是給一段數據加上包頭,這樣一來數據包就分為包頭和包體兩部分內容了(過濾非法包時封包會加入"包尾"內容)。包頭其實上是個大小固定的結構體,其中有個結構體成員變量表示包體的長度,這是個很重要的變量。其他的結構體成員可根據需要自己定義。根據包頭長度固定以及包頭中含有包體長度的變量就能正確的拆分出一個完整的數據包。

關於TCP的流量控制和阻塞控制

由於接收方緩存的限制,發送窗口不能大於接收方接收窗口。在報文段首部有一個字段就叫做窗口(rwnd),這便是用於告訴對方自己的接收窗口,可見窗口的大小是可以變化的。 

  

總結起來如上圖,TCP的流量和阻塞控制采用“慢啟動”、“加性增”、“乘性減”的策略。

  • 慢啟動:初始的窗口值很小,但是按指數規律漸漸增長,直到達到慢開始門限(ssthresh)。
  • 加性增:窗口值達到慢開始門限后,每發送一個報文段,窗口值增加一個單位量。
  • 乘性減:無論什么階段,只要出現超時,則把窗口值減小一半。

 


思考

  1. 為什么TCP包不自動實現封包呢?或者說,軟件世界里,例如 C++ 默認將流作為了字符串的外在表現形式,這似乎是存在着某種優勢的考慮。然而Python等語言又將字符串通過封裝長度屬性,指定了字符串長度(包括ZMQ中對字符串的處理,也是通過指定長度的方式作為標准處理)。那后者的考慮因素又是什么?
  2. “字節流” 與 “數據報” 傳輸效率的比較?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM