5.9-select_poll_epoll
本文演示 select 等 io 多路復用函數的應用方法,函數具體介紹可以參考我過去寫的博客。
先綁定監聽的文件描述符
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(struct sockaddr_in));serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(2052);if (-1 == bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)))
{perror("bind");return -1;
}listen(sockfd, 10);
1. select
select 函數適用于 win/linux 平臺,但是使用時每次都需要檢查位圖內所有客戶端的狀態變化情況,屬于針對于每一個文件進行處理而非針對事件處理,效率較低。打個比方:如果客戶端是小區內的住戶,那么 selcet 作為快遞員,會從快遞倉庫中挑選出要被配送到該小區指定住戶的快遞,并對于每一個住戶是否有快遞/寄快遞。
演示如下:
fd_set rfds, rset;FD_ZERO(&rfds);
FD_SET(sockfd, &rfds);int maxfd = sockfd;
while (1)
{rset = rfds;int nready = select(maxfd + 1, &rset, NULL, NULL, NULL);if (FD_ISSET(sockfd, &rset)){struct sockaddr_in clientaddr;socklen_t len = sizeof(struct sockaddr_in);int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);printf("sockfd: %d\n", clientfd);FD_SET(clientfd, &rfds);maxfd = clientfd;}int i = 0;for (i = sockfd + 1; i <= maxfd; i++){if (FD_ISSET(i, &rset)){char buffer[128] = { 0 };int count = recv(i, buffer, 128, 0);if (0 == count){printf("disconnect\n");close(i);FD_CLR(i, &rfds);break;}send(i, buffer, count, 0);printf("clientfd: %d, count: %d, buffer: %s\n", i, count, buffer);}}
}
2. poll
poll 函數適用于 Linux 平臺,效率相比于 select 有所提升。如果 selcet 是快遞員要對于每一個住戶確定一次需求,poll 則是可以直接鎖定不同客戶的需求。
演示如下:
struct pollfd fds[1024] = { 0 };fds[sockfd].fd = sockfd;
fds[sockfd].events = POLLIN;int maxfd = sockfd;while (1)
{int nready = poll(fds, maxfd + 1, -1);if (fds[sockfd].revents & POLLIN){struct sockaddr_in clientaddr;int len = sizeof(struct sockaddr_in);int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);printf("hello, %d\n", clientfd);fds[clientfd].fd = clientfd;fds[clientfd].events = POLLIN;maxfd = clientfd;}int i = 0;for (i = sockfd + 1; i <= maxfd; i++){if (fds[i].revents & POLLIN){char buffer[128] = { 0 };int count = recv(fds[i].fd, buffer, 128, 0);if (count == 0){printf("disconnect\n");close(i);fds[i].fd = -1;fds[i].events = 0;continue;}send(i, buffer, count, 0);printf("clientfd: %d, sount: %d, lbuffer: %s\n", i, count, buffer);}}
}
3.1 epoll
在支持 Linux 的函數中,epoll 是最高效的。還是上面的比方,epoll 則是在一定程度上結合了前兩者的優點,并且底層使用紅黑樹,查找速度更快。
演示如下:
int epfd = epoll_create(1);struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);struct epoll_event events[1024] = { 0 };while (1)
{int nready = epoll_wait(epfd, events, 1024, -1);int i = 0;for (i = 0; i < nready; i++){int curfd = events[i].data.fd;if (sockfd == curfd){struct sockaddr_in clientaddr;socklen_t len = sizeof(struct sockaddr_in);int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);ev.events = EPOLLIN;ev.data.fd = clientfd;epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);printf("clientfd: %d\n", clientfd);}else if (events[i].events & EPOLLIN){char buffer[10] = { 0 }; // 只要有數據就會一直觸發,因此會回復多次int count = recv(curfd, buffer, 10, 0);if (count == 0){printf("disconnect\n");close(curfd);epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);continue;}send(curfd, buffer, count, 0);printf("clientfd: %d, sount: %d, buffer: %s\n", curfd, count, buffer);}}}
3.2 基于 epoll 模擬實現面對事件的 reactor 的底層原理
定義結構體,為了方便,直接把 epfd 定位全局變量
#define BUFFER_LENGTH 1024typedef int (* RCALLBACK)(int fd);// save buffer data
struct conn_item
{int fd;char rbuffer[BUFFER_LENGTH];int rlen;char wbuffer[BUFFER_LENGTH];int wlen;union // 聯合,在后續代碼中會用到{RCALLBACK accept_callback;RCALLBACK recv_callback;}recv_t;RCALLBACK send_callback;
};struct conn_item connlist[1024];#if 1
int epfd;
#elif
struct reactor
{int epfd;struct conn_item connlist[1024];
};
#endif
reactor 的模擬借助以下回調函數實現,可以簡化代碼。
int set_cb(int fd, int event, int flag);
int accept_cb(int fd);
int recv_cb(int fd);
int send_cb(int fd);
具體實現如下,體現出面對事件的處理思想。
/*
1. listenfd 觸發 EPOLLIN 事件 -> 執行 accept_cb
2. client 觸發 EPOLLIN 事件 -> recv_cb
3. client 觸發 EPOLLOUT 事件 -> send_cb
*/// ADD: flag == 1 else 0
int set_cb(int fd, int event, int flag)
{if (flag){struct epoll_event ev;ev.events = event;ev.data.fd = fd;epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);}else{struct epoll_event ev;ev.events = event;ev.data.fd = fd;epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);}
}int accept_cb(int fd)
{struct sockaddr_in clientaddr;socklen_t len = sizeof(struct sockaddr_in);int clientfd = accept(fd, (struct sockaddr*)&clientaddr, &len);set_cb(clientfd, EPOLLIN, 1);// set connlistconnlist[clientfd].fd = clientfd;memset(connlist[clientfd].wbuffer, 0, BUFFER_LENGTH);connlist[clientfd].wlen = 0;memset(connlist[clientfd].rbuffer, 0, BUFFER_LENGTH);connlist[clientfd].rlen = 0;// set callbackconnlist[clientfd].recv_t.recv_callback = recv_cb;connlist[clientfd].send_callback = send_cb;printf("clientfd: %d\n", clientfd);return clientfd;
}int recv_cb(int fd)
{char * buffer = connlist[fd].rbuffer;int index = connlist[fd].rlen;int count = recv(fd, buffer + index, BUFFER_LENGTH - index, 0);if (count == 0){printf("disconnect\n");epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);close(fd);return -1;}connlist[fd].rlen += count;#if ENABLE_HTTP_RESPONSE// 此處可以自行修改,使對應不同輸入實現特定輸出,如 http 相應:
http_response(&connlist[fd]);#else// 不做處理直接發送返回
memcpy(connlist[fd].wbuffer, connlist[fd].rbuffer, connlist[fd].rlen);
connlist[fd].wlen = connlist[fd].rlen;#endif// eventset_cb(fd, EPOLLOUT, 0);return count;
}int send_cb(int fd)
{char * buffer = connlist[fd].wbuffer;int index = connlist[fd].wlen;int count = send(fd, buffer, index, 0);// 事件來一次執行一次set_cb(fd, EPOLLIN, 0);return count;
}
mainloop 部分
int main()
{int sockfd = socket(AF_INET, SOCK_STREAM, 0);struct sockaddr_in serveraddr;memset(&serveraddr, 0, sizeof(struct sockaddr_in));serveraddr.sin_family = AF_INET;serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);serveraddr.sin_port = htons(2048);if (-1 == bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr))){perror("bind");return -1;}listen(sockfd, 10);connlist[sockfd].fd = sockfd;connlist[sockfd].recv_t.accept_callback = accept_cb;// epoll 邊緣觸發/*對于 IO 處理:隨著時間增多會越來越復雜if(listenfd){...}else // clientfd{...}對于事件處理 -> reactor 對事件反應更緩和if (events & EPOLLIN){...}else if (events & EPOLLOUT){...}*/epfd = epoll_create(1); // int sizeset_cb(sockfd, EPOLLIN, 1);struct epoll_event events[1024] = { 0 };while (1) // main loop{int nready = epoll_wait(epfd, events, 1024, -1);int i = 0;for (i = 0; i < nready; i++){int curfd = events[i].data.fd;// 由于結構體內 accept_callback() 與 recv_callback() 共享同一塊內存,故此處 if 條件判斷可以省略。// if (sockfd == curfd)// {// // accept_cb()// // int clientfd = accept_cb(sockfd);// int clientfd = connlist[sockfd].recv_t.accept_callback(sockfd);// printf("client: %d\n", clientfd);// }if (events[i].events & EPOLLIN){// int count = recv_cb(curfd);int count = connlist[curfd].recv_t.recv_callback(curfd);if (count != -1)printf("recv <- clientfd: %d, count: %d, buffer: %s\n", curfd, count, connlist[curfd].rbuffer);}else if (events[i].events & EPOLLOUT){// int count = send_cb(curfd);int count = connlist[curfd].send_callback(curfd);printf("send -> clientfd: %d, count: %d, buffer: %s\n", curfd, count, connlist[curfd].wbuffer);}}}return 0;
}
附:本文的 http_response() 函數定義,以供測試使用。
#include <time.h>typedef struct conn_item connection_t;int http_response(connection_t *conn)
{const char *html_body = "<html><head><title>chipen.com</title></head><body><h1>chipen</h1></body></html>";int content_length = strlen(html_body);// 生成符合 HTTP 標準格式的 Date 字符串time_t now = time(NULL);struct tm *gmt = gmtime(&now);char date_str[128];strftime(date_str, sizeof(date_str), "Date: %a, %d %b %Y %H:%M:%S GMT\r\n", gmt);// 構建完整的 HTTP 響應conn->wlen = snprintf(conn->wbuffer, BUFFER_LENGTH,"HTTP/1.1 200 OK\r\n""Content-Type: text/html\r\n""Content-Length: %d\r\n""Accept-Ranges: bytes\r\n""%s""\r\n" // Header 與 Body 的分隔符"%s", // HTML 內容content_length,date_str,html_body);return conn->wlen;
}