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

上篇线程/进程并发服务器中提到,提高服务器性能在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传递给业务线程,业务线程只需要处理业务逻辑即可。

大致流程如下:

时间: 2024-10-08 21:08:57

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

《深入理解计算机系统》Tiny服务器4——epoll类型IO复用版Tiny

前几篇博客分别讲了基于多进程.select类型的IO复用.poll类型的IO复用以及多线程版本的Tiny服务器模型,并给出了主要的代码.至于剩下的epoll类型的IO复用版,本来打算草草带过,毕竟和其他两种IO复用模型差不太多.但今天在看Michael Kerrisk的<Linux/UNIX系统编程手册>时,看到了一章专门用来讲解epoll函数,及其IO复用模型.于是,自己也就动手把Tiny改版了一下.感兴趣的同学可以参考上述手册的下册1113页,有对于epoll比较详细的讲解. 前边针对IO

高性能IO之Reactor模式

讲到高性能IO绕不开Reactor模式,它是大多数IO相关组件如Netty.Redis在使用的IO模式,为什么需要这种模式,它是如何设计来解决高性能并发的呢? 最最原始的网络编程思路就是服务器用一个while循环,不断监听端口是否有新的套接字连接,如果有,那么就调用一个处理函数处理,类似:while(true){ socket = accept(); handle(socket) } 这种方法的最大问题是无法并发,效率太低,如果当前的请求没有处理完,那么后面的请求只能被阻塞,服务器的吞吐量太低.

高性能IO设计中的Reactor模式与Proactor模式

在高性能的IO设计中,有两个比较著名的模式Reactor和Proactor模式,其中Reactor模式用于同步I/O,而Proactor运用于异步I/O操作.在比较这两个模式之前,我们首先要搞明白几个概念.什么是阻塞和非阻塞?什么是同步和异步?同步和异步是针对应用程序和内核的交互而言的,同步指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪,而异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知(异步的特点就是通知).而阻塞和非阻塞是针

对于观察者模式,Reactor模式,Proactor模式的一点理解

最近就服务器程序IO效率这一块了解一下设计模式中的Reacotr模式和proactor模式,感觉跟观察者模式有些类似的地方,网上也看了一些其他人对三者之间区别的理解,都讲得很仔细,在此根据自己的理解做一点简单的记录和总结,如果理解不对的地方,以后再慢慢深入和更新. 观察者模式: 也可以称为为 发布-订阅 模式,主要适用于多个对象依赖某一个对象的状态并,当某对象状态发生改变时,要通知其他依赖对象做出更新.是一种1对多的关系.当然,如果依赖的对象只有一个时也是一种特殊的一对一关系.通常,观察者模式适

Reactor模式与Proactor模式

该文章总结了网上资源对这两种模式的描述 原文地址:http://www.cnblogs.com/dawen/archive/2011/05/18/2050358.html 1.标准定义 两种I/O多路复用模式:Reactor和Proactor 一般地,I/O多路复用机制都依赖于一个事件多路分离器(Event Demultiplexer).分离器对象可将来自事件源的I/O事件分离出来,并分发到对应的read/write事件处理器(Event Handler).开发人员预先注册需要处理的事件及其事件

IO模型之二-linux网络IO模式select,poll,epoll

1.概述 select,poll,epoll都是IO多路复用的机制.I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作.但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的: 2.select select 的方法原型,int select (int n, fd_set *readfds, fd_set *writefds, fd

简单聊下IO复用

没图,不分析API Java中IO API的发展:Socket -> SocketChannel -> AsynchronousSocketChannelServerSocket -> ServerSocketChannel -> AsynchronousServerSocketChannel 同步/阻塞 -> 同步/非阻塞(多路复用) -> 异步 想简单聊下多路复用.多路复用需要配合Reactor模式,前者解决技术上的问题,后者解决软件工程的问题. 技术上的问题,是将

(转)IO复用,AIO,BIO,NIO,同步,异步,阻塞和非阻塞 区别

本文来自:https://www.cnblogs.com/aspirant/p/6877350.html?utm_source=itdadao&utm_medium=referral,非常感谢. 如果面试问到IO操作,这篇文章提到的问题,基本是必问,百度的面试官问我三个问题 (1)什么是NIO(Non-blocked IO),AIO,BIO (2) java IO 与 NIO(New IO)的区别 (3)select 与 epoll,poll区别 我胡乱说了一气,自己边说边觉得完蛋了.果然,二面

IO复用

IO复用简单介绍 IO复用使得程序能同一时候监听多个文件描写叙述符.这对提高程序的性能至关重要.通常.网络程序在下列情况下须要使用IO复用技术: client程序要同一时候处理多个socket. client程序要同一时候处理用户输入和网络连接. TCPserver同一时候处理监听socket和连接socket. server要同一时候处理TCP请求和UDP请求. 须要指出的是.IO复用尽管能同一时候监听多个文件描写叙述符,但它本身是堵塞的.而且当多个文件描写叙述符同一时候就绪时,假设不採取额外