什麼是 Time Wait 狀態?

time wait 是 tcp connection 的狀態之一,進入這個狀態的原因只有一種:主動關閉 connection (active close)。

與其相對的是 close wait 狀態,進入該狀態是由於被動關閉 connection(passive close),也就是說接收到了對方的 FIN 信號(並且發出了自己的 ACK 信號)。

此外在弄懂這個問題時,筆者遇到了這樣一個理解困難,必須理解了這一點,下面的表述才會比較容易理解吧——就是當我們討論 tcp connection 狀態時,實際上討論的是在某個 end point 上的該 tcp connection 的狀態。

問題

工作中需要寫一個 tcp server,為了尋求快速開發,直接用了 python 來完成這一工作,開發完畢之後,遇到一個問題:因為需要每天重啟這個 tcp server,然而每次重啟的都會出錯,錯誤信息如下:

socket.error: [Errno 48] Address already in use

復現

為了解決這個問題,我寫了一個簡單的 tcp echo server/client,以此來重現我的問題:

server 代碼

# server
#!/usr/bin/env python

import socket
port = 8080
backlog = 5

def echo(conn):
conn.settimeout(1)
data = conn.recv(1024)
conn.send(data)
conn.close()
pass

def run():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

s.bind(("", port))
s.listen(backlog)
print "server listen on:", port

try:
while True:
conn, addr = s.accept()
print "new connection comes, addr=", addr
echo(conn)

except Exception as e:
print "tcp server execption occured=", e
finally:
s.close()

if __name__ == "__main__":
run()

client 代碼

import socket

host = localhost
port = 8080

def run():
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except socket.error as msg:
return
try:
s.connect((host, port))
except socket.error as msg:
s.close()
return
s.sendall(helle,world)
echo = s.recv(1024)
s.close()
print "got echo data: ", echo

if __name__ == "__main__":
run()

代碼邏輯很簡單,就是 server 接收到 client 發來的數據後,將其再返回給 client(echo)。

我們先運行 server,再運行 client,client 收到 reply 之後,會立即結束運行。此時此刻,如果我們重啟 server,會立即得到上面出現的錯誤:

socket.error: [Errno 48] Address already in use

原因

那麼這個錯誤是怎麼回事呢?

其實在本文的開始,就提到了 time wait 這一狀態,而這裡的錯誤其實並不是一種錯誤,而是 tcp 的機制導致的正常後果。

解決方法很簡單,下面主要解釋其中的原因。

來自 RFC 793 - Transmission Control Protocol

讓我們回到 client 接收到 echo 之後,立即停止運行的時候。在這之前,我們的 echo server 已經主動 close 了這個 tcp connection 了,而到了此刻,client 也發出了 close tcp connection 的 FIN 包。如果 server 端已經接收到了來自 client 端的 FIN 包,根據 tcp connection state diagram,我們可以發現 server 端會發出相應的 ACK 包,並且進入 time wait 狀態。

注意,一旦進入了 time wait 狀態,由圖中可以發現必須經過 2MSL 的時間才會真正進入 CLOSED 狀態。這裡的 MSL 是 maximum segment lifetime的縮寫,一個 MSL 是指一個 tcp segment 在網路中的最大存活時間, 不同的實現有不同的設置,一般是兩分鐘。

到這裡,我們就可以給出問題的具體原因了,其實就是在我們重啟 server 的時候,由於上一次的 tcp connection 還沒進入 closed 狀態,還處於 time wait 狀態,從而導致相應的 port 還在使用,也就是 Address already in use.

最終的解決方案很簡單,我們在開啟 socket 的之前,允許 socket 埠重用,也即是重啟的時候,我們直接使用之前的 port 來啟動 tcp server,而相應的 python 代碼其實只有一行:

s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

Time Wait 意義

到這裡,問題其實已經解決了,但是有幾個小問題,值得我們思考一下:

1. why is time wait state necessary?
2. why should connection not set as closed until 2MSL passed?
3. why does close wait act differently from time wair?

下面一一解釋這三個問題。

首先,我們最關心的一個問題就是:time wait 究竟是為了什麼而被提出來的。

在這之前我們可以先了解一下任何一個 tcp connection 都可以用一個四元組來標識:

(local_addr, local_port, remote_addr, remote_port)

這個四元組的意義十分明顯,不做過多解釋。

此外另一個事實就是,在 IP 層中,雖然當前的 tcp connection 已經關閉了,但是仍有可能存在著一些之前因為重發而導致的遊盪重複包(wandering duplicate),這些包我們認為在 [0, 2MSL)(數據包和相應的 ack 包各需要 1MSL)的時間內都有可能再次來到,因此如果我們的四元組沒有變化,並且沒有 time wait 這個狀態的話,會產生一個嚴重的問題 ——(以 echo server 舉例)遊盪重複包如果到達了一個剛剛重新啟動過的 server 端,那麼 server 端將無法對新建 connection 的包和上一次 connection 的遊盪包做出區分,從而導致當前的 tcp connection 被上次的數據包污染。但是如果我們加上了 time wait 狀態,那麼這個問題就會迎刃而解,因為 server 專門有 2MSL 的時間去處理那些 wandering 包。

除此之外,還有一個原因是,為了處理 server 最終發出的 FIN 包和 ACK 包丟失情況,也必須要有一個類似 Time Wait 狀態去支持相應的重發。

至於為什麼需要 2MSL 的時間長度,是因為 tcp 是一個全雙工的協議,發出的數據包必須得到 ack 之後才算完整,一來一回就是 2MSL 的估計量。

最後的問題是:為什麼 Close Wait 狀態不像 Time Wait 狀態那樣需要等待 2MSL ?

這個問題乍看之下有點難以回答,但是其實答案十分簡單,因為一個 connection 如果在一個 end point 上處於 close wait,那麼必然在另一個 end point 上是處於 time wait 狀態,而一個 tcp connection 是由上述提到的一個四元組所標誌的,自然只要一個 end point 能夠解決 wandering 包的問題即可,所以 close wait 自然不需要像 time wait 那樣等待 2MSL 的時間。

參考

[1] RFC 793 - Transmission Control Protocol

[2] time wait and its design implication


推薦閱讀:
相关文章