详细解析SELECT模型
生活随笔
收集整理的這篇文章主要介紹了
详细解析SELECT模型
小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.
先看一下下面的這句代碼:
int iResult = recv(s, buffer,1024);這是用來(lái)接收數(shù)據(jù)的,在默認(rèn)的阻塞模式下的套接字里,recv會(huì)阻塞在那里,直到套接字連接上有數(shù)據(jù)可讀,把數(shù)據(jù)讀到buffer里后recv函數(shù)才會(huì)返回,不然就會(huì)一直阻塞在那里。在單線程的程序里出現(xiàn)這種情況會(huì)導(dǎo)致主線程(單線程程序里只有一個(gè)默認(rèn)的主線程)被阻塞,這樣整個(gè)程序被鎖死在這里,如果永遠(yuǎn)沒(méi)數(shù)據(jù)發(fā)送過(guò)來(lái),那么程序就會(huì)被永遠(yuǎn)鎖死。這個(gè)問(wèn)題可以用多線程解決,但是在有多個(gè)套接字連接的情況下,這不是一個(gè)好的選擇,擴(kuò)展性很差。Select模型就是為了解決這個(gè)問(wèn)題而出現(xiàn)的。
再看代碼:
int iResult = ioctlsocket(s, FIOBIO, (unsigned long *)&ul);
iResult = recv(s, buffer,1024);
這一次recv的調(diào)用不管套接字連接上有沒(méi)有數(shù)據(jù)可以接收都會(huì)馬上返回。原因就在于我們用ioctlsocket把套接字設(shè)置為非阻塞模式了。不過(guò)你跟蹤一下就會(huì)發(fā)現(xiàn),在沒(méi)有數(shù)據(jù)的情況下,recv確實(shí)是馬上返回了,但是也返回了一個(gè)錯(cuò)誤:WSAEWOULDBLOCK,意思就是請(qǐng)求的操作沒(méi)有成功完成。看到這里很多人可能會(huì)說(shuō),那么就重復(fù)調(diào)用recv并檢查返回值,直到成功為止,但是這樣做效率很成問(wèn)題,開(kāi)銷(xiāo)太大。
感謝天才的微軟工程師吧,他們給我們提供了好的解決辦法。
Select在Socket編程中還是比較重要的,可是對(duì)于初學(xué)Socket的人來(lái)說(shuō)都不太愛(ài)用Select寫(xiě)程序,他們只是習(xí)慣寫(xiě)諸如connect、accept、recv或recvfrom這樣的阻塞程序(所謂阻塞方式block,顧名思義,就是進(jìn)程或是線程執(zhí)行到這些函數(shù)時(shí)必須等待某個(gè)事件的發(fā)生,如果事件沒(méi)有發(fā)生,進(jìn)程或線程就被阻塞,函數(shù)不能立即返回)。可是使用Select就可以完成非阻塞(所謂非阻塞方式non-block,就是進(jìn)程或線程執(zhí)行此函數(shù)時(shí)不必非要等待事件的發(fā)生,一旦執(zhí)行肯定返回,以返回值的不同來(lái)反映函數(shù)的執(zhí)行情況,如果事件發(fā)生則與阻塞方式相同,若事件沒(méi)有發(fā)生則返回一個(gè)代碼來(lái)告知事件未發(fā)生,而進(jìn)程或線程繼續(xù)執(zhí)行,所以效率較高)方式工作的程序,它能夠監(jiān)視我們需要監(jiān)視的文件描述符的變化情況——讀寫(xiě)或是異常。
Select的函數(shù)格式:
int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval*timeout);?
先說(shuō)明兩個(gè)結(jié)構(gòu)體:?
第一,struct fd_set可以理解為一個(gè)集合,這個(gè)集合中存放的是文件描述符(filedescriptor),即文件句柄,這可以是我們所說(shuō)的普通意義的文件,當(dāng)然Unix下任何設(shè)備、管道、FIFO等都是文件形式,全部包括在內(nèi),所以毫無(wú)疑問(wèn)一個(gè)socket就是一個(gè)文件,socket句柄就是一個(gè)文件描述符。fd_set集合可以通過(guò)一些宏由人為來(lái)操作,比如清空集合FD_ZERO(fd_set *),將一個(gè)給定的文件描述符加入集合之中FD_SET(int ,fd_set*),將一個(gè)給定的文件描述符從集合中刪除FD_CLR(int,fd_set*),檢查集合中指定的文件描述符是否可以讀寫(xiě)FD_ISSET(int ,fd_set* )。
第二,struct timeval是一個(gè)大家常用的結(jié)構(gòu),用來(lái)代表時(shí)間值,有兩個(gè)成員,一個(gè)是秒數(shù),另一個(gè)是毫秒數(shù)。?
具體解釋select的參數(shù):?
int maxfdp是一個(gè)整數(shù)值,是指集合中所有文件描述符的范圍,即所有文件描述符的最大值加1,不能錯(cuò)!在Windows中這個(gè)參數(shù)的值無(wú)所謂,可以設(shè)置不正確。?
fd_set * readfds是指向fd_set結(jié)構(gòu)的指針,這個(gè)集合中應(yīng)該包括文件描述符,我們是要監(jiān)視這些文件描述符的讀變化的,即我們關(guān)心是否可以從這些文件中讀取數(shù)據(jù)了,如果這個(gè)集合中有一個(gè)文件可讀,select就會(huì)返回一個(gè)大于0的值,表示有文件可讀,如果沒(méi)有可讀的文件,則根據(jù)timeout參數(shù)再判斷是否超時(shí),若超出timeout的時(shí)間,select返回0,若發(fā)生錯(cuò)誤返回負(fù)值。可以傳入NULL值,表示不關(guān)心任何文件的讀變化。?
fd_set * writefds是指向fd_set結(jié)構(gòu)的指針,這個(gè)集合中應(yīng)該包括文件描述符,我們是要監(jiān)視這些文件描述符的寫(xiě)變化的,即我們關(guān)心是否可以向這些文件中寫(xiě)入數(shù)據(jù)了,如果這個(gè)集合中有一個(gè)文件可寫(xiě),select就會(huì)返回一個(gè)大于0的值,表示有文件可寫(xiě),如果沒(méi)有可寫(xiě)的文件,則根據(jù)timeout參數(shù)再判斷是否超時(shí),若超出timeout的時(shí)間,select返回0,若發(fā)生錯(cuò)誤返回負(fù)值。可以傳入NULL值,表示不關(guān)心任何文件的寫(xiě)變化。?
fd_set * errorfds同上面兩個(gè)參數(shù)的意圖,用來(lái)監(jiān)視文件錯(cuò)誤異常。?
struct timeval * timeout是select的超時(shí)時(shí)間,這個(gè)參數(shù)至關(guān)重要,它可以使select處于三種狀態(tài),第一,若將NULL以形參傳入,即不傳入時(shí)間結(jié)構(gòu),就是將select置于阻塞狀態(tài),一定等到監(jiān)視文件描述符集合中某個(gè)文件描述符發(fā)生變化為止;第二,若將時(shí)間值設(shè)為0秒0毫秒,就變成一個(gè)純粹的非阻塞函數(shù),不管文件描述符是否有變化,都立刻返回繼續(xù)執(zhí)行,文件無(wú)變化返回0,有變化返回一個(gè)正值;第三,timeout的值大于0,這就是等待的超時(shí)時(shí)間,即select在timeout時(shí)間內(nèi)阻塞,超時(shí)時(shí)間之內(nèi)有事件到來(lái)就返回了,否則在超時(shí)后不管怎樣一定返回,返回值同上述。?
對(duì)該函數(shù)進(jìn)一步深究:
本函數(shù)用于確定一個(gè)或多個(gè)套接口的狀態(tài)。對(duì)每一個(gè)套接口,調(diào)用者可查詢(xún)它的可讀性、可寫(xiě)性及錯(cuò)誤狀態(tài)信息。用fd_set結(jié)構(gòu)來(lái)表示一組等待檢查的套接口。在調(diào)用返回時(shí),這個(gè)結(jié)構(gòu)存有滿(mǎn)足一定條件的套接口組的子集,并且select()返回滿(mǎn)足條件的套接口的數(shù)目。有一組宏可用于對(duì)fd_set的操作,這些宏與Berkeley Unix軟件中的兼容,但內(nèi)部的表達(dá)是完全不同的。
readfds參數(shù)標(biāo)識(shí)等待可讀性檢查的套接口。如果該套接口正處于監(jiān)聽(tīng)listen()狀態(tài),則若有連接請(qǐng)求到達(dá),該套接口便被標(biāo)識(shí)為可讀,這樣一個(gè)accept()調(diào)用保證可以無(wú)阻塞完成。對(duì)其他套接口而言,可讀性意味著有排隊(duì)數(shù)據(jù)供讀取。或者對(duì)于SOCK_STREAM類(lèi)型套接口來(lái)說(shuō),相對(duì)于該套接口的虛套接口已關(guān)閉,于是recv()或recvfrom()操作均能無(wú)阻塞完成。如果虛電路被“優(yōu)雅地”中止,則recv()不讀取數(shù)據(jù)立即返回;如果虛電路被強(qiáng)制復(fù)位,則recv()將以WSAECONNRESET錯(cuò)誤立即返回。如果SO_OOBINLINE選項(xiàng)被設(shè)置,則將檢查帶外數(shù)據(jù)是否存在(參見(jiàn)setsockopt())。
writefds參數(shù)標(biāo)識(shí)等待可寫(xiě)性檢查的套接口。如果一個(gè)套接口正在connect()連接(非阻塞),可寫(xiě)性意味著連接順利建立。如果套接口并未處于connect()調(diào)用中,可寫(xiě)性意味著send()和sendto()調(diào)用將無(wú)阻塞完成。〔但并未指出這個(gè)保證在多長(zhǎng)時(shí)間內(nèi)有效,特別是在多線程環(huán)境中〕。
exceptfds參數(shù)標(biāo)識(shí)等待帶外數(shù)據(jù)存在性或意味錯(cuò)誤條件檢查的套接口。請(qǐng)注意如果設(shè)置了SO_OOBINLINE選項(xiàng)為假FALSE,則只能用這種方法來(lái)檢查帶外數(shù)據(jù)的存在與否。對(duì)于SO_STREAM類(lèi)型套接口,遠(yuǎn)端造成的連接中止和KEEPALIVE錯(cuò)誤都將被作為意味出錯(cuò)。如果套接口正在進(jìn)行連接connect()(非阻塞方式),則連接試圖的失敗將會(huì)表現(xiàn)在exceptfds參數(shù)中。
如果對(duì)readfds、writefds或exceptfds中任一個(gè)組類(lèi)不感興趣,可將它置為空NULL。
返回值:返回狀態(tài)發(fā)生變化的描述符總數(shù)。?
負(fù)值:select錯(cuò)誤
正值:某些文件可讀寫(xiě)或出錯(cuò)
0:等待超時(shí),沒(méi)有可讀寫(xiě)或錯(cuò)誤的文件
錯(cuò)誤代碼:
? ? WSANOTINITIALISED:在使用此API之前應(yīng)首先成功地調(diào)用WSAStartup()。
? ? WSAENETDOWN: ? ? ?WINDOWS套接口實(shí)現(xiàn)檢測(cè)到網(wǎng)絡(luò)子系統(tǒng)失效。
? ? WSAEINVAL: ? ? ? ?超時(shí)時(shí)間值非法。
? ? WSAEINTR: ? ? ? ? 通過(guò)一個(gè)WSACancelBlockingCall()來(lái)取消一個(gè)(阻塞的)調(diào)用。
? ? WSAEINPROGRESS: ? 一個(gè)阻塞的WINDOWS套接口調(diào)用正在運(yùn)行中。
? ? WSAENOTSOCK: ? ? ?描述字集合中包含有非套接口的元素。
看看一些對(duì)于fd_set類(lèi)型通過(guò)下面四個(gè)宏來(lái)操作:
FD_ZERO(fd_set *fdset) 將指定的文件描述符集清空,在對(duì)文件描述符集合進(jìn)行設(shè)置前,必須對(duì)其進(jìn)行初始化,如果不清空,由于在系統(tǒng)分配內(nèi)存空間后,通常并不作清空處理,所以結(jié)果是不可知的。
FD_SET(fd_set *fdset) 用于在文件描述符集合中增加一個(gè)新的文件描述符。
FD_CLR(fd_set *fdset) 用于在文件描述符集合中刪除一個(gè)文件描述符。
FD_ISSET(int fd,fd_set *fdset) 用于測(cè)試指定的文件描述符是否在該集合中。
判斷描述符fd是否在給定的描述符集fdset中,通常配合select函數(shù)使用,由于select函數(shù)成功返回時(shí)會(huì)將未準(zhǔn)備好的描述符位清零。通常我們使用FD_ISSET是為了檢查在select函數(shù)返回后,某個(gè)描述符是否準(zhǔn)備好,以便進(jìn)行接下來(lái)的處理操作。
上面在說(shuō)明FD_SETSIZE時(shí),winsock2.h中定義FD_SETSIZE的大小為64,這樣就對(duì)readfds、writefds、exceptfds的socket句柄數(shù)進(jìn)行了限制。在實(shí)際應(yīng)用中可以使用端口分組或者重新定義FD_SETSIZE的方式進(jìn)行解決。在stdAfx.h最末行添加如下定義:
#define FD_SETSIZE 1024 ? ? ? ? ? ? ? ? ?//socket句柄數(shù)
#define MAXIMUM_WAIT_OBJECTS ? ?1024 ? ? //要等待的對(duì)象數(shù)
要注意的是我們還重定義了要另一個(gè)宏MAXIMUM_WAIT_OBJECTS,它表示要等待的對(duì)象數(shù)。重定義后,程序在現(xiàn)場(chǎng)運(yùn)行正常。
基于上面的討論,可以輕松得出select模型的特點(diǎn):
? ?(1)可監(jiān)控的文件描述符個(gè)數(shù)取決與sizeof(fd_set)的值。
? ?(2)可以有效突破select可監(jiān)控的文件描述符上限。
? ?(3)將fd加入select監(jiān)控集的同時(shí),還要再使用一個(gè)數(shù)據(jù)結(jié)構(gòu)array保存放到select監(jiān)控集中的fd,一是用于再select 返回后,array作為源數(shù)據(jù)和fd_set進(jìn)行FD_ISSET判斷。二是select返回后會(huì)把以前加入的但并無(wú)事件發(fā)生的fd清空,則每次開(kāi)始 select前都要重新從array取得fd逐一加入(FD_ZERO最先),掃描array的同時(shí)取得fd最大值maxfd,用于select的第一個(gè) 參數(shù)。
? ?(4)可見(jiàn)select模型必須在select前循環(huán)array(加fd,取maxfd),select返回后循環(huán)array(FD_ISSET判斷是否有時(shí)間發(fā)生)。
如果對(duì) Connect 進(jìn)行非阻塞調(diào)用,則可讀意味著已經(jīng)成功連接,連接不成功則不可讀。所以通過(guò)這樣的設(shè)定,我們就能夠?qū)崿F(xiàn)對(duì)connect連接時(shí)間的修改。但是,應(yīng)該注意,這樣的設(shè)置并不能保證在限定時(shí)間內(nèi)連接不上就說(shuō)明網(wǎng)絡(luò)不通。比如我們?cè)O(shè)的時(shí)間是5秒,但是由于種種原因,可能第6秒就能連接上,但是函數(shù)在5秒后就返回了。先辦socket設(shè)置非阻塞模式,但是并沒(méi)有設(shè)置connect的連接時(shí)間,我們可以通過(guò)調(diào)用select語(yǔ)句來(lái)實(shí)現(xiàn)這個(gè)功能。以下代碼設(shè)定了是連接時(shí)間為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;?
}
?以下是對(duì)select函數(shù)的解釋:?
int select (?
int nfds,?
fd_set FAR * readfds,?
fd_set FAR * writefds,?
fd_set FAR * exceptfds,?
const struct timeval FAR * timeout?
);?
第一個(gè)參數(shù)nfds:沒(méi)有用,僅僅為與伯克利Socket兼容而提供。?
第二個(gè)參數(shù)readfds:指定一個(gè)Socket數(shù)組(應(yīng)該是一個(gè),但這里主要是表現(xiàn)為一個(gè)Socket數(shù)組),select檢查該數(shù)組中的所有Socket。如果成功返回,則readfds中存放的是符合‘可讀性’條件的數(shù)組成員(如緩沖區(qū)中有可讀的數(shù)據(jù))。
第三個(gè)參數(shù)writefds:指定一個(gè)Socket數(shù)組,select檢查該數(shù)組中的所有Socket。如果成功返回,則writefds中存放的是符合‘可寫(xiě)性’條件的數(shù)組成員(如連接成功)。
第四個(gè)參數(shù)exceptfds:指定一個(gè)Socket數(shù)組,select檢查該數(shù)組中的所有Socket。如果成功返回,則cxceptfds中存放的是符合‘有異常’條件的數(shù)組成員(如連接接失敗)。
第五個(gè)參數(shù)timeout:指定select執(zhí)行的最長(zhǎng)時(shí)間,如果在timeout限定的時(shí)間內(nèi),readfds、writefds、exceptfds中指定的Socket沒(méi)有一個(gè)符合要求,就返回0。
下面給出一個(gè)簡(jiǎn)單的select模型的服務(wù)端套接字。
再看看另一個(gè)例子:
#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.將當(dāng)前所有的客戶(hù)端套接字加入到讀集fdread中;for (i = 0; i < g_iTotalConn; i++){FD_SET(g_CliSocketArr[i], &fdread);}// We only care read event//2.調(diào)用select函數(shù);執(zhí)行同步I/0ret = select(0,&fdread, NULL, NULL, &tv);//沒(méi)有可以讀的套接字,繼續(xù)進(jìn)行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]// 將數(shù)據(jù)內(nèi)容從socket中端口中讀到相應(yīng)的內(nèi)存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; }
總結(jié)
以上是生活随笔為你收集整理的详细解析SELECT模型的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: setsockopt函数全面解析
- 下一篇: lcx源代码以及免杀的研究