TCP 連接關閉及TIME_WAIT探究


這里主要記錄一下TCP連接在關閉的時刻,有哪些細節問題。方便在以后的程序設計中能夠注意這些細節, 以避免出現這些錯誤。首先我們來看一下TCP的狀態轉換圖。如《unix網絡編程》卷一所示如下圖:

TCP 四次揮手:

  • 揮手時的序號問題
  • 揮手過程中狀態轉換問題
  • TIME_WAIT 產生原因

揮手序號問題:

這里可以看出FIN也占用了一個序號,例如FIN M, 對方回應ACK 確認序號為M+1。最后發送FIN也是如此。那么這里的M和N在傳輸數據過程中怎樣得到的。看一下一個抓包的例子如下

12:40:55.908193 IP localhost.34876 > localhost.ospf-lite: Flags [P.], seq 206:236, ack 199, win 342, length 30
12:40:55.908606 IP localhost.ospf-lite > localhost.34876: Flags [P.], seq 199:221, ack 236, win 342, length 22
12:40:55.908703 IP localhost.34876 > localhost.ospf-lite: Flags [.], ack 221, win 342, length 0
12:41:00.029841 IP localhost.34876 > localhost.ospf-lite: Flags [F.], seq 236, ack 221, win 342, length 0
12:41:00.030176 IP localhost.ospf-lite > localhost.34876: Flags [F.], seq 221, ack 237, win 342, length 0
12:41:00.030225 IP localhost.34876 > localhost.ospf-lite: Flags [.], ack 222, win 342, length 0

這里可以清楚的看到 發送FIN的序列號正是真實已經確認數據的序列號的下一個序號。FIN也占用一個序列號, 所以FIN的ACK序號也是加一。

揮手過程中狀態轉換問題

這里有兩個測試程序如下:

 1 #!/usr/bin/env python
 2 # coding: utf-8
 3 import socket
 4 import os
 5 import sys
 6 import time
 7 def main(argv):
 8     host = (argv[1], int(argv[2]))
 9     filename = argv[3]
10     fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
11     try:
12         fd.connect(host)
13     except socket.error, e:
14         print e
15         sys.exit(0)
16     fp = open(filename,'rb')
17     while True:
18         buff = fp.read(2048)
19         if  buff:
20             fd.send(buff)
21         else:
22              break
23 
24 if __name__ == '__main__':
25     if len(sys.argv) != 4:
26         print "Like client.py 192.168.1.100 6666 a.dd"
27         sys.exit(0)
28     main(sys.argv)
 1 #!/usr/bin/env python
 2 # coding: utf-8
 3 import socket
 4 import os
 5 import sys
 6 import time
 7 
 8 def main(port):
 9     fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
10     host = socket.gethostname()
11     fd.bind((host, port))
12     fd.listen(10)
13     while True:
14         clifd, addr = fd.accept()
15         print 'Client address : ', addr
16         while True:
17             time.sleep(30) 
18             data =  clifd.recv(1024)
19             if data:
20                 print data
21             else:#讀取到0 連接斷開要60s
22                 print "client closed"
23                 clifd.close()
24                 break
25 
26 if __name__ == '__main__':
27     port = 8888
28     main(port)

一個客戶端 另一個是服務器端。

1. 首先在服務器接受連接后就進入等待, 客戶端連接完成后就將數據全部發送並關閉連接程序退出 抓包結果如下:

17:13:11.825971 IP cps.59302 > cps.ddi-tcp-1: Flags [S], seq 3995218772, win 43690, options [mss 65495,sackOK,TS val 18944024 ecr 0,nop,wscale 7], length 0
01:06:05.598183 IP cps.ddi-tcp-1 > cps.59302: Flags [S.], seq 4041698578, ack 3995218773, win 43690, options [mss 65495,sackOK,TS val 18944024 ecr 18944024,nop,wscale 7], length 0
17:13:11.826052 IP cps.59302 > cps.ddi-tcp-1: Flags [.], ack 1, win 342, options [nop,nop,TS val 18944024 ecr 18944024], length 0
17:13:11.826159 IP cps.59302 > cps.ddi-tcp-1: Flags [P.], seq 1:524, ack 1, win 342, options [nop,nop,TS val 18944024 ecr 18944024], length 523
17:13:11.826170 IP cps.ddi-tcp-1 > cps.59302: Flags [.], ack 524, win 350, options [nop,nop,TS val 18944024 ecr 18944024], length 0
17:13:11.826193 IP cps.59302 > cps.ddi-tcp-1: Flags [F.], seq 524, ack 1, win 342, options [nop,nop,TS val 18944024 ecr 18944024], length 0
17:13:11.865650 IP cps.ddi-tcp-1 > cps.59302: Flags [.], ack 525, win 350, options [nop,nop,TS val 18944064 ecr 18944024], length 0

 在服務器暫停的30s內 已經收到了客戶端發送的數據和FIN 並都得到了確認。再看一下連接狀態

tcp        0      0 192.168.24.126:8888     0.0.0.0:*               LISTEN     
tcp        0      0 192.168.24.126:59338    192.168.24.126:8888     FIN_WAIT2  客戶端的狀態
tcp      524      0 192.168.24.126:8888     192.168.24.126:59338    CLOSE_WAIT

這里看到即使程序退出 FIN-WAIT1 FIN-WAIT2 TIME-WAIT這三種狀態也不會消失  它們是由內核維護,有相關定時器控制。 如這里的FIN-WAIT2狀態超時后就不再進入TIME-WAIT 這時對端再回復FIN時 就會回應RST。 若在超時時間內則正常回應並徹底斷開連接。

FIN-WAIT2超時
17
:18:18.401858 IP cps.59336 > cps.ddi-tcp-1: Flags [P.], seq 1:524, ack 1, win 342, options [nop,nop,TS val 19250600 ecr 19250600], length 523 17:18:18.401872 IP cps.ddi-tcp-1 > cps.59336: Flags [.], ack 524, win 350, options [nop,nop,TS val 19250600 ecr 19250600], length 0 17:18:18.401905 IP cps.59336 > cps.ddi-tcp-1: Flags [F.], seq 524, ack 1, win 342, options [nop,nop,TS val 19250600 ecr 19250600], length 0 17:18:18.441595 IP cps.ddi-tcp-1 > cps.59336: Flags [.], ack 525, win 350, options [nop,nop,TS val 19250640 ecr 19250600], length 0 17:20:18.474816 IP cps.ddi-tcp-1 > cps.59336: Flags [F.], seq 1, ack 525, win 350, options [nop,nop,TS val 19370673 ecr 19250600], length 0 17:20:18.474871 IP cps.59336 > cps.ddi-tcp-1: Flags [R], seq 223914856, win 0, length 0   FIN-WAIT2 超時消失后發送FIN 得到RST

 

2. FIN_WAIT_1 狀態: 假如當主動方close時, 發送FIN給對方,但是在這個過程中一直沒有收到來自對方對FIN的確認, 那么主動方就會重傳一定時間的FIN,當超時后就會放棄,然后不經過TIME_WAIT 直接清理緩存斷開連接。可以參考: http://www.cnblogs.com/MaAce/p/8039119.html

3. 主動方close之后,對方還有數據在發送並在路上時: 這種情況也是常常發生, 主動方close掉連接,就是把讀寫全部關閉並把發送緩沖區的全部數據一次性發送到對端。那么這時如果有對方發送的數據包在路上時, 當數據包達到時,剛好close已返回,那么這時主動斷開的一方就會發送rst給對方。這時可以用shutdown來替換close來獲取最后接收的內容。 關閉時僅僅關閉寫端,然后再繼續read直到讀到0為止 表示收到對端的fin。當不確定關閉時還有沒有未接收的數據可以這樣使用。這里可以確保接收完整 直到收到斷開信息 保證了對方應用進程已經讀取了我們的數據。但這里要注意的是shutdown寫端會把發送緩沖區清空。

//類似這樣
shutdown(fd, FD_WR);
while(1)
{
    if(read() == 0)
    {
        break;
    }
}

 

4. close關閉連接后 默認情況下是立即返回以后就不再接收和發送普通數據 若發送緩沖區有數據就把數據一次性發送到對端。這里有可能並沒有收到對方的對 數據和FIN  的確認,然而close已返回。 這里可以設置套接字屬性SO_LINGER 延遲關閉來 確保收到對方的確認信息  在一定時間內 收到了確認 則close 返回成功。如果在延時時間內並未收到來自對端的確認,那么close就會返回錯誤EWOULDBLOCK 如下圖:。這里還要注意此時對於非阻塞而言, 直接返回錯誤EWOULDBLOCK。所以驗證close的返回值是很有必要的。至於so_linger的用法網上例子很多 : http://blog.csdn.net/factor2000/article/details/3929816

 

如果發送緩沖區的數據沒有發送完畢或者沒有收到對端確認,close就返回,內核就放棄沒有發送的數據或是不再等待B端的確認,直接發送RST復位連接不進入TIME_WAIT狀態。

 TIME_WAIT 狀態

由以上可知,即使程序退出,內核也會幫其維護timewait的定時器。維持這個狀態的原因如下:

1. 假設最終的ACK丟失,server將重發FIN,client必須維護TCP狀態信息以便可以重發最終的ACK,否則會發送RST,結果server認為發生錯誤。TCP實現必須可靠地終止連 接的兩個方向(全雙工關閉),client必須進入 TIME_WAIT 狀態,因為client可能面 臨重發最終ACK的情形。 

2. 如果 TIME_WAIT 狀態保持時間不足夠長(比如小於2MSL),第一個連接就正常終止了。 第二個擁有相同相關五元組的連接出現,而第一個連接的重復報文到達,干擾了第二 個連接。TCP實現必須防止某個連接的重復報文在連接終止后出現,所以讓TIME_WAIT 狀態保持時間足夠長(2MSL),連接相應方向上的TCP報文要么完全響應完畢,要么被 丟棄。建立第二個連接的時候,不會混淆。

Linux下我們可以設置時檢查一下time和wait的值

#sysctl -a | grep time | grep wait

net.ipv4.netfilter.ip_conntrack_tcp_timeout_time_wait = 120
net.ipv4.netfilter.ip_conntrack_tcp_timeout_close_wait = 60
net.ipv4.netfilter.ip_conntrack_tcp_timeout_fin_wait = 120

處於timewait時 內核並不會把他的相關結構清空。

其中套接字選項中還有地址和端口重用的選項SO_REUSEADDR和SO_REUSEPORT 這兩個選項就是為了避免server 重啟時 端口忙的問題。


這個套接字選項通知內核,如果端口忙,但TCP狀態位於 TIME_WAIT ,可以重用 端口。如果端口忙,而TCP狀態位於其他狀態,重用端口時依舊得到一個錯誤信息,指明"地址已經使用中"。如果你的服務程序停止后想立即重啟,而新套接字依舊使用同一端口,此時 SO_REUSEADDR 選項非常有用。必須意識到,此時任何非期望數據到達,都可能導致服務程序反應混亂,不過這只是一種可能,事實上很不 
可能。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM