1. 程式人生 > >linux非阻塞的socket傳送資料出現EAGAIN錯誤的處理方法

linux非阻塞的socket傳送資料出現EAGAIN錯誤的處理方法

一、非阻塞socket

        非阻塞套接字是指執行此套接字的網路呼叫時,不管是否執行成功,都立即返回。比如呼叫recv()函式讀取網路緩衝區中資料,不管是否讀到資料都立即返回,而不會一直掛在此函式呼叫上。在實際Windows網路通訊軟體開發中,非同步非阻塞套接字是用的最多的。平常所說的C/S(客戶端/伺服器)結構的軟體就是非同步非阻塞模式的。 

int32_t flags = fcntl(socket_fd, F_GETFL, 0);
fcntl(socket_fd, F_SETFL, flags | O_NONBLOCK);

二、EAGAIN錯誤 當應用程式在socket中設定O_NONBLOCK屬性後,如果傳送快取被佔滿,send就會返回EAGAIN或EWOULDBLOCK
的錯誤。在將socket設定O_NONBLOCK屬性後,通過socket傳送一個100K大小的資料,第一次成功傳送了13140資料,之後繼續傳送並未成功,errno數值為EAGAIN錯誤。 三、EPOLL模式下EAGAIN錯誤處理方式         方法:需要封裝socket_send()的函式用來處理這種情況,該函式會盡量將資料寫完再返回,返回傳送的位元組數。在socket_send()內部,當寫緩衝已滿(send()返回-1,且errno為EAGAIN),那麼會等待後再重試。
int32_t socket_send(int fd, char* data, int32_t size)
{
    if (NULL == data || size <= 0)
    {
        return -1;
    }
    int32_t remainded = size;
    int32_t sended = 0;
    char* pszTmp = data; 
    while(remainded > 0)
    {
        sended = send(fd, pszTmp, (size_t)remainded, 0);
        if (sended > 0)
        {
            pszTmp += sended;
            remainded -= sended;
        }
        else if (errno == EAGAIN)
        {
            continue;
        }
        else
        {
           break;
        }
    }
    return (size - remainded);
} 

       這種方式並不很完美,當傳送大資料的時候,如果客戶端一直不呼叫recv函式接受資料,那麼伺服器就會卡死在while迴圈中(持續呼叫send函式返回EAGAIN錯誤)。對伺服器來說,出現這種情況是致命的,屆時伺服器的所有功能都不能正常運轉。

       如果當send函數出現EAGAIN錯誤的時候,直到當前socket狀態變成可寫之前,不應該繼續呼叫send函式傳送資料。在傳送資料之前,將socket的監聽的事件增加EPOLLOUT,在資料全部發送之後,再取消EPOLLOUT的監聽。        socket監聽EPOLLOUT程式碼:
void epoll_event_mod(int epoll_socket_fd, int fd)
{
    struct epoll_event epollEvent;
    memset(&epollEvent, 0x0, sizeo(epollEvent));
    epollEvent.data.fd = fd;
    epollEvent.events = EPOLLIN | EPOLLERR | EPOLLHUP | EPOLLOUT; 
    epollEvent.data.ptr = NULL;
    epoll_ctl(epoll_socket_fd, EPOLL_CTL_MOD, fd, &m_epoll_event);
}
       socket快取結構體程式碼:
struct stSocketBuffer
{
    int32_t m_iHead;
    int32_t m_iTail;
    char     m_szBuffer[max_socket_buffer_size];
};
       socket待發送資料放入快取結構程式碼:
int32_t push_socket_data(int fd, char* data, int32_t size)
{
    if (NULL == data || size <= 0)
    {
        return -1;
    }
    stSocketBuffer* pstBuffer = get_socket_buffer(fd);
    if (NULL == pstBuffer)
    {
        return -2;
    }
    if ( size > max_socket_buffer_size + m_iHead - m_iTail)
    {
        return -3;
    }
    if (size + m_iTail > max_socket_buffer_size)
    {
        memcopy(&pstBuffer->m_szBuffer[0], &pstBuffer->m_szBuffer[pstBuffer->m_iHead], pstBuffer->m_iTail  - pstBuffer->m_iHead);
        pstBuffer->m_iTail -= pstBuffer->m_iHead;
        pstBuffer->m_iHead = 0;
    }
    memcpy(&pstBuffer->m_szBuffer[pstBuffer->m_iTail], data, size);
    pstBuffer->m_iTail += size;
    return 0;
}
將快取區資料傳送出去程式碼:
int32_t socket_send(int fd)
{
    stSocketBuffer* pstBuffer = get_socket_buffer(fd);
    if (NULL == pstBuffer)
    {
        return -1;
    }

    int32_t remainded = pstBuffer->m_iTail - pstBuffer->m_iHead;
    int32_t sended = 0;
    char* pszTmp = &pstBuffer->m_szBuffer[pstBuffer->m_iHead]; 
    int32_t again_count = 0;
    while(remainded > 0 && again_count < 2)
    {
        sended = send(fd, pszTmp, (size_t)remainded, 0);
        if (sended > 0)
        {
            pstBuffer->m_iHead += sended;
            pszTmp += sended;
            remainded -= sended;
        }
        else if (errno == EAGAIN)
        {
            ++ again_count;
            continue;
        }
        else
        {
            break;
        }
    }
    return (size - remainded);
}
       總結,當需要向socket傳送資料時,現將資料壓入傳送快取區(stSocketBuffer結構體中),並且將socket加入可寫事件監聽。當socket觸發可寫事件(EPOLLOUT)時,呼叫socket_send函式傳送資料,所有資料傳送完畢,再清除EPOLLOUT事件。