TCP報文傳送的那些事
今天我們來總結學習一下TCP傳送報文的相關知識,主要包括髮送報文的步驟,MSS,滑動視窗和Nagle演算法。
傳送報文
該節主要根據陶輝大神的系列文章總結而來。如下圖所示,我們一起來看一下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的幀 。
網路層的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
個人微信公眾號: