Socket編程模式理解與對比

本文主要分析了幾種Socket編程的模式。主要包括基本的阻塞Socket、非阻塞Socket、I/O多路複用。其中,阻塞和非阻塞是相對於套接字來講的,而其餘的模式本質上來講是基於Socket的併發模式。I/O多路複用又主要分析了分析linux和windows下的經常使用模型。最後,比較這幾種Socket編程模式的優缺點,並討論多線程與Socket的組合使用和服務器開發的經常使用模式。linux

阻塞模式

阻塞模式是最基本的Socket編程模式,在各類關於網絡編程的書籍中都是入門的例子。就像其名所說,阻塞模式的Socket會阻塞當前的線程,直到結果返回,不然會一直等待。編程

非阻塞模式

非阻塞模式是相對阻塞模式來講,Socket並不會阻塞當前線程,非阻塞模式不會等到結果返回,而會當即運行下去。windows

//設置套接字爲非阻塞模式
fcntl( sockfd, F_SETFL, O_NONBLOCK); //O_NONBLOCK標誌設置非阻塞模式

這裏須要注意,阻塞/非阻塞、同步/異步以前的區別。在本質上它們是不一樣的。同步和異步是相對操做結果來講,會不會等待結果結果返回。而阻塞和非阻塞是相對線程是否被阻塞來講的。其實,這二者存在本質的區別,它們的修飾對象是不一樣的。阻塞和非阻塞是指進程訪問的數據若是還沒有就緒,進程是否須要等待,簡單說這至關於函數內部的實現區別,也就是未就緒時是直接返回仍是等待就緒。而同步和異步是指訪問數據的機制,同步通常指主動請求並等待I/O操做完畢的方式,當數據就緒後在讀寫的時候必須阻塞,異步則指主動請求數據後即可以繼續處理其它任務,隨後等待I/O,操做完畢的通知,這可使進程在數據讀寫時也不阻塞。由於二者在表現上常常相同,因此常常被混淆。服務器

I/O多路複用

I/O多路複用是一種併發服務器開發技術(處理多個客戶端的鏈接)。經過該技術,系統內核緩衝I/O數據,當某個I/O準備好後,系統通知應用程序該I/O可讀或可寫,這樣應用程序能夠立刻完成相應的I/O操做,而不須要等待系統完成相應I/O操做,從而應用程序沒必要因等待I/O操做而阻塞。
在linux下主要有select、poll、epoll三種模型,在freeBSD下則有kqueue,windwos下select、事件選擇模型、重疊I/O和完成端口等。網絡

linux上I/O複用模型

select

select本質是經過設置或檢查存放fd標誌位的數據結構來進行下一步的處理。select是採用輪詢fd集合來進行處理的。數據結構

//select相關函數
int select(int maxfdp1, fd_set *readset, fd_set *writeset, 
fd_set *exceptset,const struct timeval *timeout)
//返回值:就緒描述符的數目,超時返回0,出錯返回-1
void FD_ZERO(fd_set *fdset);           //清空集合
void FD_SET(int fd, fd_set *fdset);   //將一個給定的文件描述符加入集合之中
void FD_CLR(int fd, fd_set *fdset);   //將一個給定的文件描述符從集合中刪除
int FD_ISSET(int fd, fd_set *fdset);   // 檢查集合中指定的文件描述符是否能夠讀寫

可是,select存在必定的缺陷。單個進程可監視的fd數量被限制,linux下通常爲1024。雖然是能夠修改的,可是老是有限制的。在每次調用select時,都須要把fd集合從用戶態拷貝到內核態,並且須要循環整個fd集合,這個開銷不少時候是比較大的。多線程

poll

poll的實現和select很是類似,本質上是相同,只是描述fd集合的方式不一樣。poll是基於鏈表來存儲的。這雖然沒有了最大鏈接數的限制,可是仍然還有fd集合拷貝和循環帶來的開銷。並且poll還有一個特色是水平觸發,內核通知了fd後,沒有被處理,那麼內核就會不斷的通知,直到被處理。併發

//poll相關函數
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);
epoll

epoll是對select和poll的改進。相較於poll,epoll使用「事件」的就緒通知,經過epoll_ctl註冊fd,一旦該fd就緒,內核就會採用相似callback的回調機制來激活該fd,把就緒fd放入就緒鏈表中,並喚醒在epoll_wait中進入睡眠的進程,這樣不在須要輪詢,判斷fd合計合集是否爲空。並且epoll不只支持水平觸發,還支持邊緣觸發。邊緣觸發是指內核通知fd以後,無論處不處理都不在通知了。在存儲fd的集合上,epoll也採用了更爲優秀的mmap,並且會保證fd集合拷貝只會發生一次。app

//epoll相關函數
int epoll_create(int size); //句柄的建立
int epoll_ctl(int epfd, int op, int fd,
struct epoll_event *event); //事件註冊
int epoll_wait(int epfd, struct epoll_event * events, 
int maxevents, int timeout); //等待事件的發生

Windows上的I/O複用模型

事件選擇模型

事件選擇模型是基於消息的。它容許程序經過Socket,接收以事件爲基礎的網絡事件通知。異步

//事件選擇模型相關函數
WSAEVENT WSACreatEvent(void);  //建立事件對象
int WSAEventSelect(SOCKET s, WSAEVENT hEventObject,
long  lNetworkEvents); //關聯事件
重疊I/O模型

重疊I/O模型是異步I/O模型。重疊模型的核心是一個重疊數據結構。重疊模型是讓應用程序使用重疊數據結構(WSAOVERLAPPED),一次投遞一個或多個Winsock I/O請求。若想以重疊方式使用文件,必須用FILE_FLAG_OVERLAPPED 標誌打開它。當I/O操做完成後,系統通知應用程序。利用重疊I/O模型,應用程序在調用I/O函數以後,只須要等待I/O操做完成的消息便可。

HANDLE hFile = CreateFile(lpFileName, GENERIC_READ | 
GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL); 
完成端口模型(IOCP)

IOCP完成端口是目前Windows下性能最好的I/O模型,固然也是最複雜的。簡單的說,IOCP 是一種高性能的I/O模型,是一種應用程序使用線程池處理異步I/O請求的機制。IOCP將全部用戶的請求都投遞到一個消息隊列中去,而後線程池中的線程逐一從消息隊列中去取出消息並加以處理,就能夠避免針對每個I/O請求都開線程。不只減小了線程的資源,也提升了線程的利用率。

//IOCP簡單流程    
//建立完成端口
Port port = createIoCompletionPort(INVALID_HANDLE_VALUE, 
0, 0, fixedThreadCount());

//將Socket關聯到IOCP
CreateIoCompletionPort((HANDLE )m_sockClient,m_hIocp, 
(ULONG_PTR )m_sockClient, 0);

//投遞AcceptEx請求
LPFN_ACCEPTEX     m_lpfnAcceptEx;         // AcceptEx函數指針  
GUID GuidAcceptEx = WSAID_ACCEPTEX;        // GUID,這個是識別AcceptEx函數必須的  
DWORD dwBytes = 0;    
WSAIoctl(  
    m_pListenContext->m_Socket,   
    SIO_GET_EXTENSION_FUNCTION_POINTER,   
    &GuidAcceptEx,   
    sizeof(GuidAcceptEx),   
    &m_lpfnAcceptEx,   
    sizeof(m_lpfnAcceptEx),   
    &dwBytes,   
    NULL,   
    NULL);

//使用GetQueuedCompletionStatus()監控完成端口
void *lpContext = NULL;  
OVERLAPPED        *pOverlapped = NULL;  
DWORD            dwBytesTransfered = 0;  
BOOL bReturn  =  GetQueuedCompletionStatus(  
                            pIOCPModel->m_hIOCompletionPort,  
                            &dwBytesTransfered,  
                            (LPDWORD)&lpContext,  
                            &pOverlapped,  
                            INFINITE );

//收到通知
int nBytesRecv = WSARecv(pIoContext->m_Socket, pIoContext ->p_wbuf,
1, &dwBytes, 0, pIoContext->p_ol, NULL);  

線程的使用

在以上I/O複用模型的討論中,其實都含有線程的使用。重疊I/O和I/O完成端口都是利用了線程。這也能夠看出在高併發服務器的開發中,採用線程也是十分必要的。在I/O完成端口的使用中,還會使用到線程池,這也是如今應用十分普遍的。經過線程池,能夠下降頻繁建立線程帶來的開銷。

在Windows下通常使用windows提供I/O模型就足夠應付不少場景。可是,在linux下I/O模型都是和線程不相關的。有時爲了更高的性能,也會採起線程池和I/O複用模型結合使用。好比許多Linux服務端程序就採用epoll和線程池結合的形式,固然引入線程也帶來了更多的複雜度,須要注意線程的控制和性能開銷(線程的主要開銷在線程的切換上)。而epoll原本也足夠優秀,因此僅用epoll也是能夠的,像libevent這種著名的網絡庫也是採用epoll實現的。固然,在linux下也有隻使用多進程或多線程來達到併發的。這樣會帶來必定缺點,程序須要維護大量的Scoket。在服務端開發中使用線程,也要勁量保證無鎖,鎖也是很高的開銷的。

相關文章
相關標籤/搜索