C IO複用select, epoll 簡單總結

蔡头一枚發表於2024-03-22

1. 檔案描述符型別

REG :檔案
DIR:目錄
CHR :字元
BLK:塊裝置
UNIX:unix域套接字
FIFO :先進先出佇列
IPv4:網際協議 (IP) 套接字

其中, 標準輸入STDIN(0)和STDOUT輸出(1), STDERR錯誤(2)為指定的值

2. IO複用模型

(1). select (在指定的一段時間內,輪詢監聽使用者需要的檔案描述符(使用者新增到fd_set中的),當監聽到的檔案描述符傳來可讀、可寫或異常事件發生時就會返回)

#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
nfds: 指定被監聽檔案描述符的總數,因為檔案描述符通常從0開始計數,因此nfds通常為readfd,writefd,exceptfd這三個描述符集中的最大描述符編號加1;
readfd,writefd,exceptfd: 分別指向可讀、可寫、異常事件對應的檔案描述符集合。應用程式呼叫select時,透過這三個引數傳入需要監聽的檔案描述符,輪詢等待有事件產生
timeout: 超時間設定; NULL表示一直阻塞直到某個檔案描述符就緒; 非NULL表示超時時間後立即返回;

返回值: 大於0, 描述符有資料返回; 等於0超時; 小於0, select錯誤;

理解select模型的關鍵在於理解fd_set,為說明方便,取fd_set長度為1位元組,fd_set中的每一bit可以對應一個檔案描述符fd。則1位元組長的fd_set最大可以對應8個fd。

(1)執行fd_set set;FD_ZERO(&set);則set用位表示是0000,0000。

(2)若fd=5,執行FD_SET(fd,&set);後set變為0001,0000(第5位置為1)

(3)若再加入fd=2,fd=1,則set變為0001,0011

(4)執行select(6,&set,0,0,0)阻塞等待---讀取

(5)若fd=1,fd=2上都發生可讀事件,則select返回,此時set變為0000,0011。注意:沒有事件發生的fd=5被清空。

(2). epoll
#include <sys/epoll.h>

int epoll_create(int size);
函式功能: 建立epoll專用檔案描述符的緩衝區。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函式功能: 新增、刪除、修改 監聽檔案描述符。
函式引數:
int epfd epoll專用的檔案描述符
int op 操作命令。EPOLL_CTL_ADD EPOLL_CTL_MOD EPOLL_CTL_DEL
int fd 要操作檔案描述符
struct epoll_event *event 存放監聽檔案描述符資訊的結構體

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
函式功能: 等待事件發生。
函式引數:
int epfd epoll專用的檔案描述符
struct epoll_event *events : 存放產生事件的檔案描述結構體。
int maxevents :最大監聽的數量.
int timeout :等待事件ms單位. <0 >0 ==0

返回值: 產生事件的數量。

typedef union epoll_data {
void *ptr;
int fd; //檔案描述符
uint32_t u32;
uint64_t u64;
} epoll_data_t;

struct epoll_event {
uint32_t events; /* Epoll events EPOLLIN 輸入事件 */
epoll_data_t data; /* User data variable */
};


(3). select和epoll區別:
select epoll
效能: 隨著連線數增加, 急劇下降。處理成千上萬併發連線數時, 連線數增加時, 效能基本上沒有下降, 大量併發連線時,
效能很差 效能很好

連線數: 連線數量有限制,最大連線數不超過1024 連線數無限制

內在處理機制: 線性輪詢 回撥callback

開發複雜性 低 中

簡單理解:
住校時,你的朋友來找你:
select版宿管阿姨,帶著你的朋友挨個房間找,直到找到你
epoll版阿姨,會先記下每位同學的房間號, 你的朋友來時,只需告訴你的朋友你住在哪個房間,無需親自帶著你朋友滿大樓逐個房間找人
如果來了10000個人,都要找自己住這棟樓的同學時,select版和epoll版宿管大媽,誰效率高?同理,高併發伺服器中,輪詢I/O是最耗時操作之一,epoll效能更高也是很明顯。

select的呼叫複雜度O(n)。如一個保姆照看一群孩子,如果把孩子是否需要尿尿比作網路I/O事件,select就像保姆挨個詢問每個孩子:你要尿尿嗎?若孩子回答是,保姆則把孩子拎出來放到另外一個地方。當所有孩子詢問完之後,保姆領著這些要尿尿的孩子去上廁所(處理網路I/O事件)

epoll機制下,保姆無需挨個詢問孩子是否要尿尿,而是每個孩子若自己需要尿尿,主動站到事先約定好的地方,而保姆職責就是檢視事先約定好的地方是否有孩子。若有小孩,則領著孩子去上廁所(網路事件處理)。因此,epoll的這種機制,能夠高效的處理成千上萬的併發連線,而且效能不會隨著連線數增加而下降。

Epoll 使用

EpollServer.cpp

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <dirent.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/epoll.h>

#include <iostream>
#include <string>
#include <list>

using namespace std;

typedef struct _ClientFd
{
    int m_fd = -1;                   // 連線控制代碼
    string name = string();        // 名稱

} ClientFd;


// socket控制代碼
int sockfd;

//訊息結構體, 目前協定的訊息接受傳送的格式
struct MSG_DATA
{
    char type; //訊息型別.  0表示有聊天的訊息資料  1表示好友上線  2表示好友下線
//    char name[50]; //好友名稱
//    unsigned char buff[100];  //傳送的聊天資料訊息
    string name;   //好友名稱
    string buff;  //傳送的聊天資料訊息
    int number;   //線上人數的數量
};

#define MAX_EPOLL_FD 100
struct epoll_event events[MAX_EPOLL_FD];
struct epoll_event event;
int epfd;
int nfd;
struct MSG_DATA msg_data;

// 存放客戶端控制代碼列表
std::list<ClientFd*> clientFdList;

// 新增描述符
void List_addFd(int fd)
{
    ClientFd *pFd = new ClientFd();
    pFd->m_fd = fd;

    clientFdList.emplace_back(pFd);
}


// 獲取成員名稱
void List_getName(struct MSG_DATA *msg_data,int client_fd)
{
    auto iter = clientFdList.begin();
    while( iter != clientFdList.end() )
    {
        if ( (*iter)->m_fd == client_fd )
        {
            msg_data->name = (*iter)->name;
            break;
        }
        iter++;
    }

}


// 刪除描述符
void List_DelFd(int clientFd)
{
    auto iter = clientFdList.begin();
    while ( iter != clientFdList.end() )
    {
        if ( (*iter)->m_fd == clientFd )
        {
            iter = clientFdList.erase(iter);
            break;
        }
        ++iter;
    }

}


int List_GetCnt()
{
    return clientFdList.size();
}


// 儲存檔案
void List_SaveName(struct MSG_DATA *msg_data, int clientFd)
{
    auto iter = clientFdList.begin();
    while ( iter != clientFdList.end() )
    {
        if ( (*iter)->m_fd == clientFd )
        {
            msg_data->name = (*iter)->name;
            break;
        }
        ++iter;
    }
}


// 伺服器轉發訊息
void Server_SendMsgData(struct MSG_DATA *msg_data,int clientFd)
{

    printf("%d | %s\n", __LINE__, __FUNCTION__);
    auto iter = clientFdList.begin();
    while ( iter != clientFdList.end() )
    {
        if ( (*iter)->m_fd == clientFd )
        {
            printf("");
            write((*iter)->m_fd, msg_data, sizeof(struct MSG_DATA));
            break;
        }
        ++iter;
    }
}



/*訊號工作函式*/
void signal_work_func(int sig)
{
    (void)sig;
    close(sockfd);
    exit(0); //結束程序
}



// 函式入口
int main(int argc,char **argv)
{

    if(argc!=2)
    {
        printf("./app <埠號>\n");
        return 0;
    }

    signal(SIGPIPE,SIG_IGN); //忽略 SIGPIPE 訊號--防止伺服器異常退出
    signal(SIGINT,signal_work_func);


    /*1. 建立socket套接字*/
    sockfd=socket(AF_INET,SOCK_STREAM,0);


    /*2. 繫結埠號與IP地址*/
   struct sockaddr_in addr;
   addr.sin_family=AF_INET;
   addr.sin_port=htons(atoi(argv[1])); // 埠號0~65535
   addr.sin_addr.s_addr=INADDR_ANY;    //inet_addr("0.0.0.0"); //IP地址
   if(bind(sockfd,(const struct sockaddr *)&addr,sizeof(struct sockaddr))!=0)
   {
       perror("伺服器:埠號繫結失敗.\n");
       return -1;
   }


    /*3. 設定監聽的數量*/
    listen(sockfd,20);


    /*4. 等待客戶端連線*/
    int client_fd;
    struct sockaddr_in client_addr;
    socklen_t addrlen;
    int i;
    int cnt;


    /*5. 建立epoll相關的介面*/
    epfd=epoll_create(MAX_EPOLL_FD);
    event.events=EPOLLIN;  //監聽的事件
    event.data.fd=sockfd; //監聽的套接字
    epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&event);


    while(1)
    {
        //等待事件發生, 有資料
        nfd=epoll_wait(epfd,events,MAX_EPOLL_FD,-1);

        for(i=0;i<nfd;i++)
        {
           if(events[i].data.fd==sockfd)  //表示有新的客戶端連線上伺服器
           {
               client_fd=accept(sockfd,(struct sockaddr *)&client_addr,&addrlen);
               printf("連線的客戶端IP地址:%s\n",inet_ntoa(client_addr.sin_addr));
               printf("連線的客戶端埠號:%d\n",ntohs(client_addr.sin_port));
               //儲存已經連線上來的客戶端
//               List_AddNode(list_head,client_fd);

               // 新增新連線客戶端的控制代碼
               List_addFd(client_fd);

               //將新連線的客戶端套接字新增到epoll函式監聽佇列裡
               event.data.fd=client_fd; //監聽的套接字
               epoll_ctl(epfd,EPOLL_CTL_ADD,client_fd,&event);
           }
           else  //表示客戶端給伺服器傳送了訊息-----實現訊息的轉發
           {
                //讀取客戶端傳送的訊息, 儲存在msg_data中
               cnt = read(events[i].data.fd,&msg_data,sizeof(struct MSG_DATA));
               if(cnt<=0)  //表示當前客戶端斷開了連線
               {
                   //獲取名稱
//                   List_GetName(list_head,&msg_data,events[i].data.fd);
                   //刪除節點
//                   List_DelNode(list_head,events[i].data.fd);

                   //獲取名稱
                   List_getName(&msg_data, events[i].data.fd);
                   //刪除節點
                   List_DelFd(events[i].data.fd);

                   msg_data.type=2;             //

                   //將斷開連線的客戶端套接字從epoll函式監聽佇列裡刪除呼叫
                   event.data.fd = events[i].data.fd; //監聽的套接字
                   epoll_ctl(epfd,EPOLL_CTL_DEL,events[i].data.fd,&event);
                   close(event.data.fd);
               }

               if(msg_data.type==1) //好友上線的時候儲存一次名稱
               {
                   //儲存名稱
                   List_SaveName(&msg_data,events[i].data.fd);
               }

               //轉發訊息給其他好友
               msg_data.number = List_GetCnt(); //當前線上好友人數
               Server_SendMsgData(&msg_data,events[i].data.fd);

           }   
        }
    }

    //退出程序
    signal_work_func(0);

    return 0;
}

EpollClient.cpp

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <dirent.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <signal.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <sys/select.h>
#include <sys/time.h>
#include <poll.h>
#include <sys/epoll.h>

//訊息結構體
struct MSG_DATA
{
    char type; //訊息型別.  0表示有聊天的訊息資料  1表示好友上線  2表示好友下線
    char name[50]; //好友名稱
    int number;   //線上人數的數量
    unsigned char buff[100];  //傳送的聊天資料訊息
};
struct MSG_DATA msg_data;

#define MAX_EVENTS 2
struct epoll_event ev, events[MAX_EVENTS];
int epollfd;
int nfds;

//檔案接收端
int main(int argc,char **argv)
{
    if(argc!=4)
    {
        printf("./app  <IP地址> <埠號> <名稱>\n");
        return 0;
    }
    int sockfd;
    //忽略 SIGPIPE 訊號--方式伺服器向無效的套接字寫資料導致程序退出
    signal(SIGPIPE,SIG_IGN);

    /*1. 建立socket套接字*/
    sockfd=socket(AF_INET,SOCK_STREAM,0);
    /*2. 連線伺服器*/
    struct sockaddr_in addr;
    addr.sin_family=AF_INET;
    addr.sin_port=htons(atoi(argv[2])); // 埠號0~65535
    addr.sin_addr.s_addr=inet_addr(argv[1]); //IP地址
    if(connect(sockfd,(const struct sockaddr *)&addr,sizeof(struct sockaddr_in))!=0)
    {
        printf("客戶端:伺服器連線失敗.\n");
        return 0;
    }

    /*3. 傳送訊息表示上線*/
    msg_data.type=1;
    strcpy(msg_data.name,argv[3]);
    write(sockfd,&msg_data,sizeof(struct MSG_DATA));

    int cnt;
    int i;
    //建立專用檔案描述符
    epollfd = epoll_create(10);
    //新增要監聽的檔案描述符
    ev.events = EPOLLIN;
    ev.data.fd = sockfd;
    epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev);

    ev.events = EPOLLIN;
    ev.data.fd = 0; //標準輸入檔案描述符
    epoll_ctl(epollfd, EPOLL_CTL_ADD, 0, &ev);

    while(1)
    {
        //監聽事件
        nfds=epoll_wait(epollfd,events,MAX_EVENTS,-1);
        if(nfds)
        {
            for(i=0;i<nfds;i++)
            {
                if(events[i].data.fd==sockfd) //判斷收到伺服器的訊息
                {
                    cnt=read(sockfd,&msg_data,sizeof(struct MSG_DATA));
                    if(cnt<=0) //判斷伺服器是否斷開了連線
                    {
                        printf("伺服器已經退出.\n");
                        goto SERVER_ERROR;
                    }
                    else if(cnt>0)
                    {
                        if(msg_data.type==0)
                        {
                            printf("%s:%s  線上人數:%d\n",msg_data.name,msg_data.buff,msg_data.number);
                        }
                        else if(msg_data.type==1)
                        {
                            printf("%s 好友上線. 線上人數:%d\n",msg_data.name,msg_data.number);
                        }
                        else if(msg_data.type==2)
                        {
                            printf("%s 好友下線. 線上人數:%d\n",msg_data.name,msg_data.number);
                        }
                    }
                }
                else if(events[i].data.fd==0) //表示鍵盤上有資料輸入
                {
                    gets(msg_data.buff); //讀取鍵盤上的訊息
                    msg_data.type=0; //表示正常訊息
                    strcpy(msg_data.name,argv[3]); //名稱
                    write(sockfd,&msg_data,sizeof(struct MSG_DATA));
                }
            }
        }
    }
SERVER_ERROR:
    close(sockfd);
    return 0;
}

相關文章