TCP关闭
以下描述主要是針對windows平臺下的TCP socket而言。
首先需要區分一下關閉socket和關閉TCP連接的區別,關閉TCP連接是指TCP協議層的東西,就是兩個TCP端之間交換了一些協議包(FIN,RST等),具體的交換過程可以看TCP協議,這里不詳細描述了。而關閉socket是指關閉用戶應用程序中的socket句柄,釋放相關資源。但是當用戶關閉socket句柄時會隱含的觸發TCP連接的關閉過程。
TCP連接的關閉過程有兩種,一種是優雅關閉(graceful close),一種是強制關閉(hard close或abortive close)。所謂優雅關閉是指,如果發送緩存中還有數據未發出則其發出去,并且收到所有數據的ACK之后,發送FIN包,開始關閉過程。而強制關閉是指如果緩存中還有數據,則這些數據都將被丟棄,然后發送RST包,直接重置TCP連接。
下面說一下shutdown及closesocket函數。
shutdown函數的原型是:
int shutdown(
??SOCKET s,
??int how
);
該函數用于關閉TCP連接,但并不關閉socket句柄。其第二個參數可以取三個值:SD_RECEIVE,SD_SEND,SD_BOTH。
SD_RECEIVE表明關閉接收通道,在該socket上不能再接收數據,如果當前接收緩存中仍有未取出數據或者以后再有數據到達,則TCP會向發送端發送RST包,將連接重置。
SD_SEND表明關閉發送通道,TCP會將發送緩存中的數據都發送完畢并在收到所有數據的ACK后向對端發送FIN包,表明本端沒有更多數據發送。這個是一個優雅關閉過程。
SD_BOTH則表示同時關閉接收通道和發送通道。
closesocket函數的原型是:
int closesocket(
??SOCKET s
);
該函數用于關閉socket句柄,并釋放相關資源。前面說過,關閉socket句柄時會隱含觸發TCP連接的關閉過程,那么closesocket觸發的是一個優雅關閉過程還是強制關閉過程呢?這個與一個socket選項有關:SO_LINGER 選項,該選項的設置值決定了closesocket的行為。該選項的參數值是linger結構,其定義是:
typedef struct linger {
??u_short l_onoff;
??u_short l_linger;
} linger;
當l_onoff值設置為0時,closesocket會立即返回,并關閉用戶socket句柄。如果此時緩沖區中有未發送數據,則系統會在后臺將這些數據發送完畢后關閉TCP連接,是一個優雅關閉過程,但是這里有一個副作用就是socket的底層資源會被保留直到TCP連接關閉,這個時間用戶應用程序是無法控制的。
當l_onoff值設置為非0值,而l_linger也設置為0,那么closesocket也會立即返回并關閉用戶socket句柄,但是如果此時緩沖區中有未發送數據,TCP會發送RST包重置連接,所有未發數據都將丟失,這是一個強制關閉過程。
當l_onoff值設置為非0值,而l_linger也設置為非0值時,同時如果socket是阻塞式的,此時如果緩沖區中有未發送數據,如果TCP在l_linger表明的時間內將所有數據發出,則發完后關閉TCP連接,這時是優雅關閉過程;如果如果TCP在l_linger表明的時間內沒有將所有數據發出,則會丟棄所有未發數據然后TCP發送RST包重置連接,此時就是一個強制關閉過程了。
另外還有一個socket選項SO_DONTLINGER,它的參數值是一個bool類型的,如果設置為true,則等價于在SO_LINGER中將l_onoff設置為0。
注意SO_LINGER和SO_DONTLINGER選項只影響closesocket的行為,而與shutdown函數無關,shutdown總是會立即返回的。
所以建議的最好的關閉方式是這樣的:
發送完了所有數據后:
(1)調用shutdown(s, SD_SEND),如果本端同時也接收數據時則執行第二步,否則跳到第4步。
(2)繼續接收數據,
(3)收到FD_CLOSE事件后,調用recv函數直到recv返回0或-1(保證收到所有數據),
(4)調用closesocket,關閉socket句柄。
在實際編程中,我們經常也不調用shutdown,而是直接調用closesocket,利用closesocket隱含觸發TCP連接關閉過程的特性。此時的過程就是:
當發送完所有數據后:
(1)如果本端同時也接受數據則執行第二步,否則跳到第4步。
(2)繼續接收數據,
(3)收到FD_CLOSE事件后,調用recv函數直到recv返回0或-1(保證收到所有數據),
(4)調用closesocket,關閉socket句柄。
但是此時為了保證數據不丟失,則需要設置SO_DONTLINGER選項,不過windows平臺下這個也是默認設置。
經過實驗發現,發送端應用程序即便是異常退出或被kill掉進程,操作系統也不會丟棄發送緩沖區中的未發送數據,而是會在后臺將這些數據發送出去。但是這是在socket的發送緩存不為0的前提下,當socket的發送緩存設置為0(通過SO_SNDBUF選項)時比較特殊,此時不論socket是否是阻塞的,send函數都會被阻塞直到傳入的用戶緩存中的數據都被發送出去并被確認,因為此時在驅動層沒有分配緩存存放用戶數據,而是直接使用的應用層的用戶緩存,所以必須阻塞直到數據都發出,否則可能會造成系統崩潰。
另外,如果是接收端的應用程序異常退出或被kill掉進程,并且接收緩存中還有數據沒有取出的話,那么接收端的TCP會向發送端發送RST包,重置連接,因為后續數據已經無法被提交應用層了。
不久前,我的Socket Client程序遇到了一個非常尷尬的錯誤。它本來應該在一個socket長連接上持續不斷地向服務器發送數據,如果socket連接斷開,那么程序會自動不斷地重試建立連接。
有一天發現程序在不斷嘗試建立連接,但是總是失敗。用netstat查看,這個程序竟然有上千個socket連接處于CLOSE_WAIT狀態,以至于達到了上限,所以無法建立新的socket連接了。
為什么會這樣呢?
它們為什么會都處在CLOSE_WAIT狀態呢?
CLOSE_WAIT狀態的生成原因
首先我們知道,如果我們的Client程序處于CLOSE_WAIT狀態的話,說明套接字是被動關閉的!
因為如果是Server端主動斷掉當前連接的話,那么雙方關閉這個TCP連接共需要四個packet:
?????? Server ---> FIN ---> Client
?????? Server <--- ACK <--- Client
??? 這時候Server端處于FIN_WAIT_2狀態;而我們的程序處于CLOSE_WAIT狀態。
?????? Server <--- FIN <--- Client
這時Client發送FIN給Server,Client就置為LAST_ACK狀態。
??????? Server ---> ACK ---> Client
Server回應了ACK,那么Client的套接字才會真正置為CLOSED狀態。
?
我們的程序處于CLOSE_WAIT狀態,而不是LAST_ACK狀態,說明還沒有發FIN給Server,那么可能是在關閉連接之前還有許多數據要發送或者其他事要做,導致沒有發這個FIN packet。
?
原因知道了,那么為什么不發FIN包呢,難道會在關閉己方連接前有那么多事情要做嗎?
還有一個問題,為什么有數千個連接都處于這個狀態呢?難道那段時間內,服務器端總是主動拆除我們的連接嗎?
?
不管怎么樣,我們必須防止類似情況再度發生!
首先,我們要防止不斷開辟新的端口,這可以通過設置SO_REUSEADDR套接字選項做到:
重用本地地址和端口
以前我總是一個端口不行,就換一個新的使用,所以導致讓數千個端口進入CLOSE_WAIT狀態。如果下次還發生這種尷尬狀況,我希望加一個限定,只是當前這個端口處于CLOSE_WAIT狀態!
在調用
sockConnected = socket(AF_INET, SOCK_STREAM, 0);
之后,我們要設置該套接字的選項來重用:
/// 允許重用本地地址和端口:
/// 這樣的好處是,即使socket斷了,調用前面的socket函數也不會占用另一個,而是始終就是一個端口
/// 這樣防止socket始終連接不上,那么按照原來的做法,會不斷地換端口。
int nREUSEADDR = 1;
setsockopt(sockConnected,
????????????? SOL_SOCKET,
????????????? SO_REUSEADDR,
????????????? (const char*)&nREUSEADDR,
????????????? sizeof(int));
教科書上是這么說的:這樣,假如服務器關閉或者退出,造成本地地址和端口都處于TIME_WAIT狀態,那么SO_REUSEADDR就顯得非常有用。
也許我們無法避免被凍結在CLOSE_WAIT狀態永遠不出現,但起碼可以保證不會占用新的端口。
其次,我們要設置SO_LINGER套接字選項:
從容關閉還是強行關閉?
LINGER是“拖延”的意思。
默認情況下(Win2k),SO_DONTLINGER套接字選項的是1;SO_LINGER選項是,linger為{l_onoff:0,l_linger:0}。
如果在發送數據的過程中(send()沒有完成,還有數據沒發送)而調用了closesocket(),以前我們一般采取的措施是“從容關閉”:
因為在退出服務或者每次重新建立socket之前,我都會先調用
/// 先將雙向的通訊關閉
???? shutdown(sockConnected, SD_BOTH);
???? /// 安全起見,每次建立Socket連接前,先把這個舊連接關閉
closesocket(sockConnected);
?
我們這次要這么做:
設置SO_LINGER為零(亦即linger結構中的l_onoff域設為非零,但l_linger為0),便不用擔心closesocket調用進入“鎖定”狀態(等待完成),不論是否有排隊數據未發送或未被確認。這種關閉方式稱為“強行關閉”,因為套接字的虛電路立即被復位,尚未發出的所有數據都會丟失。在遠端的recv()調用都會失敗,并返回WSAECONNRESET錯誤。
在connect成功建立連接之后設置該選項:
linger m_sLinger;
m_sLinger.l_onoff = 1;? // (在closesocket()調用,但是還有數據沒發送完畢的時候容許逗留)
m_sLinger.l_linger = 0; // (容許逗留的時間為0秒)
setsockopt(sockConnected,
???????? SOL_SOCKET,
???????? SO_LINGER,
???????? (const char*)&m_sLinger,
???????? sizeof(linger));
?
總結
也許我們避免不了CLOSE_WAIT狀態凍結的再次出現,但我們會使影響降到最小,希望那個重用套接字選項能夠使得下一次重新建立連接時可以把CLOSE_WAIT狀態踢掉
總結
- 上一篇: 子网掩码和网关的关系
- 下一篇: TCP如何能正常关闭连接?