問題出現的情景及表現
最近寫一個網絡應用,裏面有TCP類型socket通信。因爲需要這同一份代碼在Linux和sylixos上都能編譯運行,所以要採用POSIX標準進行編寫。
開始在兩個系統下運行都是正常的,客戶端每秒向服務端發送數據並得到應答。後來修改客戶端代碼,希望在客戶端發現連接異常或斷開時能間隔10秒再從新連接,這樣客戶端主體其實是個無限循環,不應該會有退出。
測試發現當運行中如果直接關閉了服務端程序,sylixos下客戶端會打印異常狀態並休眠10秒後在次嘗試連接,而Linux下客戶端卻是直接退出進程。
關鍵代碼如下,tcpClientPthread是一個線程執行函數,裏面是一個while(1)無線循環,tcpClient函數中進行TCP客戶端操作,預想的是運行中檢查到錯誤或服務端斷開此函數才退出,然後休眠10秒再次嘗試連接通信。
static void *tcpClientPthread (void * pvArg)
{
ARG_ST *parg = (ARG_ST *)pvArg;
while (1) {
tcpClient(parg);
sleep(10);
}
return (NULL);
}
sylixos下客戶端在服務端主動斷開時表現的情況和預期一致,服務端斷開時會立即輸出錯誤信息,並在等待10秒後再次嘗試連接。如下:
Linux下客戶端在服務端主動斷開時表現的情況和預期不一致,客戶端進程直接退出,也沒有任何錯誤信息輸出。如下:
問題原因分析及結論
網上查閱資料發現,Linux下的TCP連接有這樣一個特性:對一個已經關閉的tcp socket進行寫操作,會觸發系統向當前進程發送SIGPIPE信號,而該信號的默認操作是退出當前進程
。基於這樣的一個特性就不難了理解Linux下的表現了,同時也說明sylixos下沒有該特性。
具體的分析可以結合TCP的”四次握手”關閉。TCP是全雙工的信道,可以看作兩條單工信道,TCP連接兩端的兩個端點各負責一條。當對端調用close時,雖然本意是關閉整個兩條信道, 但本端只是收到FIN包。 按照TCP協議的語義,表示對端只是關閉了其所負責的那一條單工信道, 仍然可以繼續接收數據。也就是說,因爲TCP協議的限制,一個端點無法獲知對端已經完全關閉
。
對一個已經收到FIN包的socket調用read方法,如果接收緩衝已空,則返回0, 這就是常說的表示連接關閉。但第一次對其調用write方法時, 如果發送緩衝沒問題,會返回正確寫入。 但發送的報文會導致對端發送RST報文,因爲對端的socket已經調用了close,完全關閉,既不發送,也不接收數據。所以,第二次調用write方法(假設在收到RST之後), 會生成SIGPIPE信號,導致進程退出。
問題處理方法
如果不想讓直行進程直接退出,則可以有如下幾種方法解決:
- 設置socket屬性爲SO_NOSIGPIPE(異常時不發送SIGPIPE信號):
setsocketopt(sock_fd, SOL_SOCKET, SO_NOSIGPIPE, &(int){1}, sizeof(int));
- 設置對SIGPIPE信號的處理方式爲SIG_IGN(忽略):
struct sigaction sa;
sa.sa_handler = SIG_IGN;
sigaction(SIGPIPE, &sa, 0 );
或:
signal(SIGPIPE, SIG_IGN);
或:
sigignore(SIGPIPE);
- 設置send主線程屬性:
sigset_t signal_mask;
sigemptyset(&signal_mask);
sigaddset(&signal_mask, SIGPIPE);
pthread_sigmask(SIG_BLOCK, &signal_mask, NULL);
改進後測試
在循環前設置對SIGPIPE信號的處理方式爲忽略,修改代碼如下:
static void *tcpClientPthread (void * pvArg)
{
ARG_ST *parg = (ARG_ST *)pvArg;
signal(SIGPIPE, SIG_IGN);
while (1) {
tcpClient(parg);
sleep(10);
}
return (NULL);
}
測試修改後的代碼,現象如下,可見已經不會載自動退出當前進程了,和預期效果一樣。