基于Reactor模式深入探讨Libevent

在这里插入图片描述

欢迎诸位来阅读在下的博文~
在这里,在下会不定期发表一些浅薄的知识和经验,望诸位能与在下多多交流,共同努力!
江山如画,客心如若,欢迎到访,一展风采

参考书籍

《Linux C/C++ 服务器开发实践》——朱文伟 李建英

一、Libevent的介绍

Libevent是一个用c语言编写的、轻量的开源高性能事件通知库,主要有以下几个特点:

  1. 事件驱动,高性能
  2. 轻量级,专注于网络,不像ACE那么庞大
  3. 源代码相当精炼,易读
  4. 跨平台,支持Windows、Linux、*BSD 和 MacOS
  5. 支持多种IO多路复用技术,如epoll、poll、dev/poll、select 和 kqueue等
  6. 支持I/O,定时器 和 信号等事件
  7. 支持注册事件优先级。

Libevent是一个事件通知库,内部使用select、epoll、kqueue、IOCP等系统调用管理事件机制。
Libevent使用C语言编写的,而且几乎是无处不用的函数指针。
LIbevent支持多线程编程。
Libevent已经被广泛应用,作为不少知名软件的底层应用网络库,比如memcached、Vomit、Nylon、Netchat等。
事实上Libevent本身就是一个典型的Reactor模式,理解Reactor模式是理解Libevent的基石。

二、Reactor的讲解

Reactor模式是一种在并发编程中广泛使用的设计模式,特别是在网络编程和事件驱动的应用程序中。它主要用于处理多个输入源(通常是网络连接)同时产生的事件。Reactor 模式的核心思想是将事件的处理分离出来,由一个中心的事件处理器(称为 Reactor)来管理。
以下是 Reactor 模式的基本组成部分和讲解:

基本组成

  1. 事件源(Event Sources)
    • 事件源可以是网络连接、文件、定时器等,它们能够产生事件。在Linux上是文件描述符,在Windows上是socket或者Handle,这里统一称为“句柄集”。程序在指定的句柄上注册关心的事件,如:iO事件。
  2. 事件多路分离器(Event Demultiplexer)
    • 事件多路分离器负责监听事件源,并将产生的事件分发到相应的处理器。在 Unix 系统中,这个角色通常由 selectpollepoll 这样的系统调用实现。程序首先将其关心的句柄(事件源)及其事件注册到Event Demultiplexer上,当有事件到达时,Event Demultiplexer会发出通知“在已经注册的句柄集合中,一个或者多个句柄的事件已经就绪”,反应器收到通知后,就可以在非阻塞的情况下对事件进行处理了。
    • 对应到Libevent中,依然时select、poll、epoll等,但是Libevent使用结构体eventop进行了封装,以统一的接口来支持这些I/O多路复用机制,达到了对外隐藏底层系统机制的目的。
  3. 反应器(Reactor)
    • Reactor 是一个对象,它知道如何处理所有类型的事件。它通过事件多路分离器获取事件,并将每个事件分发给对应的处理器。对应到Libevent中,就是event_base结构体。一个典型的Reactor声明如下所示:
class Reactor{
public:
	int register_handler(Event_Handler* pHandler, int event);
	int remove_handler(Event_handler* pHandler, int event);
	void handle_events(timeval* ptv);
	//...
};
  1. 事件处理器(Event Handlers)
    • 事件处理器是具体处理事件的组件。每个事件处理器都对应一种类型的事件,例如,一个网络连接的读事件或写事件。对应到Libevent中,就是Event结构体。下面是两种典型的Event Handler类型声明方式,两者各有优缺点。
class Event_Handler{
public:
	virtual void handle_read() = 0;
	virtual void handle_write() = 0;
	virtual void handle_timeout() = 0;
	virtual void handle_close() = 0;
	//...
};
class Event_Handler{
public:
	virtual void handle_events(int events) = 0;
	virtual HANDLE get_handle() = 0;
	//...
};

工作流程

  1. 初始化
    • Reactor 初始化事件多路分离器,并注册感兴趣的事件类型。
  2. 事件循环
    • Reactor 进入一个无限循环,等待事件多路分离器通知有新的事件发生。
  3. 事件分发
    • 当事件发生时,事件多路分离器通知 Reactor,Reactor 根据事件的类型将其分发给相应的事件处理器。
  4. 事件处理
    • 事件处理器执行具体的处理逻辑,例如,读取数据、发送响应、处理错误等。
  5. 返回循环
    • 处理完事件后,Reactor 继续等待下一个事件。

优点

  • 响应性:Reactor 模式能够同时处理多个事件,提高了应用程序的响应性,虽然Reactor本身依旧是同步的。
  • 可扩展性:由于事件处理是分离的,可以很容易地添加或删除事件处理器,从而扩展系统的功能。
  • 解耦:事件源和事件处理器之间解耦,使得系统更加模块化。
  • 编程相对简单:可以最大程度地避免复杂的多线程及同步问题,且避免了多线程/进程的切换开销。

缺点

  • 复杂性:Reactor 模式可能会增加应用程序的复杂性,特别是当事件类型和处理器数量增加时。
  • 调试困难:事件驱动的程序可能难以调试,因为程序的执行流程不是线性的。

Reactor 模式在现代网络应用程序中非常流行,特别是在使用高性能网络库(如 Java 的 Netty、C++ 的 Boost.Asio)时。它为处理大量并发连接提供了有效的解决方案。

三、使用Libevent的基本流程

在下打算用设置定时器来讲解Libevent的使用流程。

01 初始化Libevent库,并保存返回的指针。

struct event_base* base = event_init();

// 实际上这一步相当于初始化一个Reactor实例。在初始化Libevent后,就可以注册事件了。

02 初始化事件event,设置回调函数和关注的事件

evtimer_set(&ev, timer_cb, NULL);

// 事实上这等价于调用event_set(&ev, -1, 0, timer_cb, NULL); .
event_set的函数原型是:

void event_set(struct event* ev, int fd, short event, 
				void (*cb)(int, short, void*),void *arg)
  • ev: 执行要初始化的event对象。
  • fd:该event绑定的“句柄”,对于信号事件,它是关注的信号。
  • event:在该fd上关注的事件类型,它可以是EV_READ、EV_WRITE、EV_SIGNAL.
  • cb:这是一个函数指针,当fd上的事件event发生时,调用该函数执行处理,它有三个参数,调用时由event_base负责按顺序传入,实际上就是event_set时的fd、event和arg。
  • arg:传递给cb函数指针的参数。

由于定时事件不需要fd,并且定时事件是根据添加时(event_add)的超时值设定的,因此这里的event也需要设置。
这一步相当于初始化一个event handler,在Libevent中事件类型保存在event结构体中。
然后,值得一提的是,Libevent并不会管理event事件集合,这需要应用程序自行管理。

03 设置event从属的event_base

event_base_set(base, &ev);

//这一步相当于指明event要注册到哪个event_base实例上。

04 正式添加事件

event_add(&ev, timeout);

第二步和第三步是事件的属性设置(设置回调函数、事件类型和base从属),属性设置完成后,只要简单调用event_add()函数,就正式将事件加入到了监听队列,其中timeout是定时器值。
这一步相当于调用Reactor::register_handler()函数注册事件。

05 程序进入无限循环,等待就绪事件并执行事件处理

event_base_dispatch(base);

上面的程序代码可以描述如下:

struct event ev; // 声明一个 event 结构体,用于存储事件信息
struct timeval tv; // 声明一个 timeval 结构体,用于设置定时器超时时间

// 定时器回调函数,当定时器事件发生时会调用这个函数
void time_cb(int fd, short event, void *argc) {
    printf("timer wakeup\n"); // 打印一条消息,表明定时器已经触发
    event_add(&ev, &tv); // 重新添加定时器事件,以便它再次触发
}

int main() {
    struct event_base *base = event_init(); // 初始化事件库,并获得一个 event_base 结构体
    tv.tv_sec = 10; // 设置定时器的秒数,这里是10秒
    tv.tv_usec = 0; // 设置定时器的微秒数,这里不使用微秒

    // 设置 ev 事件为定时器事件,并指定 time_cb 作为回调函数,NULL 作为回调函数的参数
    evtimer_set(&ev, time_cb, NULL);

    // 设置 ev 事件的事件基地为 base
    event_base_set(base, &ev);

    // 添加 ev 事件到事件循环中,这里应该使用 &tv 作为超时参数
    event_add(&ev, &tv);

    // 开始事件循环,程序将在这里等待事件发生并处理它们
    event_base_dispatch(base);

    return 0;
}

当应用程序向Libevent注册一个事件后,Libevent内部的处理流程如下:
(1)应用程序准备并初始化event,设置好事件类型和回调函数。
(2)向Libevent添加该事件event。对于定时事件,Libevent使用一个小根堆管理,key为超时事件;对于Signal和I/O事件,Libevent将其放入到等待链表(Wait List)中,这是一个双向链表结构。
(3)程序调用event_base_dispatch()系列函数进入无限循环,等待事件,以select()函数为例,每次循环前Libevent会检查定时事件的最小超时事件tv,根据tv设置select()的最大等待时间,以便后面及时处理超时事件;当select()返回后,首先检查超时事件,然后检查I/O事件。Libevent将所有的就绪事件放入到激活链表中,然后对激活链表中的事件调用事件的回调函数执行事件处理。

四、下载和编译Libevent

诸位可以自行到官网下载链接下载源码,然后放到Linux下进行编译生成动态库so文件。
当然,为了方便诸位,在下也提供了百度网盘链接
提取码:qqsp

编译命令:

//解压
tar -xvf libevent-2.1.12-stable.tar.gz
//进入解压后的文件夹里面,生成make.  安装的路径参数可以自行自定
./configure --prefix=/opt/libevent
//make
make
make install

解压后,可以在/opt/libevent路径下看到如下:
在这里插入图片描述

简单例子测试

#include <sys/types.h>
#include <event2/event-config.h>
#include <stdio.h>
#include <event.h>

struct event ev; // 声明一个 event 结构体,用于存储事件信息
struct timeval tv; // 声明一个 timeval 结构体,用于设置定时器超时时间

void time_cb(int fd, short event, void *argc) {
    printf("timer wakeup\n"); 
    event_add(&ev, &tv); 
}

int main() {
    struct event_base *base = event_init(); 
    tv.tv_sec = 10; 
    tv.tv_usec = 0; 
    evtimer_set(&ev, time_cb, NULL);
    event_base_set(base, &ev);
    event_add(&ev, &tv);
    event_base_dispatch(base);
    return 0;
}

编译:

gcc test.c -o testEvent -I /opt/libevent/include/ -L /opt/libevent/lib/ -levent

运行:

./testEvent 

结果:每10秒中断一次
在这里插入图片描述
至此,结束~

后记:

下一次我会出一篇基于libevent的FTP服务器,诸位若想不错过,记得关注在下哦~
在这里插入图片描述
望诸位不忘三连支持一下~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值