TCP報文發送的那些事

 今天咱們來總結學習一下TCP發送報文的相關知識,主要包括髮送報文的步驟,MSS,滑動窗口和Nagle算法。算法

發送報文

 該節主要根據陶輝大神的系列文章總結而來。以下圖所示,咱們一塊兒來看一下TCP發送報文時操做系統內核都作了那些事情。其中有些概念在接下來的小節中會介紹。緩存

clipboard.png

 首先,用戶程序在用戶態調用send方法來發送一段較長的數據。而後send函數調用內核態的tcp_sendmsg方法進行處理。微信

 主要注意的是,send方法返回成功,內核也不必定真正將IP報文都發送到網絡中,也就是說內核發送報文和send方法是不一樣步的。因此,內核須要將用戶態內存中的發送數據,拷貝到內核態內存中,不依賴於用戶態內存,使得進程能夠快速釋放發送數據佔用的用戶態內存。網絡

 在拷貝過程當中,內核將待發送的數據,按照MSS來劃分紅多個儘可能接近MSS大小的分片,放到這個TCP鏈接對應的tcp_write_queue發送隊列中。架構

 內核中爲每一個TCP鏈接分配的內核緩存,也就是tcp_write_queue隊列的大小是有限的。當沒有多餘的空間來複制用戶態的待發送數據時,就須要調用sk_stream_wait_memory方法來等待空間,等到滑動窗口移動,釋放出一些緩存出來(收到發送報文相對應的ACK後,不須要再緩存該已發送出的報文,由於既然已經確認對方收到,就不須要重發,能夠釋放緩存)。tcp

 當這個套接字是阻塞套接字時,等待的超時時間就是SO_SNDTIMEO選項指定的發送超時時間。若是這個套接字是非阻塞套接字,則超時時間就是0。也就是說,sk_stream_wait_memory對於非阻塞套接字會直接返回,並將 errno錯誤碼置爲EAGAIN。ide

 咱們假定使用了阻塞套接字,且等待了足夠久的時間,收到了對方的ACK,滑動窗口釋放出了緩存。因此,能夠將剩下的用戶態數據都組成MSS報文拷貝到內核態的緩存隊列中。函數

 最後,調用tcp_push等方法,它最終會調用IP層的方法來發送tcp_write_queue隊列中的報文。注意的是,IP層方法返回時,也不意味着報文發送了出去。學習

 在發送函數處理過程當中,Nagle算法、滑動窗口、擁塞窗口都會影響發送操做。ui

MTU和MSS

 咱們都知道TCP/IP架構有五層協議,低層協議的規則會影響到上層協議,好比說數據鏈路層的最大傳輸單元MTU和傳輸層TCP協議的最大報文段長度MSS。

 數據鏈路層協議會對網絡分組的長度進行限制,也就是不能超過其規定的MTU,例如以太網限制爲1500字節,802.3限制爲1492字節。可是,須要注意的時,如今有些網卡具有自動分包功能,因此也能夠傳輸遠大於MTU的幀

MTU示意圖

 網絡層的IP協議試圖發送報文時,若報文的長度大於MTU限制,就會被分紅若干個小於MTU的報文,每一個報文都會有獨立的IP頭部。IP協議能自動獲取所在局域網的MTU值,而後按照這個MTU來分片。IP協議的分片機制對於傳輸層是透明的,接收方的IP協議會根據收到的多個IP包頭部,將發送方IP層分片出的IP包重組爲一個消息。

 這種IP層的分片效率是不好的,由於首先作了額外的分片操做,而後全部分片都到達後,接收方纔能重組成一個包,其中任何一個分片丟失了,都必須重發全部分片。

 因此,TCP層爲了不IP層執行數據報分片定義了最大報文段長度MSS。在TCP創建鏈接時會通知各自指望接收到的MSS的大小。

 須要注意的是MSS的值是預估值。兩臺主機只是根據其所在局域網的計算MSS,可是TCP鏈接上可能會穿過許多中間網絡,這些網絡分別具備不一樣的數據鏈路層,致使問題。好比說,若中間途徑的MTU小於兩臺主機所在的網絡MTU時,選定的MSS仍然太大了,會致使中間路由器出現IP層的分片或者直接返回錯誤(設置IP頭部的DF標誌位)。

 好比阿里中間件的這篇文章(連接不見的話,請看文末)所說,當上述狀況發生時,可能會致使卡死狀態,好比scp的時候進度卡着不懂,或者其餘更復雜操做的進度卡死。

滑動窗口

 IP層協議屬於不可靠的協議,IP層並不關心數據是否發送到了接收方,TCP經過確認機制來保證數據傳輸的可靠性。

 除了保證數據一定發送到對端,TCP還要解決包亂序(reordering)和流控的問題。包亂序和流控會涉及滑動窗口和接收報文的out_of_order隊列,另外擁塞控制算法也會處理流控,詳情請看TCP擁塞控制算法簡介

 TCP頭裏有一個字段叫Window,又叫Advertised-Window,這個字段是接收端告訴發送端本身還有多少緩衝區能夠接收數據。因而發送端就能夠根據這個接收端的處理能力來發送數據,不然會致使接收端處理不過來。

 咱們能夠將發送的數據分爲如下四類,將它們放在時間軸上統一觀察。

滑動窗口

  • Sent and Acknowledged: 表示已經發送成功並已經被確認的數據,好比圖中的前31個字節的數據
  • Send But Not Yet Acknowledged:表示發送但沒有被確認的數據,數據被髮送出去,沒有收到接收端的ACK,認爲並無完成發送,這個屬於窗口內的數據。
  • Not Sent,Recipient Ready to Receive:表示須要儘快發送的數據,這部分數據已經被加載到緩存等待發送,也就是發送窗口中。接收方ACK表示有足夠空間來接受這些包,因此發送方須要儘快發送這些包。
  • Not Sent,Recipient Not Ready to Receive: 表示屬於未發送,同時接收端也不容許發送的,由於這些數據已經超出了發送端所接收的範圍

 除了四種不一樣範疇的數據外,咱們能夠看到上邊的示意圖中還有三種窗口。

  • Window Already Sent:已經發送了,可是沒有收到ACK,和Send But Not Yet Acknowledged部分重合。
  • Usable Window : 可用窗口,和Not Sent,Recipient Ready to Receive部分重合
  • Send Window: 真正的窗口大小。創建鏈接時接收方會告知發送方本身可以處理的發送窗口大小,同時在接收過程當中也不斷的通告能處理窗口的大小,來實時調節。

 下面,咱們來看一下滑動窗口的滑動。下圖是滑動窗口滑動的示意圖。

滑動窗口滑動

 當發送方收到發送數據的確認消息時,會移動發送窗口。好比上圖中,接收到36字節的確認,將其以前的5個字節都移除發送窗口,而後46-51的字節發出,最後將52到56的字節加入到可用窗口。

 下面咱們來看一下總體的示意圖。

接受端控制發送端

 圖片來源爲tcpipguide.

 client端窗口中不一樣顏色的矩形塊表明的含義和上邊滑動窗口示意圖中相同。咱們只簡單看一下第二三四步。接收端發送的TCP報文window爲260,表示發送窗口減小100,能夠發現黑色矩形縮短了,也就是發送窗口減小了100。而且ack爲141,因此發送端將140個字節的數據從發送窗口中移除,這些數據從Send But Not Yet Acknowledged變爲Sent and Acknowledged,也就是從藍色變成紫色。而後發送端發送180字節的數據,就有180字節的數據從Not Sent,Recipient Ready to Receive變爲Send But Not Yet Acknowledged,也就是從綠色變爲藍色。

Nagle算法

 上述滑動窗口會出現一種Silly Window Syndrome的問題,當接收端來不及取走Receive Windows裏的數據,會致使發送端的發送窗口愈來愈小。到最後,若是接收端騰出幾個字節並告訴發送端如今有幾個字節的window,而咱們的發送端會義無反顧地發送這幾個字節。

 只爲了發送幾個字節,要加上TCP和IP頭的40多個字節。這樣,效率過低,就像你搬運物品,明明一次能夠所有搬完,可是卻恰恰一次只搬一個物品,來回搬屢次。

 爲此,TCP引入了Nagle算法。應用進程調用發送方法時,可能每次只發送小塊數據,形成這臺機器發送了許多小的TCP報文。對於整個網絡的執行效率來講,小的TCP報文會增長網絡擁塞的可能。所以,若是有可能,應該將相臨的TCP報文合併成一個較大的TCP報文(固然仍是小於MSS的)發送。

 Nagle算法的規則以下所示(可參考tcp_output.c文件裏tcp_nagle_check函數註釋):

  • 若是包長度達到MSS,則容許發送;
  • 若是該包含有FIN,則容許發送;
  • 設置了TCP_NODELAY選項,則容許發送;
  • 未設置TCP_CORK選項時,若全部發出去的小數據包(包長度小於MSS)均被確認,則容許發送;
  • 上述條件都未知足,但發生了超時(通常爲200ms),則當即發送。

當對請求的時延很是在乎且網絡環境很是好的時候(例如同一個機房內),Nagle算法能夠關閉。使用TCP_NODELAY套接字選項就能夠關閉Nagle算法

 訂閱最新文章,歡迎關注個人微信公衆號

我的博客: Remcarpediem

我的微信公衆號:

參考

相關文章
相關標籤/搜索