背景
在linux網絡編程中,經常需要編寫關閉socket的代碼,比如心跳檢測失敗需要關閉重連;網絡報異常需要關閉重連。但究竟關閉操作做了什么,卻不太清楚。目前項目使用Netty框架來實現的網絡編程,查看netty源碼可以得知,netty最終是調用了java Nio的close接口做的關閉操作,那么想研究清楚這個close操作究竟做了什么,可以從兩個方向入手,這兩個方向也是從下至上的。
- 搞清楚如果使用C/C++編程,應該調用哪個系統(tǒng)調用函數?函數內部做了什么,涉及到什么TCP/IP的協(xié)議參數
- 搞清楚java nio在調用close方法時,究竟使用了哪個系統(tǒng)調用?
本文首先解決的是第一步,搞清楚系統(tǒng)調用相關的知識。
相關系統(tǒng)調用
Linux平臺下,提供了兩個系統(tǒng)調用函數供開發(fā)人員使用:
- close函數
- shutdown函數
close函數
int close(int sockfd);
這個函數的具體行為由一個TCP/IP套接字選項控制:SO_LINGER
SO_LINGER的在頭文件<sys/socket.h>中定義如下:
struct linger{
int l_onoff;
int l_linger;
}
根據這個選項參數的不同,close的邏輯如下:
1)l_onoff=0,l_linger=1或者0時(這個是默認選項)
- close會立即返回,0為成功-1為失敗
- 調用進程在該套接字上不能再發(fā)送或接收請求
- 接收緩沖區(qū)中的數據將會被拋棄
- 如果發(fā)送緩沖區(qū)中還有數據,會由操作系統(tǒng)在后臺繼續(xù)發(fā)送
- 如果套接字的引用計數變?yōu)?,則發(fā)送FIN表示關閉
- 引用計數:進程和子進程可以共享一個套接字,每當一個進程做了close操作,引用計數就會減1
- 最后釋放套接字的系統(tǒng)資源
2)l_onoff=1,l_linger=0時
- close會立即返回
- 調用進程在該套接字上不能再發(fā)送或接收請求
- 發(fā)送和接收緩沖區(qū)中的數據都會被拋棄
- 如果套接字的引用計數變?yōu)?,則發(fā)送RST到對端,并且狀態(tài)直接變成CLOSED
- 注:RST沒有超時重發(fā)機制,如果對端沒有收到RST,繼續(xù)發(fā)送,那么又會促使本端發(fā)送RST,直到對方收到
- 最后釋放套接字的系統(tǒng)資源
3)l_onoff=1,l_linger>0時
- 如果是阻塞的socket,close函數不會立即返回;非阻塞的會立即返回
- 調用進程在該套接字上不能再發(fā)送或接收請求
- 接收緩沖區(qū)中的數據將會被拋棄
- 如果發(fā)送緩沖區(qū)中還有數據,會由操作系統(tǒng)在后臺繼續(xù)發(fā)送
- 如果套接字的引用計數變?yōu)?,則發(fā)送FIN表示關閉,在套接字狀態(tài)編程CLOSED前,如果超時時間到,返回EWOULDBLOCK錯誤
- 最后釋放套接字的系統(tǒng)資源
總結一下:
默認情況和第三種情況對比,默認情況相當于一個異步請求,并且無法得知操作結果;第三種情況,可以在超時時間范圍內做close處理,發(fā)送未發(fā)送完畢的數據。第二種情況屬于粗暴的關閉socket,在2MSL時間范圍內如果新建立了一個“化身”(ip port dip dport都一樣的套接字),可能會被前一個套接字相關的數據所影響。
注:對2MSL不理解的小伙伴,可以看下這篇博客,講解的很清晰:
為什么tcp的TIME_WAIT狀態(tài)要維持2MSL
shutdown函數
有一種業(yè)務場景,客戶端發(fā)送數據到服務端,發(fā)送完畢后,客戶端就可以關閉客戶端寫方向的連接了,等待服務端處理。
業(yè)務需求是保證客戶端發(fā)送的數據都會被服務端應用程序接收并處理。如果使用close函數關閉連接,最多只能保證,全部數據都已經發(fā)送到了對端的接收緩沖區(qū)中(使用SO_LINGER相關配置項),但是無法確保對端的應用程序一定讀取到數據(close以后,本端socket就無法讀了)。
在這種業(yè)務場景下,如果需要確保服務端一定讀取到了數據,可以考慮使用shutdown函數。
int shutdown(int sockfd,int howto);
執(zhí)行shutdown函數,成功返回0,出錯返回-1。
howto是這個函數的設置選項:
- SHUT_RD:關閉套接字的讀方向。讀緩沖區(qū)中的數據都會被拋棄,如果有新數據到達,都將被ACK,并且被悄悄丟棄。
- SHUT_WR:關閉套接字的寫方向。在套接字發(fā)送緩沖區(qū)的數據都會被繼續(xù)發(fā)送過去,然后發(fā)送正常的FIN開始揮手流程。
- SHUT_RDWR:讀和寫兩個方向都關閉
只使用shutdown函數,也無法保證滿足我們上面提到的業(yè)務需求,即保證服務端應用程序是否正確讀取數據。目前有兩種解決方式可以實現上述業(yè)務需求:
- shutdown后,使用read函數,等待對端的FIN發(fā)送過來,此時read函數返回0
- 應用級別確認:完全發(fā)送數據后,再讀取一個字節(jié)的數據(這個數據是客戶端和服務端的自定義協(xié)議,比如:服務端完全接受數據后,可以繼續(xù)發(fā)送一字節(jié)的數據,代表讀取成功)
第一種方式流程圖如下(摘自《Unix網絡編程》):

第二種方式流程圖如下(摘自《Unix網絡編程》):

close函數和shutdow函數的區(qū)別
- close函數會計算引用計數,當計數為0時才觸發(fā)揮手操作;shutdown函數則不需要判斷引用計數來觸發(fā)揮手操作
- close函數可以終止兩個方向的傳輸,shutdown可以控制只終止一個方向的
- close函數會關閉資源,shutdown函數不會