Unix環境高級編程多路複用之Select的基本實現

目錄

 

select函數簡介

參數說明

select的不足之處

select的多路複用實現網絡socket的多併發服務器的流程圖

服務器實現代碼

頭文件

源文件

運行結果

單個客戶端連接

多客戶端連接


  • select函數簡介

  select()函數允許進程指示內核等待多個事件(文件描述符)中的任何一個發生,並只在有一個或多個事件發生或經歷一段指定時 間後才喚醒它,然後接下來判斷究竟是哪個文件描述符發生了事件並進行相應的處理。

#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);   

void FD_CLR(int fd, fd_set *set);  //用來刪除一個已經沒有使用的文件描述符fd
int  FD_ISSET(int fd, fd_set *set); //判斷文件描述是否在集合中
void FD_SET(int fd, fd_set *set);   //將文件描述符fd加入的select的文件描述符集合
void FD_ZERO(fd_set *set);          //將select的文件描述符集合清空

/*可以用來設置select的超時時間*/
struct timeval { 
    long tv_sec;   //秒 
    long tv_usec;  //毫秒
 };
  • 參數說明

  1.  select監視並等待多個文件描述符的屬性發生變化,它監視的屬性分3類,分別是readfds(文件描述符有數據到來可讀)、 writefds(文件描述符可寫)、和exceptfds(文件描述符異常)。如果我們只是監視其中的一個或兩個的文件描述符的話,其他不足要的可以將其值置爲NULL。

  2. 調用後select函數會阻塞,直到有描述符就緒(有數據可讀、可寫、 或者有錯誤異常),或者超時( timeout 指定等待時間)發生函數才返回。當select()函數返回後,可以通過遍歷 fdset,來找到 究竟是哪些文件描述符就緒。    

  3. select函數的返回值是就緒描述符的數目,超時時返回0,出錯返回-1;

  4. 第一個參數max_fd指待測試的fd的總個數,它的值是待測試的最大文件描述符加1。Linux內核從0開始到max_fd-1掃描文件描述 符,如果有數據出現事件(讀、寫、異常)將會返回;假設需要監測的文件描述符是8,9,10,那麼Linux內核實際也要監測0~7,此時真 正帶測試的文件描述符是0~10總共11個,即max(8,9,10)+1,所以第一個參數是所有要監聽的文件描述符中最大的+1。

  5. 中間三個參數readset、writeset和exceptset指定要讓內核測試讀、寫和異常條件的fd集合,如果不需要測試的可以設置爲 NULL; 

  6. 最後一個參數是設置select的超時時間,如果設置爲NULL則永不超時;
     

  7. 需要注意的是待測試的描述集總是從0, 1, 2, ...開始的。 所以, 假如你要檢測的描述符爲8, 9, 10, 那麼系統實際也要 監測0, 1, 2, 3, 4, 5, 6,  7,  此時真正待測試的描述符的個數爲11個, 也就是max(8, 9, 10) + 1
       

  8. 在Linux內核有個參數__FD_SETSIZE定義了每個FD_SET的句柄個數中,這也意味着select所用到的FD_SET是有限的,也正是這個原因select()默認只能同時處理1024個客戶端的連接請求:  /linux/posix_types.h:  #define __FD_SETSIZE         1024

  • select的不足之處

  1. 每次有事件到來他都需要遍歷整個文件描述符的集合,不能精準的處理;比如:集合中有0,1,2,3 ..... 30多個文件描述符時,如果第29個文件描述符有事件到來,那麼他就要遍歷前0...29 + 1;這樣的遍歷對我們來說幾乎沒用多長的時間,但是對內核和CPU來說已經很間了。如果有更多的文描述符呢?但是最多不會超過1024個;

  2. 最大連接數的限制,如果前1024個文件描述符均已連接成功,但是再有客戶來連接的話就會連接不上。試想一下,大多數服務器如果同時只能連接1024個客戶端的話,那麼12306的購票和淘寶的雙十一還有那麼有活力嗎?一次就允許1024客戶訪問,這還不把我們等的着急瘋了。雖然可以通過setrlimit()、修改宏定義甚至重 新編譯內核等方式來提升這一限制,但是這樣也會造成效率的降低;

  3.  每次調用 select()都需要把fd集合從用戶態拷貝到內核態,之後內核需要遍歷所有傳遞進來的fd,這時如果客戶端fd很多 時會導致系統開銷很大。

  • select的多路複用實現網絡socket的多併發服務器的流程圖

  • 服務器實現代碼

  • 頭文件

    #ifndef  __SOCKET_SELECT_SERVER_H__
    #define __SOCKET_SELECT_SERVER_H__
    
    #include <stdlib.h>
    #include <stdio.h>
    #include <unistd.h>
    #include <string.h>
    #include <errno.h> 
    #include <ctype.h>
    #include <time.h>
    #include <pthread.h> 
    #include <getopt.h>
    #include <libgen.h>
    #include <sys/types.h>   
    #include <sys/socket.h>
    #include <arpa/inet.h> 
    #include <netinet/in.h>
    #define ARRAY_SIZE(x)       (sizeof(x)/sizeof(x[0]))
    #define BUF_SIZE            1024
    
    int get_opt(int argc, char * const argv[],const char *optstring);  //Linux下的參數解析函數
    int socket_server_init(int listen_port, char *msg);                //socket的封裝函數
    void select_start(int   listenfd, char *msg);                      //select的封裝函數
    void print_usage(char *prograname)
    {
        printf("%s usage : \n", prograname);
        printf("-p(--port): specify sever listen port.\n");
        printf("-m(--msg): specify sever write msg to client.\n");
        printf("-d(--daemon): specify sever will go to run with daemon.\n");
        printf("-h(--help): print this help information.\n");
    
        return  ;
    
    }
    
    #endif
    

     

 

  • 源文件

    
    #include "socket_select_server.h"
    
    int main(int argc, char *argv[])
    {
        int       listenfd, connfd;
        int       ser_port;
        char      *progname = NULL;
        int       opt;
        fd_set    rdset;
        int       rv;
        int       i, j;
        int       found;
        int       maxfd = 0;
        char      buf[BUF_SIZE];
        int       fds_arr[1024];
    
    
    
        get_opt(argc, argv,"p:dm:h"); //Linux下的參數解析函數
        return 0;
    }
    
    int get_opt(int argc, char * const argv[],const char *optstring)
    {
        int     port = 0;
        int     ch;
        char    *msg = NULL;
    
        struct option        opts[] = {
            {"port", required_argument, NULL, 'p'},
            {"write_msg", required_argument, NULL, 'm'},
            {"daemon", no_argument, NULL, 'd'},
            {"help", no_argument, NULL, 'h'},
            {NULL, 0, NULL, 0}
    
        };
    
        while((ch=getopt_long(argc, argv, "p:m:dh", opts, NULL)) != -1 )
        {
            switch(ch)
            {
                case 'p':
                    port=atoi(optarg);
                    break;
                case 'm':
                    msg = optarg;
                    break;
                case 'd':
                    daemon(0,0);
                    break;
                case 'h':
                    print_usage(argv[0]);
                    return 0;
            }
        }
    
        if( !port||!msg)
        {
            print_usage(argv[0]);
    
            return 0;
        }
    
        socket_server_init(NULL,port, msg);  //socket的封裝函數
    }
    int socket_server_init(char * ip,int listen_port, char *msg)
    {
        int                   lisfd = 0;
        int                   clifd = 0;
        int                   on = 1;
        int                   rv = 0;
        pid_t                 pid;
        char                  buf[BUF_SIZE];
        struct sockaddr_in    serv_addr, cli_addr;
        socklen_t             len = sizeof(serv_addr);
    
        if ((lisfd = socket(AF_INET,SOCK_STREAM, 0))< 0)  //服務器第一步,socket();
        {
            printf("Socket error:%s\n", strerror(errno));
            return -1;
        }
    
        printf("socket[%d] successfuly!\n", lisfd);
    
        setsockopt(lisfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));  //端口短時間內複用
        memset(&serv_addr, 0, sizeof(serv_addr));
        serv_addr.sin_family = AF_INET;
        serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); //可以接受任意ip的客戶端訪問,並且將本地字節序轉化爲網絡字節序
        serv_addr.sin_port = htons(listen_port);       //並且將本地字節序轉化爲網絡字節序
    
        if ((rv = bind(lisfd, (struct sockaddr *)&serv_addr, len)) < 0) //服務器第二部bind;
        {
            printf("Bind error %s\n", strerror(errno));
            goto EXIT;
        }
    	
        if ((rv = listen(lisfd, 13)) < 0)  //服務器第三步,listen
        {
            printf("Listen error:%s\n", strerror(errno));
            goto EXIT;
        }
    
        select_start(lisfd, msg);  //select的封裝函數
        return clifd;
    EXIT:
        close(lisfd );
        close(clifd );
        return -1;
    }
    
    
    void select_start(int   listenfd, char *msg)
    {
        int       maxfd = 0;
        char      buf[BUF_SIZE];
        int       fds_array[1024];
        int       i, j;
        int       rv ;
        int       found;
        int       connfd;
        fd_set    rdset;
    
    /* 初始化fds_arr */
        for (i = 0; i < ARRAY_SIZE(fds_arr); ++i) 
        {
            fds_array[i] = -1;
        }
        fds_arr[0] = listenfd; //將listenfd傳入到fds_arr[0]
        
    
        for ( ; ; )
        {
            FD_ZERO(&rdset);  //select 第一步,將reset置零
            
            for ( i = 0; i<ARRAY_SIZE(fds_arr); i++)  //遍歷所有傳入的fd
            {
                if (fds_arr[i] < 0)
                {
                    continue;
                }
    
                maxfd = fds_arr[i] > maxfd ?fds_arr[i]:maxfd;  //取得最大fd +1,
                
                FD_SET(fds_arr[i], &rdset);  //將fds_arr[i]設置爲refd;
            }
    
    
            rv = select(maxfd + 1, &rdset, NULL, NULL, NULL);  //select開始
            if (rv < 0)
            {
                printf("Select error:%s\n", strerror(errno));
    
                break;
            }
            else if (rv == 0)  //斷開或連接超時
            {
                printf("select get timeout.\n");
                continue;
            }
    
            if (FD_ISSET(listenfd, &rdset))  //判斷fd是否爲所設置fd;
            {
                if ( ( connfd = accept(listenfd, (struct sockaddr *)NULL, NULL)) < 0)  //服務器第三步,accept
                {
                    printf("Accept new client error: %s\n", strerror(errno));
                    continue;
                }
    
                found = 0;
    
                for (i = 0; i <ARRAY_SIZE(fds_arr); i++) //將clifd入數組
                {
                    if (fds_array[i] < 0)
                    {
                        printf("Accept new client[%d] and it into array.\n", connfd);
                        fds_arr[i] = connfd;
                        found = 1;
                        break;
                    }
                }
    
                if (!found)  //已接受達到最大的文件描述符的數目,將其餘的文件描述符關閉
                {
                    printf("Accept new client[%d] sueecssful but array is full, so refuse it.\n", connfd);
                    close(connfd);
                }
    
            }
            else 
            {
                for (i = 0; i<ARRAY_SIZE( fds_arr); i++)
                {
                    if (fds_arr[i] < 0 || !FD_ISSET(fds_arr[i], &rdset))  //參數合法性判斷,是否有描述符或描述符是否爲所設置的
                        continue;
                    if ((rv = read(fds_array[i], buf, BUF_SIZE)) <= 0)  //服務器第四步read /write
                    {
                        printf("Socket[%d] read failure or get disconnected.\n", fds_array[i]);
                        close(fds_arr[i]);
                        fds_arr[i] = -1;
                    }
                    else 
                    {
                        printf("socket[%d] read get %d bytes data\n", fds_arr[i], rv);
    
                        if (write(fds_arr[i], msg, rv) < 0)
                        {
                            printf("socket[%d] write failure: %s\n", fds_arr[i], strerror(errno));
                            close(fds_array[i]);
                            fds_arr[i] = -1;
                        }
                    }
                }
            }
    
        }
    
    }
    
  • 運行結果

  • 單個客戶端連接

 

  • 多客戶端連接

  1. 多客戶端併發連接服務器的服務器端

  2.多客戶端併發連接服務器的客戶端端

 

3.客戶端斷開之後的服務器端

注:學識尚淺,如有不足地方敬請指出。謝謝!

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章