欢迎诸位来阅读在下的博文~
在这里,在下会不定期发表一些浅薄的知识和经验,望诸位能与在下多多交流,共同努力!
江山如画,客心如若,欢迎到访,一展风采
文章目录
参考书籍
《Linux C/C++ 服务器开发实践》——朱文伟 李建英
一、Libevent的介绍
Libevent是一个用c语言编写的、轻量的开源高性能事件通知库,主要有以下几个特点:
- 事件驱动,高性能
- 轻量级,专注于网络,不像ACE那么庞大
- 源代码相当精炼,易读
- 跨平台,支持Windows、Linux、*BSD 和 MacOS
- 支持多种IO多路复用技术,如epoll、poll、dev/poll、select 和 kqueue等
- 支持I/O,定时器 和 信号等事件
- 支持注册事件优先级。
Libevent是一个事件通知库,内部使用select、epoll、kqueue、IOCP等系统调用管理事件机制。
Libevent使用C语言编写的,而且几乎是无处不用的函数指针。
LIbevent支持多线程编程。
Libevent已经被广泛应用,作为不少知名软件的底层应用网络库,比如memcached、Vomit、Nylon、Netchat等。
事实上Libevent本身就是一个典型的Reactor模式,理解Reactor模式是理解Libevent的基石。
二、Reactor的讲解
Reactor模式是一种在并发编程中广泛使用的设计模式,特别是在网络编程和事件驱动的应用程序中。它主要用于处理多个输入源(通常是网络连接)同时产生的事件。Reactor 模式的核心思想是将事件的处理分离出来,由一个中心的事件处理器(称为 Reactor)来管理。
以下是 Reactor 模式的基本组成部分和讲解:
基本组成
- 事件源(Event Sources):
- 事件源可以是网络连接、文件、定时器等,它们能够产生事件。在Linux上是文件描述符,在Windows上是socket或者Handle,这里统一称为“句柄集”。程序在指定的句柄上注册关心的事件,如:iO事件。
- 事件多路分离器(Event Demultiplexer):
- 事件多路分离器负责监听事件源,并将产生的事件分发到相应的处理器。在 Unix 系统中,这个角色通常由
select
、poll
或epoll
这样的系统调用实现。程序首先将其关心的句柄(事件源)及其事件注册到Event Demultiplexer上,当有事件到达时,Event Demultiplexer会发出通知“在已经注册的句柄集合中,一个或者多个句柄的事件已经就绪”,反应器收到通知后,就可以在非阻塞的情况下对事件进行处理了。 - 对应到Libevent中,依然时select、poll、epoll等,但是Libevent使用结构体eventop进行了封装,以统一的接口来支持这些I/O多路复用机制,达到了对外隐藏底层系统机制的目的。
- 事件多路分离器负责监听事件源,并将产生的事件分发到相应的处理器。在 Unix 系统中,这个角色通常由
- 反应器(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);
//...
};
- 事件处理器(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;
//...
};
工作流程
- 初始化:
- Reactor 初始化事件多路分离器,并注册感兴趣的事件类型。
- 事件循环:
- Reactor 进入一个无限循环,等待事件多路分离器通知有新的事件发生。
- 事件分发:
- 当事件发生时,事件多路分离器通知 Reactor,Reactor 根据事件的类型将其分发给相应的事件处理器。
- 事件处理:
- 事件处理器执行具体的处理逻辑,例如,读取数据、发送响应、处理错误等。
- 返回循环:
- 处理完事件后,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服务器,诸位若想不错过,记得关注在下哦~
望诸位不忘三连支持一下~