好文分享:TCP報文發送的那些事

作者:remcarpediem
來源:http://remcarpediem.net/2019/04/17/TCP報文發送的那些事

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

發送報文

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

好文分享:TCP報文發送的那些事

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

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

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

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

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

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

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

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

MTU和MSS

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

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

好文分享:TCP報文發送的那些事

網絡層的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標誌位)。

比如阿里中間件的這篇文章

[1](鏈接不見的話,請看文末)所說,當上述情況發生時,可能會導致卡死狀態,比如scp的時候進度卡著不懂,或者其他更復雜操作的進度卡死。

滑動窗口

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

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

TCP頭裡有一個字段叫Window,又叫Advertised-Window,這個字段是接收端告訴發送端自己還有多少緩衝區可以接收數據。於是發送端就可以根據這個接收端的處理能力來發送數據,否則會導致接收端處理不過來。

我們可以將發送的數據分為以下四類,將它們放在時間軸上統一觀察。

好文分享:TCP報文發送的那些事

  • 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: 真正的窗口大小。建立連接時接收方會告知發送方自己能夠處理的發送窗口大小,同時在接收過程中也不斷的通告能處理窗口的大小,來實時調節。

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

好文分享:TCP報文發送的那些事

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

下面我們來看一下整體的示意圖。

好文分享:TCP報文發送的那些事

 圖片來源為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算法


分享到:


相關文章: