網(wǎng)絡(luò)編程-再看TCP的四次揮手
前言
四次揮手
四次揮手的流程在很多地方都可以看到,這里簡(jiǎn)略介紹一下,其常見流程如下圖所示:
其大體流程如下:
- 客戶端發(fā)其結(jié)束請(qǐng)求,發(fā)送seq=X,處于FIN_WAIT_1狀態(tài)
- 服務(wù)端收到結(jié)束請(qǐng)求,發(fā)送應(yīng)答ACK=X+1,處于CLOSE_WAIT狀態(tài)
- 客戶端收到X的應(yīng)答后,處于FIN_WAIT_2狀態(tài),此時(shí)還可以接收來(lái)自服務(wù)端的數(shù)據(jù)
- 服務(wù)端沒(méi)有數(shù)據(jù)要發(fā)送,也發(fā)送結(jié)束請(qǐng)求,seq=Y,處于LAST_ACK狀態(tài)
- 客戶端又收到服務(wù)端的結(jié)束請(qǐng)求,客戶端回應(yīng)ACK,此時(shí)處于TIME_WAIT狀態(tài),確保ACK能夠到達(dá)服務(wù)端;服務(wù)端收到客戶端最終ACK,關(guān)閉連接。
- 2MSL時(shí)間結(jié)束后,無(wú)論服務(wù)端是否收到最終ACK,客戶端完全結(jié)束連接
作為一種常見的四次揮手場(chǎng)景,我們可能習(xí)以為常了,但需要注意的是,連接的斷開并不只有這種情況,還可以是服務(wù)端發(fā)起主動(dòng)關(guān)閉,或者雙方同時(shí)發(fā)起,但這不是本文關(guān)注的重點(diǎn)。我們直接看看四次揮手有哪些需要注意的。
什么是TCP的半關(guān)閉
TCP半關(guān)閉指的是一端結(jié)束發(fā)送后還能夠接受來(lái)自另一端的數(shù)據(jù)。也就是說(shuō),雖然客戶端準(zhǔn)備斷開連接并且發(fā)送了FIN報(bào)文,客戶端還是可以接收來(lái)自服務(wù)端的數(shù)據(jù)。不過(guò)這種關(guān)閉方式不能使用close接口,而需要使用shutdown:
- #include <sys/socket.h>
- int shutdown(int sockfd, int how);
并且how參數(shù)值為SHUT_WR,即1,表明shutdown for writing ,僅關(guān)閉本端的發(fā)送。
為什么要四次揮手
為什么建立一個(gè)TCP連接需要三次握手,而終止一個(gè)連接需要四次揮手呢?這是因?yàn)門CP半關(guān)閉造成的。由于一個(gè)TCP連接是全雙工的,在兩個(gè)方向上都能傳輸數(shù)據(jù),因此兩個(gè)方向就需要單獨(dú)關(guān)閉。所以這個(gè)流程是這樣的:
- 客戶端執(zhí)行主動(dòng)關(guān)閉,發(fā)送FIN報(bào)文,告訴服務(wù)端,我沒(méi)有數(shù)據(jù)要發(fā)送了,我要關(guān)閉連接,當(dāng)然了,你有啥數(shù)據(jù)要給我,我隨時(shí)候著
- 服務(wù)端收到后,必須及時(shí)告訴客戶端我收到了,因此先回復(fù)客戶端一個(gè)ACK。但是服務(wù)端可能還有未發(fā)送完的數(shù)據(jù),因此它可以將自己未完成的數(shù)據(jù)進(jìn)行發(fā)送,發(fā)送完成之后,再發(fā)送給客戶端FIN報(bào)文,表明我也沒(méi)啥要發(fā)送的了,關(guān)閉吧
- 客戶端收到后,也回復(fù)ACK響應(yīng),最終關(guān)閉連接
因而整個(gè)過(guò)程需要四次揮手。
為什么要TIME_WAIT狀態(tài)
TIME_WAIT也稱為2MSL等待時(shí)間。MSL為報(bào)文最大生存時(shí)間,它是任何報(bào)文在被丟棄前存在于網(wǎng)絡(luò)內(nèi)的最長(zhǎng)時(shí)間。這個(gè)時(shí)間在不同類型的系統(tǒng)中可能有所不同,但這不是關(guān)鍵。在我個(gè)人的機(jī)器上,可以借助netstat命令和nc命令通過(guò)下面的方式觀察到。在終端1監(jiān)聽1234端口:
- $ nc -l 1234
在終端2連接到1234端口:
- $ nc 127.0.0.1 1234
在終端3通過(guò)netstat命令觀察:
- $ netstat -anpoc|grep :1234
然后在終端1按ctrl+c,終止連接,立刻觀察終端3的結(jié)果,我們發(fā)現(xiàn):
- tcp 0 0 127.0.0.1:1234 127.0.0.1:33524 TIME_WAIT - timewait (59.76/0/0)
- tcp 0 0 127.0.0.1:1234 127.0.0.1:33524 TIME_WAIT - timewait (58.74/0/0)
- tcp 0 0 127.0.0.1:1234 127.0.0.1:33524 TIME_WAIT - timewait (57.71/0/0)
- tcp 0 0 127.0.0.1:1234 127.0.0.1:33524 TIME_WAIT - timewait (56.69/0/0)
我們可以觀察到,服務(wù)端當(dāng)前處于TIME_WAIT,且有一個(gè)timewait的定時(shí)器,為1分鐘。
netstat命令和nc命令的使用可以分別參考《不可不知的網(wǎng)絡(luò)命令-netstat》和《網(wǎng)絡(luò)工具中的”瑞士軍刀“-nc》。
TIME_WAIT狀態(tài)的存在主要考慮以下兩個(gè)方面:
- 實(shí)現(xiàn)可靠的四次揮手
- 避免收到老的報(bào)文
為什么說(shuō)TIME_WAIT是為了實(shí)現(xiàn)可靠的四次揮手呢?試想一下,如果客戶端最后回應(yīng)的ACK丟了,那么服務(wù)端會(huì)再次發(fā)送FIN報(bào)文,此時(shí),客戶端必須處于一個(gè)等待狀態(tài),否則服務(wù)端永遠(yuǎn)無(wú)法收到這個(gè)ACK,而會(huì)收到一個(gè)RST,以為出錯(cuò)。而如果客戶端此時(shí)處于TIME_WAIT狀態(tài),即等待2MSL時(shí)間,它還可以再次回應(yīng)服務(wù)端ACK。這也就保證了可靠的四次揮手。
當(dāng)然了,如果在2MSL時(shí)間內(nèi),服務(wù)端還沒(méi)有收到,那么對(duì)不起,客戶端已經(jīng)仁至義盡了,不會(huì)再等待了。
這里需要注意,最終執(zhí)行主動(dòng)關(guān)閉的那一端會(huì)處于TIME_WAIT狀態(tài)。
那么為什么又說(shuō)是為了避免收到老的重復(fù)報(bào)文呢?
試想這樣的場(chǎng)景:
假設(shè)一開始已經(jīng)有一個(gè)連接在1234端口建立,我們關(guān)閉這個(gè)連接;過(guò)一會(huì)我們?cè)谕瑯拥膇p和端口建立連接,但是TCP必須防止在前一次連接中的老的報(bào)文在它原先的連接已終止后,還出現(xiàn)在這個(gè)新的連接中,因此,TCP將不允許在處于TIME_WAIT狀態(tài)的ip和端口處建立新的連接。而2MSL時(shí)間過(guò)后,老的報(bào)文早已在網(wǎng)絡(luò)中消失了,也就避免了這種情況的發(fā)生。
這種情況可以很容易通過(guò)《網(wǎng)絡(luò)編程-一個(gè)簡(jiǎn)單的echo程序》的server程序來(lái)觀察:
- $ ./server #在一個(gè)終端啟動(dòng)server,
- $ ./client 127.0.0.1 1234 #在另一個(gè)終端啟動(dòng)client
在服務(wù)端終端ctrl+c終止服務(wù)端,然后再次啟動(dòng)server:
- $ ./server
- bind error: Address already in use
- $ netstat -anop|grep :1234
- tcp 1 0 127.0.0.1:33722 127.0.0.1:1234 CLOSE_WAIT 11691/client off (0.00/0/0)
- tcp 0 0 127.0.0.1:1234 127.0.0.1:33722 FIN_WAIT2 - timewait (57.92/0/0)
終止服務(wù)端后,服務(wù)端處于TIME_WAIT狀態(tài),此時(shí)再次啟動(dòng)server,將不能使用原來(lái)的ip和端口建立連接,因此出現(xiàn)Address already in use的報(bào)錯(cuò)。
但是需要注意:
- 由于客戶端通常使用的是臨時(shí)端口(仔細(xì)觀察會(huì)發(fā)現(xiàn),客戶端每次啟動(dòng)使用的端口基本都不一樣),因此客戶端即便處于TIME_WAIT狀態(tài),也不影響它馬上再次啟動(dòng)
- 一些實(shí)現(xiàn)允許一個(gè)新的連接請(qǐng)求仍然處于TIME_WAIT狀態(tài)的連接,只要新的seq大于該連接的前一個(gè)連接的最后序號(hào)
- 通過(guò)設(shè)置選項(xiàng)SO_REUSEADDR,可以讓一個(gè)進(jìn)程重新使用仍處于TIME_WAIT狀態(tài)的socket
半打開的TCP連接
假設(shè)一個(gè)連接建立之后,突然有一方異常終止連接了,但是另一個(gè)不知道,這個(gè)時(shí)候TCP的連接就是半打開的。如果服務(wù)端不加處理,那么最終就會(huì)導(dǎo)致服務(wù)端有大量的半打開連接。那么服務(wù)端如何知道客戶端的連接已經(jīng)異常終止了呢?如果等待服務(wù)端發(fā)送數(shù)據(jù)出錯(cuò)時(shí)發(fā)現(xiàn),那么這個(gè)時(shí)候可能已經(jīng)太晚了。
幸運(yùn)的是,TCP有?;疃〞r(shí)器。即服務(wù)端可以通過(guò)設(shè)置?;钸x項(xiàng)來(lái)了解客戶端是否已經(jīng)終止連接。
通過(guò)下面的方式可以看到很多連接有這樣的定時(shí)器:
- $ netstat -npo|grep keepalive
- tcp 0 0 192.168.0.103:50832 59.111.179.136:443 ESTABLISHED 5882/chrome keepalive (37.33/0/0)
- tcp 0 0 192.168.0.103:50638 154.8.131.191:443 ESTABLISHED 5882/chrome keepalive (0.00/0/0)
- tcp 0 0 192.168.0.103:59330 203.107.41.32:9026 ESTABLISHED 5882/chrome keepalive (0.35/0/0)
- tcp 0 0 127.0.0.1:45632 127.0.0.1:1080 ESTABLISHED 5886/firefox keepalive (335.28/0/0)
- tcp 0 0 192.168.0.103:49940 59.56.78.189:443 ESTABLISHED 5882/chrome keepalive (26.36/0/0)
但可惜的是,這樣的定時(shí)器時(shí)間太長(zhǎng)了,并且它不能代表應(yīng)用程序能夠正常工作,能夠正常收發(fā)數(shù)據(jù),因此應(yīng)用層常常也會(huì)實(shí)現(xiàn)一個(gè)心跳機(jī)制。
總結(jié)
本文花了大量篇幅介紹了TIME_WAIT狀態(tài),這也是面試中常問(wèn)的問(wèn)題,重新梳理TCP的四次揮手是很有必要的。