SIGPIPE錯誤出現的一種場景和網絡編程異常處理的梳理

網絡編程中異常處理

SIGPIPE信號的使用

在涉及到網絡交互的程序中,我們經常會在程序的伊始就執行一個信號註冊

signal(SIGPIPE, SIG_IGN);

忽略了 SIGPIPE這個信號錯誤,那爲什麼要忽略這個錯誤?如果這個錯誤永遠都是默認要被忽略,那其存在的意義是啥?通過查閱資料可以瞭解到,SIGPIPE信號出現的場景是在建立好連接,對端關閉之後,我們這端仍然連續地往套接字中發送消息,纔會出現SIGPIPE信號。

那既然知道會出現這種錯誤情況,如果是對端已經關閉了套接字,就編程上進行判斷,不往套接字裏寫不就搞定了麼,而直接關閉套接字不就搞定了麼?況且代碼中確實會出現這樣的錯誤麼?

我對日誌做了一下排查,發現瞭如下結果

[Func:yyy_get_status]: yyy_send ERROR!
[Func:xxx_writen]: writeSocket 2 os Error 32 Broken pipe
[Func:xxx_writen]: writeSocket 2 os Error 32 Broken pipe

排查過程中確認例如上面的日誌出現的情況還是非常多的,在很多接口中都有頻繁的出現。

一種出錯導致SIGPIPE的場景與解決辦法

從日誌中可以瞭解到是 yyy_get_status 接口在執行時出錯,導致觸發了 broken pipe錯誤。該接口是app端通過 yyy_get_status 關鍵字請求的接口。設備端使用的web服務器 是多線程 多路複用響應模型,每一條連接由一個獨立線程託管處理。從上下文的日誌中搜尋發現,yyy_get_status 接口觸發了兩次,可以猜測APP端在第二次調用接口時,關閉了第一個連接,使得設備端進入了異常的處理分支。那這爲何會引起設備端出現該SIG_PIPE的問題呢?分析代碼流程

如下爲這個接口進行響應的一個主要結構

    while (1)
    {
        sleep(100);
        ...
        /*organize data to pDest  & length iLen */
        ...
        while(writeLen<iLen)
       {
        if(xxx_poll(pSock, MMM_WRITABLE, WAIT_MIL_SEC) <= 0)
        {
            XXX_WARN("poll 1 os Error %d %s", errCode, strerror(errCode));
            break;
        }
        len = writeSocket(pSock, pDest+writeLen, iLen-writeLen);
        if(len<=0)
        {
            errCode = GetErrorCode();
            if(errCode== EAGAIN || errCode == EWOULDBLOCK)
            {
                continue;
            }
            XXX_WARN("writeSocket 2 os Error %d %s", errCode, strerror(errCode));
            break;
        }
        writeLen += len;
        }

    }

開頭的sleep的是爲了避免異常狀態下出現死循環,佔用cpu。假定在兩次xxx_poll之間,也就是sleep的時間,對端關閉了套接字,執行xxx_poll會返回什麼呢,這需要具體瞭解xxx_poll的調用

int xxx_poll(Socket *pSock, int iMask, int iWaitMilSec)
{
    struct pollfd   fd;

    memset(&fd,0, sizeof(fd));

    if(!pSock || !((iMask & MMM_READABLE) || (iMask & MMM_WRITABLE)))
    {
        return 0;
    }
    
    if (iMask & MMM_READABLE) {
        fd.events |= POLLIN | POLLHUP;
    }
    if (iMask & MMM_WRITABLE) {
        fd.events |= POLLOUT;
    }
    fd.fd = pSock->fd;
    
    return poll(&fd, 1, iWaitMilSec);
}

可以看到,該接口使用poll調用實現了讀寫的超時,在入參爲 MMM_WRITABLE的時候,會通過poll 等待 socket處於可寫的狀態。

那對端關閉套接字的時候,poll是如何返回的呢?通過手寫一個簡單的交互demo可以知道,poll會返回1,這種情況相當於與 正常可寫的情況返回結果一致,返回的是認爲可寫的fd event的數量。這種情況下,導致上層認爲fd只是正常可寫,執行第一次 send或者write,可以正常寫入本地緩衝區,但會收到對端的一個RST消息;再第二次執行poll時,結果一致,也是返回1,第二次再往socket裏面寫消息,就會返回錯誤32,Broken pipe的錯誤,也就出現了上面的錯誤打印。如果此時沒有註冊忽略SIG_PIPE信號,就會引起崩潰。

那麼應該如何操作呢?。

poll裏面其實已經提供了 檢測的機制,將 POLLHUP事件在寫套接字時 也應加入監聽事件列表。在poll返回時,需判定 POLLHUP事件是否觸發,如果觸發,則認爲當前連接套接字已關閉,也直接關閉即可,這樣便不會再執行寫操作,也就不會觸發SIG_PIPE問題。需要對代碼按如下方式進行改造。


int xxx_poll(Socket *pSock, int iMask, int iWaitMilSec)
{
    struct pollfd   fd;

    memset(&fd,0, sizeof(fd));

    if(!pSock || !((iMask & MMM_READABLE) || (iMask & MMM_WRITABLE)))
    {
        return 0;
    }
    
    if (iMask & MMM_READABLE) {
        fd.events |= POLLIN | POLLHUP;
    }
    if (iMask & MMM_WRITABLE) {
        fd.events |= POLLOUT;
    }
    fd.fd = pSock->fd;
    
    if ( (fd.revents & POLLHUP) > 0 ){
		return 0;    /*close needed*/
    }
     /* 返回0表徵 poll結果超時,並且無套接字就緒 */
    return poll(&fd, 1, iWaitMilSec);
}

經過如上修改,在這種對端關閉套接字的場景下,纔不會觸發 SIG_PIPE 問題。但在實際的編程場景中,可能還有非常多其他的場景需要完善返回值和異常處理。很難要求每個人都做這種完善的異常處理,故而需要通過忽略SIG_PIPE信號,讓這種錯誤成爲一種一般化的錯誤,只做記錄,而不引起程序退出。

對網絡編程中的異常做簡單梳理(參考自極客時間,網絡編程實戰)

我們這裏對網絡編程中可能出現的異常情況再做一個總結。我們以我們這端是服務器端爲例,這是我們目前應用中最常用的使用場景,作爲設備端,基於web服務器,爲APP端或者客戶端提供REST服務。

假設連接已經正常建立,3次握手已經完成,雙方在歡快地進行正常的數據傳輸。

如果正常消息傳輸結束,正常關閉流程時,APP端發起FIN包,對應用層而言就是調用close調用,另一端通過上述poll或者recv調用,在獲知對端已經關閉連接的時候,也關閉當前的套接字,完成一套正常關閉的流程。衆所周知,要完成如下的流程

A FIN —— > B
A < —— SYN B

A < —— FIN B
A SYN —— > B

但實際場景中可能會出現如下兩種異常場景。

  • 場景1是 程序正常交互時,對端突然掛了,即沒有任何響應,也沒有關閉套接字,這種情況可能有兩種。
    • A是網絡掛了,比如手機端wifi斷了,或者關閉了4G。或者寬帶網絡斷了。
      • 如果是服務器端在阻塞read,如果沒有設置read超時,也沒有設置心跳保活的話,那麼很不幸,阻塞的read將會一直阻塞下去。如果是獨立線程,那麼這個線程會持續地存在,直到所處的進程退出。
      • 如果是服務器端在進行write操作,因爲write只是將數據寫入到內核緩衝區,故而會返回正確,根據TCP協議的屬性,linux會將數據包進行重傳,直到重傳的次數和時間超過了內核設定的閾值,就會將這個連接狀態置爲異常,並在後續再次read或者write這個套接字時,返回對應的錯誤。
    • B是系統掛了,比如交互端是客戶端,客戶端所在的臺式機被踢掉電源出現硬關機。那麼此時,設備端所在的服務器端,仍然是認爲對應的套接字連接是正常的。
      • 這種情況與網絡阻斷的情況類似,如果對端沒有重啓,那麼就會是與上述的情況完全一致。
      • 如果對端重啓,重啓之後在原先的端口地址上,是沒有之前的連接的信息的。如果還在重傳的過程中,對端接收到了重傳的消息,這種情況下對端會返回一個RST分節。如果我們調用read調用,就會返回一個非常常見的錯誤叫做 Connection Reset By Peer 的系統錯誤。這種情況下很顯然就需要執行異常處理,關閉原先的會話。而重新開啓。
  • 場景2是 程序正常交互時,對端正常掛了。這種情況比如APP崩潰了,系統層面不管是ios還是Android,都會進行收尾操作將 已經打開的socket連接全部關閉,也就是發送FIN分節給對端。
    • 這時就容易出現我們上面這裏描述的異常情況。
    • 雖然與正常流程表現上都是服務器端收到FIN分節,但在IO這一層是無法直接區分的。所以即使對端正常的關閉,也是可能出現如下的異常流程。
    • 對方發送時FIN包時,我們這端沒有進行IO的處理時。而我們後續的處理可能就出現兩種情況。
      • 如果我們調用write往對端發送消息,會直接接收到一個RST消息,內核層就會感知到該連接異常,如果再調用write發送消息,就會觸發SIGPIPE 信號。
      • 如果我們調用read接收消息,那麼會直接返回0,即標誌對端已關閉連接,需要我們這邊進行對應的處理。

網絡編程的流程梳理和異常處理時,尤其要注意上述幾種情況的異常處理。

參考

極客時間-網絡編程實戰-TCP並不總是可靠的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章