C++網路程式設計之select
select函式決定一個或者多個套接字(socket)的狀態,如果需要的話,等待執行非同步I/O。
int select(
__in int nfds,
__inout fd_set *readfds,
__inout fd_set *writefds,
__inout fd_set *exceptfds,
__int const struct timeval *timeout
);
這是在Windows系統:
引數:
nfds:忽略。
readnfds: 指向檢查可讀性的套接字集合的可選的指標。
writefds: 指向檢查可寫性的套接字集合的可選的指標。
exceptfds: 指向檢查錯誤的套接字集合的可選的指標。
timeout: select函式需要等待的最長時間,需要以TIMEVAL結構體格式提供此引數,對於阻塞操作,此引數為null。
這是在linux系統:
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時間內阻塞,超時時間之內有事件到來就返回了,否則在超時後不管怎樣一定返回,返回值同上述。
返回值:返回狀態發生變化的描述符總數。
負值:select錯誤
正值:某些檔案可讀寫或出錯
0:等待超時,沒有可讀寫或錯誤的檔案
返回值:
select函式返回那些準備好並且包含在fd_set結構體的套接字的總數,如果超時,則返回0;如果錯誤發生,返回SOCKET_ERROR。如果返回值為SOCKET_ERROR,可以通過WSAGetLastError函式檢索指定的錯誤碼。
錯誤碼 |
解釋 |
WSANOTINITIALISTED |
在使用此函式之前,WSAStartup函式必須成功的執行 |
WSAEFALUT |
套接字執行時不能分配需要的資源或者readfds、writefds、exceptfds、timeval引數不是使用者地址空間的一部分。 |
WSAENETDOWN |
網路子系統失敗 |
WSAEINVAL |
超時值不合法的,或者其他的三個引數為空。 |
WSAEINTR |
阻塞的套接字1.1呼叫通過WSACancelBlockingCall取消 |
WSAEINPROGRESS |
阻塞的套接字1.1呼叫正在處理或者服務提供者正在處理一個掉使用者函式。 |
WSAENOTSOCK |
描述集中包括一個不是套接字的入口。 |
說明
select函式用於決定一個或者多個套接字的狀態。對於每一個套接字,呼叫者可以請求讀、寫或者錯誤狀態資訊。一個請求給定狀態的套接字集由fd_set結構體指定。在fd_set結構體中的套接字必須和單個服務提供者聯絡在一起。基於此,如果WSAPROTOCOL_INFO結構體中有相同的providerId值,套接字被認為來自同一個服務提供者。直到返回,結構體更新去反映滿足指定條件套接字子集。select函式返回滿足條件的套接字個數。fd_set集合可以通過一些巨集手動操作。這些巨集也適合伯克利套接字,但是它們的機理是根本不同的。
引數readfds指示檢查套接字的可讀性。當套接字在listen狀態,如果已經接收一個連線請求,這個套接字會被標記為可讀,例如一個accept會確保不會阻塞的完成。對於其他的套接字,可讀性意味著佇列中的資料適合讀,當呼叫recv,WSARecv,WSARecvFrom或者recvfrom後不會阻塞。
對於面向連線的套接字,可讀性也可以指示關閉套接字的從另一端接收的請求。如果虛電路正常關閉,並且所有的資料都已經接收,然後recv會立刻返回(沒有資料接收),如果虛電路重置,recv會立刻返回錯誤碼,例如WSAECONNRESET。如果套接字選項SO_OOBINLINE置位(參見setsockop),出現的OOB資料將會被檢查。
引數writefds指示檢查套接字的可寫性。如果套接字處理connect呼叫(非阻塞的),並且完全建立連線,這時套接字是可寫。如果套接字沒有處理connect呼叫,可寫性意味著擔保send,sendto或者WSASendto執行成功。但是,如果len引數超過系統的快取空間大小,它們在阻塞套接字中是可以阻塞的。不確定多長的長度是合法的,尤其在多執行緒環境下。
引數exceptfds指示套接字被檢查OOB資料出現或者異常錯誤環境。
注意:OOB資料僅僅應用當SO_OOBINLINE設定為FALSE的情況下。如果一個套接字處理連線呼叫(非阻塞模式),試圖連線的錯誤資訊在exceptfds中,這個文件並沒有定義那些錯誤需要包含其中。
readfd,writefds或者exceptfds中任何兩個引數在呼叫的時候需要為null。至少一個必須為非空,並且任何一個非空描述設定必須包括至少一個套接字控制代碼。
總之,一個套接字將會被指定在一個特殊的集合當select返回如果:
readfds:
① 如果listen函式已經呼叫並且連線掛起,accept會執行成功。
② 資料適合讀(如果SO_OOBINLINE置位,包括OOB資料)
③ 連線被關/重置/終止
writefds:
① 如果處理一個connect呼叫(非阻塞),連線成功。
② 資料可以傳送。
exceptfds:
① 如果處理一個connect呼叫(非阻塞),連線失敗。
② OOB資料適合讀(僅當SO_OOBINLINE未置位)
在標頭檔案Winsock2.h中定義四個巨集來操作和檢查描述集。FD_SETSIZE決定在描述集合中最大數量(FD_SETSIZE的預設值為64,此值可以在匯入Winsock2.h之前通過FD_SETSIZE修改)。
使用這些巨集是為了在不同的套接字環境中維護軟體便利。這些巨集操作和檢查fd_set內容為:
FD_CLR(s, *set)
從set集合中移除描述符s
FD_ISSET(s, *set)
如果s在set中,返回非0,否則返回0
FD_SET(s, *set)
增加描述符s到set中
FD_ZERO(*set)
初始化set集合為null集合
#include<WinSock2.h> #include<stdio.h> #include<Windows.h> #include<string> #include<iostream> #include<thread> #include<exception> #include<future> #include<vector> using namespace std; #pragma comment(lib,"WS2_32.lib")//顯示連線套接字型檔 #define _WINSOCK_DEPRECATED_NO_WARNINGS #define SIZE 5 FILE * ffp; struct fd_set rfds; struct sockaddr_in ipadd; struct timeval timeout = { 0, 200 }; char * readbuff[10] = { 0 }; string ss; vector<SOCKET> v; char sztext[1024] = { 0 }; char sztext1[1024] = { 0 }; SOCKET s; int n; sockaddr_in addr, addr2; int ret; void Close() { ::closesocket(s); if (!v.empty()) { v.clear(); } ::WSACleanup(); } void Initialize() { WSADATA data; WORD w = MAKEWORD(2, 0);//版本號 //strcpy(sztext, lastSend.c_str()); ::WSAStartup(w, &data); //動態連結庫初始化 s = ::socket(AF_INET, SOCK_STREAM, 0); n = sizeof(addr2); addr.sin_family = AF_INET; addr.sin_port = htons(75); addr.sin_addr.S_un.S_addr = INADDR_ANY; ::bind(s, (sockaddr*)&addr, sizeof(addr)); ::listen(s, SIZE); printf("伺服器已經啟動\n"); } void MyRecv() { try{ while (true) { for (int i = 0; i < v.size(); ++i) { FD_ZERO(&rfds); /* 清空集合 */ FD_SET(v.at(i), &rfds); /* 將fp新增到集合,後面的FD_ISSET和FD_SET沒有必然關係,這裡是新增檢測 */ switch (select(0, &rfds, NULL, NULL, &timeout)) //select使用 { case -1: v.erase(v.begin() + i); printf("客服端斷開\n"); break; //select錯誤退出程式 case 0: continue; //再次輪詢 default: if (FD_ISSET(v.at(i), &rfds)) //測試sock是否可讀即是否網路上有資料 { if (::recv(v.at(i), sztext1, sizeof(sztext1), 0) != -1) { printf("%s\r\n", sztext1); } else { v.erase(v.begin() + i); printf("客服端斷開\n"); continue; } ss = "有"; ss += sztext1; strcpy(sztext, ss.c_str()); for (int j = 0; j < v.size(); ++j) { ::send(v.at(j), sztext, sizeof(sztext), 0); } } } } } } catch (const exception& e) { cerr << "出錯了" << endl; Close(); } return; } int main() { SOCKET s1; Initialize(); auto w = async(launch::async, [&]{ while (true) { if (v.size() < SIZE) { for (int i = v.size(); i < SIZE; ++i) { s1 = ::accept(s, (sockaddr*)&addr2, &n); if (s1 != NULL) { v.push_back(s1); printf("%s已經連線上\r\n", inet_ntoa(addr2.sin_addr)); } } printf("伺服器接收額已滿!\n"); } } return; }); try{ thread t1(MyRecv); t1.join(); } catch (const exception& e) { cerr << "出錯了" << endl; } Close(); system("pause"); return 0; }
引數time-out控制select函式完成的時間(超過這個時間返回超時)。如果time-out是個空指標,select會一直保持阻塞指導至少一個描述符符合指定的準則。否則,time-out指向一個TIMEVAL結構體,這個結構體指定select在返回之前應該等待最大時間。當select返回,TIMEVAL結構體中的內容是不會改變的。如果TIMEVAL初始化為{0,0},select會立刻返回;這用於得到選擇的套接字的狀態。如果select立刻返回,然後select呼叫認為是非阻塞的,此時非阻塞呼叫的標準假設適用。例如,阻塞鉤子不會呼叫,窗體套接字不會退出。