Linux/C++ I/O多路複用——select模型實現服務端Socket通信

目錄

文件描述符簡介

select模型理解

select缺點

select函數

函數原型

參數說明

 fd_set 相關

程序實例


文件描述符簡介

        在進程中,每打開一個文件,操作系統就會創建相應的數據結構來描述這個文件,這就是描述文件的文件結構體,同時,在進程的PCB中,會有一個struct files_struct *files的指針,它實際上是指向一個指針數組,而這個指針數組中就存放了每個打開的文件的指針,而所謂的文件描述符,就是這個指針數組的下標,根據文件描述符就能找到對應的文件的指針,就能對該文件進行操作了。在文件描述符進行分配時,會找到當前沒有被使用的最小的下標,作爲新的文件描述符。其中,0,1,2分別對應標準輸出文件描述符,標準輸出文件描述符和標準錯誤文件描述符。因此當打開其他文件時,分配的文件描述符實際上是從3開始的。

select模型理解

         在前面提到的多進程和多線程來實現服務端與客戶端通信中,不管是多進程還是多線程,一般來說,每一個連接成功的文件描述符(socket)都需要一個進程/線程來進行監控,然後就需要不斷的對每個進程/線程中的文件描述符進行輪詢,例如accept、read等函數,如果沒有連接請求、緩衝區中沒有數據,它們就會一直阻塞住。這樣一來一回就在反覆切換進程/線程,系統的開銷是非常大的。這兩種併發模型都需要進程或者線程自己去等待。

        還有一種就是I/O多路複用模型,select就是其中一種。之前多進程多線程是讓進程和線程去阻塞等待,而現在則是直接讓內核去等待,只需要主進程去直接詢問內核哪些描述符準備好了即可。select模型的關鍵就是事先將感興趣的文件描述符放到一個集合中,當用戶進程調用了select,那麼整個進程會被阻塞,而同時,內核就會“監視”所有select感興趣的文件描述符,當任何一個文件描述符中的數據準備好了,select就會返回。這個時候用戶進程再調用read操作,將數據從內核拷貝到用戶進程。

select缺點

1、內核中對select模型可監視的fd數量限制爲1024,如果要修改這個數字就必須對內核重新編譯;

2、 對socket進行掃描時是線性掃描,即採用輪詢的方法,效率較低:當套接字比較多的時候,每次select()都要通過遍歷FD_SETSIZE個Socket來完成調度,不管哪個Socket是活躍的,都遍歷一遍。這會浪費很多CPU時間。如果能給套接字註冊某個回調函數,當他們活躍時,自動完成相關操作,那就避免了輪詢,這正是epoll與kqueue做的。

3、需要維護一個用來存放大量fd的數據結構,這樣會使得用戶空間和內核空間在傳遞該結構時複製開銷大。

select函數

函數原型

int select(int maxfdp, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);

參數說明

maxfdp:被監聽的文件描述符的最大值大1,因爲文件描述符是從0開始計數的;

readfds、writefds、exceptset:分別指向可讀、可寫和異常等事件對應的描述符集合。

timeout:用於設置select函數的超時時間,即告訴內核select等待多長時間之後就放棄等待。timeout == NULL 表示等待無限長的時間

timeval結構體定義如下:

struct timeval
{      
    long tv_sec;   /*秒 */
    long tv_usec;  /*微秒 */   
};

返回值:超時返回0;失敗返回-1;成功返回大於0的整數,這個整數表示就緒描述符的數目。

 fd_set 相關

int FD_ZERO(int fd, fd_set *fdset);   //一個 fd_set類型變量的所有位都設爲 0 (可理解爲清空集合中的所有感興趣描述符)
int FD_CLR(int fd, fd_set *fdset);  //清除某個位時可以使用 (可理解爲清空集合中的某一個描述符)
int FD_SET(int fd, fd_set *fd_set);   //設置變量的某個位置位  (可理解爲向集合中添加一個描述符)
int FD_ISSET(int fd, fd_set *fdset); //測試某個位是否被置位   (可理解爲判斷一個描述符是否在集合中)

程序流程

1.綁定、監聽.....

2.創建集合,由於調用select函數時,傳入的集合參數在函數返回後可能會改變,因此創建一個集合來保存所有感興趣的描述符allset,再創建一個集合rset用來作爲select的調用參數;再創建一個數組client用來存放所有有效的描述符,並初始化各項爲-1;

3.將監聽描述符lfd加入allset,此時最大描述符maxfd = lfd;

4.創建while循環,將allset賦值給rset,將rset作爲讀集合參數,調用select函數開始阻塞等待;

5.select函數返回後,先判斷監聽描述符lfd是否還存在於rset中,判斷方式爲if(FD_SET(lfd,&rset))。

6.如果判斷爲真,說明有新連接,則調用accept函數新連接的文件描述符並存在變量connfd中,然後再將connfd加入allset中和client數組中;

7.然後處理除監聽描述符以外的描述符。遍歷client數組,查看有效描述符是否發生了讀事件,判斷方式爲if(FD_SET(client[i],&rset));如果判斷爲真,說明有數據傳來,就進行read和write操作;

8.繼續下一次循環....

程序實例

#include <iostream>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>

using namespace std;

#define SERV_IP "127.1.2.3"
#define SERV_PORT 8888
#define MAX_CONN 1024

int main()
{
    sockaddr_in servaddr,clitaddr;
    sockaddr_in clit_info[MAX_CONN];  //存放成功連接的客戶端地址信息
    int client[1024];   //存放成功連接的文件描述符
    char buf[1024];  //讀寫緩衝區
    int lfd;      //用於監聽
    int connfd;   //連接描述符
    int readyfd;  //保存select返回值
    int maxfd = 0;  //保存最大文件描述符
    int maxi = 0;  //maxi反映了client中最後一個成功連接的文件描述符的索引
    socklen_t addr_len = sizeof(clitaddr);;

    fd_set allset;  //存放所有可以被監控的文件描述符
    fd_set rset;

    FD_ZERO(&allset);
    FD_ZERO(&rset);

    if((lfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
    {
        cout<<"creat socket fault : "<<strerror(errno)<<endl;
        return 0;
    }

    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    servaddr.sin_addr.s_addr = inet_addr(SERV_IP);

    if(bind(lfd,(sockaddr *)&servaddr,sizeof(servaddr)) == -1)
    {
        cout<<"bind fault : "<<strerror(errno)<<endl;
        return 0;
    }

    if(listen(lfd,128) == -1)
    {
        cout<<"listen fault : "<<strerror(errno)<<endl;
        return 0;
    }

    maxfd = lfd;  //此時只用監控lfd,因此lfd就是最大文件描述符

    //初始化client數組
    for(int i=0;i<MAX_CONN;i++)client[i] = -1;

    FD_SET(lfd,&allset);

    cout<<"Init Success ! "<<endl;
    cout<<"host ip : "<<inet_ntoa(servaddr.sin_addr)<<"  port : "<<ntohs(servaddr.sin_port)<<endl;

    cout<<"Waiting for connections ... "<<endl;

    while(1)
    {
        rset = allset ;  //rset作爲select參數時,表示需要監控的所有文件描述符集合,select返回時,rset中存放的是成功監控的文件描述符。因此在select前後rset是可能改變的,所以在調用select前將rset置爲所有需要被監控的文件描述符的集合,也就是allset
        readyfd = select(maxfd+1,&rset,NULL,NULL,NULL); //服務端只考慮讀的情況
        //執行到這裏,說明select返回,返回值保存在readyfd中,表示有多少個文件描述符被監控成功
        if(readyfd == -1)
        {
            cout<<"select fault : "<<strerror(errno)<<endl;
            return 0;
        }

        if(FD_ISSET(lfd,&rset))  //監聽描述符監控成功,說明有連接請求
        {
            int i=0;
            connfd = accept(lfd,(sockaddr *)&clitaddr,&addr_len);  //處理新連接,此時accept直接可以返回而不用一直阻塞
            if(connfd == -1)
            {
                cout<<"accept fault : "<<strerror(errno)<<endl;
                continue ;
            }
            cout<<inet_ntoa(clitaddr.sin_addr)<<":"<<ntohs(clitaddr.sin_port)<<" connected ...  "<<endl;
            //成功連接後,就將connfd加入監控描述符表中
            FD_SET(connfd,&allset);

            for(;i<MAX_CONN;i++)
            {
                if(client[i] == -1)
                {
                    client[i] = connfd;
                    clit_info[i] = clitaddr;
                    break;
                }
            }

            if(connfd>maxfd)maxfd = connfd;  //更新最大文件描述符
            if(i>maxi)maxi = i;

            readyfd --;
            if(readyfd == 0)continue;  //如果只有lfd被監控成功,那麼就重新select
        }
        //處理lfd之外監控成功的文件描述符,進行輪詢
        for(int i=0;i<=maxi;i++)
        {
            if(client[i] == -1)continue; //等於-1說明這個描述符已經無效
            if(FD_ISSET(client[i],&rset))   //在client數組中尋找是否有被監控成功的文件描述符
            {
                //此時說明client[i]對於的文件描述符監控成功,有消息發來,直接讀取即可
                int readcount = read(client[i],buf,sizeof(buf));
                if(readcount == 0)  //對方客戶端關閉
                {
                    close(client[i]);  //關閉描述符
                    FD_CLR(client[i],&allset);   //將該描述符從描述符集合中去除
                    client[i] = -1;  //相應位置置爲-1,表示失效

                    cout<<inet_ntoa(clit_info[i].sin_addr)<<":"<<ntohs(clit_info[i].sin_port)<<" exit ... "<<endl;
                }
                else if(readcount == -1)
                {
                    cout<<"read fault : "<<strerror(errno)<<endl;
                    continue;
                }
                else
                {
                    cout<<"(From "<<inet_ntoa(clit_info[i].sin_addr)<<":"<<ntohs(clit_info[i].sin_port)<<")";
		    for(int j=0;j<readcount;j++)cout<<buf[j];
		    cout<<endl;
                    for(int j=0;j<readcount;j++)buf[j] = toupper(buf[j]);
                    write(client[i],buf,readcount);
                }
                readyfd--;
                if(readyfd == 0)break;
            }
        }
    }
    close(lfd);
    return 0;
}

 

 

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