上篇線程/進程併發服務器中提到,提高服務器性能在IO層需要關注兩個地方,一個是文件描述符處理,一個是線程調度。
IO複用是什麼?IO即Input/Output,在網絡編程中,文件描述符就是一種IO操作。
為什麼要IO複用?
1.網絡編程中非常多函數是阻塞的,如connect,利用IO複用可以以非阻塞形式執行代碼。
2.之前提到listen維護兩個隊列,完成握手的隊列可能有多個就緒的描述符,IO複用可以批處理描述符。
3.有時候可能要同時處理TCP和UDP,同時監聽多個端口,同時處理讀寫和連接等。
為什麼epoll效率要比select高?
1.在連接數量較大的場景,select遍歷需要每個描述符,epoll由內核維護事件表,只需要處理有響應的描述符。
2.select本身處理文件描述符受到限制,默認1024。
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傳遞給業務線程,業務線程只需要處理業務邏輯即可。
大致流程如下: