寫過TCP伺服器的人都知道,要解決主動關閉後的TIME_WAIT狀态是件很麻煩的事情,如果伺服器設定Linger生效且延遲為0秒,則伺服器發送給Client的最後一個資料包極可能丢失。Server端TIME_WAIT過多會導緻伺服器效率急劇下降,Client端TIME_WAIT過多會導緻connect to server失敗(報WSAEADDRINUSE錯誤,休息一段時間讓部分處于TIME_WAIT狀态的句柄逾時後又能connect成功到伺服器)。
幾乎翻遍網際網路也沒找到比較好的解決方案,其中有老外提出修改TCP/IP協定棧的方案(前幾天我也在考慮這個方案的可行性)。
下面給出我的關于解決TIME_WAIT的方案。
首先分析下從ESTABLISHED狀态到關閉狀态的過程:
隻有兩種方式: 1,主動關閉socket連接配接。
2,被動關閉socket連接配接。
我們知道主動關閉至少會經過TIME_WAIT_1--->TIME_WAIT(2MSL timeout)--->CLOSED
而被動關閉會經過CLOSE_WAIT--->LAST_ACK--->CLOSED
Step 1:
配置設定SERVER端處理CLIENT并發TCP connection請求的節點數目要大于Client單次select()能發起的最大連接配接數目。這樣SERVER端保證不會有請求得不到臨時節點而無法接入到SERVER.
設定新accept接入的句柄reuse address, XSetSocketReuseAddress(),這個是我自己封裝的跨平台函數,後面同,大家可以自己用setsockopt()實作。
Step 2:
将新接入的節點壓入讀檢測隊列(select or epoll).
Step 3:
有資料到來,讀取資料(如果讀取長度為0,則說明對方關閉了該TCP connection)。如果讀取完畢,準備好回應資料,并壓入寫檢測隊列。
Step 4:
檢測資料可寫,發送資料。如果發送完畢,将TCP connection壓入讀檢測隊列(重複Step 2~Step 4以實作長連接配接J)。
Step 5:
SERVER檢測逾時節點。
----------------------------------------------------------------------------------------------------
Step 6:
CLIENT new socket handle,設定Linger生效,且延遲為0秒XSetSocketLinger(),設定為非阻塞模式XSetSocketBlockingMode(),便于快速connect到SERVER ,異步檢測是否連接配接成功。
将句柄壓入寫檢測隊列。
Step 7:
如果可寫,CLIENT發送請求資料給SERVER,發送完畢将句柄壓入讀檢測隊列。
Step 8:
如果有資料可讀,則讀取資料,讀取完畢可以直接關閉連接配接。
Step 9:
CLIENT檢測逾時節點。
效果:
D:/vcoutput/Release>SimuCGIClient.exe 172.30.14.13 25000 1000000
usage:SimuCGIClient.exe CacheServerHost Port LoopTimes
total ssuccess query:1000012 in 703 seconds.
1422.49 querys/second.
在windows下向SERVER請求100萬個連接配接,在703seconds内完成響應,平均每秒約能完成1422個TCP并發請求。SERVER,CLIENT均為單線程程式。
在這100萬個節點查詢過程中,檢視伺服器和client的協定棧句柄消耗情況:
----------------------------------------------------------------------------------------------------
Active Connections
Proto Local Address Foreign Address State
TCP WOOKIN-NB:1847 WOOKIN-NB:25000 ESTABLISHED
TCP WOOKIN-NB:1848 WOOKIN-NB:25000 ESTABLISHED
TCP WOOKIN-NB:1849 WOOKIN-NB:25000 ESTABLISHED
TCP WOOKIN-NB:1850 WOOKIN-NB:25000 ESTABLISHED
TCP WOOKIN-NB:1851 WOOKIN-NB:25000 ESTABLISHED
TCP WOOKIN-NB:1852 WOOKIN-NB:25000 ESTABLISHED
TCP WOOKIN-NB:1853 WOOKIN-NB:25000 ESTABLISHED
TCP WOOKIN-NB:25000 WOOKIN-NB:0 LISTENING
TCP WOOKIN-NB:25000 WOOKIN-NB:1847 ESTABLISHED
TCP WOOKIN-NB:25000 WOOKIN-NB:1848 ESTABLISHED
TCP WOOKIN-NB:25000 WOOKIN-NB:1849 ESTABLISHED
TCP WOOKIN-NB:25000 WOOKIN-NB:1850 ESTABLISHED
TCP WOOKIN-NB:25000 WOOKIN-NB:1851 ESTABLISHED
TCP WOOKIN-NB:25000 WOOKIN-NB:1852 ESTABLISHED
TCP WOOKIN-NB:25000 WOOKIN-NB:1853 ESTABLISHED
UDP WOOKIN-NB:microsoft-ds *:*
----------------------------------------------------------------------------------------------------
隻看到LISTENING, ESTABLISHED狀态,太漂亮了。
接下來我會在Linux環境下進行繼續驗證。
方案關鍵點:
将通常情況下應該由SERVER端主動關閉的句柄逆向思維轉由CLIENT端主動關閉,則SERVER端socket handle能快速優雅地進入到CLOSED狀态。 CLIENT端接收完資料以後采用Linger on延遲0秒的模式,“武斷”地關閉client socket handle。其實這種做法一點都不武斷,因為CLIENT已經收完資料包,延遲為0關閉句柄并不會造成資料丢失(如果是發送方這樣關閉則會造成資料丢失,接收方可以安全快速關閉)。
SERVER端采用支援長連接配接的方式巧妙地檢測到CLIENT關閉socket connection,又能正常支援長短連接配接.
請問我如何
檢查 自己的socket連接配接目前處于什麼TCP狀态呢?
因為我想如果檢查到目前連接配接處于CLOSE_WAIT狀态的話,就調用closesocket()函數。
是否是使用select+fd_isset來做到這一點呢?
Thanks!
是否是使用select+fd_isset來做到這一點呢?
---
肯定不是。
因為我想如果檢查到目前連接配接處于CLOSE_WAIT狀态的話,就調用closesocket()函數。
--------
我印象中CLOSE_WAIT和調用closesocket沒關系。你為什麼要檢測CLOSE_WAIT狀态?奇怪的想法。
Top
回複人: xiaohaiyan(xiaohaiyan) ( ) 信譽:100 2005-02-03 12:15:00 得分: 0
如果是linux
系統 ,可以參考一下這個,
enum {
TCP_ESTABLISHED = 1,
TCP_SYN_SENT,
TCP_SYN_RECV,
TCP_FIN_WAIT1,
TCP_FIN_WAIT2,
TCP_TIME_WAIT,
TCP_CLOSE,
TCP_CLOSE_WAIT,
TCP_LAST_ACK,
TCP_LISTEN,
TCP_CLOSING,
TCP_MAX_STATES
};
socket->sock->sk_state 記錄上述狀态
調用getsockname ,可以通過fd得到socket結構位址。
Top
回複人: zhengyun_ustc(鄭昀) ( ) 信譽:100 2005-02-03 12:42:00 得分: 0
因為如果我是用戶端,當我給伺服器端發送資料後,正準備recv接收響應時,伺服器突然直接關閉session和socket,于是我這邊立刻進入 CLOSE_WAIT狀态,并且始終處于這種狀态;這是我的猜想,我可以通過檢查send和recv的傳回值,以找到這種錯誤。
但是為了以防萬一,我想主動檢查TCP狀态。
我的OS是Windows。
請讀一下PlatformSDK的示例
Samples/NetDS/IPHelp/IPStat
void DumpTcpTable(PMIB_TCPTABLE pTcpTable)
{
char strState[MAX_STRLEN];
struct in_addr inadLocal, inadRemote;
DWORD dwRemotePort = 0;
char szLocalIp[MAX_STRLEN];
char szRemIp[MAX_STRLEN];
if (pTcpTable != NULL)
{
printf("TCP TABLE/n");
printf("%20s %10s %20s %10s %s/n", "Loc Addr", "Loc Port", "Rem Addr",
"Rem Port", "State");
for (UINT i = 0; i < pTcpTable->dwNumEntries; ++i)
{
switch (pTcpTable->table[i].dwState)
{
case MIB_TCP_STATE_CLOSED:
strcpy(strState, "CLOSED");
break;
case MIB_TCP_STATE_TIME_WAIT:
strcpy(strState, "TIME_WAIT");
break;
case MIB_TCP_STATE_LAST_ACK:
strcpy(strState, "LAST_ACK");
break;
case MIB_TCP_STATE_CLOSING:
strcpy(strState, "CLOSING");
break;
case MIB_TCP_STATE_CLOSE_WAIT:
strcpy(strState, "CLOSE_WAIT");
break;
case MIB_TCP_STATE_FIN_WAIT1:
strcpy(strState, "FIN_WAIT1");
break;
case MIB_TCP_STATE_ESTAB:
strcpy(strState, "ESTAB");
break;
case MIB_TCP_STATE_SYN_RCVD:
strcpy(strState, "SYN_RCVD");
break;
case MIB_TCP_STATE_SYN_SENT:
strcpy(strState, "SYN_SENT");
break;
case MIB_TCP_STATE_LISTEN:
strcpy(strState, "LISTEN");
break;
case MIB_TCP_STATE_DELETE_TCB:
strcpy(strState, "DELETE");
break;
default:
printf("Error: unknown state!/n");
break;
}
inadLocal.s_addr = pTcpTable->table[i].dwLocalAddr;
if (strcmp(strState, "LISTEN") != 0)
{
dwRemotePort = pTcpTable->table[i].dwRemotePort;
}
else
dwRemotePort = 0;
inadRemote.s_addr = pTcpTable->table[i].dwRemoteAddr;
strcpy(szLocalIp, inet_ntoa(inadLocal));
strcpy(szRemIp, inet_ntoa(inadRemote));
printf("%20s %10u %20s %10u %s/n",
szLocalIp, ntohs((unsigned short)(0x0000FFFF & pTcpTable->table[i].dwLocalPort)),
szRemIp, ntohs((unsigned short)(0x0000FFFF & dwRemotePort)),
strState);
}
}
}