為什麼tcp的TIME_WAIT狀態要維持2MSL

愛寫程式的阿波張發表於2019-06-04

本文主要分析為什麼TIME_WAIT狀態的持續時間是2MSL而不是1MSL,3MSL或其它的時長,而不會詳細描述為什麼需要TIME_WAIT狀態。

閱讀本文需要的預備知識:

  • 瞭解TCP協議的狀態變遷;

  • 瞭解TCP拆鏈的四次揮手過程;

  • 瞭解為什麼需要TIME_WAIT狀態。

正文

其實這個問題在《TCP/IP詳解》以及《UNIX網路程式設計》這兩本書中都有提及,但這兩本書上的描述都比較簡潔並不是特別容易理解,記得在第一次看《UNIX網路程式設計》時,我曾經反覆閱讀相關段落並花了不少時間來想這個問題,但並沒有搞得很清楚,始終是懂非懂的樣子,直至後來有機會參與TCP/IP協議棧的開發後才真正got到這個問題的關鍵點。

根據第三版《UNIX網路程式設計 卷1》2.7節,TIME_WAIT狀態的主要目的有兩個:

  • 優雅的關閉TCP連線,也就是儘量保證被動關閉的一端收到它自己發出去的FIN報文的ACK確認報文;

  • 處理延遲的重複報文,這主要是為了避免前後兩個使用相同四元組的連線中的前一個連線的報文干擾後一個連線。

很明顯,要實現上述兩個目標,TIME_WAIT狀態需要持續一段時間,但這段時間應該是多長呢?

如果只考慮上述第一個目標,則TIME_WAIT狀態需要持續的時間應該參考對端的RTO(重傳超時時間)以及MSL(報文在網路中的最大生存時間)來計算而不是僅僅按MSL來計算,因為只要對端沒有收到針對FIN報文的ACK,就會一直持續重傳FIN報文直到重傳超時,所以最能實現完美關閉連線的時長計算方式應該是從對端傳送第一個FIN報文開始計時到它最後一次重傳FIN報文這段時長加上MSL,但這個計算方式過於保守,只有在所有的ACK報文都丟失的情況下才需要這麼長的時間;另外,第一個目標雖然重要,但並不十分關鍵,因為既然已經到了關閉連線的最後一步,說明在這個TCP連線上的所有使用者資料已經完成可靠傳輸,所以要不要完美的關閉這個連線其實已經不是那麼關鍵了。因此,(我猜)RFC標準的制定者才決定以網路丟包不太嚴重為前提條件,然後根據第二個目標來計算TIME_WAIT狀態應該持續的時長。

再來看一下《UNIX網路程式設計》在描述為什麼需要TIME_WAIT狀態時的一段話:

Since the duration of the TIME_WAIT state is twice the MSL, this allows MSL seconds for packet in one direction to be lost, and another MSL seconds for the reply to be lost. By enforcing this rule, we are guaranteed that when we successfully establish a TCP connecton, all old duplicates from previous incarnations of the connection have expired in the network.

這段文字說明了TIME_WAIT狀態持續2MSL的時間可以讓一個TCP連線的兩端發出的報文都從網路中消失,從而保證下一個使用了相同四元組的tcp連線不會被上一個連線的報文所干擾。

如何理解TIME_WAIT狀態持續2MSL的時間就可以讓一個TCP連線的兩端發出的報文都從網路中消失呢?

首先我們需要了解如下要點:

  1. TCP連線中的一端傳送了FIN報文之後如果收不到對端針對該FIN的ACK,則會反覆多次重傳FIN報文,大約持續幾分鐘;

  2. 被動關閉處於LAST_ACK狀態的一端在收到最後一個ACK之後不會傳送任何報文,立即進入CLOSED狀態;

  3. 主動關閉的一端在收到被動關閉端傳送過來的FIN報文並回復ACK之後進入TIME_WAIT狀態;

  4. 之所以TIME_WAIT狀態需要維持一段時間而不是進入CLOSED狀態,是因為需要處理對端可能重傳的FIN報文或其它一些因網路原因而延遲的資料包文,不處理這些報文可能導致前後兩個使用相同四元組的連線中的後一個連線出現異常(詳見UNIX網路程式設計卷1的2.7節 第三版);

  5. 處於TIME_WAIT狀態的一端在收到重傳的FIN時會重新計時(rfc793 以及 linux kernel原始碼tcp_timewait_state_process函式)。

下面我們開始分析為什麼在傳送了最後一個ACK報文之後需要等待2MSL時長來確保沒有任何屬於當前連線的報文還存活於網路之中(前提是在這2MSL時間內不再收到對方的FIN報文,但即使收到了對端的FIN報文也並不影響我們的討論,因為如果收到FIN則會回覆ACK並重新計時)。

為了便於描述,我們設想有一個處於拆鏈過程中的TCP連線,這個連線的兩端分別是A和B,其中A是主動關閉連線的一端,因為剛剛向對端傳送了針對對端傳送過來的FIN報文的ACK,此時正處於TIME_WAIT狀態;而B是被動關閉的一端,此時正處於LAST_ACK狀態,在收到最後一個ACK之前它會一直重傳FIN報文直至超時。隨著時間的流逝,A傳送給B的ACK報文將會有兩種結局:

  1. ACK報文在網路中丟失;如前所述,這種情況我們不需要考慮,因為除非多次重傳失敗,否則AB兩端的狀態不會發生變化直至某一個ACK不再丟失。

  2. ACK報文被B接收到。我們假設A傳送了ACK報文後過了一段時間t之後B才收到該ACK,則有 0 < t <= MSL。因為A並不知道它傳送出去的ACK要多久對方才能收到,所以A至少要維持MSL時長的TIME_WAIT狀態才能保證它的ACK從網路中消失。同時處於LAST_ACK狀態的B因為收到了ACK,所以它直接就進入了CLOSED狀態,而不會向網路傳送任何報文。所以晃眼一看,A只需要等待1個MSL就夠了,但仔細想一下其實1個MSL是不行的,因為在B收到ACK前的一剎那,B可能因為沒收到ACK而重傳了一個FIN報文,這個FIN報文要從網路中消失最多還需要一個MSL時長,所以A還需要多等一個MSL。

綜上所述,TIME_WAIT至少需要持續2MSL時長,這2個MSL中的第一個MSL是為了等自己發出去的最後一個ACK從網路中消失,而第二MSL是為了等在對端收到ACK之前的一剎那可能重傳的FIN報文從網路中消失。另外,雖然說維持TIME_WAIT狀態一段時間有2個目的,但這段時間具體應該多長主要是為了達成上述第二個目的而設計的。

相關文章