IO複用(Reactor模式和Preactor模式)——用epoll來提升服務器併發能力

上篇線程/進程併發服務器中提到,提升服務器性能在IO層須要關注兩個地方,一個是文件描述符處理,一個是線程調度。react

IO複用是什麼?IO即Input/Output,在網絡編程中,文件描述符就是一種IO操做。ios

爲何要IO複用?編程

1.網絡編程中很是多函數是阻塞的,如connect,利用IO複用能夠以非阻塞形式執行代碼。數組

2.以前提到listen維護兩個隊列,完成握手的隊列可能有多個就緒的描述符,IO複用能夠批處理描述符。服務器

3.有時候可能要同時處理TCP和UDP,同時監聽多個端口,同時處理讀寫和鏈接等。網絡

爲何epoll效率要比select高?併發

1.在鏈接數量較大的場景,select遍歷須要每一個描述符,epoll由內核維護事件表,只須要處理有響應的描述符。異步

2.select自己處理文件描述符受到限制,默認1024。socket

3.效率並非絕對的,當鏈接率高,斷開和鏈接頻繁時,select不必定比epoll差。因此要根據具體場合使用。函數

epoll的兩種模式,電平觸發和邊沿觸發。

1.電平觸發效率較邊沿觸發低,電平觸發模式下,當epoll_wait返回的事件沒有所有相應處理完畢,內核緩衝區還存在數據時,會反覆通知,直處處理完成。epoll默認使用這種模式。

2.邊沿觸發效率較高,內核緩衝區事件只通知一次。

一個epoll實現demo

  1 #include <iostream>
  2 #include <sys/socket.h>
  3 #include <sys/epoll.h>
  4 #include <netinet/in.h>
  5 #include <arpa/inet.h>
  6 #include <fcntl.h>
  7 #include <unistd.h>
  8 #include <stdio.h>  
  9 #include <stdlib.h>  
 10 #include <string.h>  
 11 #include <errno.h>
 12 
 13 using namespace std;
 14 
 15 #define MAXLINE 5
 16 #define OPEN_MAX 100
 17 #define LISTENQ 20
 18 #define SERV_PORT 5000
 19 #define INFTIM 1000
 20 
 21 int main(int argc, char* argv[])
 22 {
 23     int listen_fd, connfd_fd, socket_fd, epfd, nfds;
 24     ssize_t n;
 25     char line[MAXLINE];
 26     socklen_t clilen;
 27 
 28     //聲明epoll_event結構體的變量,ev用於註冊事件,數組用於回傳要處理的事件
 29     struct epoll_event ev,events[20];
 30     //生成用於處理accept的epoll專用的文件描述符
 31     epfd=epoll_create(5);
 32     struct sockaddr_in clientaddr;
 33     struct sockaddr_in serveraddr;
 34     listen_fd = socket(AF_INET, SOCK_STREAM, 0);
 35     //設置與要處理的事件相關的文件描述符
 36     ev.data.fd = listen_fd;
 37     //設置要處理的事件類型
 38     ev.events=EPOLLIN|EPOLLET;
 39     //註冊epoll事件
 40     epoll_ctl(epfd,EPOLL_CTL_ADD,listen_fd,&ev);
 41     
 42     memset(&serveraddr, 0, sizeof(serveraddr));  
 43     serveraddr.sin_family = AF_INET;  
 44     serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
 45     serveraddr.sin_port = htons(SERV_PORT);
 46     
 47     if (bind(listen_fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1)
 48     {  
 49         printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);  
 50         exit(0);  
 51     }  
 52     
 53     if (listen(listen_fd, LISTENQ) == -1)
 54     {  
 55         exit(0);  
 56     }   
 57     
 58     for ( ; ; ) 
 59     {
 60         //等待epoll事件的發生
 61         nfds = epoll_wait(epfd,events,20,500);
 62         //處理所發生的全部事件
 63         for (int i = 0; i < nfds; ++i)
 64         {
 65             if (events[i].data.fd == listen_fd)//若是新監測到一個SOCKET用戶鏈接到了綁定的SOCKET端口,創建新的鏈接。
 66 
 67             {
 68                 connfd_fd = accept(listen_fd,(sockaddr *)&clientaddr, &clilen);
 69                 if (connfd_fd < 0){
 70                     perror("connfd_fd < 0");
 71                     exit(1);
 72                 }
 73                 char *str = inet_ntoa(clientaddr.sin_addr);
 74                 cout << "accapt a connection from " << str << endl;
 75                 //設置用於讀操做的文件描述符
 76                 ev.data.fd = connfd_fd;
 77                 //設置用於注測的讀操做事件
 78                 ev.events = EPOLLIN|EPOLLET;
 79                 //註冊ev
 80                 epoll_ctl(epfd,EPOLL_CTL_ADD,connfd_fd,&ev);
 81             }
 82             else if (events[i].events&EPOLLIN)//若是是已經鏈接的用戶,而且收到數據,那麼進行讀入。
 83             {            
 84                 memset(&line,'\0', sizeof(line));  
 85                 if ( (socket_fd = events[i].data.fd) < 0)
 86                     continue;
 87                 if ( (n = read(socket_fd, line, MAXLINE)) < 0) {
 88                     if (errno == ECONNRESET) {
 89                         close(socket_fd);
 90                         events[i].data.fd = -1;
 91                     } else
 92                         std::cout<<"readline error"<<std::endl;
 93                 } else if (n == 0) {
 94                     close(socket_fd);
 95                     events[i].data.fd = -1;
 96                 }                
 97                 cout << line << endl;
 98                 //設置用於寫操做的文件描述符
 99                 ev.data.fd = socket_fd;
100                 //設置用於注測的寫操做事件
101                 ev.events = EPOLLOUT|EPOLLET;
102                 //修改socket_fd上要處理的事件爲EPOLLOUT
103                 //epoll_ctl(epfd,EPOLL_CTL_MOD,socket_fd,&ev);
104             }
105             else if (events[i].events&EPOLLOUT) // 若是有數據發送
106             {
107                 socket_fd = events[i].data.fd;
108                 write(socket_fd, line, n);
109                 //設置用於讀操做的文件描述符
110                 ev.data.fd = socket_fd;
111                 //設置用於注測的讀操做事件
112                 ev.events = EPOLLIN|EPOLLET;
113                 //修改socket_fd上要處理的事件爲EPOLIN
114                 epoll_ctl(epfd,EPOLL_CTL_MOD,socket_fd,&ev);
115             }
116         }
117     }
118     return 0;
119 }

 

執行效果以下:

QQ圖片20160512223244

第一次學epoll時,容易錯誤的認爲epoll也能夠實現併發,其實正確的話是epoll能夠實現高性能併發服務器,epoll只是提供了IO複用,在IO「併發」,真正的併發只能經過線程進程實現。

那爲何能夠同時鏈接兩個客戶端呢?實際上這兩個客戶端都是在一個進程上運行的,前面提到過各個描述符之間是相互不影響的,因此是一個進程輪循在處理多個描述符。

Reactor模式:

Reactor模式實現很是簡單,使用同步IO模型,即業務線程處理數據須要主動等待或詢問,主要特色是利用epoll監聽listen描述符是否有相應,及時將客戶鏈接信息放於一個隊列,epoll和隊列都是在主進程/線程中,由子進程/線程來接管各個描述符,對描述符進行下一步操做,包括connect和數據讀寫。主程讀寫就緒事件。

大體流程圖以下:

image

Preactor模式:

Preactor模式徹底將IO處理和業務分離,使用異步IO模型,即內核完成數據處理後主動通知給應用處理,主進程/線程不只要完成listen任務,還須要完成內核數據緩衝區的映射,直接將數據buff傳遞給業務線程,業務線程只須要處理業務邏輯便可。

大體流程以下:

image

相關文章
相關標籤/搜索