1、什么是I/O多路復用??
I/O復用無非就是多個進程共同使用一個I/O輸入輸出流。一旦發現進程指定的一個或者多個描述符可進行無阻塞IO訪問時,它就通知該進程。
服務器端工作流程:
調用 socket() 函數創建套接字 用 bind() 函數將創建的套接字與服務端IP地址綁定
調用listen()函數監聽socket() 函數創建的套接字,等待客戶端連接 當客戶端請求到來之后
調用 accept()函數接受連接請求,返回一個對應於此連接的新的套接字,做好通信准備
調用 write()/read() 函數和 send()/recv()函數進行數據的讀寫,通過 accept() 返回的套接字和客戶端進行通信 關閉socket(close)
客戶端工作流程:
調用 socket() 函數創建套接字
調用 connect() 函數連接服務端
調用write()/read() 函數或者 send()/recv() 函數進行數據的讀寫
關閉socket(close)
此技術的目的:I/O 多路復用是為了解決進程或線程阻塞到某個 I/O 系統調用而出現的技術,使進程或線程不阻塞於某個特定的 I/O 系統調用。
什么?聽不懂啥意思? --->沒事,點擊這里:I/O復用的理解
2、IO多路復用適用以下場合:
(1) 當客戶處理多個描述字時(一般是交互式輸入和網絡套接口),必須使用I/O復用。
(2) 當一個客戶同時處理多個套接口時,而這種情況是可能的,但很少出現。
(3) 如果一個TCP服務器既要處理監聽套接口,又要處理已連接套接口,一般也要用到I/O復用。
(4) 如果一個服務器既要處理TCP,又要處理UDP,一般要使用I/O復用。
(5) 如果一個服務器要處理多個服務或多個協議,一般要使用I/O復用。
與多進程和多線程技術相比,I/O多路復用技術的大優勢是系統開銷小,系統不必創建進程/線程,也不必維護這些進程/線程,從而大大減少了系統的開銷。
對於應用層來說,使用非阻塞I/O的應用程序通常會使用select()和poll()系統調用查詢是否可對設備進行無阻塞的訪問。
總的來說,I/O處理的模型有5種:
● 阻塞I/O模型:在這種模型下,若所調用的I/O函數沒有完成相關的功能,則會使進程掛起,直到相關數據到達才會返回。如常見對管道設備、終端設備和網絡設備進行讀寫時經常會出現這種情況。
● 非阻塞I/O模型:在這種模型下,當請求的I/O操作不能完成時,則不讓進程睡眠,而且立即返回。非阻塞I/O使用戶可以調用不會阻塞的I/O操作,如open()、write()和read()。如果該操作不能完成,則會立即返回出錯(如打不開文件)或者返回0(如在緩沖區中沒有數據可以讀取或者沒空間可以寫入數據)。
● I/O多路轉接模型:在這種模型下,如果請求的I/O操作阻塞,且它不是真正阻塞I/O,而是讓其中的一個函數等待,在此期間,I/O還能進行其他操作。如本小節要介紹的select()和poll()函數,就是屬於這種模型。
● 信號驅動I/O模型:在這種模型下,進程要定義一個信號處理程序,系統可以自動捕獲特定信號的到來,從而啟動I/O。這是由內核通知用戶何時可以啟動一個I/O操作決定的。
它是非阻塞的。當有就緒的數據時,內核就向該進程發送SIGIO信號。 無論我們如何處理SIGIO信號,這種模型的好處是當等待數據到達時,可以不阻塞。主程序繼續執行,只有收到SIGIO信號時才去處理數據即可。
● 異步I/O模型:在這種模型下,進程先讓內核啟動I/O操作,並在整個操作完成后通知該進程。這種模型與信號驅動模型的主要區別在於:信號驅動I/O是由內核通知我們何時可以啟動一個I/O操作,而異步I/O模型是由內核通知進程I/O操作何時完成的。現在,並不是所有的系統都支持這種模型。
可以看到,select()和poll()的I/O多路轉接模型是處理I/O復用的一個高效的方法。它可以具體設置程序中每一個所關心的文件描述符的條件、希望等待的時間等,從select()和poll()函數返回時,內核會通知用戶已准備好的文件描述符的數量、已准備好的條件(或事件)等。通過使用select()和poll()函數的返回結果(可能是檢測到某個文件描述符的注冊事件或是超時,或是調用出錯),就可以調用相應的I/O處理函數了。
select系統調用的用途:在一段時間內,監聽用戶感興趣的文件描述符上的可讀,可寫和異常等事件
select原理:
select 函數監視的文件描述符分3類,分別是writefds、readfds、和exceptfds。調用后select函數會阻塞,直到有描述符就緒(有數據 可讀、可寫、或者有except),或者超時(timeout指定等待時間,如果立即返回設為null即可),函數返回。當select函數返回后,可以通過遍歷fdset,來找到就緒的描述符。
poll系統調用:在制定時間內輪詢一定數量的文件描述符,已測試其中是否有就緒者
poll原理:
poll本質上和select沒有區別,它將用戶傳入的數組拷貝到內核空間,然后查詢每個fd對應的設備狀態,如果設備就緒則在設備等待隊列中加入一項並繼續遍歷,如果遍歷完所有fd后沒有發現就緒設備,則掛起當前進程,直到設備就緒或者主動超時,被喚醒后它又要再次遍歷fd。這個過程經歷了多次無謂的遍歷。
epoll系列系統調用:
epoll原理:
epoll支持水平觸發和邊緣觸發,最大的特點在於邊緣觸發,它只告訴進程哪些fd剛剛變為就緒態,並且只會通知一次。還有一個特點是,epoll使用“事件”的就緒通知方式,通過epoll_ctl注冊fd,一旦該fd就緒,內核就會采用類似callback的回調機制來激活該fd,epoll_wait便可以收到通知。
epoll是linux特有的I/O復用函數
文件描述符(用來標識內核中的事件表)的創建有epoll_creat來完成
3、select()函數的語法要點
select()函數根據希望進行的文件操作對文件描述符進行了分類處理,這里對文件描述符的處理主要涉及4個宏函數,如下表所示。
select()文件描述符處理函數
當應用程序使用FD_ZERO/FD_SET/FD_CLR宏設置好要監聽的文件描述符集合后,調用select()函數執行監聽,如果沒有一個描述符准備好IO並且沒有指定超時時間,那么select()函數會一直等待下去不會返回。
當函數正常返回后,監聽的文件描述符集合中沒有准備好的文件描述符會被刪除,只剩下已經准備好的文件描述符(但是不知道是哪一個,導致后面要用FD_ISSET來判斷),之后可以使用FD_ISSET(fd, set);宏來判斷set集合中是否有fd文件描述符來判斷fd是否准備好IO。
一般來說,在每次使用select()函數之前,首先使用FD_ZERO()和FD_SET()來初始化文件描述符集(在需要重復調用select()函數時,先把一次初始化好的文件描述符集備份下來,每次讀取它即可)。在select()函數返回后,可循環使用FD_ISSET()來測試描述符集,在執行完對相關文件描述符的操作后,使用FD_CLR()來清除描述符集。
另外,select()函數中的timeout是一個struct timeval類型的指針,該結構體如下所示:
struct timeval
{
long tv_sec; /* 秒 */
long tv_unsec; /* 微秒 */
}
當使用select()函數時,存在一系列的問題,例如,內核必須檢查多余的文件描述符,每次調用select()之后必須重置被監聽的文件描述符集,而且可監聽的文件個數受限制(使用FD_SETSIZE宏來表示fd_set結構能夠容納的文件描述符的大數目)等。
基本流程,如圖所示:
多個客戶端與服務端同時通信:
/* update_select_server.cpp */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/types.h>
#include <sys/time.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/errno.h> ///errno的頭文件,不能用errno.h
const int BUFFER_SIZE = 2048;
const int SERVER_PORT = 4321;
const int CLIENT_MAX_COUNT = 15;//客戶端最大鏈接數
inline int max(int a,int b){ return (a>b ? (a+1) : (b+1)); }//內聯函數
int exit_size = 0;
int main(int argc, char const *argv[])
{
int server_socket_fd;//服務端套接字標識符
int maxfd;//最大標識符
int connfd,sockfd;//客戶端連接的標識符
int real_client_count = 0;//客戶端真實連接個數
int client_socket_fd[CLIENT_MAX_COUNT];//客戶端的標識符
int j;
struct sockaddr_in server_addr;//服務端的信息
struct sockaddr_in client_addr;//客戶端的信息
char buffer[BUFFER_SIZE] = {0};//接受客戶端的消息
fd_set fds,allset;//文件標識符集
int ret;//返回值
int n;//接受的字節數
struct timeval tv;
/* 創建套節字 */
server_socket_fd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if (server_socket_fd < 0) {
fprintf(stderr,"create socket ERROR:%s\n",strerror(errno));
exit(1);
}
/* 將套接字和IP、端口綁定 */
memset(&server_addr, 0, sizeof(server_addr)); //每個字節都用0初始化
server_addr.sin_family = AF_INET;//使用ipv4地址
//server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具體的IP地址
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); //具體的IP地址
server_addr.sin_port = htons(SERVER_PORT); //端口
bind(server_socket_fd,(struct sockaddr*)&server_addr,sizeof(server_addr));
/* 創建套接字監聽隊列 */
if (listen(server_socket_fd,CLIENT_MAX_COUNT) <0 ) {//監聽隊列最大的長度為15
fprintf(stderr,"listen ERROR:%s\n",strerror(errno));
close(server_socket_fd);
exit(1);
}
printf("I am server.waiting!!!!\n");
socklen_t client_addr_size = sizeof(struct sockaddr_in);//客戶端信息的長度
maxfd = server_socket_fd;
for (int i = 0; i < CLIENT_MAX_COUNT; i++)
{
/* code */
client_socket_fd[i] = -1;//初始化為-1
}
FD_ZERO(&fds);//清空文件描述符集
FD_SET(0,&allset);//把標准輸入加進集合中
FD_SET(server_socket_fd,&allset);
tv.tv_sec = 100;//100秒鍾
tv.tv_usec = 0;//0微秒
while (1)
{
/* code */
FD_ZERO(&fds);//清空文件描述符集
fds = allset;
switch (select(maxfd+1, &fds, NULL, NULL, NULL))
{
case -1:
perror("select():");
exit(1);
break;
case 0:
printf("拐子,搞快點設,時間超時了!\n");
break;
default:
if (FD_ISSET(server_socket_fd, &fds)) {//客戶端有連接請求
connfd = accept(server_socket_fd,(struct sockaddr*)&client_addr,&client_addr_size);
if (-1 == connfd) {
perror("socket accept:");
exit(1);
} else {
printf("connect form %s:%d\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
for (j = 0; j < CLIENT_MAX_COUNT; j++) {
/* 把當前連接的客戶端的標識符加入到數組中 */
if (-1 == client_socket_fd[j]) {
client_socket_fd[j] = connfd;
real_client_count++;
break;
}
}
FD_SET(connfd,&allset);//添加此客戶端標識符給allset集合
if (connfd > maxfd) {
maxfd = connfd;
}
if (real_client_count > CLIENT_MAX_COUNT) {
real_client_count = CLIENT_MAX_COUNT;//真實連接不能超過最大連接
}
}
break;
}
if (FD_ISSET(0, &fds)) {
memset(buffer, 0, BUFFER_SIZE);
fgets(buffer, BUFFER_SIZE, stdin);
printf("從標准輸入的數據為:%s\n", buffer);
if (0 == strncmp(buffer, "exit", 4)) {
printf("I will exit!\n");
for (int x = 0; x < real_client_count; ++x) {
if (client_socket_fd[x] != -1) {
close(client_socket_fd[x]);
}
}
return 0;
}
}
//對客戶端進行讀操作
printf("對客戶端進行操作\n");
for (int k = 0;k < real_client_count; ++k) {
if ((sockfd = client_socket_fd[k]) < 0) {
continue; // 沒有連接數
}
if (FD_ISSET(sockfd, &fds)) {
n = recv(sockfd,buffer,BUFFER_SIZE,0);
printf("從客戶端傳來的數據為:%d個\n",n);
if (n >0) {
buffer[n] = '\0';
printf("來自客戶端%d的數據為:%s\n",k+1,buffer);
}else if (0 == n) {
printf("客戶端%d退出\n",k+1);
FD_CLR(client_socket_fd[k], &allset);
client_socket_fd[k] = -1;
continue;
}
}
}
break;
}
}
/* 關閉套接字 */
close(server_socket_fd);
return 0;
}
/* multiplex_client1.cpp */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
const int BUFFER_SIZE = 2048;
int main(int argc, char const *argv[])
{
/* code */
int client_socket_fd;
int result;
struct sockaddr_in server_addr;//服務端的信息
/* 創建套節字 */
client_socket_fd = socket(AF_INET,SOCK_STREAM,0);
/* 將套接字和IP、端口綁定 */
memset(&server_addr, 0, sizeof(server_addr)); //每個字節都用0初始化
server_addr.sin_family = AF_INET;//使用ipv4地址
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具體的IP地址
server_addr.sin_port = htons(4321); //端口
result = connect(client_socket_fd,(struct sockaddr*)&server_addr,sizeof(server_addr));
if(result == -1)
{
perror("ops:client\n");
exit(1);
}
char sendmsg[BUFFER_SIZE] = {0};//發送數據
while (1)
{
/* 發送數據 */
memset(sendmsg,0,BUFFER_SIZE);
printf("請輸入你要發給服務端的數據:\n");
fgets(sendmsg,BUFFER_SIZE,stdin);
//write(client_socket_fd,sendmsg,BUFFER_SIZE);
send(client_socket_fd,sendmsg,strlen(sendmsg),0);
if (0 == strncmp(sendmsg,"exit",4)) {
break;
}
}
/* 關閉套接字 */
close(client_socket_fd);
return 0;
}
客戶端二:
/* multiplex_client2.cpp */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
const int BUFFER_SIZE = 2048;
int main(int argc, char const *argv[])
{
/* code */
int client_socket_fd;
int result;
struct sockaddr_in server_addr;//服務端的信息
/* 創建套節字 */
client_socket_fd = socket(AF_INET,SOCK_STREAM,0);
/* 將套接字和IP、端口綁定 */
memset(&server_addr, 0, sizeof(server_addr)); //每個字節都用0初始化
server_addr.sin_family = AF_INET;//使用ipv4地址
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具體的IP地址
server_addr.sin_port = htons(4321); //端口
result = connect(client_socket_fd,(struct sockaddr*)&server_addr,sizeof(server_addr));
if(result == -1)
{
perror("ops:client\n");
exit(1);
}
char sendmsg[BUFFER_SIZE] = "你好,我是客戶端2";//發送數據
while (1)
{
/* 發送數據 */
send(client_socket_fd,sendmsg,strlen(sendmsg),0);
sleep(4);
}
/* 關閉套接字 */
close(client_socket_fd);
return 0;
}
當然,可以把客戶端一進程開多個與服務端進行通信。
select的優點:
1、select目前幾乎在所有的平台上支持,其良好跨平台支持也是它的一個優點
select本質上是通過設置或者檢查存放fd標志位的數據結構來進行下一步處理,這樣所帶來的缺點是::
1、select最大的缺陷就是單個進程所打開的FD是有一定限制的,它由FD_SETSIZE設置,默認值是1024。可以通過修改宏定義甚至重新編譯內核的方式提升這一限制,但是這樣也會造成效率的降低。
一般來說這個數目和系統內存關系很大,具體數目可以cat /proc/sys/fs/file-max察看。32位機默認是1024個。64位機默認是2048.
2、對socket進行掃描時是線性掃描,即采用輪詢的方法,效率較低。
當套接字比較多的時候,每次select()都要通過遍歷FD_SETSIZE個Socket來完成調度,不管哪個Socket是活躍的,都遍歷一遍。這會浪費很多CPU時間。如果能給套接字注冊某個回調函數,當他們活躍時,自動完成相關操作,那就避免了輪詢,這正是epoll與kqueue做的。
3、需要維護一個用來存放大量fd的數據結構,這樣會使得用戶空間和內核空間在傳遞該結構時復制開銷大。
4、poll()函數的語法要點
每一個pollfd結構體指定了一個被監視的文件描述符,可以傳遞多個結構體,指示poll()監視多個文件描述符。每個結構體的events域是監視該文件描述符的事件掩碼,由用戶來設置這個域。revents域是文件描述符的操作結果事件掩碼,內核在調用返回時設置這個域。
events域中請求的任何事件都可能在revents域中返回。合法的事件如下:
標志 | 解釋 |
---|---|
POLLIN | 有數據可讀。 |
POLLRDNORM | 有普通數據可讀。 |
POLLRDBAND | 有優先數據可讀。 |
POLLPRI | 有緊迫數據可讀。 |
POLLOUT | 寫數據不會導致阻塞。 |
POLLWRNORM | 寫普通數據不會導致阻塞。 |
POLLWRBAND | 寫優先數據不會導致阻塞。 |
POLLMSGSIGPOLL | 消息可用。 |
POLLER | 指定的文件描述符發生錯誤。 |
POLLHUP | 指定的文件描述符掛起事件。 |
POLLNVAL | 指定的文件描述符非法。 |
這些事件在events域中無意義,因為它們在合適的時候總是會從revents中返回。
使用poll()和select()不一樣,你不需要顯式地請求異常情況報告。
poll的優點:
1、它沒有最大連接數的限制,原因是它是基於鏈表來存儲的。
poll的缺點:
1、大量的fd的數組被整體復制於用戶態和內核地址空間之間,而不管這樣的復制是不是有意義。
2、poll還有一個特點是“水平觸發”,如果報告了fd后,沒有被處理,那么下次poll時會再次報告該fd。
POLLIN等價於POLLRDNORM |POLLRDBAND,而POLLOUT則等價於POLLWRNORM。例如,要同時監視一個文件描述符是否可讀和可寫,我們可以設置 events為POLLIN |POLLOUT。在poll返回時,我們可以檢查revents中的標志,對應於文件描述符請求的events結構體。如果POLLIN事件被設置,則文件描述符可以被讀取而不阻塞。如果POLLOUT被設置,則文件描述符可以寫入而不導致阻塞。這些標志並不是互斥的:它們可能被同時設置,表示這個文件描述符的讀取和寫入操作都會正常返回而不阻塞。
select()和poll()函數的比較
select()和poll()函數本質上沒有多大差別,管理多個描述符也是進行輪詢,根據描述符的狀態進行處理,但是poll沒有大文件描述符數量的限制。並且select()返回后,之前沒有准備好的文件描述符會從集合當中刪除,這樣如果下次需要再次添加所有文件描述符或者使用兩個相同的文件描述符集合,一個用於備份,一個用於監聽,比較復雜。poll不需要這個復雜的操作。poll和select同樣存在一個缺點就是包含大量文件描述符的數組被整體復制於用戶態和內核的地址空間之間,而無論這些文件描述符是否就緒。它的開銷隨着文件描述符數量的增加而線性增加。
一個服務端與多個客戶端進行通信----socket---poll
/*************************************************************************
> File Name: socket_poll.cpp
> Author: XiaZhaoJian
> Mail: xiazhaojian@iauto.com
> Created Time: 2019年 08月 09日 星期五 13:49:29 CST
************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>
#include <sys/types.h>
#include <sys/time.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/errno.h> ///errno的頭文件,不能用errno.h
const int BUFFER_SIZE = 2048;//緩沖池大小,用於接受消息的
const int SERVER_PORT = 4321;//端口號
const int CLIENT_MAX_COUNT = 50;//客戶端最大鏈接數
int main(int argc, char const *argv[])
{
int server_socket_fd;//服務端套接字標識符
int connfd,sockfd;//客戶端連接的標識符
int real_client_index = 0;//客戶端client_socket_fd最大不空閑下標
int n;//接受的字節數
int i;
char buffer[BUFFER_SIZE] = {0};//接受客戶端的消息
struct sockaddr_in server_addr;//服務端的信息
struct sockaddr_in client_addr;//客戶端的信息
struct pollfd client_socket_fd[CLIENT_MAX_COUNT];//客戶端的標識符
/* 創建套節字 */
server_socket_fd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if (server_socket_fd < 0) {
fprintf(stderr,"create socket ERROR:%s\n",strerror(errno));
exit(1);
}
/* 將套接字和IP、端口綁定 */
memset(&server_addr, 0, sizeof(server_addr)); //每個字節都用0初始化
server_addr.sin_family = AF_INET;//使用ipv4地址
//server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具體的IP地址
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); //具體的IP地址
server_addr.sin_port = htons(SERVER_PORT); //端口
bind(server_socket_fd,(struct sockaddr*)&server_addr,sizeof(server_addr));
/* 創建套接字監聽隊列 */
if (listen(server_socket_fd,CLIENT_MAX_COUNT) <0 ) {//監聽隊列最大的長度為15
fprintf(stderr,"listen ERROR:%s\n",strerror(errno));
close(server_socket_fd);
exit(1);
}
printf("I am server.waiting!!!!\n");
socklen_t client_addr_size = sizeof(struct sockaddr_in);//客戶端信息的長度
for (i = 0; i < CLIENT_MAX_COUNT; i++)
{
/* code */
client_socket_fd[i].fd = -1;//初始化為-1
}
client_socket_fd[0].fd = server_socket_fd;//把服務端當前Socket套接字加入監聽中
client_socket_fd[0].events = POLLIN;//數據可讀標志事件
while (1) {
switch (poll(client_socket_fd,real_client_index+1,-1))
{
case -1:
perror("poll():");
exit(1);
break;
case 0:
printf("拐子,搞快點設,時間超時了!\n");
break;
default:
if(client_socket_fd[0].revents & POLLIN) {//客戶端有連接請求
connfd = accept(server_socket_fd,(struct sockaddr*)&client_addr,&client_addr_size);
if (-1 == connfd) {
perror("socket accept:");
exit(1);
} else {
printf("connect form %s:%d\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
for (i = 0; i < CLIENT_MAX_COUNT; i++) {
/* 把當前連接的客戶端的標識符加入到數組中 */
if (-1 == client_socket_fd[i].fd) {
client_socket_fd[i].fd = connfd;
if (i > real_client_index) {
real_client_index = i;
}
break;
}
}
if (i == CLIENT_MAX_COUNT) {
fprintf(stderr, "too many clients\n");
exit(1);
}
client_socket_fd[i].events = POLLIN;//添加此客戶端標識符讀事件
}
break;
}
for (i = 0; i <= real_client_index; ++i) {
if ((sockfd = client_socket_fd[i].fd) < 0) {
continue; // 沒有連接數
}
if (client_socket_fd[i].revents & POLLIN) {
n = recv(sockfd,buffer,BUFFER_SIZE,0);
if (n < 0) {
perror("recv():");
exit(1);
}
printf("從客戶端傳來的數據為:%d個\n",n);
if (n >0) {
buffer[n] = '\0';
printf("來自客戶端%d的數據為:%s\n",i+1,buffer);
}else if (0 == n) {
printf("客戶端%d退出\n",i+1);
client_socket_fd[i].fd = -1;
close(sockfd);
continue;
}
}
}
break;
}
}
/* 關閉套接字 */
close(server_socket_fd);
return 0;
}
5、epoll(epoll生動講解)
epoll是在2.6內核中提出的,是之前的select和poll的增強版本。相對於select和poll來說,epoll更加靈活,沒有描述符限制。epoll使用一個文件描述符管理多個描述符,將用戶關系的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的copy只需一次。
epoll接口是linux中特有的,其他平台是不支持的。epoll接口可以檢測的描述符的個數要遠大於select,而且使用的靈活性更高。接下來我們來分析一下epoll的函數API,它主要包括三個函數:epoll_create、epoll_ctl、epoll_wait。
int epoll_create(int size);
功能:創建一個epoll的標示符。
參數:size 現在已經無用,並不表示檢測的大值。
返回值:就是獲得的epoll的標示符。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:控制需要檢測的文件描述符。
參數:
epfd: epoll_create得到的標識符。
op:表示此次調用要執行的操作,包括添加描述符、刪除和修改屬性。
EPOLL_CTL_ADD 添加描述符
EPOLL_CTL_DEL 刪除描述符
EPOLL_CTL_MOD 修改描述符觸發的屬性
fd: 要檢測的文件描述符的值
event: 執行要檢測的描述符的特性,結構體成員如下
struct epoll_event
{
uint32_t events; /* Epoll events ,指定描述符被檢測的條件*/
epoll_data_t data; /* User data variable */
};
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
功能:等待就緒的文件描述符
參數:epfd: epoll_create得到的標識符
events: 用來存儲就緒的文件描述符狀態的結構體數組指針
maxevents: 指定要檢測的描述符的大個數
timeout: 指定超時檢測的時間(如果不指定超時,將該參數指定為-1即可)
返回值:就緒的文件描述符的個數。
基於socket-epoll,一對多通信:
/*************************************************************************
> File Name: socket_epoll.cpp
> Author: XiaZhaoJian
> Mail: xiazhaojian@iauto.com
> Created Time: 2019年 08月 09日 星期五 16:49:59 CST
************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <sys/time.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/errno.h> ///errno的頭文件,不能用errno.h
const int BUFFER_SIZE = 2048;//緩沖池大小,用於接受消息的
const int SERVER_PORT = 4321;//端口號
const int MAX_EPOLL_EVENTS = 520;//最大連接數
const int CLIENT_MAX_COUNT = 50;//客戶端最大鏈接數
//epoll描述符
int epollfd;
//事件數組
struct epoll_event eventList[MAX_EPOLL_EVENTS];
int main(int argc, char const *argv[])
{
int server_socket_fd;//服務端套接字標識符
int connfd,sockfd;//客戶端連接的標識符
int ret;//epoll_wait的返回值
int n;//接受的字節數
int i;
//當前的連接數
int currentClient = 0;
char buffer[BUFFER_SIZE] = {0};//接受客戶端的消息
struct sockaddr_in server_addr;//服務端的信息
struct sockaddr_in client_addr;//客戶端的信息
/* 創建套節字 */
server_socket_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (server_socket_fd < 0) {
fprintf(stderr,"create socket ERROR:%s\n",strerror(errno));
exit(1);
}
/* 將套接字和IP、端口綁定 */
memset(&server_addr, 0, sizeof(server_addr)); //每個字節都用0初始化
server_addr.sin_family = AF_INET;//使用ipv4地址
//server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具體的IP地址
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); //具體的IP地址
server_addr.sin_port = htons(SERVER_PORT); //端口
bind(server_socket_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
/* 創建套接字監聽隊列 */
if (listen(server_socket_fd, CLIENT_MAX_COUNT) <0 ) {//監聽隊列最大的長度為15
fprintf(stderr, "listen ERROR:%s\n", strerror(errno));
close(server_socket_fd);
exit(1);
}
printf("I am server.waiting!!!!\n");
socklen_t client_addr_size = sizeof(struct sockaddr_in);//客戶端信息的長度
/* 初始化epoll */
epollfd = epoll_create(MAX_EPOLL_EVENTS);//生成epoll標識符
if (-1 == epollfd) {
fprintf(stderr, "epoll_create failed!!!:%s\n", strerror(errno));
exit(1);
}
struct epoll_event myevent;
myevent.data.fd = server_socket_fd;
myevent.events = EPOLLIN | EPOLLET;
/* 添加 */
if (-1 == epoll_ctl(epollfd,EPOLL_CTL_ADD,server_socket_fd,&myevent)) {
fprintf(stderr, "epoll_ctl failed!!!:%s\n", strerror(errno));
exit(1);
}
int timeout = -1;//超時時間
/* 循環監聽-epoll */
while (1) {
/* 返回就緒的文件描述符的個數 */
ret = epoll_wait(epollfd,eventList,MAX_EPOLL_EVENTS,timeout);
switch (ret)
{
case -1:
fprintf(stderr, "epoll_wait error!!!:%s\n", strerror(errno));
exit(1);
break;
case 0:
printf("the waiting time is running out!\n");
break;
default:
for (int i = 0; i < ret; ++i) {
if (eventList[i].data.fd == server_socket_fd) {//表示有客戶端的連接請求了
connfd = accept(server_socket_fd,(struct sockaddr*)&client_addr,&client_addr_size);
if (-1 == connfd) {
perror("socket accept:");
exit(1);
}else {
printf("connect form %s:%d\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
myevent.data.fd = connfd;
myevent.events = EPOLLIN|EPOLLET;
epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &myevent);
}
}else {
n = recv(eventList[i].data.fd,buffer,BUFFER_SIZE,0);
if (n < 0) {
perror("recv():");
exit(1);
}
printf("從客戶端傳來的數據為:%d個\n",n);
if (n >0) {
buffer[n] = '\0';
printf("來自客戶端的數據為:%s\n",buffer);
}else if (0 == n) {
printf("客戶端%d退出\n",i+1);
close(eventList[i].data.fd);
continue;
}
}
}
break;
}
}
/* 關閉套接字 */
close(epollfd);
close(server_socket_fd);
return 0;
}
epoll的優點:
1、沒有最大並發連接的限制,能打開的FD的上限遠大於1024(1G的內存上能監聽約10萬個端口)。
2、效率提升,不是輪詢的方式,不會隨着FD數目的增加效率下降。只有活躍可用的FD才會調用callback函數;即Epoll最大的優點就在於它只管你“活躍”的連接,而跟連接總數無關,因此在實際的網絡環境中,Epoll的效率就會遠遠高於select和poll。
3、內存拷貝,利用mmap()文件映射內存加速與內核空間的消息傳遞;即epoll使用mmap減少復制開銷。
epoll在原理上與select和poll的不同之處
在select/poll中,進程只有在調用一定的方法后,內核才對所有監視的文件描述符進行掃描,而epoll事先通過epoll_ctl()來注冊一個文件描述符,一旦基於某個文件描述符就緒時,內核會采用類似callback的回調機制,迅速激活這個文件描述符,當進程調用epoll_wait()時便得到通知。(此處去掉了遍歷文件描述符,而是通過監聽回調的的機制。這正是epoll的魅力所在。)
注意:
如果沒有大量的idle-connection或者dead-connection,epoll的效率並不會比select/poll高很多,但是當遇到大量的idle-connection,就會發現epoll的效率大大高於select/poll。