详细解析SELECT模型
生活随笔
收集整理的這篇文章主要介紹了
详细解析SELECT模型
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
先看一下下面的這句代碼:
int iResult = recv(s, buffer,1024);這是用來接收數據的,在默認的阻塞模式下的套接字里,recv會阻塞在那里,直到套接字連接上有數據可讀,把數據讀到buffer里后recv函數才會返回,不然就會一直阻塞在那里。在單線程的程序里出現這種情況會導致主線程(單線程程序里只有一個默認的主線程)被阻塞,這樣整個程序被鎖死在這里,如果永遠沒數據發送過來,那么程序就會被永遠鎖死。這個問題可以用多線程解決,但是在有多個套接字連接的情況下,這不是一個好的選擇,擴展性很差。Select模型就是為了解決這個問題而出現的。
再看代碼:
int iResult = ioctlsocket(s, FIOBIO, (unsigned long *)&ul);
iResult = recv(s, buffer,1024);
這一次recv的調用不管套接字連接上有沒有數據可以接收都會馬上返回。原因就在于我們用ioctlsocket把套接字設置為非阻塞模式了。不過你跟蹤一下就會發現,在沒有數據的情況下,recv確實是馬上返回了,但是也返回了一個錯誤:WSAEWOULDBLOCK,意思就是請求的操作沒有成功完成。看到這里很多人可能會說,那么就重復調用recv并檢查返回值,直到成功為止,但是這樣做效率很成問題,開銷太大。
感謝天才的微軟工程師吧,他們給我們提供了好的解決辦法。
Select在Socket編程中還是比較重要的,可是對于初學Socket的人來說都不太愛用Select寫程序,他們只是習慣寫諸如connect、accept、recv或recvfrom這樣的阻塞程序(所謂阻塞方式block,顧名思義,就是進程或是線程執行到這些函數時必須等待某個事件的發生,如果事件沒有發生,進程或線程就被阻塞,函數不能立即返回)。可是使用Select就可以完成非阻塞(所謂非阻塞方式non-block,就是進程或線程執行此函數時不必非要等待事件的發生,一旦執行肯定返回,以返回值的不同來反映函數的執行情況,如果事件發生則與阻塞方式相同,若事件沒有發生則返回一個代碼來告知事件未發生,而進程或線程繼續執行,所以效率較高)方式工作的程序,它能夠監視我們需要監視的文件描述符的變化情況——讀寫或是異常。
Select的函數格式:
int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval*timeout);?
先說明兩個結構體:?
第一,struct fd_set可以理解為一個集合,這個集合中存放的是文件描述符(filedescriptor),即文件句柄,這可以是我們所說的普通意義的文件,當然Unix下任何設備、管道、FIFO等都是文件形式,全部包括在內,所以毫無疑問一個socket就是一個文件,socket句柄就是一個文件描述符。fd_set集合可以通過一些宏由人為來操作,比如清空集合FD_ZERO(fd_set *),將一個給定的文件描述符加入集合之中FD_SET(int ,fd_set*),將一個給定的文件描述符從集合中刪除FD_CLR(int,fd_set*),檢查集合中指定的文件描述符是否可以讀寫FD_ISSET(int ,fd_set* )。
第二,struct timeval是一個大家常用的結構,用來代表時間值,有兩個成員,一個是秒數,另一個是毫秒數。?
具體解釋select的參數:?
int maxfdp是一個整數值,是指集合中所有文件描述符的范圍,即所有文件描述符的最大值加1,不能錯!在Windows中這個參數的值無所謂,可以設置不正確。?
fd_set * readfds是指向fd_set結構的指針,這個集合中應該包括文件描述符,我們是要監視這些文件描述符的讀變化的,即我們關心是否可以從這些文件中讀取數據了,如果這個集合中有一個文件可讀,select就會返回一個大于0的值,表示有文件可讀,如果沒有可讀的文件,則根據timeout參數再判斷是否超時,若超出timeout的時間,select返回0,若發生錯誤返回負值。可以傳入NULL值,表示不關心任何文件的讀變化。?
fd_set * writefds是指向fd_set結構的指針,這個集合中應該包括文件描述符,我們是要監視這些文件描述符的寫變化的,即我們關心是否可以向這些文件中寫入數據了,如果這個集合中有一個文件可寫,select就會返回一個大于0的值,表示有文件可寫,如果沒有可寫的文件,則根據timeout參數再判斷是否超時,若超出timeout的時間,select返回0,若發生錯誤返回負值。可以傳入NULL值,表示不關心任何文件的寫變化。?
fd_set * errorfds同上面兩個參數的意圖,用來監視文件錯誤異常。?
struct timeval * timeout是select的超時時間,這個參數至關重要,它可以使select處于三種狀態,第一,若將NULL以形參傳入,即不傳入時間結構,就是將select置于阻塞狀態,一定等到監視文件描述符集合中某個文件描述符發生變化為止;第二,若將時間值設為0秒0毫秒,就變成一個純粹的非阻塞函數,不管文件描述符是否有變化,都立刻返回繼續執行,文件無變化返回0,有變化返回一個正值;第三,timeout的值大于0,這就是等待的超時時間,即select在timeout時間內阻塞,超時時間之內有事件到來就返回了,否則在超時后不管怎樣一定返回,返回值同上述。?
對該函數進一步深究:
本函數用于確定一個或多個套接口的狀態。對每一個套接口,調用者可查詢它的可讀性、可寫性及錯誤狀態信息。用fd_set結構來表示一組等待檢查的套接口。在調用返回時,這個結構存有滿足一定條件的套接口組的子集,并且select()返回滿足條件的套接口的數目。有一組宏可用于對fd_set的操作,這些宏與Berkeley Unix軟件中的兼容,但內部的表達是完全不同的。
readfds參數標識等待可讀性檢查的套接口。如果該套接口正處于監聽listen()狀態,則若有連接請求到達,該套接口便被標識為可讀,這樣一個accept()調用保證可以無阻塞完成。對其他套接口而言,可讀性意味著有排隊數據供讀取。或者對于SOCK_STREAM類型套接口來說,相對于該套接口的虛套接口已關閉,于是recv()或recvfrom()操作均能無阻塞完成。如果虛電路被“優雅地”中止,則recv()不讀取數據立即返回;如果虛電路被強制復位,則recv()將以WSAECONNRESET錯誤立即返回。如果SO_OOBINLINE選項被設置,則將檢查帶外數據是否存在(參見setsockopt())。
writefds參數標識等待可寫性檢查的套接口。如果一個套接口正在connect()連接(非阻塞),可寫性意味著連接順利建立。如果套接口并未處于connect()調用中,可寫性意味著send()和sendto()調用將無阻塞完成。〔但并未指出這個保證在多長時間內有效,特別是在多線程環境中〕。
exceptfds參數標識等待帶外數據存在性或意味錯誤條件檢查的套接口。請注意如果設置了SO_OOBINLINE選項為假FALSE,則只能用這種方法來檢查帶外數據的存在與否。對于SO_STREAM類型套接口,遠端造成的連接中止和KEEPALIVE錯誤都將被作為意味出錯。如果套接口正在進行連接connect()(非阻塞方式),則連接試圖的失敗將會表現在exceptfds參數中。
如果對readfds、writefds或exceptfds中任一個組類不感興趣,可將它置為空NULL。
返回值:返回狀態發生變化的描述符總數。?
負值:select錯誤
正值:某些文件可讀寫或出錯
0:等待超時,沒有可讀寫或錯誤的文件
錯誤代碼:
? ? WSANOTINITIALISED:在使用此API之前應首先成功地調用WSAStartup()。
? ? WSAENETDOWN: ? ? ?WINDOWS套接口實現檢測到網絡子系統失效。
? ? WSAEINVAL: ? ? ? ?超時時間值非法。
? ? WSAEINTR: ? ? ? ? 通過一個WSACancelBlockingCall()來取消一個(阻塞的)調用。
? ? WSAEINPROGRESS: ? 一個阻塞的WINDOWS套接口調用正在運行中。
? ? WSAENOTSOCK: ? ? ?描述字集合中包含有非套接口的元素。
看看一些對于fd_set類型通過下面四個宏來操作:
FD_ZERO(fd_set *fdset) 將指定的文件描述符集清空,在對文件描述符集合進行設置前,必須對其進行初始化,如果不清空,由于在系統分配內存空間后,通常并不作清空處理,所以結果是不可知的。
FD_SET(fd_set *fdset) 用于在文件描述符集合中增加一個新的文件描述符。
FD_CLR(fd_set *fdset) 用于在文件描述符集合中刪除一個文件描述符。
FD_ISSET(int fd,fd_set *fdset) 用于測試指定的文件描述符是否在該集合中。
判斷描述符fd是否在給定的描述符集fdset中,通常配合select函數使用,由于select函數成功返回時會將未準備好的描述符位清零。通常我們使用FD_ISSET是為了檢查在select函數返回后,某個描述符是否準備好,以便進行接下來的處理操作。
上面在說明FD_SETSIZE時,winsock2.h中定義FD_SETSIZE的大小為64,這樣就對readfds、writefds、exceptfds的socket句柄數進行了限制。在實際應用中可以使用端口分組或者重新定義FD_SETSIZE的方式進行解決。在stdAfx.h最末行添加如下定義:
#define FD_SETSIZE 1024 ? ? ? ? ? ? ? ? ?//socket句柄數
#define MAXIMUM_WAIT_OBJECTS ? ?1024 ? ? //要等待的對象數
要注意的是我們還重定義了要另一個宏MAXIMUM_WAIT_OBJECTS,它表示要等待的對象數。重定義后,程序在現場運行正常。
基于上面的討論,可以輕松得出select模型的特點:
? ?(1)可監控的文件描述符個數取決與sizeof(fd_set)的值。
? ?(2)可以有效突破select可監控的文件描述符上限。
? ?(3)將fd加入select監控集的同時,還要再使用一個數據結構array保存放到select監控集中的fd,一是用于再select 返回后,array作為源數據和fd_set進行FD_ISSET判斷。二是select返回后會把以前加入的但并無事件發生的fd清空,則每次開始 select前都要重新從array取得fd逐一加入(FD_ZERO最先),掃描array的同時取得fd最大值maxfd,用于select的第一個 參數。
? ?(4)可見select模型必須在select前循環array(加fd,取maxfd),select返回后循環array(FD_ISSET判斷是否有時間發生)。
如果對 Connect 進行非阻塞調用,則可讀意味著已經成功連接,連接不成功則不可讀。所以通過這樣的設定,我們就能夠實現對connect連接時間的修改。但是,應該注意,這樣的設置并不能保證在限定時間內連接不上就說明網絡不通。比如我們設的時間是5秒,但是由于種種原因,可能第6秒就能連接上,但是函數在5秒后就返回了。先辦socket設置非阻塞模式,但是并沒有設置connect的連接時間,我們可以通過調用select語句來實現這個功能。以下代碼設定了是連接時間為5秒,如果還未能連上,則直接返回。
struct timeval timeout ;?
fd_set r;?
int ret;?
connect( sock, (LPSOCKADDR)sockAddr, sockAddr.Size());?
FD_ZERO(&r);?
FD_SET(sock,&r);?
timeout.tv_sec = 5;?
timeout.tv_usec =0;?
ret = select(0,0,&r,0,&timeout);?
if ( ret <= 0 )?
{?
? ? closesocket(sock);?
? ? return false;?
}
?以下是對select函數的解釋:?
int select (?
int nfds,?
fd_set FAR * readfds,?
fd_set FAR * writefds,?
fd_set FAR * exceptfds,?
const struct timeval FAR * timeout?
);?
第一個參數nfds:沒有用,僅僅為與伯克利Socket兼容而提供。?
第二個參數readfds:指定一個Socket數組(應該是一個,但這里主要是表現為一個Socket數組),select檢查該數組中的所有Socket。如果成功返回,則readfds中存放的是符合‘可讀性’條件的數組成員(如緩沖區中有可讀的數據)。
第三個參數writefds:指定一個Socket數組,select檢查該數組中的所有Socket。如果成功返回,則writefds中存放的是符合‘可寫性’條件的數組成員(如連接成功)。
第四個參數exceptfds:指定一個Socket數組,select檢查該數組中的所有Socket。如果成功返回,則cxceptfds中存放的是符合‘有異常’條件的數組成員(如連接接失敗)。
第五個參數timeout:指定select執行的最長時間,如果在timeout限定的時間內,readfds、writefds、exceptfds中指定的Socket沒有一個符合要求,就返回0。
下面給出一個簡單的select模型的服務端套接字。
再看看另一個例子:
#pragma once #include <winsock.h> #include <stdio.h> #define PORT 5150 #define MSGSIZE 1024 #pragma comment(lib, "ws2_32.lib") int g_iTotalConn = 0; SOCKET g_CliSocketArr[FD_SETSIZE]; DWORD WINAPI WorkerThread(LPVOID lpParameter); int main() {WSADATA wsaData;SOCKET sListen, sClient;SOCKADDR_IN local, client;int iaddrSize = sizeof(SOCKADDR_IN);DWORD dwThreadId;// Initialize Windows socket libraryWSAStartup(0x0202, &wsaData);// Create listening socketsListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);// Bindlocal.sin_addr.S_un.S_addr = htonl(INADDR_ANY);local.sin_family = AF_INET;local.sin_port = htons(PORT);bind(sListen, (struct sockaddr *)&local, sizeof(SOCKADDR_IN));// Listenlisten(sListen, 3);// Create worker threadCreateThread(NULL, 0, WorkerThread, NULL, 0, &dwThreadId); while (TRUE){// Accept a connectionsClient = accept(sListen, (struct sockaddr *)&client, &iaddrSize);printf("Accepted client:%s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));// Add socket to g_CliSocketArrg_CliSocketArr[g_iTotalConn++] = sClient;}return 0; } DWORD WINAPI WorkerThread(LPVOID lpParam) {int i;fd_set fdread;int ret;struct timeval tv = {1, 0};char szMessage[MSGSIZE];while (TRUE){FD_ZERO(&fdread);//1.將當前所有的客戶端套接字加入到讀集fdread中;for (i = 0; i < g_iTotalConn; i++){FD_SET(g_CliSocketArr[i], &fdread);}// We only care read event//2.調用select函數;執行同步I/0ret = select(0,&fdread, NULL, NULL, &tv);//沒有可以讀的套接字,繼續進行selectif (ret == 0){continue;}//有可以讀的套接字for (i = 0; i < g_iTotalConn; i++){if (FD_ISSET(g_CliSocketArr[i], &fdread)){// A read event happened on g_CliSocketArr[i]// 將數據內容從socket中端口中讀到相應的內存szMessage中ret = recv(g_CliSocketArr[i], szMessage, MSGSIZE, 0);//(同步I/O)if (ret == 0 || (ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET)){// Client socket closedprintf("Client socket %d closed.\n", g_CliSocketArr);closesocket(g_CliSocketArr[i]);if (i < g_iTotalConn - 1){ g_CliSocketArr[i--] = g_CliSocketArr[--g_iTotalConn];}}else{// We received a message from clientszMessage[ret] = '\0';send(g_CliSocketArr[i], szMessage, strlen(szMessage), 0);}}}}return 0; }
總結
以上是生活随笔為你收集整理的详细解析SELECT模型的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: setsockopt函数全面解析
- 下一篇: lcx源代码以及免杀的研究