這里主要記錄一下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 選項非常有用。必須意識到,此時任何非期望數據到達,都可能導致服務程序反應混亂,不過這只是一種可能,事實上很不
可能。