实现百万并发连接的Reactor

作为一个高性能服务器程序通常需要处理三类事件:I/O事件,定时事件及信号。两种高效的事件处理模型:Reactor和Proactor。

Reactor是Linux基于epoll实现的事件模型,Proactor是Windows基于IOCP的异步事件处理。

一、Reactor模型

首先来回想一下普通函数调用的机制:程序调用某函数,函数执行,程序等待,函数将 结果和控制权返回给程序,程序继续处理。Reactor 释义“反应堆”,是一种事件驱动机制。 和普通函数调用的不同之处在于:应用程序不是主动的调用某个 API 完成处理,而是恰恰 相反,Reactor 逆置了事件处理流程,应用程序需要提供相应的接口并注册到 Reactor 上, 如果相应的时间发生,Reactor 将主动调用应用程序注册的接口,这些接口又称为“回调函 数”。

 

 Reactor 模式是处理并发 I/O 比较常见的一种模式,用于同步 I/O,中心思想是将所有要 处理的 I/O 事件注册到一个中心 I/O 多路复用器上,同时主线程/进程阻塞在多路复用器上; 一旦有 I/O 事件到来或是准备就绪(文件描述符或 socket 可读、写),多路复用器返回并将事 先注册的相应 I/O 事件分发到对应的处理器中。

Reactor模型有三个重要的组件:

  • 多路复用器:由操作系统提供,在Linux上一般是select,poll,epoll等系统调用。
  • 事件分发器:将多路复用器中返回的就绪事件分到对应的处理函数中。
  • 事件处理器:复制处理特定事件的处理函数(回调函数)

 

具体流程如下:

  1. 注册读就绪事件和相应的事件处理器;
  2. 事件分离器等待事件;
  3. 事件到来,激活分离器,分离器调用事件对应的处理器;
  4. 事件处理器完成事件的读操作,处理读到的数据,注册新的事件,然后返还控制权。 

Reactor模式是编写高性能网络服务器的技术之一,它具有如下的优点:

  • 响应快,不必为单个同步时间所阻塞,虽然Reactor本身依然是同步的;
  • 编程相对简单,可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/多进程的切换开销;
  • 可扩展性,可以方便的通过增加Reactor实例个数了充分利用CPU资源;
  • 可复用性,reactor框架本身与具体事件处理逻辑无关,具有很高的复用性

Reactor 模型开发效率上比起直接使用 IO 复用要高,它通常是单线程的,设计目标是 希望单线程使用一颗 CPU 的全部资源,但也有附带优点,即每个事件处理中很多时候可以 不考虑共享资源的互斥访问。可是缺点也是明显的,现在的硬件发展,已经不再遵循摩尔定 律,CPU 的频率受制于材料的限制不再有大的提升,而改为是从核数的增加上提升能力, 当程序需要使用多核资源时,Reactor 模型就会悲剧。

如果程序业务很简单,例如只是简单的访问一些提供了并发访问的服务,就可以直接开 启多个反应堆,每个反应堆对应一颗 CPU 核心,这些反应堆上跑的请求互不相关,这是完 全可以利用多核的。例如 Nginx 这样的 http 静态服务器。

二、Reactor模型设计

1. 结构体设计

typedef int NCALLBACK(int, int, void*);  //fd, events, arg=reactor

struct ntyevent {
    int fd;
    int events;
    void* arg;
    int (*callback)(int fd, int events, void* arg);

    int status; //标注该ntyevent是否已经被监测
    char buffer[BUFFER_LENGTH+1]; //缓冲区
    int length;  //缓冲区内数据长度
    long last_active;
};

struct eventblock {
    struct eventblock* next;  //指向下一个eventblock
    struct ntyevent* events; //events数组,长度为 MAX_EPOLL_EVENTS
};

struct ntyreactor {
    int epfd;  
    int blkcnt;  //counts of eventblock
    struct eventblock* evblk;  //
};

1.1 struct ntyevent

这是一个事件结构体,它绑定了socket的fd,以及需要监听的事件(events)和事件处理器(callback),还绑定了所属的reactor对象(arg)。

1.2 struct eventblock

这是一个事件块,每一个事件块包含了定长的事件存储数组,以及指向下一个块的指针。

1.3 struct ntyreactor

这是一个reactor结构体,绑定了epoll对象和eventblock空间,并且对eventblock的块数量进行了计数。

在整体设计中,先根据fd找到ntyreactor中对应的ntyevent空间,再设置ntyevent。

2. 函数设置

struct ntyevent* ntyreactor_idx(struct ntyreactor* reactor, int sockfd)

这个函数是根据传入的sockfd,在ntyreactor对象中找到对应的ntyevent空间。

int ntyreactor_alloc(struct ntyreactor* reactor)

这个函数是为ntyreactor申请ntyevent空间,当调用ntyreactor_idx()时,如果fd没有对应的ntyevent空间,会先调用本函数申请到对应的ntyevent空间。ntyreactor_alloc()申请空间是以eventblock为单位申请的。

void nty_event_set(struct ntyevent* ev, int fd, NCALLBACK *callback, void* arg)

这个函数是去设置ntyevent空间的。

int nty_event_add(int epfd, int events, struct ntyevent* ev)

这个函数是设置ntyevent的事件events并将ntyevent纳入epoll对象的监测。

在此函数中,会根据ntyevent对象的status状态判断,是调用EPOLL_CTL_ADD还是EPOLL_CTL_MOD。

int nty_event_del(int epfd, struct ntyevent* ev)

这个函数取消对ntyevent的事件检测。

int recv_cb(int fd, int events, void* arg)

这是接收数据事件的回调函数

int send_cb(int fd, int events, void* arg)

这是发送数据的回调函数

int accept_cb(int fd, int events, void* arg)

这是listenfd的回调函数。在此函数中,会将创建的连接的socket设置为非阻塞状态。

int init_sock(short port)

在此函数中,创建对port端口绑定的服务器listenfd。

int ntyreactor_init(struct ntyreactor* reactor)

这是ntyreactor对象的初始化函数,主要处理申请内存空间。

int ntyreactor_destroy(struct ntyreactor* reactor)

销毁ntyreactor对象中申请的内存空间(eventblock),但不会销毁ntyreactor对象本身。

int ntyreactor_addlistener(struct ntyreactor* reactor, int sockfd, NCALLBACK* acceptor)

将listenfd注册到ntyreactor对象上。

三、Reactor的应用

上面设计的reactor是单线程模式的,除此之外,reactor模式还有多线程和多进程。

1. 单线程Reactor

 

redis使用的就是这种单线程模型,在6.0版本之后支持IO多线程。

 

2. 多线程Reactor

 

多线程的Reactor模型引入线程池的概念,将一些耗时的操作交由新线程执行。

memcached使用的就是多线程的Reactor模型。

3. 多进程Reactor 

 

每个进程都有reactor对象。nginx使用的是多进程的Reactor模型。同时,nginx使用ET模式,因为nginx是做反向代理,做转发数据工作的,不需要界定数据包。

 

-----------------------------------------------------------------------------------------------

github:https://github.com/illusorycat/Reactor_mudel.git

posted @ 2022-03-10 17:05  幻cat  阅读(112)  评论(0编辑  收藏  举报