上篇線程/進程併發服務器中提到,提升服務器性能在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 }
執行效果以下:
第一次學epoll時,容易錯誤的認爲epoll也能夠實現併發,其實正確的話是epoll能夠實現高性能併發服務器,epoll只是提供了IO複用,在IO「併發」,真正的併發只能經過線程進程實現。
那爲何能夠同時鏈接兩個客戶端呢?實際上這兩個客戶端都是在一個進程上運行的,前面提到過各個描述符之間是相互不影響的,因此是一個進程輪循在處理多個描述符。
Reactor模式:
Reactor模式實現很是簡單,使用同步IO模型,即業務線程處理數據須要主動等待或詢問,主要特色是利用epoll監聽listen描述符是否有相應,及時將客戶鏈接信息放於一個隊列,epoll和隊列都是在主進程/線程中,由子進程/線程來接管各個描述符,對描述符進行下一步操做,包括connect和數據讀寫。主程讀寫就緒事件。
大體流程圖以下:
Preactor模式:
Preactor模式徹底將IO處理和業務分離,使用異步IO模型,即內核完成數據處理後主動通知給應用處理,主進程/線程不只要完成listen任務,還須要完成內核數據緩衝區的映射,直接將數據buff傳遞給業務線程,業務線程只須要處理業務邏輯便可。
大體流程以下: