linux非阻塞的socket傳送資料出現EAGAIN錯誤的處理方法
阿新 • • 發佈:2019-02-10
一、非阻塞socket
二、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),那麼會等待後再重試。
非阻塞套接字是指執行此套接字的網路呼叫時,不管是否執行成功,都立即返回。比如呼叫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
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程式碼:socket快取結構體程式碼: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); }
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事件。