在上一篇中,咱們已經創建好的TCP鏈接,對應着操做系統分配的1個套接字。操做TCP協議發送數據時,面對的是數據流。一般調用諸如send或者write方法來發送數據到另外一臺主機,那麼,調用這樣的方法時,在操做系統內核中發生了什麼事情呢?咱們帶着如下3個問題來細細分析:發送方法成功返回時,能保證TCP另外一端的主機接收到嗎?能保證數據已經發送到網絡上了嗎?套接字爲阻塞或者非阻塞時,發送方法作的事情有何不一樣?
要回答上面3個問題涉及了很多知識點,咱們先在TCP層面上看看,發送方法調用時內核作了哪些事。我不想去羅列內核中的數據結構、方法等,畢竟大部分應用程序開發者不須要了解這些,僅以一幅示意圖粗略表示,以下:
圖1 一種典型場景下發送TCP消息的流程
再詳述上圖10個步驟前,先要澄清幾個概念:MTU、MSS、tcp_write_queue發送隊列、阻塞與非阻塞套接字、擁塞窗口、滑動窗口、Nagle算法。
當咱們調用發送方法時,會把咱們代碼中構造好的消息流做爲參數傳遞。這個消息流可大可小,例如幾個字節,或者幾兆字節。當消息流較大時,將有可能出現分片。咱們先來討論分片問題。
一、MSS與TCP的分片
由上一篇文中可知,TCP層是第4層傳輸層,第3層IP網絡層、第2層數據鏈路層具有的約束條件一樣對TCP層生效。下面來看看數據鏈路層中的一個概念:最大傳輸單元MTU。
不管何種類型的數據鏈路層,都會對網絡分組的長度有一個限制。例如以太網限制爲1500字節,802.3限制爲1492字節。當內核的IP網絡層試圖發送報文時,若一個報文的長度大於MTU限制,就會被分紅若干個小於MTU的報文,每一個報文都會有獨立的IP頭部。
看看IP頭部的格式:
圖2 IP頭部格式
能夠看到,其指定IP包總長度的是一個16位(2字節)的字段,這意味一個IP包最大能夠是65535字節。
若TCP層在以太網中試圖發送一個大於1500字節的消息,調用IP網絡層方法發送消息時,IP層會自動的獲取所在局域網的MTU值,並按照所在網絡的MTU大小來分片。IP層同時但願這個分片對於傳輸層來講是透明的,接收方的IP層會根據收到的多個IP包頭部,將發送方IP層分片出的IP包重組爲一個消息。
這種IP層的分片效率是不好的,由於必須全部分片都到達才能重組成一個包,其中任何一個分片丟失了,都必須重發全部分片。因此,TCP層會試圖避免IP層執行數據報分片。
爲了不IP層的分片,TCP協議定義了一個新的概念:最大報文段長度MSS。它定義了一個TCP鏈接上,一個主機指望對端主機發送單個報文的最大長度。TCP3次握手創建鏈接時,鏈接雙方都要互相告知本身指望接收到的MSS大小。例如(使用tcpdump抓包):
15:05:08.230782 IP 10.7.80.57.64569 > houyi-vm02.dev.sd.aliyun.com.tproxy: S 3027092051:3027092051(0) win 8192 <mss 1460,nop,wscale 8,nop,nop,sackOK>
15:05:08.234267 IP houyi-vm02.dev.sd.aliyun.com.tproxy > 10.7.80.57.64569: S 26006838:26006838(0) ack 3027092052 win 5840 <mss 1460,nop,nop,sackOK,nop,wscale 9>
15:05:08.233320 IP 10.7.80.57.64543 > houyi-vm02.dev.sd.aliyun.com.tproxy: P 78972532:78972923(391) ack 12915963 win 255
因爲例子中兩臺主機都在以太網內,以太網的MTU爲1500,減去IP和TCP頭部的長度,MSS就是1460,三次握手中,SYN包都會攜帶指望的MSS大小。
當應用層調用TCP層提供的發送方法時,內核的TCP模塊在tcp_sendmsg方法裏,會按照對方告知的MSS來分片,把消息流分爲多個網絡分組(如圖1中的3個網絡分組),再調用IP層的方法發送數據。
這個MSS就不會改變了嗎?
會的。上文說過,MSS就是爲了不IP層分片,在創建握手時告知對方指望接收的MSS值並不必定靠得住。由於這個值是預估的,TCP鏈接上的兩臺主機若處於不一樣的網絡中,那麼,鏈接上可能有許多中間網絡,這些網絡分別具備不一樣的數據鏈路層,這樣,TCP鏈接上有許多個MTU。特別是,若中間途徑的MTU小於兩臺主機所在的網絡MTU時,選定的MSS仍然太大了,會致使中間路由器出現IP層的分片。
怎樣避免中間網絡可能出現的分片呢?
經過IP頭部的DF標誌位,這個標誌位是告訴IP報文所途經的全部IP層代碼:不要對這個報文分片。若是一個IP報文太大必需要分片,則直接返回一個ICMP錯誤,說明必需要分片了,且待分片路由器網絡接受的MTU值。這樣,鏈接上的發送方主機就能夠從新肯定MSS。
二、發送方法返回成功後,數據必定發送到了TCP的另外一端嗎?
答案固然是否認的。解釋這個問題前,先來看看TCP是如何保證可靠傳輸的。
TCP把本身要發送的數據流裏的每個字節都當作一個序號,可靠性是要求鏈接對端在接收到數據後,要發送ACK確認,告訴它已經接收到了多少字節的數據。也就是說,怎樣確保數據必定發送成功了呢?必須等待發送數據對應序號的ACK到達,才能確保數據必定發送成功。TCP層提供的send或者write這樣的方法是不會作這件事的,看看圖1,它究竟作了哪些事。
圖1中分爲10步。
(1)應用程序試圖調用send方法來發送一段較長的數據。
(2)內核主要經過tcp_sendmsg方法來完成。
(3)(4)內核真正執行報文的發送,與send方法的調用並非同步的。即,send方法返回成功了,也不必定把IP報文都發送到網絡中了。所以,須要把用戶須要發送的用戶態內存中的數據,拷貝到內核態內存中,不依賴於用戶態內存,也使得進程能夠快速釋放發送數據佔用的用戶態內存。但這個拷貝操做並非簡單的複製,而是把待發送數據,按照MSS來劃分紅多個儘可能達到MSS大小的分片報文段,複製到內核中的sk_buff結構來存放,同時把這些分片組成隊列,放到這個TCP鏈接對應的tcp_write_queue發送隊列中。
(5)內核中爲這個TCP鏈接分配的內核緩存是有限的(/proc/sys/net/core/wmem_default)。當沒有多餘的內核態緩存來複制用戶態的待發送數據時,就須要調用一個方法sk_stream_wait_memory來等待滑動窗口移動,釋放出一些緩存出來(收到ACK後,不須要再緩存原來已經發送出的報文,由於既然已經確認對方收到,就不須要定時重發,天然就釋放緩存了)。例如:
這裏的sk_stream_wait_memory方法接受一個參數timeo,就是等待超時的時間。這個時間是tcp_sendmsg方法剛開始就拿到的,以下:
也就是說,當這個套接字是阻塞套接字時,timeo就是SO_SNDTIMEO選項指定的發送超時時間。若是這個套接字是非阻塞套接字, timeo變量就會是0。
實際上,sk_stream_wait_memory對於非阻塞套接字會直接返回,並將 errno錯誤碼置爲EAGAIN。
(6)在圖1的例子中,咱們假定使用了阻塞套接字,且等待了足夠久的時間,收到了對方的ACK,滑動窗口釋放出了緩存。
(7)將剩下的用戶態數據都組成MSS報文拷貝到內核態的sk_buff中。
(8)最後,調用tcp_push等方法,它最終會調用IP層的方法來發送tcp_write_queue隊列中的報文。
注意,IP層返回時,並不必定是把報文發送了出去。
(9)(10)發送方法返回。
從圖1的10個步驟中可知,不管是使用阻塞仍是非阻塞套接字,發送方法成功返回時(不管所有成功或者部分紅功),既不表明TCP鏈接的另外一端主機接收到了消息,也不表明本機把消息發送到了網絡上,只是說明,內核將會試圖保證把消息送達對方。
三、Nagle算法、滑動窗口、擁塞窗口對發送方法的影響
圖1第8步tcp_push方法作了些什麼呢?先來看看主要的流程:
圖3 發送TCP消息的簡易流程
下面簡單看看這幾個概念:
(1)滑動窗口
滑動窗口你們都比較熟悉,就不詳細介紹了。TCP鏈接上的雙方都會通知對方本身的接收窗口大小。而對方的接收窗口大小就是本身的發送窗口大小。tcp_push在發送數據時固然須要與發送窗口打交道。發送窗口是一個時刻變化的值,隨着ACK的到達會變大,隨着發出新的數據包會變小。固然,最大也只能到三次握手時對方通告的窗口大小。tcp_push在發送數據時,最終會使用tcp_snd_wnd_test方法來判斷當前待發送的數據,其序號是否超出了發送滑動窗口的大小,例如:
(2)慢啓動和擁塞窗口
因爲兩臺主機間的網絡可能很複雜,經過廣域網時,中間的路由器轉發能力多是瓶頸。也就是說,若是一方簡單的按照另外一方主機三次握手時通告的滑動窗口大小來發送數據的話,可能會使得網絡上的轉發路由器性能雪上加霜,最終丟失更多的分組。這時,各個操做系統內核都會對TCP的發送階段加入慢啓動和擁塞避免算法。慢啓動算法說白了,就是對方通告的窗口大小隻表示對方接收TCP分組的能力,不表示中間網絡可以處理分組的能力。因此,發送方請悠着點發,確保網絡很是通暢了後,再按照對方通告窗口來敞開了發。
擁塞窗口就是下面的cwnd,它用來幫助慢啓動的實現。鏈接剛創建時,擁塞窗口的大小遠小於發送窗口,它其實是一個MSS。每收到一個ACK,擁塞窗口擴大一個MSS大小,固然,擁塞窗口最大隻能到對方通告的接收窗口大小。固然,爲了不指數式增加,擁塞窗口大小的增加會更慢一些,是線性的平滑的增加過程。
因此,在tcp_push發送消息時,還會檢查擁塞窗口,飛行中的報文數要小於擁塞窗口個數,而發送數據的長度也要小於擁塞窗口的長度。
以下所示,首先用unsigned int tcp_cwnd_test方法檢查飛行的報文數是否小於擁塞窗口個數(多少個MSS的個數):
再經過tcp_window_allows方法獲取擁塞窗口與滑動窗口的最小長度,檢查待發送的數據是否超出:
(3)是否符合NAGLE算法?
Nagle算法的初衷是這樣的:應用進程調用發送方法時,可能每次只發送小塊數據,形成這臺機器發送了許多小的TCP報文。對於整個網絡的執行效率來講,小的TCP報文會增長網絡擁塞的可能,所以,若是有可能,應該將相臨的TCP報文合併成一個較大的TCP報文(固然仍是小於MSS的)發送。
Nagle算法要求一個TCP鏈接上最多隻能有一個發送出去還沒被確認的小分組,在該分組的確認到達以前不能發送其餘的小分組。
在上一篇中,咱們已經創建好的TCP鏈接,對應着操做系統分配的1個套接字。操做TCP協議發送數據時,面對的是數據流。一般調用諸如send或者write方法來發送數據到另外一臺主機,那麼,調用這樣的方法時,在操做系統內核中發生了什麼事情呢?咱們帶着如下3個問題來細細分析:發送方法成功返回時,能保證TCP另外一端的主機接收到嗎?能保證數據已經發送到網絡上了嗎?套接字爲阻塞或者非阻塞時,發送方法作的事情有何不一樣?
要回答上面3個問題涉及了很多知識點,咱們先在TCP層面上看看,發送方法調用時內核作了哪些事。我不想去羅列內核中的數據結構、方法等,畢竟大部分應用程序開發者不須要了解這些,僅以一幅示意圖粗略表示,以下:
圖1 一種典型場景下發送TCP消息的流程
再詳述上圖10個步驟前,先要澄清幾個概念:MTU、MSS、tcp_write_queue發送隊列、阻塞與非阻塞套接字、擁塞窗口、滑動窗口、Nagle算法。
當咱們調用發送方法時,會把咱們代碼中構造好的消息流做爲參數傳遞。這個消息流可大可小,例如幾個字節,或者幾兆字節。當消息流較大時,將有可能出現分片。咱們先來討論分片問題。
一、MSS與TCP的分片
由上一篇文中可知,TCP層是第4層傳輸層,第3層IP網絡層、第2層數據鏈路層具有的約束條件一樣對TCP層生效。下面來看看數據鏈路層中的一個概念:最大傳輸單元MTU。
不管何種類型的數據鏈路層,都會對網絡分組的長度有一個限制。例如以太網限制爲1500字節,802.3限制爲1492字節。當內核的IP網絡層試圖發送報文時,若一個報文的長度大於MTU限制,就會被分紅若干個小於MTU的報文,每一個報文都會有獨立的IP頭部。
看看IP頭部的格式:
圖2 IP頭部格式
能夠看到,其指定IP包總長度的是一個16位(2字節)的字段,這意味一個IP包最大能夠是65535字節。
若TCP層在以太網中試圖發送一個大於1500字節的消息,調用IP網絡層方法發送消息時,IP層會自動的獲取所在局域網的MTU值,並按照所在網絡的MTU大小來分片。IP層同時但願這個分片對於傳輸層來講是透明的,接收方的IP層會根據收到的多個IP包頭部,將發送方IP層分片出的IP包重組爲一個消息。
這種IP層的分片效率是不好的,由於必須全部分片都到達才能重組成一個包,其中任何一個分片丟失了,都必須重發全部分片。因此,TCP層會試圖避免IP層執行數據報分片。
爲了不IP層的分片,TCP協議定義了一個新的概念:最大報文段長度MSS。它定義了一個TCP鏈接上,一個主機指望對端主機發送單個報文的最大長度。TCP3次握手創建鏈接時,鏈接雙方都要互相告知本身指望接收到的MSS大小。例如(使用tcpdump抓包):
15:05:08.230782 IP 10.7.80.57.64569 > houyi-vm02.dev.sd.aliyun.com.tproxy: S 3027092051:3027092051(0) win 8192 <mss 1460,nop,wscale 8,nop,nop,sackOK>
15:05:08.234267 IP houyi-vm02.dev.sd.aliyun.com.tproxy > 10.7.80.57.64569: S 26006838:26006838(0) ack 3027092052 win 5840 <mss 1460,nop,nop,sackOK,nop,wscale 9>
15:05:08.233320 IP 10.7.80.57.64543 > houyi-vm02.dev.sd.aliyun.com.tproxy: P 78972532:78972923(391) ack 12915963 win 255
因爲例子中兩臺主機都在以太網內,以太網的MTU爲1500,減去IP和TCP頭部的長度,MSS就是1460,三次握手中,SYN包都會攜帶指望的MSS大小。
當應用層調用TCP層提供的發送方法時,內核的TCP模塊在tcp_sendmsg方法裏,會按照對方告知的MSS來分片,把消息流分爲多個網絡分組(如圖1中的3個網絡分組),再調用IP層的方法發送數據。
這個MSS就不會改變了嗎?
會的。上文說過,MSS就是爲了不IP層分片,在創建握手時告知對方指望接收的MSS值並不必定靠得住。由於這個值是預估的,TCP鏈接上的兩臺主機若處於不一樣的網絡中,那麼,鏈接上可能有許多中間網絡,這些網絡分別具備不一樣的數據鏈路層,這樣,TCP鏈接上有許多個MTU。特別是,若中間途徑的MTU小於兩臺主機所在的網絡MTU時,選定的MSS仍然太大了,會致使中間路由器出現IP層的分片。
怎樣避免中間網絡可能出現的分片呢?
經過IP頭部的DF標誌位,這個標誌位是告訴IP報文所途經的全部IP層代碼:不要對這個報文分片。若是一個IP報文太大必需要分片,則直接返回一個ICMP錯誤,說明必需要分片了,且待分片路由器網絡接受的MTU值。這樣,鏈接上的發送方主機就能夠從新肯定MSS。
二、發送方法返回成功後,數據必定發送到了TCP的另外一端嗎?
答案固然是否認的。解釋這個問題前,先來看看TCP是如何保證可靠傳輸的。
TCP把本身要發送的數據流裏的每個字節都當作一個序號,可靠性是要求鏈接對端在接收到數據後,要發送ACK確認,告訴它已經接收到了多少字節的數據。也就是說,怎樣確保數據必定發送成功了呢?必須等待發送數據對應序號的ACK到達,才能確保數據必定發送成功。TCP層提供的send或者write這樣的方法是不會作這件事的,看看圖1,它究竟作了哪些事。
圖1中分爲10步。
(1)應用程序試圖調用send方法來發送一段較長的數據。
(2)內核主要經過tcp_sendmsg方法來完成。
(3)(4)內核真正執行報文的發送,與send方法的調用並非同步的。即,send方法返回成功了,也不必定把IP報文都發送到網絡中了。所以,須要把用戶須要發送的用戶態內存中的數據,拷貝到內核態內存中,不依賴於用戶態內存,也使得進程能夠快速釋放發送數據佔用的用戶態內存。但這個拷貝操做並非簡單的複製,而是把待發送數據,按照MSS來劃分紅多個儘可能達到MSS大小的分片報文段,複製到內核中的sk_buff結構來存放,同時把這些分片組成隊列,放到這個TCP鏈接對應的tcp_write_queue發送隊列中。
(5)內核中爲這個TCP鏈接分配的內核緩存是有限的(/proc/sys/net/core/wmem_default)。當沒有多餘的內核態緩存來複制用戶態的待發送數據時,就須要調用一個方法sk_stream_wait_memory來等待滑動窗口移動,釋放出一些緩存出來(收到ACK後,不須要再緩存原來已經發送出的報文,由於既然已經確認對方收到,就不須要定時重發,天然就釋放緩存了)。例如:
這裏的sk_stream_wait_memory方法接受一個參數timeo,就是等待超時的時間。這個時間是tcp_sendmsg方法剛開始就拿到的,以下:
也就是說,當這個套接字是阻塞套接字時,timeo就是SO_SNDTIMEO選項指定的發送超時時間。若是這個套接字是非阻塞套接字, timeo變量就會是0。
實際上,sk_stream_wait_memory對於非阻塞套接字會直接返回,並將 errno錯誤碼置爲EAGAIN。
(6)在圖1的例子中,咱們假定使用了阻塞套接字,且等待了足夠久的時間,收到了對方的ACK,滑動窗口釋放出了緩存。
(7)將剩下的用戶態數據都組成MSS報文拷貝到內核態的sk_buff中。
(8)最後,調用tcp_push等方法,它最終會調用IP層的方法來發送tcp_write_queue隊列中的報文。
注意,IP層返回時,並不必定是把報文發送了出去。
(9)(10)發送方法返回。
從圖1的10個步驟中可知,不管是使用阻塞仍是非阻塞套接字,發送方法成功返回時(不管所有成功或者部分紅功),既不表明TCP鏈接的另外一端主機接收到了消息,也不表明本機把消息發送到了網絡上,只是說明,內核將會試圖保證把消息送達對方。
三、Nagle算法、滑動窗口、擁塞窗口對發送方法的影響
圖1第8步tcp_push方法作了些什麼呢?先來看看主要的流程:
圖3 發送TCP消息的簡易流程
下面簡單看看這幾個概念:
(1)滑動窗口
滑動窗口你們都比較熟悉,就不詳細介紹了。TCP鏈接上的雙方都會通知對方本身的接收窗口大小。而對方的接收窗口大小就是本身的發送窗口大小。tcp_push在發送數據時固然須要與發送窗口打交道。發送窗口是一個時刻變化的值,隨着ACK的到達會變大,隨着發出新的數據包會變小。固然,最大也只能到三次握手時對方通告的窗口大小。tcp_push在發送數據時,最終會使用tcp_snd_wnd_test方法來判斷當前待發送的數據,其序號是否超出了發送滑動窗口的大小,例如:
(2)慢啓動和擁塞窗口
因爲兩臺主機間的網絡可能很複雜,經過廣域網時,中間的路由器轉發能力多是瓶頸。也就是說,若是一方簡單的按照另外一方主機三次握手時通告的滑動窗口大小來發送數據的話,可能會使得網絡上的轉發路由器性能雪上加霜,最終丟失更多的分組。這時,各個操做系統內核都會對TCP的發送階段加入慢啓動和擁塞避免算法。慢啓動算法說白了,就是對方通告的窗口大小隻表示對方接收TCP分組的能力,不表示中間網絡可以處理分組的能力。因此,發送方請悠着點發,確保網絡很是通暢了後,再按照對方通告窗口來敞開了發。
擁塞窗口就是下面的cwnd,它用來幫助慢啓動的實現。鏈接剛創建時,擁塞窗口的大小遠小於發送窗口,它其實是一個MSS。每收到一個ACK,擁塞窗口擴大一個MSS大小,固然,擁塞窗口最大隻能到對方通告的接收窗口大小。固然,爲了不指數式增加,擁塞窗口大小的增加會更慢一些,是線性的平滑的增加過程。
因此,在tcp_push發送消息時,還會檢查擁塞窗口,飛行中的報文數要小於擁塞窗口個數,而發送數據的長度也要小於擁塞窗口的長度。
以下所示,首先用unsigned int tcp_cwnd_test方法檢查飛行的報文數是否小於擁塞窗口個數(多少個MSS的個數):
再經過tcp_window_allows方法獲取擁塞窗口與滑動窗口的最小長度,檢查待發送的數據是否超出:
(3)是否符合NAGLE算法?
Nagle算法的初衷是這樣的:應用進程調用發送方法時,可能每次只發送小塊數據,形成這臺機器發送了許多小的TCP報文。對於整個網絡的執行效率來講,小的TCP報文會增長網絡擁塞的可能,所以,若是有可能,應該將相臨的TCP報文合併成一個較大的TCP報文(固然仍是小於MSS的)發送。
Nagle算法要求一個TCP鏈接上最多隻能有一個發送出去還沒被確認的小分組,在該分組的確認到達以前不能發送其餘的小分組。
內核中是經過?tcp_nagle_test方法實現該算法的。咱們簡單的看下:
再來看看tcp_nagle_check方法,它與上一個方法不一樣,返回0表示能夠發送,返回非0則不能夠,正好相反。
最後看看tcp_minshall_check作了些什麼:
想象一種場景,當對請求的時延很是在乎且網絡環境很是好的時候(例如同一個機房內)
英文美文,Nagle算法能夠關閉,這實在也不必。使用TCP_NODELAY套接字選項就能夠關閉Nagle算法。看看setsockopt是怎麼與上述方法配合工做的:
固然,調用了IP層的方法返回後,也未必就保證此時數據必定發送到網絡中去了。
下一篇咱們探討如何接收TCP消息,以及接收到ack後內核作了些什麼。
內核中是經過?tcp_nagle_test方法實現該算法的。咱們簡單的看下:
再來看看tcp_nagle_check方法,它與上一個方法不一樣,返回0表示能夠發送,返回非0則不能夠,正好相反。
最後看看tcp_minshall_check作了些什麼:
想象一種場景,當對請求的時延很是在乎且網絡環境很是好的時候(例如同一個機房內),Nagle算法能夠關閉,這實在也不必。使用TCP_NODELAY套接字選項就能夠關閉Nagle算法。看看setsockopt是怎麼與上述方法配合工做的:
固然,調用了IP層的方法返回後,也未必就保證此時數據必定發送到網絡中去了。
下一篇咱們探討如何接收TCP消息,以及接收到ack後內核作了些什麼。