天天看點

windows和linux套接字中的select機制淺析

先來談談為什麼會出現select函數,也就是select是解決什麼問題的?

平常使用的recv函數時阻塞的,也就是如果沒有資料可讀,recv就會一直阻塞在那裡,這是如果有另外一個連接配接過來,就得一直等待,這樣實時性就不是太好。

這個問題的幾個解決方法:1. 使用ioctlsocket函數,将recv函數設定成非阻塞的,這樣不管套接字上有沒有資料都會立刻傳回,可以重複調用recv函數,這種方式叫做輪詢(polling),但是這樣效率很是問題,因為,大多數時間實際上是無資料可讀的,花費時間不斷反複執行read系統調用,這樣就比較浪費CPU的時間。并且循環之間的間隔不好确定。2. 使用fork,使用多程序來解決,這裡終止會比較複雜(待研究)。 3.使用多線程來解決,這樣避免了終止的複雜性,但卻要求處理線程之間的同步,在減少複雜性方面這可能會得不償失。4. 使用異步IO(待研究)。5. 就是本文所使用的I/O多路轉接(多路複用)--其實就是在套接字阻塞和非阻塞之間做了一個均衡,我們稱之為半阻塞。

經過對select的初步了解,在windows和linux下的實作小有差別,是以分開來寫。這裡先寫windows下的select機制。

select的大概思想:将多個套接字放在一個集合裡,然後統一檢查這些套接字的狀态(可讀、可寫、異常等),調用select後,會更新這些套接字的狀态,然後做判斷,如果套接字可讀,就執行read操作。這樣就巧妙地避免了阻塞,達到同時處理多個連接配接的目的。當然如果沒有事件發生,select會一直阻塞,如果不想一直讓它等待,想去處理其它事情,可以設定一個最大的等待時間。

/***********************************************************************************************************/

int select(  

  _In_     int nfds,  

  _Inout_  fd_set *readfds,  

  _Inout_  fd_set *writefds,  

  _Inout_  fd_set *exceptfds,  

  _In_     const struct timeval *timeout  

);  

函數的參數,第一個是輸入參數nfds,表示滿足條件的套接字的個數,windows下可以設定為0,因為fd_set結構體中已經包含了這個參數,這個參數已經是多餘的了,之是以還存在,隻是是為了與FreeBSD相容。

第二三四參數都是輸入輸出參數(值-結果參數,輸入和輸出會不一樣),表示套接字的可讀、可寫和異常三種狀态的集合。調用select之後,如果指定套接字不可讀或者不可寫,就會從相應隊列中清除,這樣就可以判斷哪些套接字可讀或者可寫。 

說明一下,這裡的可讀性是指:如果有客戶的連接配接請求到達,套接口就是可讀的,調用accept能夠立即完成,而不發生阻塞;如果套接口接收隊列緩沖區中的位元組數大于0,調用recv或者recvfrom就不會阻塞。可寫性是指,可以向套接字發送資料(套接字建立成功後,就是可寫的)。當然不是套接字可寫就會去發送資料,就像不是看到電話就去打電話一樣,而是由打電話的需求了,才去看電話是否可打;可讀就不一樣了,電話響了,自然要去接電話(除非,你有事忙或者不想接,一般都是要接的)。可讀已經包含了緩沖區中有資料可以讀取,可寫隻是說明了緩沖區有空間讓你寫,你需不需要寫就要看你有沒有資料要寫了.關于異常,就是指一些意外情況,自己用的比較少,以後用到了,再過來補上。

第五個參數是等待的最大時間,是一個結構體:struct timeval,它的定義是:

/* 

* Structure used in select() call, taken from the BSD file sys/time.h. 

*/  

struct timeval {  

        long    tv_sec;         /* seconds */  

        long    tv_usec;        /* and microseconds */  

};  

具體到秒和微妙,按照等待的時間長短可以分為不等待、等待一定時間、一直等待。對應的設定分别為,(0,0)是不等待,這是select是非阻塞的,(x,y)最大等待時間x秒y微妙(如果有事件就會提前傳回,而不繼續等待),NULL表示一直等待,直到有事件發生。這裡可以将timeout分别設定成0(不阻塞)或者1微妙(阻塞很短的時間),然後觀察CPU的使用率,會發現設定成非阻塞後,CPU的使用率已下載下傳就上升到了50%左右,這樣可以看出非阻塞占用CPU很多,但使用率不高。

跟select配合使用的幾個宏和fd_set結構體介紹:

套接字描述符為了友善管理是放在一個集合裡的,這個集合是fd_set,它的具體定義是:

typedef struct fd_set {  

        u_int   fd_count;               /* how many are SET? */  

        SOCKET  fd_array[FD_SETSIZE];   /* an array of SOCKETs */  

} fd_set;  

fd_count是集合中已經設定的套接口描述符的數量。fd_array數組儲存已經設定的套接口描述符,其中FD_SETSIZE的定義是:

#ifndef FD_SETSIZE  

#define FD_SETSIZE      64  

#endif /* FD_SETSIZE */  

這個預設值在一般的程式中已經夠用,如果需要,可以将其更改為更大的值。

集合的管理操作,比如元素的清空、加入、删除以及判斷元素是否在集合中都是用宏來完成的。四個宏是:

FD_ZERO(*set)  

FD_SET(s, *set)  

FD_ISSET(s, *set)  

FD_CLR(s, *set)  

下面一一介紹這些宏的作用和定義:

FD_ZERO(*set),是把集合清空(初始化為0,确切的說,是把集合中的元素個數初始化為0,并不修改描述符數組).使用集合前,必須用FD_ZERO初始化,否則集合在棧上作為自動變量配置設定時,fd_set配置設定的将是随機值,導緻不可預測的問題。它的宏定義如下:

#define FD_ZERO(set) (((fd_set FAR *)(set))->fd_count=0)  

FD_SET(s,*set),向集合中加入一個套接口描述符(如果該套接口描述符s沒在集合中,并且數組中已經設定的個數小于最大個數時,就把該描述符加入到集合中,集合元素個數加1)。這裡是将s的值直接放入數組中。它的宏定義如下:

#define FD_SET(fd, set) do { \  

    if (((fd_set FAR *)(set))->fd_count < FD_SETSIZE) \  

        ((fd_set FAR *)(set))->fd_array[((fd_set FAR *)(set))->fd_count++]=(fd);\  

} while(0)  

FD_ISSET(s,*set),檢查描述符是否在集合中,如果在集合中傳回非0值,否則傳回0. 它的宏定義并沒有給出具體實作,但實作的思路很簡單,就是搜尋集合,判斷套接字s是否在數組中。它的宏定義是:

#define FD_ISSET(fd, set) __WSAFDIsSet((SOCKET)(fd), (fd_set FAR *)(set))  

FD_CLR(s,*set),從集合中移出一個套接口描述符(比如一個套接字連接配接中斷後,就應該移除它)。實作思路是,在數組集合中找到對應的描述符,然後把後面的描述依次前移一個位置,最後把描述符的個數減1. 它的宏定義是:

#define FD_CLR(fd, set) do { \  

    u_int __i; \  

    for (__i = 0; __i < ((fd_set FAR *)(set))->fd_count ; __i++) { \  

        if (((fd_set FAR *)(set))->fd_array[__i] == fd) { \  

            while (__i < ((fd_set FAR *)(set))->fd_count-1) { \  

                ((fd_set FAR *)(set))->fd_array[__i] = \  

                    ((fd_set FAR *)(set))->fd_array[__i+1]; \  

                __i++; \  

            } \  

            ((fd_set FAR *)(set))->fd_count--; \  

            break; \  

        } \  

    } \  

至此,一些基礎的點基本就講完了,然後給出大概流程和一個示例:

1.調用FD_ZERO來初始化套接字狀态;

2.調用FD_SET将感興趣的套接字描述符加入集合中(每次循環都要重新加入,因為select更新後,會将一些沒有滿足條件的套接字移除隊列);

3.設定等待時間後,調用select函數--更新套接字的狀态;

4.調用FD_ISSET,來判斷套接字是否有相應狀态,然後做相應操作,比如,如果套接字可讀,就調用recv函數去接收資料。

關鍵技術:套接字隊列和狀态的表示與處理。

server端得程式如下(套接字管理隊列一個很重要的作用就是儲存套接字描述符,因為accept得到的套接字描述符會覆寫掉原來的套接字描述符,而readfs中的描述符在select後會删除這些套接字描述符):

// server.cpp :   

//程式中加入了套接字管理隊列,這樣管理起來更加清晰、友善,當然也可以不用這個東西  

#include "winsock.h"  

#include "stdio.h"  

#pragma comment (lib,"wsock32.lib")  

struct socket_list{  

    SOCKET MainSock;  

    int num;  

    SOCKET sock_array[64];  

void init_list(socket_list *list)  

{  

    int i;  

    list->MainSock = 0;  

    list->num = 0;  

    for(i = 0;i < 64;i ++){  

        list->sock_array[i] = 0;  

    }  

}  

void insert_list(SOCKET s,socket_list *list)  

    for(i = 0;i < 64; i++){  

        if(list->sock_array[i] == 0){  

            list->sock_array[i] = s;  

            list->num += 1;  

            break;  

        }  

void delete_list(SOCKET s,socket_list *list)  

        if(list->sock_array[i] == s){  

            list->sock_array[i] = 0;  

            list->num -= 1;  

void make_fdlist(socket_list *list,fd_set *fd_list)  

    FD_SET(list->MainSock,fd_list);  

    for(i = 0;i < 64;i++){  

        if(list->sock_array[i] > 0){  

            FD_SET(list->sock_array[i],fd_list);  

int main(int argc, char* argv[])  

    SOCKET s,sock;  

    struct sockaddr_in ser_addr,remote_addr;  

    int len;  

    char buf[128];  

    WSAData wsa;  

    int retval;  

    struct socket_list sock_list;  

    fd_set readfds,writefds,exceptfds;  

    timeval timeout;        //select的最多等待時間,防止一直等待  

    unsigned long arg;  

    WSAStartup(0x101,&wsa);  

    s = socket(AF_INET,SOCK_STREAM,0);  

    ser_addr.sin_family = AF_INET;  

    ser_addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);  

    ser_addr.sin_port = htons(0x1234);  

    bind(s,(sockaddr*)&ser_addr,sizeof(ser_addr));  

    listen(s,5);  

    timeout.tv_sec = 5;     //如果套接字集合中在1s内沒有資料,select就會傳回,逾時select傳回0  

    timeout.tv_usec = 0;  

    init_list(&sock_list);  

    FD_ZERO(&readfds);  

    FD_ZERO(&writefds);  

    FD_ZERO(&exceptfds);  

    sock_list.MainSock = s;  

    arg = 1;  

    ioctlsocket(sock_list.MainSock,FIONBIO,&arg);  

    while(1){  

        make_fdlist(&sock_list,&readfds);  

        //make_fdlist(&sock_list,&writefds);  

        //make_fdlist(&sock_list,&exceptfds);  

        retval = select(0,&readfds,&writefds,&exceptfds,&timeout);     //超過這個時間,就不阻塞在這裡,傳回一個0值。  

        if(retval == SOCKET_ERROR){  

            retval = WSAGetLastError();  

        else if(retval == 0) {  

            printf("select() is time-out! There is no data or new-connect coming!\n");  

            continue;  

        if(FD_ISSET(sock_list.MainSock,&readfds)){  

            len = sizeof(remote_addr);  

            sock = accept(sock_list.MainSock,(sockaddr*)&remote_addr,&len);  

            if(sock == SOCKET_ERROR)  

                continue;  

            printf("accept a connection\n");  

            insert_list(sock,&sock_list);  

        for(i = 0;i < 64;i++){  

            if(sock_list.sock_array[i] == 0)  

            sock = sock_list.sock_array[i];  

            if(FD_ISSET(sock,&readfds)){  

                retval = recv(sock,buf,128,0);  

                if(retval == 0){  

                    closesocket(sock);  

                    printf("close a socket\n");  

                    delete_list(sock,&sock_list);  

                    continue;  

                }else if(retval == -1){  

                    retval = WSAGetLastError();  

                    if(retval == WSAEWOULDBLOCK)  

                        continue;  

                    delete_list(sock,&sock_list);   //連接配接斷開後,從隊列中移除該套接字  

                }  

                buf[retval] = 0;  

                printf("->%s\n",buf);  

                send(sock,"ACK by server",13,0);  

            }  

            //if(FD_ISSET(sock,&writefds)){  

            //}  

            //if(FD_ISSET(sock,&exceptfds)){  

        FD_ZERO(&readfds);  

        FD_ZERO(&writefds);  

        FD_ZERO(&exceptfds);  

    closesocket(sock_list.MainSock);  

    WSACleanup();  

    return 0;  

關于linux下的select跟windows下的差別還有待學習。

參考書籍:

《WinSock網絡程式設計經絡》第19章

《UNIX環境進階程式設計》

繼續閱讀