上篇线程/进程并发服务器中提到,提高服务器性能在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) ",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,'