天天看點

小議socket關閉

socket程式設計過程中往往會遇到這樣那樣的問題,出現了這些問題,有的是由于并發通路量太大造成的,有些卻是由于代碼中程式設計不慎造成的。比如說,最常見的錯誤就是程式中報打開的檔案數過多這個錯誤。socket建立連接配接的時候是三次握手,這個大家都很清楚,但是socket關閉連接配接的時候,需要進行四次揮手,但很多人對于這四次揮手的具體流程不清楚,吃了很多虧。

CLOSE_WAIT分析

socket是一種全雙工的通信方式,建立完socket連接配接後,連接配接的任何一方都可以發起關閉操作。這裡不妨假設連接配接的關閉是用戶端發起。用戶端的代碼如下:

ret = CS_GetConnect(&client,ipAddr,9010);
if (ret == 0) {
    printf("connected success.");
}
CloseSocket(client);
           

代碼片段1.1

基本邏輯就是,連接配接建立後立即關閉。其中CloseSocket函數是自定義函數,僅僅封裝了在windows和linux下關閉socket的不同實作而已

#if defined(WIN32) || defined(WIN64)
#define CloseSocket(fd) do{ closesocket(fd);/* shutdown(fd, 2);*/ }while(0)
#else
#define CloseSocket(fd) do{ close(fd); /*shutdown(fd,2);*/ }while(0)
#endif
           

代碼片段1.2

小議socket關閉

圖1.1 CLOSE_WAIT出現流程

用戶端調用了CloseSocket之後,發送FIN信号到伺服器端,告訴socket程式,連接配接已經斷開。伺服器端接收到FIN信号後,會将自身的TCP狀态置為 `CLOSE_WAIT`,同時回複 一個ACK信号給用戶端,用戶端接收到這個ACK信号後,自身将處于 `FIN_WAIT_2`狀态。

但是tcp是全雙工的通信協定,雖然用戶端關閉了連接配接,但是伺服器端對于這個關閉動作不予理睬怎麼辦。對于伺服器端來說,這是個不幸的消息,因為它将一直處于 `CLOSE_WAIT`狀态,雖然用戶端已經不需要和伺服器間進行通信了,但是伺服器端的socket連接配接句柄一直得不到釋放;如果老是有這種情況出現,久而久之伺服器端的連接配接句柄就會被耗盡。對于發起關閉的用戶端來說,他處于 `FIN_WAIT_2`狀态,如果出現伺服器端一直處于 `CLOSE_WATI`狀态的情況,用戶端并不會一直處在 `FIN_WAIT_2`狀态,因為這個狀态有一個逾時時間,這個值可以在/etc/sysctl.conf中進行配置。在這個檔案中配置 `net.ipv4.tcp_fin_timeout=30`即可保證 `FIN_WAIT_2`狀态最多保持30秒,超過這個時間後就進入TIME_WAIT狀态(下面要講到這個狀态)。

**注意:這裡socket的關閉從用戶端發起,僅僅是為了舉例說明,socket的關閉完全也可以從伺服器端發起。比如說你寫了一個爬蟲程式去下載下傳網際網路上的某些web伺服器上的資源的時候,某些要下載下傳的web資源不存在,web伺服器會立即關閉目前的socket連接配接,但是你的爬蟲程式不夠健壯,對于這種情況沒有做處理,同樣會使你的爬蟲用戶端處于CLOSE_WAIT狀态。**

那麼怎樣預防SOCKET處于CLOSE_WATI狀态呢,答案在這裡:

while(true) {
        memset(getBuffer,0,MY_SOCKET_BUFFER_SIZE);
        Ret = recv(client, getBuffer, MY_SOCKET_BUFFER_SIZE, 0);
        if ( Ret == 0 || Ret == SOCKET_ERROR ) 
        {
            printf("對方socket已經退出,Ret【%d】!\n",Ret);
            Ret = SOCKET_READE_ERROR;//接收伺服器端資訊失敗
            break;
        }
    }

clear:
    if (getBuffer != NULL) {
        free(getBuffer);
        getBuffer = NULL;
    }
    closesocket(client);
           

代碼片段1.3

這裡摘錄了伺服器端部分代碼,注意這個recv函數,這個函數在連接配接建立時,會堵塞住目前代碼,等有資料接收成功後才傳回,傳回值為接收到的位元組數;但是對于連接配接對方socket關閉情況,它能立即感應到,并且傳回0.是以對于傳回0的時候,可以跳出循環,結束目前socket處理,進行一些垃圾回收工作,注意最後一句closesocket操作是很重要的,假設沒有寫這句話,伺服器端會一直處于CLOSE_WAIT狀态。如果寫了這句話,那麼socket的流程就會是這樣的:

小議socket關閉

圖1.2  `TIME_WAIT`出現流程

TIME_WAIT分析

伺服器端調用了CloseSocket操作後,會發送一個FIN信号給用戶端,用戶端進入 `TIME_WAIT`狀态,而且将維持在這個狀态一段時間,這個時間也被成為2MSL(MSL是maximum segment lifetime的縮寫,意指最大分節生命周期,這是IP資料包能在網際網路上生存的最長時間,超過這個時間将在網際網路上消失),在這個時間段内如果用戶端的發出的資料還沒有被伺服器端确認接收的話,可以趁這個時間等待服務端的确認消息。注意,用戶端最後發出的ACK N+1消息,是一進入 `TIME_WAIT`狀态後就發出的,并不是在 `TIME_WAIT`狀态結束後發出的。如果在發送ACK N+1的時候,由于某種原因伺服器端沒有收到,那麼伺服器端會重新發送FIN N消息,這個時候如果用戶端還處于 `TIME_WAIT`狀态的,會重新發送ACK N+1消息,否則用戶端會直接發送一個RST消息,告訴伺服器端socket連接配接已經不存在了。

有時,我們在使用netstat指令檢視web伺服器端的tcp狀态的時候,會發現有成千上萬的連接配接句柄處在 `TIME_WAIT`狀态。web伺服器的socket連接配接一般都是伺服器端主動關閉的,當web伺服器的并發通路量過大的時候,由于web伺服器大多情況下是短連接配接,socket句柄的生命周期比較短,于是乎就出現了大量的句柄堵在 `TIME_WAIT`狀态,等待系統回收的情況。如果這種情況太過頻繁,又由于作業系統本身的連接配接數就有限,勢必會影響正常的socket連接配接的建立。在linux下對于這種情況倒是有解救措施,方法就是修改/etc/sysctl.conf檔案,保證裡面含有以下三行配置:

    #表示開啟重用。允許将TIME-WAIT sockets重新用于新的TCP連接配接,預設為0,表示關閉  

    net.ipv4.tcp_tw_reuse = 1  

    #表示開啟TCP連接配接中TIME-WAIT sockets的快速回收,預設為0,表示關閉  

    net.ipv4.tcp_tw_recycle = 1  

    #表示系統同時保持TIME_WAIT的最大數量,如果超過這個數字,

    #TIME_WAIT将立刻被清除并列印警告資訊。預設為180000,改為5000。

    net.ipv4.tcp_max_tw_buckets = 5000

配置型 2.1

關于重用 `TIME_WAIT`狀态的句柄的操作,也可以在代碼中設定:

int on = 1;
if (setsockopt(socketfd/*socket句柄*/,SOL_SOCKET,SO_REUSEADDR,(char *)&on,sizeof(on)))
{
    return ERROR_SET_REUSE_ADDR;
}
           

代碼片段2.1

如果在代碼中設定了關于重用的操作,程式中将使用代碼中設定的選項決定重用或者不重用,/etc/sysctl.conf中 `net.ipv4.tcp_tw_reuse`中的設定将不再其作用。

當然這樣設定是有悖TCP的設計标準的,因為處于 `TIME_WAIT`狀态的TCP連接配接,是有其存在的積極作用的,前面已經介紹過。假設用戶端的ACK N+1信号發送失敗,伺服器端在1MSL時間過後會重發FIN N信号,而此時用戶端重用了之前關閉的連接配接句柄建立了新的連接配接,但是此時就會收到一個FIN信号,導緻自己被莫名其妙關閉。

一般 `TIME_WAIT`會維持在2MSL(linux下1MSL預設為30秒)時間,但是這個時間可以通過代碼修改:

struct linger so_linger;
so_linger.l_onoff = 1;
so_linger.l_linger = 10;
if (setsockopt(socketfd,SOL_SOCKET,SO_LINGER,(char *)&so_linger,sizeof(struct linger)))
{
    return ERROR_SET_LINGER;
}
           

代碼片段2.2

這裡代碼将 `TIME_WAIT`的時間設定為10秒(在BSD系統中,将會是0.01*10s)。TCP中的 `TIME_WAIT`機制使得socket程式可以“優雅”的關閉,如果你想你的程式更優雅,最好不要設定 `TIME_WAIT`的停留時間,讓老的tcp資料包在合理的時間内自生自滅。當然對于 `SO_LINGER`參數,它不僅僅能夠自定義 `TIME_WAIT`狀态的時間,還能夠将TCP的四次揮手直接禁用掉,假設對于so_linger結構體變量的設定是這個樣子的:

    so_linger.l_onoff = 1;

    so_linger.l_linger = 0;

如果用戶端的socket是這麼設定的那麼socket的關閉流程就直接是這個樣子了:

小議socket關閉

圖2.1 RST關閉流程

這相當于用戶端直接告訴伺服器端,我這邊異常終止了,對于我稍後給出的所有資料包你都可以丢棄掉。伺服器端如果接受到這種RST消息,會直接把對應的socket句柄回收掉。有一些socket程式不想讓TCP出現 `TIME_WAIT`狀态,會選擇直接使用RST方式關閉socket,以保證socket句柄在最短的時間内得到回收,當然前提是接受有可能被丢棄老的資料包這種情況的出現。如果socket通信的前後資料包的關聯性不是很強的話,換句話說每次通信都是一個單獨的事務,那麼可以考慮直接發送RST信号來快速關閉連接配接。

補充

1.文中提到的修改/etc/sysctl.conf檔案的情況,修改完成之後需要運作 `/sbin/sysctl -p`後才能生效。

2.圖1中發送完FIN M信号後,被動關閉端的socket程式中輸入流會接收到一個EOF标示,是在C代碼中處理時recv函數傳回0代表對方關閉,在java代碼中會在InputStream的read函數中接收到-1:

Socket client = new Socket();//,9090
    try {
        client.connect(
            new InetSocketAddress("192.168.56.101",9090));

        while(true){                
            int c = client.getInputStream().read();
            if (c > 0) {
                System.out.print((char) c);
            } else {//如果對方socket關閉,read函數傳回-1
                break;
            }

            try {
                Thread.currentThread().sleep(2000);                 
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    } catch (IOException e2) {
        e2.printStackTrace();
    } finally {
        try {
            client.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}
           

代碼片段3.1

3.如果主動關閉方已經發起了關閉的FIN信号,被動關閉方不予理睬,依然往主動關閉方發送資料,那麼主動關閉方會直接傳回RST新号,連接配接雙方的句柄就被雙方的作業系統回收,如果此時雙方的路由節點之前還存在未到達的資料,将會被丢棄掉。

4.通信的過程中,socket雙發中有一方的程序意外退出,則這一方将向其對應的另一方發送RST消息,所有雙發建立的連接配接将會被回收,未接收完的消息就會被丢棄。

5.項目的配套代碼可以從這裡得到http://git.oschina.net/yunnysunny/socket_close

繼續閱讀