1. TCP 之11種狀態變遷
TCP 為一個連接定義了 11 種狀態,並且 TCP 規則規定如何基於當前狀態及在該狀態下所接收的分節從一個狀態轉換到另一個狀態。如,當某個應用進程在 CLOSED 狀態下執行主動打開時,TCP 將發送一個 SYN,且新的狀態是 SYN_SENT。如果這個 TCP 接着接收到一個帶 ACK 的 SYN,它將發送一個 ACK,且新的狀態是 ESTABLISHED。這個最終狀態是絕大多數數據傳送發送的狀態。
自 ESTABLISHED 狀態引出的兩個箭頭處理連接的終止。如果某個應用進程在接收到一個 FIN 之前調用 close(主動關閉),那就轉換到 FIN_WAIT_1 狀態。但如果某個應用進程在 ESTABLISHED 狀態期間接收到一個 FIN(被動關閉),那就轉換到 CLOSE_WAIT 狀態。
TCP 的狀態變遷圖 1
1.1 觀察分組
如下圖示例中的客戶通告一個值為 536 的 MSS(表示該客戶只實現了最小重組緩沖區大小),服務通告一個值為 1460 的 MSS(以太網上 IPv4 的典型值)。不同方向上 MSS 值不相同不成問題。
TCP 連接的分組交換圖 2
如上圖,一旦建立一個連接,客戶就構造一個請求並發送給服務器。這里假設該請求適合於單個 TCP 分節(即請求大小小於服務器通告的值為 1460 字節的 MSS)。服務器處理該請求並發送一個應答,同樣假設該應答也適合於單個分節(本例即小於 536 字節)。如上圖的粗箭頭所示。注意,服務器對客戶請求的確認是伴隨其應答發送的。這種做法稱為捎帶(piggybacking),它通常在服務器處理請求並產生應答的時間少於 200ms 時發生。如果服務器耗用更長時間,譬如說 1s,那么我們將看到先是確認后是應答。
1.2 TIME_WAIT 狀態
TIME_WAIT 狀態也稱為 2MSL 等待狀態。每個具體 TCP 實現必須選擇一個報文段最大生存時間 MSL(Maximum Segment Lifetime)。它是任何報文段被丟棄前在網絡內的最長時間。這個時間是有限的,因為 TCP 報文段以 IP 數據報在網絡內傳輸,而 IP 數據報則有限制其生存時間的 TTL 字段。
注:RFC 793[Postel 1981c] 指出 MSL 為 2 分鍾。然而,實現中的常用值是 30 秒,1 分鍾,或 2 分鍾。
對一個具體實現所給定的 MSL 值,處理的原則是:當 TCP 執行一個主動關閉,並發回最后一個 ACK,該連接必須在 TIME_WAIT 狀態停留的時間為 2 倍的 MSL。這樣可讓 TCP 再次發送最后的 ACK 以防這個 ACK 丟失(另一端超時並重發最后的 FIN)。
這種 2MSL 等待的另一個結果是這個 TCP 連接在 2MSL 等待期間,定義這個連接的 socket(客戶的 IP 地址和端口號,服務器的 IP 地址和端口號)不能再被使用。這個連接只能在 2MSL 結束后才能再被使用。
大多數 TCP 實現(如伯克利版)強加了更加嚴格的限制。在 2MSL 等待期間,socket 中使用的本地端口在默認情況下不能再被使用。
某些實現和 API 提供了一種避開這個限制的方法。可使用 setsockopt API 對該套接字設置 SO_REUSEADDR 屬性,該屬性可讓處於 2MSL 等待的本地端口再次被使用。
TIME_WAIT 狀態有兩個存在的理由:
- 可靠地實現 TCP 全雙工連接的終止;
- 允許老的重復分節在網絡中消逝。
第一個理由的解釋:
如上圖 2,假設最終的 ACK 丟失了,服務器將重新發送它的最終的那個 FIN,因此客戶必須維護狀態信息,以允許它重新發送最終那個 ACK。要是客戶不維護狀態信息,它將響應一個 RST(另外一種類型的 TCP 分節),該分節將被服務器解釋成一個錯誤。如果 TCP 打算執行所有必要的工作以徹底終止某個連接上兩個方向的數據流(即全雙工關閉),那么它必須正確處理連接終止序列 4 個分節中任何一個分節丟失的情況。本例也說明為什么執行主動關閉的那一端是處於 TIME_WAIT 狀態的那一端:因為可能不得不重傳最終那個 ACK 的就是那一端。
第二個理由的解釋:
假設在 12.106.32.254 的 1500 端口和 206.168.112.219 的 21 端口之間有一個 TCP 連接。我們關閉這個連接,過一段時間后在相同的 IP 地址和端口之間建立另一個連接。后一個連接稱為前一個連接的化身,因為它們的 IP 地址和端口號都相同。TCP 必須防止來自某個連接的老的重復分組在該連接已終止后再現,從而被誤解成屬於同一個連接的某個新的化身。為做到這一點,TCP 將不給處於 TIME_WAIT 狀態的連接發起新的化身。既然 TIME_WAIT 狀態的持續時間是 MSL 的 2 倍,這就足以讓某個方向上的分組最多存活 MSL 秒即被丟棄,另一個方向上的應答最多存活 MSL 秒也被丟棄。通過實施這個規則,我們就能保證每成功建立一個 TCP 連接時,來自該連接先前化身的重復分組都已在網絡中消逝。
1.3 半連接狀態: FIN_WAIT_2
若服務器發送了一個 FIN 分節給客戶端,並接收到客戶端回復的 ACK,但是客戶端沒有發送 FIN 該服務器,此時導致服務器停留在半連接狀態(FIN_WAIT_2),客戶端也將處於 CLOSE_WAIT 狀態。
1.3.1 close 和 shutdown 的區別
- close 把描述符的引用計數減 1,僅在該計數變為 0 時才關閉套接字;
- close 終止了數據傳輸的兩個方向;
- shutdown 可以有選擇的終止某個方向的數據傳送或者終止數據傳送的兩個方向;
- shutdown how = 1 就可以保證對方接收到一個 EOF 字符,而不管其他進程是否打開了套接字。而 close 不能保證,直到套接字引用計數值減為 0 時才會發送 EOF。也就是說直到所有的進程都關閉套接字。
1.3.2 shutdown 函數
int shutdown(int sockfd, int how);
how 的有如下取值:
- SHUT_RD:關閉連接的讀這一半。套接字中不再有數據可接收,而且套接字接收緩沖區的現有數據都被丟棄。進程不能再對這樣的套接字調用任何的讀函數。
- SHUT_WR:關閉連接的寫這一半。對於 TCP 套接字,這稱為半關閉。但以前留在套接字發送緩沖區中的數據將被發送掉,后跟 TCP 的正常終止序列。
- SHUT_RDWR:連接的讀半部和寫半部都關閉。這與調用 shutdown 兩次等效:第一次調用指定 SHUT_RD,第二次調用指定 SHUT_WR。