天天看點

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頭裡有一個字段叫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

個人微信公衆号:

位址

參考

繼續閱讀