Linux:TCP狀態/半關閉/2MSL/端口復用


TCP狀態

CLOSED:表示初始狀態。

LISTEN:該狀態表示服務器端的某個SOCKET處於監聽狀態,可以接受連接。

SYN_SENT:這個狀態與SYN_RCVD遙相呼應,當客戶端SOCKET執行CONNECT連接時,它首先發送SYN報文,隨即進入到了SYN_SENT狀態,並等待服務端的發送三次握手中的第2個報文。SYN_SENT狀態表示客戶端已發送SYN報文。

SYN_RCVD: 該狀態表示接收到SYN報文,在正常情況下,這個狀態是服務器端的SOCKET在建立TCP連接時的三次握手會話過程中的一個中間狀態,很短暫。此種狀態時,當收到客戶端的ACK報文后,會進入到ESTABLISHED狀態。

ESTABLISHED:表示連接已經建立。

FIN_WAIT_1: FIN_WAIT_1和FIN_WAIT_2狀態的真正含義都是表示等待對方的FIN報文。區別是:

FIN_WAIT_1狀態是當socket在ESTABLISHED狀態時,想主動關閉連接,向對方發送了FIN報文,此時該socket進入到FIN_WAIT_1狀態。

FIN_WAIT_2狀態是當對方回應ACK后,該socket進入到FIN_WAIT_2狀態,正常情況下,對方應馬上回應ACK報文,所以FIN_WAIT_1狀態一般較難見到,而FIN_WAIT_2狀態可用netstat看到。

FIN_WAIT_2:主動關閉鏈接的一方,發出FIN收到ACK以后進入該狀態。稱之為半連接或半關閉狀態。該狀態下的socket只能接收數據,不能發。

TIME_WAIT: 表示收到了對方的FIN報文,並發送出了ACK報文,等2MSL后即可回到CLOSED可用狀態。如果FIN_WAIT_1狀態下,收到對方同時帶 FIN標志和ACK標志的報文時,可以直接進入到TIME_WAIT狀態,而無須經過FIN_WAIT_2狀態。

CLOSING: 這種狀態較特殊,屬於一種較罕見的狀態。正常情況下,當你發送FIN報文后,按理來說是應該先收到(或同時收到)對方的 ACK報文,再收到對方的FIN報文。但是CLOSING狀態表示你發送FIN報文后,並沒有收到對方的ACK報文,反而卻也收到了對方的FIN報文。什么情況下會出現此種情況呢?如果雙方幾乎在同時close一個SOCKET的話,那么就出現了雙方同時發送FIN報文的情況,也即會出現CLOSING狀態,表示雙方都正在關閉SOCKET連接。

CLOSE_WAIT: 此種狀態表示在等待關閉。當對方關閉一個SOCKET后發送FIN報文給自己,系統會回應一個ACK報文給對方,此時則進入到CLOSE_WAIT狀態。接下來呢,察看是否還有數據發送給對方,如果沒有可以 close這個SOCKET,發送FIN報文給對方,即關閉連接。所以在CLOSE_WAIT狀態下,需要關閉連接。

LAST_ACK: 該狀態是被動關閉一方在發送FIN報文后,最后等待對方的ACK報文。當收到ACK報文后,即可以進入到CLOSED可用狀態。

這個圖很重要,它對於排除和定位網絡或系統故障時大有幫助,但是怎樣牢牢地將這張圖刻在腦中呢?一定要對這張圖的每一個狀態,及轉換的過程有深刻的認識,不能只停留在一知半解之中.

半關閉

當TCP連接中A發送FIN請求關閉,B端回應ACK后(A端進入FIN_WAIT_2狀態),B沒有立即發送FIN給A時,A方處在半關閉狀態,此時A可以接收B發送的數據,但是A已不能再向B發送數據。

從程序的角度,可以使用API來控制實現半連接狀態:

#include <sys/socket.h>

int shutdown(int sockfd, int how);

sockfd: 需要關閉的socket的描述符

how:    允許為shutdown操作選擇以下幾種方式:

    SHUT_RD(0):    關閉sockfd上的讀功能,此選項將不允許sockfd進行讀操作。該套接字不再接受數據,任何當前在套接字接受緩沖區的數據將被無聲的丟棄掉。

    SHUT_WR(1):        關閉sockfd的寫功能,此選項將不允許sockfd進行寫操作。進程不能在對此套接字發出寫操作。

    SHUT_RDWR(2):    關閉sockfd的讀寫功能。相當於調用shutdown兩次:首先是以SHUT_RD,然后以SHUT_WR。

為什么不用close?因為close一次只中止一個連接,它只是減少描述符的引用計數,並不直接關閉連接,只有當描述符的引用計數為0時才關閉連接。

shutdown不考慮描述符的引用計數,直接關閉所有的描述符。也可選擇中止一個方向的連接,只中止讀或只中止寫。

注意:

  1. 如果有多個進程共享一個套接字,close每被調用一次,計數減1,直到計數為0時,也就是所用進程都調用了close,套接字將被釋放。
  2. 在多進程中如果一個進程調用了shutdown(sfd, SHUT_RDWR)后,其它的進程將無法進行通信。但,如果一個進程close(sfd)將不會影響到其它進程。

2MSL

2MSL (Maximum Segment Lifetime) TIME_WAIT狀態的存在有兩個理由:

(1)讓4次握手關閉流程更加可靠;4次握手的最后一個ACK是是由主動關閉方發送出去的,若這個ACK丟失,被動關閉方會再次發一個FIN過來。若主動關閉方能夠保持一個2MSL的TIME_WAIT狀態,則有更大的機會讓丟失的ACK被再次發送出去。

(2)防止lost duplicate對后續新建正常鏈接的傳輸造成破壞。lost uplicate在實際的網絡中非常常見,經常是由於路由器產生故障,路徑無法收斂,導致一個packet在路由器A,B,C之間做類似死循環的跳轉。IP頭部有個TTL,限制了一個包在網絡中的最大跳數,因此這個包有兩種命運,要么最后TTL變為0,在網絡中消失;要么TTL在變為0之前路由器路徑收斂,它憑借剩余的TTL跳數終於到達目的地。但非常可惜的是TCP通過超時重傳機制在早些時候發送了一個跟它一模一樣的包,並先於它達到了目的地,因此它的命運也就注定被TCP協議棧拋棄。

另外一個概念叫做incarnation connection,指跟上次的socket pair一摸一樣的新連接,叫做incarnation of previous connection。lost uplicate加上incarnation connection,則會對我們的傳輸造成致命的錯誤。

TCP是流式的,所有包到達的順序是不一致的,依靠序列號由TCP協議棧做順序的拼接;假設一個incarnation connection這時收到的seq=1000, 來了一個lost duplicate為seq=1000,len=1000, 則TCP認為這個lost duplicate合法,並存放入了receive buffer,導致傳輸出現錯誤。通過一個2MSL TIME_WAIT狀態,確保所有的lost duplicate都會消失掉,避免對新連接造成錯誤。

該狀態為什么設計在主動關閉這一方

(1)發最后ACK的是主動關閉一方。

(2)只要有一方保持TIME_WAIT狀態,就能起到避免incarnation connection在2MSL內的重新建立,不需要兩方都有。

如何正確對待2MSL TIME_WAIT?

RFC要求socket pair在處於TIME_WAIT時,不能再起一個incarnation connection。但絕大部分TCP實現,強加了更為嚴格的限制。在2MSL等待期間,socket中使用的本地端口在默認情況下不能再被使用。

若A 10.234.5.5 : 1234和B 10.55.55.60 : 6666建立了連接,A主動關閉,那么在A端只要port為1234,無論對方的port和ip是什么,都不允許再起服務。這甚至比RFC限制更為嚴格,RFC僅僅是要求socket pair不一致,而實現當中只要這個port處於TIME_WAIT,就不允許起連接。這個限制對主動打開方來說是無所謂的,因為一般用的是臨時端口;但對於被動打開方,一般是server,就悲劇了,因為server一般是熟知端口。比如http,一般端口是80,不可能允許這個服務在2MSL內不能起來。

解決方案是給服務器的socket設置SO_REUSEADDR選項,這樣的話就算熟知端口處於TIME_WAIT狀態,在這個端口上依舊可以將服務啟動。當然,雖然有了SO_REUSEADDR選項,但sockt pair這個限制依舊存在。比如上面的例子,A通過SO_REUSEADDR選項依舊在1234端口上起了監聽,但這時我們若是從B通過6666端口去連它,TCP協議會告訴我們連接失敗,原因為Address already in use.

RFC 793中規定MSL為2分鍾,實際應用中常用的是30秒,1分鍾和2分鍾等。

RFC (Request For Comments),是一系列以編號排定的文件。收集了有關因特網相關資訊,以及UNIX和因特網社群的軟件文件。

 

 

這里我要解決自己的一個疑問,主動關閉一方半關閉狀態之后還是能發送數據:半關閉狀態是指主動關閉一方接收到了對方的ACK但是還沒有接收到對方的FIN,這個時間段才叫半關閉,這個時間段主動方的確不能發送數據給對方.這是對的.但是當本方接收到了對方的FIN之后,本方又可以發送數據了.在這個時間段中不是半關閉狀態了.為什么?因為本方要回復對方啊,要發送ACK給對方回復.所以我猜測之前的半關閉並不是真的不能發送數據了,而是被協議通過軟件手段屏蔽掉了.畢竟要傳輸數據socket就要打開啊.半關閉是不能發送數據,但是接收到了FIN之后就不是半關閉了,所以就可以發送數據了.

端口復用

做一個測試,首先啟動server,然后啟動client,用Ctrl-C終止server,馬上再運行server,運行結果:

itcast$ ./server

bind error: Address already in use

這是因為,雖然server的應用程序終止了,但TCP協議層的連接並沒有完全斷開,因此不能再次監聽同樣的server端口。我們用netstat命令查看一下:

itcast$ netstat -apn |grep 6666

tcp 1 0 192.168.1.11:38103 192.168.1.11:6666 CLOSE_WAIT 3525/client

tcp 0 0 192.168.1.11:6666 192.168.1.11:38103 FIN_WAIT2 -

server終止時,socket描述符會自動關閉並發FIN段給client,client收到FIN后處於CLOSE_WAIT狀態,但是client並沒有終止,也沒有關閉socket描述符,因此不會發FIN給server,因此server的TCP連接處於FIN_WAIT2狀態。

現在用Ctrl-C把client也終止掉,再觀察現象:

itcast$ netstat -apn |grep 6666

tcp 0 0 192.168.1.11:6666 192.168.1.11:38104 TIME_WAIT -

itcast$ ./server

bind error: Address already in use

client終止時自動關閉socket描述符,server的TCP連接收到client發的FIN段后處於TIME_WAIT狀態。TCP協議規定,主動關閉連接的一方要處於TIME_WAIT狀態,等待兩個MSL(maximum segment lifetime)的時間后才能回到CLOSED狀態,因為我們先Ctrl-C終止了server,所以server是主動關閉連接的一方,在TIME_WAIT期間仍然不能再次監聽同樣的server端口。

MSL在RFC 1122中規定為兩分鍾,但是各操作系統的實現不同,在Linux上一般經過半分鍾后就可以再次啟動server了。至於為什么要規定TIME_WAIT的時間,可參考UNP 2.7節.

在server的TCP連接沒有完全斷開之前不允許重新監聽是不合理的。因為,TCP連接沒有完全斷開指的是connfd(127.0.0.1:6666)沒有完全斷開,而我們重新監聽的是lis-tenfd(0.0.0.0:6666),雖然是占用同一個端口,但IP地址不同,connfd對應的是與某個客戶端通訊的一個具體的IP地址,而listenfd對應的是wildcard address。解決這個問題的方法是使用setsockopt()設置socket描述符的選項SO_REUSEADDR為1,表示允許創建端口號相同但IP地址不同的多個socket描述符。

在server代碼的socket()和bind()調用之間插入如下代碼:

    int opt = 1;

    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

就可以解決問題了.

有關setsockopt可以設置的其它選項請參考UNP第7章。


免責聲明!

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



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