“一段數據流從應用程式發送端,一直到應用程式接收端,總共經過了多少次拷貝?” 先看發送端,當應用程式將數據送到發送緩衝區時,調用的是 send 或 write 方法,如果緩存中沒有空間,系統調用就會失敗或者阻塞。我們說,這個動作事實上是一次”顯式拷貝“。而在這之後,數據將會按照 TCP/IP 的分層... ...
“一段數據流從應用程式發送端,一直到應用程式接收端,總共經過了多少次拷貝?”
先看發送端,當應用程式將數據送到發送緩衝區時,調用的是 send 或 write 方法,如果緩存中沒有空間,系統調用就會失敗或者阻塞。我們說,這個動作事實上是一次”顯式拷貝“。而在這之後,數據將會按照 TCP/IP 的分層再次進行拷貝,這層的拷貝對我們來說就不是顯式的了。
接下來輪到 TCP 協議棧工作,創建 Packet 報文,並把報文發送到傳輸隊列中(qdisc),傳輸隊列是一個典型的 FIFO 隊列,隊列的最大值可以通過 ifocnfig 命令輸出的 txqueuelen 來查看。通常情況下,這個值有幾千報文大小。
TX ring 在網路驅動和網卡之間,也是一個傳輸請求的隊列。
網卡作為物理設備工作在物理層,主要工作是把要發送的報文保存到內部的緩存中,併發送出去。
接下來再看接收端,報文首先到達網卡,由網卡保存在自己的接收緩存中,接下來報文被髮送至網路驅動和網卡之間的 RX ring,網路驅動從 RX ring 獲取報文 ,然後把報文發送到上層。
這裡值得註意的是,網路驅動和上層之間沒有緩存,因為網路驅動使用 Napi 進行數據傳輸。因此,可以認為上層直接從 RX ring 中讀取報文。
最後,報文的數據保存在套接字接收緩存中,應用程式從套接字接收緩存中讀取數據。
這就是數據流從應用程式發送端,一直到應用程式接收端的整個歷程,你看懂了嗎?
上面的任何一個環節稍有積壓,都會對程式性能產生影響。但好消息是,內核和網路設備供應商已經幫我們把一切都打點好了,我們看到和用到的,其實只是冰山上的一角而已。
TIME_WAIT 的作用
TIME_WAIT 停留持續時間是固定的,是最長分節生命期 MSL(maximum segment lifetime)的兩倍,一般稱之為 2MSL。和大多數 BSD 派生的系統一樣,Linux 系統里有一個硬編碼的欄位,名稱為TCP_TIMEWAIT_LEN
,其值為 60 秒。也就是說,Linux 系統停留在 TIME_WAIT 的時間為固定的 60 秒。
#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME- WAIT state, about 60 seconds */
TCP 在設計的時候,做了充分的容錯性設計,比如,TCP 假設報文會出錯,需要重傳。在這裡,如果圖中主機 1 的 ACK 報文沒有傳輸成功,那麼主機 2 就會重新發送 FIN 報文。
如果主機 1 沒有維護 TIME_WAIT 狀態,而直接進入 CLOSED 狀態,它就失去了當前狀態的上下文,只能回覆一個 RST 操作,從而導致被動關閉方出現錯誤。
現在主機 1 知道自己處於 TIME_WAIT 的狀態,就可以在接收到 FIN 報文之後,重新發出一個 ACK 報文,使得主機 2 可以進入正常的 CLOSED 狀態。
第二個理由和連接“化身”和報文迷走有關係,為了讓舊連接的重覆分節在網路中自然消失。
我們知道,在網路中,經常會發生報文經過一段時間才能到達目的地的情況,產生的原因是多種多樣的,如路由器重啟,鏈路突然出現故障等。如果迷走報文到達時,發現 TCP 連接四元組(源 IP,源埠,目的 IP,目的埠)所代表的連接不復存在,那麼很簡單,這個報文自然丟棄。
close 和 shutdown 的差別
第一個差別:close 會關閉連接,並釋放所有連接對應的資源,而 shutdown 並不會釋放掉套接字和所有的資源。
第二個差別:close 存在引用計數的概念,並不一定導致該套接字不可用;shutdown 則不管引用計數,直接使得該套接字不可用,如果有別的進程企圖使用該套接字,將會受到影響。
第三個差別:close 的引用計數導致不一定會發出 FIN 結束報文,而 shutdown 則總是會發出 FIN 結束報文,這在我們打算關閉連接通知對端的時候,是非常重要的。
TCP 是一種流式協議
在發送端,當我們調用 send 函數完成數據“發送”以後,數據並沒有被真正從網路上發送出去,只是從應用程式拷貝到了操作系統內核協議棧中,至於什麼時候真正被髮送,取決於發送視窗、擁塞視窗以及當前發送緩衝區的大小等條件。也就是說,我們不能假設每次 send 調用發送的數據,都會作為一個整體完整地被髮送出去。
如果我們考慮實際網路傳輸過程中的各種影響,假設發送端陸續調用 send 函數先後發送 network 和 program 報文
接收端緩衝區保留了沒有被取走的數據,隨著應用程式不斷從接收端緩衝區讀出數據,接收端緩衝區就可以容納更多新的數據。如果我們使用 recv 從接收端緩衝區讀取數據,發送端緩衝區的數據是以位元組流的方式存在的,無論發送端如何構造 TCP 分組,接收端最終受到的位元組流總是像下麵這樣:
xxxxxxxxxxxxxxxxxnetworkprogramxxxxxxxxxxxx
關於接收端位元組流,有兩點需要註意:
第一,這裡 netwrok 和 program 的順序肯定是會保持的,也就是說,先調用 send 函數發送的位元組,總在後調用 send 函數發送位元組的前面,這個是由 TCP 嚴格保證的;
第二,如果發送過程中有 TCP 分組丟失,但是其後續分組陸續到達,那麼 TCP 協議棧會緩存後續分組,直到前面丟失的分組到達,最終,形成可以被應用程式讀取的數據流。
保證網路位元組序一致,POSIX 標準提供瞭如下的轉換函數:
uint16_t htons (uint16_t hostshort)
uint16_t ntohs (uint16_t netshort)
uint32_t htonl (uint32_t hostlong)
uint32_t ntohl (uint32_t netlong)
這裡函數中的 n 代表的就是 network,h 代表的是 host,s 表示的是 short,l 表示的是 long,分別表示 16 位和 32 位的整數。
發送端通過調用 send 函數之後,數據流並沒有馬上通過網路傳輸出去,而是存儲在套接字的發送緩衝區中,由網路協議棧決定何時發送、如何發送。當對應的數據發送給接收端,接收端回應 ACK,存儲在發送緩衝區的這部分數據就可以刪除了,但是,發送端並無法獲取對應數據流的 ACK 情況,也就是說,發送端沒有辦法判斷對端的接收方是否已經接收發送的數據流,如果需要知道這部分信息,就必須在應用層自己添加處理邏輯,例如顯式的報文確認機制。
從接收端來說,也沒有辦法保證 ACK 過的數據部分可以被應用程式處理,因為數據需要接收端程式從接收緩衝區中拷貝,可能出現的狀況是,已經 ACK 的數據保存在接收端緩衝區中,接收端處理程式突然崩潰了,這部分數據就沒有辦法被應用程式繼續處理。
TCP 連接建立之後,能感知 TCP 鏈路的方式是有限的,一種是以 read 為核心的讀操作,另一種是以 write 為核心的寫操作。
網路中斷造成的對端無 FIN 包
很多原因都會造成網路中斷,在這種情況下,TCP 程式並不能及時感知到異常信息。除非網路中的其他設備,如路由器發出一條 ICMP 報文,說明目的網路或主機不可達,這個時候通過 read 或 write 調用就會返回 Unreachable 的錯誤。
在沒有 ICMP 報文的情況下,TCP 程式並不能理解感應到連接異常。如果程式是阻塞在 read 調用上,那麼很不幸,程式無法從異常中恢復。這顯然是非常不合理的,不過,我們可以通過給 read 操作設置超時來解決
如果程式先調用了 write 操作發送了一段數據流,接下來阻塞在 read 調用上,結果會非常不同。Linux 系統的 TCP 協議棧會不斷嘗試將發送緩衝區的數據發送出去,大概在重傳 12 次、合計時間約為 9 分鐘之後,協議棧會標識該連接異常,這時,阻塞的 read 調用會返回一條 TIMEOUT 的錯誤信息。如果此時程式還執著地往這條連接寫數據,寫操作會立即失敗,返回一個 SIGPIPE 信號給應用程式。
系統崩潰造成的對端無 FIN 包
當系統突然崩潰,如斷電時,網路連接上來不及發出任何東西。這裡和通過系統調用殺死應用程式非常不同的是,沒有任何 FIN 包被髮送出來。
這種情況和網路中斷造成的結果非常類似,在沒有 ICMP 報文的情況下,TCP 程式只能通過 read 和 write 調用得到網路連接異常的信息,超時錯誤是一個常見的結果。
不過還有一種情況需要考慮,那就是系統在崩潰之後又重啟,當重傳的 TCP 分組到達重啟後的系統,由於系統中沒有該 TCP 分組對應的連接數據,系統會返回一個 RST 重置分節,TCP 程式通過 read 或 write 調用可以分別對 RST 進行錯誤處理。
如果是阻塞的 read 調用,會立即返回一個錯誤,錯誤信息為連接重置(Connection Resest)。
如果是一次 write 操作,也會立即失敗,應用程式會被返回一個 SIGPIPE 信號。
對端有 FIN 包發出
對端如果有 FIN 包發出,可能的場景是對端調用了 close 或 shutdown 顯式地關閉了連接,也可能是對端應用程式崩潰,操作系統內核代為清理所發出的。從應用程式角度上看,無法區分是哪種情形。
阻塞的 read 操作在完成正常接收的數據讀取之後,FIN 包會通過返回一個 EOF 來完成通知,此時,read 調用返回值為 0。這裡強調一點,收到 FIN 包之後 read 操作不會立即返回。你可以這樣理解,收到 FIN 包相當於往接收緩衝區里放置了一個 EOF 符號,之前已經在接收緩衝區的有效數據不會受到影響。
TCP並不總是“可靠”的?
https://www.cnblogs.com/jiu0821/p/7229568.html