1. 程式人生 > >C++網路程式設計之select

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呼叫認為是非阻塞的,此時非阻塞呼叫的標準假設適用。例如,阻塞鉤子不會呼叫,窗體套接字不會退出。