深入淺出TCP之半關閉與CLOSE_WAIT


轉自:https://www.2cto.com/net/201309/243585.html(相關鏈接)

深入淺出TCP之半關閉與CLOSE_WAIT

    終止一個連接要經過4次握手。這由TCP的半關閉(half-close)造成的。既然一個TCP連接是全雙工(即數據在兩個方向上能同時傳遞,可理解為兩個方向相反的獨立通道),因此每個方向必須單獨地進行關閉。

這原則就是當一方完成它的數據發送任務后就能發送一個FIN來終止這個方向連接。當一端收到一個FIN,內核讓read返回0來通知應用層另一端已經終止了向本端的數據傳送。發送FIN通常是應用層對socket進行關閉的結果。

例如:TCP客戶端發送一個FIN,用來關閉從客戶到服務器的數據傳送。

    半關閉對服務器究竟有什么影響呢?先看看下面的TCP狀態轉化圖

                    tcp狀態裝換圖

TCP狀態說明

   CLOSED:表示初始狀態。對服務端和C客戶端雙方都一樣。
   LISTEN:表示監聽狀態。服務端調用了listen函數,可以開始accept連接了。
   SYN_SENT:表示客戶端已經發送了SYN報文。當客戶端調用connect函數發起連接時,首先發SYN給服務端,然后自己進入SYN_SENT狀態,並等待服務端發送ACK+SYN。
   SYN_RCVD:表示服務端收到客戶端發送SYN報文。服務端收到這個報文后,進入SYN_RCVD狀態,然后發送ACK+SYN給客戶端。
   ESTABLISHED:表示連接已經建立成功了。服務端發送完ACK+SYN后進入該狀態,客戶端收到ACK后也進入該狀態。
   FIN_WAIT_1:表示主動關閉連接。無論哪方調用close函數發送FIN報文都會進入這個這個狀態。
   FIN_WAIT_2:表示被動關閉方同意關閉連接。主動關閉連接方收到被動關閉方返回的ACK后,會進入該狀態。
   TIME_WAIT:表示收到對方的FIN報文並發送了ACK報文,就等2MSL后即可回到CLOSED狀態了。如果FIN_WAIT_1狀態下,收到對方同時帶FIN標志和ACK標志的報文時,可以直接進入TIME_WAIT狀態,而無須經過FIN_WAIT_2狀態。
   CLOSING:表示雙方同時關閉連接。如果雙方幾乎同時調用close函數,那么會出現雙方同時發送FIN報文的情況,此時就會出現CLOSING狀態,表示雙方都在關閉連接。
   CLOSE_WAIT:表示被動關閉方等待關閉。當收到對方調用close函數發送的FIN報文時,回應對方ACK報文,此時進入CLOSE_WAIT狀態。
   LAST_ACK:表示被動關閉方發送FIN報文后,等待對方的ACK報文狀態,當收到ACK后進入CLOSED狀態。

   特別提示的是:為什么TIME_WAIT狀態還需要等待2MSL才能回到CLOSED狀態?或者為什么TCP要引入TIME_WAIT狀態?
   《TCP/IP詳解》中如此解釋:當TCP執行一個主動關閉,並發回最后一個ACK后,該連接必須在TIME_WAIT狀態停留的時間為2倍的MSL,這樣可以讓TCP再次發送最后的ACK以防止這個ACK丟失(另一端超時重發最后的FIN)。
   附注:MSL(Maximum Segment Lifetime)即最大生存時間,RFC 793中指出MSL為2分鍾,但是實現中的常用值為30秒、1分鍾或者2分鍾。

     客戶端主動關閉時,發出FIN包,收到服務器的ACK,客戶端停留在FIN_WAIT2狀態。而服務端收到FIN,發出ACK后,停留在COLSE_WAIT狀態。

    這個CLOSE_WAIT狀態非常討厭,它持續的時間非常長,服務器端如果積攢大量的COLSE_WAIT狀態的socket,有可能將服務器資源耗盡,進而無法提供服務。

    那么,服務器上是怎么產生大量的失去控制的COLSE_WAIT狀態的socket呢?我們來追蹤一下。

    一個很淺顯的原因是,服務器沒有繼續發FIN包給客戶端。

    服務器為什么不發FIN,可能是業務實現上的需要,現在不是發送FIN的時機,因為服務器還有數據要發往客戶端,發送完了自然就要通過系統調用發FIN了,這個場景並不是上面我們提到的持續的COLSE_WAIT狀態,這個在受控范圍之內。

    那么究竟是什么原因呢,咱們引入兩個系統調用close(sockfd)和shutdown(sockfd,how)接着往下分析。

    在這兒,需要明確的一個概念---- 一個進程打開一個socket,然后此進程再派生子進程的時候,此socket的sockfd會被繼承。socket是系統級的對象,現在的結果是,此socket被兩個進程打開,此socket的引用計數會變成2。

 

    繼續說上述兩個系統調用對socket的關閉情況。

    調用close(sockfd)時,內核檢查此fd對應的socket上的引用計數。如果引用計數大於1,那么將這個引用計數減1,然后返回。如果引用計數等於1,那么內核會真正通過發FIN來關閉TCP連接。

    調用shutdown(sockfd,SHUT_RDWR)時,內核不會檢查此fd對應的socket上的引用計數,直接通過發FIN來關閉TCP連接。

 

     現在應該真相大白了,可能是服務器的實現有點問題,父進程打開了socket,然后用派生子進程來處理業務,父進程繼續對網絡請求進行監聽,永遠不會終止。客戶端發FIN過來的時候,處理業務的子進程的read返回0,子進程發現對端已經關閉了,直接調用close()對本端進行關閉。實際上,僅僅使socket的引用計數減1,socket並沒關閉。從而導致系統中又多了一個CLOSE_WAIT的socket。。。

 

如何避免這樣的情況發生?

子進程的關閉處理應該是這樣的:

shutdown(sockfd, SHUT_RDWR);

close(sockfd);

這樣處理,服務器的FIN會被發出,socket進入LAST_ACK狀態,等待最后的ACK到來,就能進入初始狀態CLOSED。

 

補充一下shutdown()的函數說明

linux系統下使用shutdown系統調用來控制socket的關閉方式

int shutdown(int sockfd,int how);

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

SHUT_RD:關閉連接的讀端。也就是該套接字不再接受數據,任何當前在套接字接受緩沖區的數據將被丟棄。進程將不能對該套接字發出任何讀操作。對TCP套接字該調用之后接受到的任何數據將被確認然后被丟棄。

SHUT_WR:關閉連接的寫端。

SHUT_RDWR:相當於調用shutdown兩次:首先是以SHUT_RD,然后以SHUT_WR

注意:

在多進程中如果一個進程中shutdown(sfd, SHUT_RDWR)后其它的進程將無法進行通信. 如果一個進程close(sfd)將不會影響到其它進程.


免責聲明!

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



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