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