1. 程式人生 > 實用技巧 >網路程式設計-關閉連線(1)-C/C++相關係統呼叫

網路程式設計-關閉連線(1)-C/C++相關係統呼叫

背景

在linux網路程式設計中,經常需要編寫關閉socket的程式碼,比如心跳檢測失敗需要關閉重連;網路報異常需要關閉重連。但究竟關閉操作做了什麼,卻不太清楚。目前專案使用Netty框架來實現的網路程式設計,檢視netty原始碼可以得知,netty最終是呼叫了java Nio的close介面做的關閉操作,那麼想研究清楚這個close操作究竟做了什麼,可以從兩個方向入手,這兩個方向也是從下至上的。

  1. 搞清楚如果使用C/C++程式設計,應該呼叫哪個系統呼叫函式?函式內部做了什麼,涉及到什麼TCP/IP的協議引數
  2. 搞清楚java nio在呼叫close方法時,究竟使用了哪個系統呼叫?

本文首先解決的是第一步,搞清楚系統呼叫相關的知識。

相關係統呼叫

Linux平臺下,提供了兩個系統呼叫函式供開發人員使用:

  • 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為失敗
  • 呼叫程序在該套接字上不能再發送或接收請求
  • 接收緩衝區中的資料將會被拋棄
  • 如果傳送緩衝區中還有資料,會由作業系統在後臺繼續傳送
  • 如果套接字的引用計數變為0,則傳送FIN表示關閉
    • 引用計數:程序和子程序可以共享一個套接字,每當一個程序做了close操作,引用計數就會減1
  • 最後釋放套接字的系統資源

2)l_onoff=1,l_linger=0時

  • close會立即返回
  • 呼叫程序在該套接字上不能再發送或接收請求
  • 傳送和接收緩衝區中的資料都會被拋棄
  • 如果套接字的引用計數變為0,則傳送RST到對端,並且狀態直接變成CLOSED
    • 注:RST沒有超時重發機制,如果對端沒有收到RST,繼續傳送,那麼又會促使本端傳送RST,直到對方收到
  • 最後釋放套接字的系統資源

3)l_onoff=1,l_linger>0時

  • 如果是阻塞的socket,close函式不會立即返回;非阻塞的會立即返回
  • 呼叫程序在該套接字上不能再發送或接收請求
  • 接收緩衝區中的資料將會被拋棄
  • 如果傳送緩衝區中還有資料,會由作業系統在後臺繼續傳送
  • 如果套接字的引用計數變為0,則傳送FIN表示關閉,在套接字狀態程式設計CLOSED前,如果超時時間到,返回EWOULDBLOCK錯誤
  • 最後釋放套接字的系統資源

總結一下:
預設情況和第三種情況對比,預設情況相當於一個非同步請求,並且無法得知操作結果;第三種情況,可以在超時時間範圍內做close處理,傳送未傳送完畢的資料。第二種情況屬於粗暴的關閉socket,在2MSL時間範圍內如果新建立了一個“化身”(ip port dip dport都一樣的套接字),可能會被前一個套接字相關的資料所影響。
注:對2MSL不理解的小夥伴,可以看下這篇部落格,講解的很清晰:
[ 為什麼tcp的TIME_WAIT狀態要維持2MSL
](https://www.cnblogs.com/abozhang/p/10974627.html)

shutdown函式

有一種業務場景,客戶端傳送資料到服務端,傳送完畢後,客戶端就可以關閉客戶端寫方向的連線了,等待服務端處理。
業務需求是保證客戶端傳送的資料都會被服務端應用程式接收並處理。如果使用close函式關閉連線,最多隻能保證,全部資料都已經發送到了對端的接收緩衝區中(使用SO_LINGER相關配置項),但是無法確保對端的應用程式一定讀取到資料(close以後,本端socket就無法讀了)。

在這種業務場景下,如果需要確保服務端一定讀取到了資料,可以考慮使用shutdown函式。

int shutdown(int sockfd,int howto);

執行shutdown函式,成功返回0,出錯返回-1。

howto是這個函式的設定選項:

  • SHUT_RD:關閉套接字的讀方向。讀緩衝區中的資料都會被拋棄,如果有新資料到達,都將被ACK,並且被悄悄丟棄。
  • SHUT_WR:關閉套接字的寫方向。在套接字傳送緩衝區的資料都會被繼續傳送過去,然後傳送正常的FIN開始揮手流程。
  • SHUT_RDWR:讀和寫兩個方向都關閉

只使用shutdown函式,也無法保證滿足我們上面提到的業務需求,即保證服務端應用程式是否正確讀取資料。目前有兩種解決方式可以實現上述業務需求:

  • shutdown後,使用read函式,等待對端的FIN傳送過來,此時read函式返回0
  • 應用級別確認:完全傳送資料後,再讀取一個位元組的資料(這個資料是客戶端和服務端的自定義協議,比如:服務端完全接受資料後,可以繼續傳送一位元組的資料,代表讀取成功)

第一種方式流程圖如下(摘自《Unix網路程式設計》):

第二種方式流程圖如下(摘自《Unix網路程式設計》):

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