在可靠的TCP網(wǎng)絡(luò)通信中,,客戶端和服務(wù)器端通信建立連接的過程可簡單表述為三次握手(建立連接的階段)和四次揮手(釋放連接階段),,下圖是這兩個階段的一個完整的表述:
其狀態(tài)圖可以表示為,
在TCP連接建立的時候,,存在一個如下的有限狀態(tài)機:
在狀態(tài)轉(zhuǎn)化圖中,,其中客戶端的狀態(tài)轉(zhuǎn)移用帶箭頭的粗實線表示,服務(wù)器端的狀態(tài)轉(zhuǎn)換用帶箭頭的粗虛線表示,。帶箭頭的細(xì)線表示一些不常見的事件,,如復(fù)位、同時打開,、同時關(guān)閉等。關(guān)于有限狀態(tài)圖可以參考博客http://blog.csdn.NET/lycb_gz/article/details/8515062,,里面的細(xì)節(jié)都將的很清楚,;如果要深入理解TCP連接建立和釋放的過程就需要結(jié)合socket編程里的connect(),socket(),,bind(),,listen(),send(),,close()等函數(shù),。
從圖中看到,三次握手對應(yīng)的Berkeley Socket API:connect, listen, accept 3個,,connect用在客戶端,,另外2個用在服務(wù)端。對于TCP/IP protocol stack來說,,TCP層的tcp_in&tcp_out也參與這個過程,。我們這里只討論這3個應(yīng)用層的API干了什么事情。
(1) connect()
發(fā)送了一個SYN,,收到Server的SYN+ACK后,,代表連接完成。發(fā)送最后一個ACK是protocol stack,tcp_out完成的,。
(2)listen()
在server這端,,準(zhǔn)備了一個未完成的連接隊列,保存只收到SYN_C的socket結(jié)構(gòu),;還準(zhǔn)備了已完成的連接隊列,,即保存了收到了最后一個ACK的socket結(jié)構(gòu)。
(3)accept()
應(yīng)用進(jìn)程調(diào)用accept的時候,,就是去檢查上面說的已完成的連接隊列,,如果隊列里有連接,就返回這個連接,;如果沒有,,即空的,,blocking方試調(diào)用,就睡眠等待,;客戶端調(diào)用connect函數(shù)之后就發(fā)起完成TCP的三次握手,,客戶端調(diào)用connect后,由內(nèi)核中的TCP協(xié)議完成TCP的三次握手,,close操作會完成四次揮手,。
其中accept發(fā)生在三次握手之后。
第一次握手:客戶端發(fā)送syn包(syn=j)到服務(wù)器,。
第二次握手:服務(wù)器收到syn包,,必須確認(rèn)客戶的SYN(ack=j+1),同時自己也發(fā)送一個ASK包(ask=k),。
第三次握手:客戶端收到服務(wù)器的SYN+ACK包,,向服務(wù)器發(fā)送確認(rèn)包ACK(ack=k+1)。
三次握手完成后,,客戶端和服務(wù)器就建立了tcp連接,。這時可以調(diào)用accept函數(shù)獲得此連接。
我們?nèi)绾闻袛嘤幸粋€建立鏈接請求或一個關(guān)閉鏈接請求:
建立鏈接請求:
1,、connect將完成三次握手,,accept所監(jiān)聽的fd上,產(chǎn)生讀事件,,表示有新的鏈接請求,,但此時accept函數(shù)并沒有調(diào)用,在內(nèi)核中維持了一個完成連接的隊列,;
關(guān)閉鏈接請求:
1,、close將完成四次揮手,如果有一方關(guān)閉sockfd,,對方將感知到有讀事件,,如果read讀取數(shù)據(jù)時,返回0,,即讀取到0個數(shù)據(jù),,表示有斷開鏈接請求。(在操作系統(tǒng)中已經(jīng)這么定義) 關(guān)閉鏈接過程中的TCP狀態(tài)和SOCKET處理,,及可能出現(xiàn)的問題:
1. TIME_WAIT
TIME_WAIT 是主動關(guān)閉 TCP 連接的那一方出現(xiàn)的狀態(tài),,系統(tǒng)會在 TIME_WAIT 狀態(tài)下等待 2MSL(maximum segment lifetime )后才能釋放連接(端口)。通常約合 4 分鐘以內(nèi),。TIME_WAIT 狀態(tài)等待 2MSL 的意義:
1,、確保連接可靠地關(guān)閉; 即防止最后一個ACK丟失,。
2,、避免產(chǎn)生套接字混淆(同一個端口對應(yīng)多個套接字),。
為什么說可以用來避免套接字混淆呢?一方close發(fā)送了關(guān)閉鏈接請求,,對方的應(yīng)答遲遲到不了(例如網(wǎng)絡(luò)原因),,導(dǎo)致TIME_WAIT超時,此時這個端口又可用了,,我們在這個端口上又建立了另外一個socket鏈接,。 如果此時對方的應(yīng)答到了,怎么處理呢,?其實這個在TCP層已經(jīng)處理了,,由于有TCP序列號,所以內(nèi)核TCP層,,就會將包丟掉,,并給對方發(fā)包,讓對方將sockfd關(guān)閉,。所以應(yīng)用層是沒有關(guān)系的。即我們用socket API編寫程序,,就不用處理,。
注意:TIME_WAIT是指操作系統(tǒng)的定時器會等2MSL,而主動關(guān)閉sockfd的一方,,并不會阻塞,。(即應(yīng)用程序在close時,并不會阻塞),。當(dāng)主動方關(guān)閉sockfd后,對方可能不知道這個事件,。那么當(dāng)對方(被動方)寫數(shù)據(jù),,即send時,將會產(chǎn)生錯誤,,即errno為: ECONNRESET,。服務(wù)器產(chǎn)生大量 TIME_WAIT 的原因:(一般我們不這樣開發(fā)Server,但是web服務(wù)器等這種多客戶端的Server,,是需要在完成一次請求后,,主動關(guān)閉連接的,否則可能因為句柄不夠用,,而造成無法提供服務(wù),。)服務(wù)器存在大量的主動關(guān)閉操作,需關(guān)注程序何時會執(zhí)行主動關(guān)閉(如批量清理長期空閑的套接字等操作),。一般我們自己寫的服務(wù)器進(jìn)行主動斷開連接的不多,,除非做了空閑超時之類的管理,。(TCP短鏈接是指,客戶端發(fā)送請求給服務(wù)器,,客戶端收到服務(wù)器端的響應(yīng)后,,關(guān)閉鏈接)。
2. CLOSE_WAIT
CLOSE_WAIT 是被動關(guān)閉 TCP 連接時產(chǎn)生的,,如果收到另一端關(guān)閉連接的請求后,本地(Server端)不關(guān)閉相應(yīng)套接字就會導(dǎo)致本地套接字進(jìn)入這一狀態(tài),。
(如果對方關(guān)閉了,沒有收到關(guān)閉鏈接請求,,就是下面的不正常情況)按TCP狀態(tài)機,我方收到FIN,,則由TCP實現(xiàn)發(fā)送ACK,因此進(jìn)入CLOSE_WAIT狀態(tài),。但如果我方不執(zhí)行close(),就不能由CLOSE_WAIT遷移到LAST_ACK,,則系統(tǒng)中會存在很多CLOSE_WAIT狀態(tài)的連接,。如果存在大量的 CLOSE_WAIT,則說明客戶端并發(fā)量大,,且服務(wù)器未能正常感知客戶端的退出,,也并未及時 close 這些套接字。(如果不及時處理,,將會出現(xiàn)沒有可用的socket描述符的問題,,原因是sockfd耗盡)。
正常情況下:一方關(guān)閉sockfd,,另外一方將會有讀事件產(chǎn)生,, 當(dāng)recv數(shù)據(jù)時,如果返回值為0,,表示對端已經(jīng)關(guān)閉。此時我們應(yīng)該調(diào)用close,,將對應(yīng)的sockfd也關(guān)閉掉,。
不正常情況下:一方關(guān)閉sockfd,另外一方并不知道,(比如在close時,,自己斷網(wǎng)了,,對方就收不到發(fā)送的數(shù)據(jù)包)。此時,,如果另外一方在對應(yīng)的sockfd上寫send或讀recv數(shù)據(jù),。
recv時,將會返回0,,表示鏈接已經(jīng)斷開,。
send時, 將會產(chǎn)生錯誤,,errno為ECONNRESET,。
3.close()函數(shù)和shutdown()函數(shù)的區(qū)別:
首先我們來看看close()函數(shù)的原型:
- 頭文件:#include <unistd.h>
- 定義函數(shù):int close(int fd);
close 一個套接字的默認(rèn)行為是把套接字標(biāo)記為已關(guān)閉,然后立即返回到調(diào)用進(jìn)程,,該套接字描述符不能再由調(diào)用進(jìn)程使用,,也就是說它不能再作為read或write的第一個參數(shù),然而TCP將嘗試發(fā)送已排隊等待發(fā)送到對端的任何數(shù)據(jù),,發(fā)送完畢后發(fā)生的是正常的TCP連接終止序列,。在多進(jìn)程并發(fā)服務(wù)器中,父子進(jìn)程共享著套接字,,套接字描述符引用計數(shù)記錄著共享著的進(jìn)程個數(shù),,當(dāng)父進(jìn)程或某一子進(jìn)程close掉套接字時,描述符引用計數(shù)會相應(yīng)的減一,,當(dāng)引用計數(shù)仍大于零時,這個close調(diào)用就不會引發(fā)TCP的四路握手?jǐn)噙B過程,。如果是主動調(diào)用close()則會發(fā)起內(nèi)核TCP協(xié)議的四次揮手,,斷開連接.
再來看看shutdown()函數(shù)的原型:
- int shutdown(int sockfd,int howto); //返回成功為0,出錯為-1.
- 該函數(shù)的行為依賴于howto的值
- 1.SHUT_RD:值為0,,關(guān)閉連接的讀這一半,。
- 2.SHUT_WR:值為1,關(guān)閉連接的寫這一半,。
- 3.SHUT_RDWR:值為2,,連接的讀和寫都關(guān)閉。
- 終止網(wǎng)絡(luò)連接的通用方法是調(diào)用close函數(shù),。但使用shutdown能更好的控制斷連過程(使用第二個參數(shù)),。
當(dāng)調(diào)用SHUT_RD的時候,套接字sockfd的讀端將會關(guān)閉,,不能調(diào)用接受數(shù)據(jù)的函數(shù),,這對于協(xié)議層沒有影響。然和當(dāng)前在sockfd讀端的數(shù)據(jù)緩沖區(qū)的數(shù)據(jù)都會被舍棄掉,進(jìn)程將不能對該套接字發(fā)起讀操作,,對TCP套接字調(diào)用SHUT_RD將會導(dǎo)致協(xié)議層將接收到的數(shù)據(jù)無聲的丟掉?。?!如果想要繼續(xù)接受數(shù)據(jù)都要重置鏈接,;
當(dāng)調(diào)用SHUT_WR的時候,對于tcp套接字來說,,這意味著會在所有數(shù)據(jù)發(fā)送出并得到接受端確認(rèn)后產(chǎn)生一個FIN包,。而此時套接字的狀態(tài)會由ESTABLISHED變成FIN_WAIT_1,然后對方發(fā)送一個 ACK包作為回應(yīng),,套接字又變成FIN_WAIT_2,。如果對方也關(guān)閉了連接則對方會發(fā)出FIN,我方會回應(yīng)一個ACK并將套接字置為 TIME_WAIT,。
4.如何判斷socket連接斷開:
非阻塞模式,,如果暫時沒有數(shù)據(jù),返回的值也會是<=0的,,如果用阻塞模式的話,,返回<=0的值是可以認(rèn)為socket已經(jīng)無效了。當(dāng)使用 select()函數(shù)測試一個socket是否可讀時,,如果select()函數(shù)返回值為1,,且使用recv()函數(shù)讀取的數(shù)據(jù)長度為0 時,就說明該socket已經(jīng)斷開,。經(jīng)過代碼試驗,,如果進(jìn)程受到一些信號時,例如:EINTR,,recv()返回值小于等于0時,,這是就需要判斷 errno是否等于 EINTR , 如果errno
== EINTR 則說明recv函數(shù)是由于程序接收到信號后返回的,socket連接還是正常的,,不應(yīng)close掉socket連接,。如果write,我覺得還有一些情況需要考慮,,那就是寫的太快的時候,,有可能buffer寫滿了,errno是EAGAIN,,可以根據(jù)實際需要,,如果errno是EAGAIN的話,再寫幾次,。
|