IO复用(Reactor模式和Preactor模式)——用epoll来提高服务器并发能力

前端之家收集整理的这篇文章主要介绍了IO复用(Reactor模式和Preactor模式)——用epoll来提高服务器并发能力前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

文章来源:http://www.cnblogs.com/binchen-china/p/5487795.html

上篇线程/进程并发服务器中提到,提高服务器性能在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. 1 #include <iostream>
  2. 2 #include <sys/socket.h>
  3. 3 #include <sys/epoll.h>
  4. 4 #include <netinet/in.h>
  5. 5 #include <arpa/inet.h>
  6. 6 #include <fcntl.h>
  7. 7 #include <unistd.h>
  8. 8 #include <stdio.h>
  9. 9 #include <stdlib.h>
  10. 10 #include <string.h>
  11. 11 #include <errno.h>
  12. 12
  13. 13 using namespace std;
  14. 14
  15. 15 #define MAXLINE 5
  16. 16 #define OPEN_MAX 100
  17. 17 #define LISTENQ 20
  18. 18 #define SERV_PORT 5000
  19. 19 #define INFTIM 1000
  20. 20
  21. 21 int main(int argc,char* argv[])
  22. 22 {
  23. 23 int listen_fd,connfd_fd,socket_fd,epfd,nfds;
  24. 24 ssize_t n;
  25. 25 char line[MAXLINE];
  26. 26 socklen_t clilen;
  27. 27
  28. 28 //声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件
  29. 29 struct epoll_event ev,events[20];
  30. 30 生成用于处理acceptepoll专用的文件描述符
  31. 31 epfd=epoll_create(5);
  32. 32 struct sockaddr_in clientaddr;
  33. 33 struct sockaddr_in serveraddr;
  34. 34 listen_fd = socket(AF_INET,SOCK_STREAM,0);
  35. 35 设置与要处理的事件相关的文件描述符
  36. 36 ev.data.fd = listen_fd;
  37. 37 设置要处理的事件类型
  38. 38 ev.events=EPOLLIN|EPOLLET;
  39. 39 注册epoll事件
  40. 40 epoll_ctl(epfd,EPOLL_CTL_ADD,listen_fd,&ev);
  41. 41
  42. 42 memset(&serveraddr,128); line-height:1.5!important">0,255); line-height:1.5!important">sizeof(serveraddr));
  43. 43 serveraddr.sin_family = AF_INET;
  44. 44 serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
  45. 45 serveraddr.sin_port = htons(SERV_PORT);
  46. 46
  47. 47 if (bind(listen_fd,(struct sockaddr*)&serveraddr,255); line-height:1.5!important">sizeof(serveraddr)) == -1)
  48. 48 {
  49. 49 printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);
  50. 50 exit(0);
  51. 51 }
  52. 52
  53. 53 if (listen(listen_fd,LISTENQ) == - 54 {
  54. 55 exit( 56 }
  55. 57
  56. 58 for ( ; ; )
  57. 59 {
  58. 60 等待epoll事件的发生
  59. 61 nfds = epoll_wait(epfd,events,128); line-height:1.5!important">20,128); line-height:1.5!important">500);
  60. 62 处理所发生的所有事件
  61. 63 for (int i = 0; i < nfds; ++i)
  62. 64 {
  63. 65 if (events[i].data.fd == listen_fd)如果新监测到一个SOCKET用户连接到了绑定的SOCKET端口,建立新的连接。
  64. 66
  65. 67 {
  66. 68 connfd_fd = accept(listen_fd,(sockaddr *)&clientaddr,&clilen);
  67. 69 if (connfd_fd < 0){
  68. 70 perror(connfd_fd < 0");
  69. 71 exit(1);
  70. 72 }
  71. 73 char *str = inet_ntoa(clientaddr.sin_addr);
  72. 74 cout << accapt a connection from " << str << endl;
  73. 75 设置用于读操作的文件描述符
  74. 76 ev.data.fd = connfd_fd;
  75. 77 设置用于注测的读操作事件
  76. 78 ev.events = EPOLLIN|EPOLLET;
  77. 79 注册ev
  78. 80 epoll_ctl(epfd,128); line-height:1.5!important"> 81 }
  79. 82 else if (events[i].events&EPOLLIN)如果是已经连接的用户,并且收到数据,那么进行读入。
  80. 83 {
  81. 84 memset(&line,0); line-height:1.5!important">'\0',255); line-height:1.5!important">sizeof(line));
  82. 85 if ( (socket_fd = events[i].data.fd) < 0)
  83. 86 continue;
  84. 87 if ( (n = read(socket_fd,line,MAXLINE)) < 0) {
  85. 88 if (errno == ECONNRESET) {
  86. 89 close(socket_fd);
  87. 90 events[i].data.fd = -1;
  88. 91 } else
  89. 92 std::cout<<readline error"<<std::endl;
  90. 93 } if (n == 94 close(socket_fd);
  91. 95 events[i].data.fd = - 96 }
  92. 97 cout << line << endl;
  93. 98 设置用于写操作的文件描述符
  94. 99 ev.data.fd = socket_fd;
  95. 100 设置用于注测的写操作事件
  96. 101 ev.events = EPOLLOUT|EPOLLET;
  97. 102 修改socket_fd上要处理的事件为EPOLLOUT
  98. 103 epoll_ctl(epfd,EPOLL_CTL_MOD,&ev);
  99. 104 }
  100. 105 if (events[i].events&EPOLLOUT) 如果有数据发送
  101. 106 {
  102. 107 socket_fd = events[i].data.fd;
  103. 108 write(socket_fd,n);
  104. 109 110 ev.data.fd = socket_fd;
  105. 111 112 ev.events = EPOLLIN|EPOLLET;
  106. 113 修改socket_fd上要处理的事件为EPOLIN
  107. 114 epoll_ctl(epfd,128); line-height:1.5!important">115 }
  108. 116 }
  109. 117 }
  110. 118 return 0;
  111. 119 }

执行效果如下:

第一次学epoll时,容易错误的认为epoll也可以实现并发,其实正确的话是epoll可以实现高性能并发服务器,epoll只是提供了IO复用,在IO“并发”,真正的并发只能通过线程进程实现。

那为什么可以同时连接两个客户端呢?实际上这两个客户端都是在一个进程上运行的,前面提到过各个描述符之间是相互不影响的,所以是一个进程轮循在处理多个描述符。

Reactor模式:

Reactor模式实现非常简单,使用同步IO模型,即业务线程处理数据需要主动等待或询问,主要特点是利用epoll监听listen描述符是否有相应,及时将客户连接信息放于一个队列,epoll和队列都是在主进程/线程中,由子进程/线程来接管各个描述符,对描述符进行下一步操作,包括connect和数据读写。主程读写就绪事件。

大致流程图如下:

Preactor模式:

Preactor模式完全将IO处理和业务分离,使用异步IO模型,即内核完成数据处理后主动通知给应用处理,主进程/线程不仅要完成listen任务,还需要完成内核数据缓冲区的映射,直接将数据buff传递给业务线程,业务线程只需要处理业务逻辑即可。

大致流程如下:

猜你在找的React相关文章