当前位置: 首页 > news >正文

libevent(1)之基础概述

Libevent(1)之基础概述


Author: Once Day Date: 2025年6月23日

一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦…

漫漫长路,有人对你微笑过嘛…

全系列文章可参考专栏: 十年代码训练_Once-Day的博客-CSDN博客

参考文章:

  • 详解libevent网络库(一)—框架的搭建_libevent详解-CSDN博客
  • 深度思考高性能网络库Libevent,从13个维度来解析Libevent到底是怎么回事 - 知乎
  • 深入浅出理解libevent——2万字总结_libev 堆-CSDN博客
  • Fast portable non-blocking network programming with Libevent
  • libevent
  • C++网络库:Libevent网络库的原理及使用方法 - 知乎
  • 深入理解libevent事件库的原理与实践技巧-腾讯云开发者社区-腾讯云

文章目录

  • Libevent(1)之基础概述
        • 1. 概述
          • 1.1 介绍
          • 1.2 异步事件
          • 1.3 helloworld示例
        • 2. libevent原理
          • 2.1 事件处理流程
          • 2.2 事件状态
          • 2.3 缓冲区事件(bufferevent)
          • 2.4 连接监听器(evconnlistener)
          • 2.5 libevent数据结构
          • 2.6 事件抽象原理
          • 2.7 多线程处理

1. 概述
1.1 介绍

libevent是一个功能强大的跨平台事件驱动库,在高性能网络编程领域有着广泛的应用。它最初由Niels Provos等人开发,最早可以追溯到2000年。经过多年的发展演进,目前已经成为了业内公认的优秀开源项目之一。

作为一个专注于事件驱动和异步I/O的编程库,libevent帮助开发者以一种高效而统一的方式处理各种类型的事件,比如网络I/O、定时器、信号等。它能够自动利用操作系统提供的最佳I/O多路复用机制,如Linux上的epoll、BSD上的kqueue等,从而实现了可移植性和高性能。

除了跨平台和高性能之外,libevent的一大特点就是接口灵活且使用简单。它提供了基本的事件驱动编程原语,开发者可以在此基础上实现自己的应用逻辑。同时libevent也提供了一些高级组件如bufferevent,使得处理一些常见场景变得更加方便。

与libevent类似的还有libev和libuv等事件驱动库。libev较为精简,注重性能表现,但功能不如libevent丰富。而libuv则起初是为Node.js项目开发,提供了更多高层的抽象。相比之下,libevent在灵活性和跨平台支持上更胜一筹。

当然,libevent也并非完美无缺。它的接口较为底层,在实现一些复杂逻辑时还是需要写不少代码。而且在Windows平台上发挥最佳性能需要较新的系统支持。对于SSL/TLS的处理,libevent也依赖于第三方库如OpenSSL。

即便有这些限制,但瑕不掩瑜,libevent仍然是一个优秀的开源项目,在许多知名软件中都能找到它的身影。对于希望打造高性能网络应用的开发者而言,libevent无疑是一个值得掌握和运用的利器。它不仅仅是一个库,更是一种高效处理并发的编程思路和模式。

1.2 异步事件

三种:网络 io 事件、定时事件以及信号事件。

linux:epoll、poll、select,mac:kqueue,window:iocp。

定时事件:红黑树,最小堆:二叉树、四叉树,跳表,时间轮。

信号事件。

在libevent中,事件主要分为三大类:网络I/O事件、定时事件和信号事件。这三类事件分别对应了网络编程、定时任务调度和进程信号处理等常见场景。

(1)网络I/O事件是指发生在文件描述符上的可读、可写等事件。当某个文件描述符(通常是socket)上有数据可读或者可写时,就会触发相应的事件回调函数。libevent会根据不同操作系统平台,选择最优的I/O多路复用机制来监听这些事件。在Linux上,依次选择epoll、poll和select;在BSD系(如macOS)上使用kqueue;而在Windows上则使用IOCP。这些机制都能够同时监听多个文件描述符,并在事件发生时通知应用程序,从而避免了阻塞等待的情况,提高了并发性能。

(2)定时事件用于在指定的时间点触发回调函数。libevent内部使用高效的数据结构来管理定时事件,常见的有以下几种:红黑树、最小堆(二叉堆、四叉堆)、跳表和时间轮。红黑树是一种自平衡二叉搜索树,插入、删除和搜索的平均时间复杂度都是O(logN);最小堆可以在O(logN)时间内插入和删除,且能够在O(1)时间内取得最小值;跳表通过多级链表实现,其插入、删除和搜索的平均复杂度也是O(logN);时间轮则是一种环形队列结构了,通过哈希的方式将事件分散到不同的槽位中,定时操作的时间复杂度可以达到O(1)。libevent会根据定时事件的数量和分布情况,自动选择最合适的数据结构。

(3)信号事件对应进程接收到的信号,如SIGINT、SIGTERM等。libevent允许注册信号处理函数,以异步的方式响应信号。这种方式避免了信号处理函数中的阻塞操作影响主程序loop的问题。在实现上,libevent在不同系统中采取了不同的方案:在类Unix系统中,通过sigaction()注册信号处理函数,并利用socketpair()在信号发生时通知事件循环;而在Windows上,则使用Event对象来模拟信号事件。不过需要注意,在信号处理函数中要尽量避免调用非异步安全(async-signal-safe)的函数,以免引起竞态条件等问题。

1.3 helloworld示例

下面是一个使用libevent编写的简单的TCP echo服务器示例,它监听指定端口,将客户端发送的数据原样返回:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <event2/listener.h>
#include <event2/bufferevent.h>
#include <event2/buffer.h>static void echo_read_cb(struct bufferevent *bev, void *ctx) {struct evbuffer *input = bufferevent_get_input(bev);struct evbuffer *output = bufferevent_get_output(bev);evbuffer_add_buffer(output, input);
}static void echo_event_cb(struct bufferevent *bev, short events, void *ctx) {if (events & BEV_EVENT_ERROR)perror("Error from bufferevent");if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {bufferevent_free(bev);}
}static void accept_conn_cb(struct evconnlistener *listener, evutil_socket_t fd, struct sockaddr *address, int socklen, void *ctx) {struct event_base *base = evconnlistener_get_base(listener);struct bufferevent *bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);bufferevent_setcb(bev, echo_read_cb, NULL, echo_event_cb, NULL);bufferevent_enable(bev, EV_READ|EV_WRITE);
}int main(int argc, char **argv) {struct event_base *base;struct evconnlistener *listener;struct sockaddr_in sin;int port = 9876;base = event_base_new();if (!base) {fprintf(stderr, "Could not initialize libevent!\n");return 1;}memset(&sin, 0, sizeof(sin));sin.sin_family = AF_INET;sin.sin_port = htons(port);listener = evconnlistener_new_bind(base, accept_conn_cb, NULL, LEV_OPT_REUSEABLE|LEV_OPT_CLOSE_ON_FREE, -1, (struct sockaddr*)&sin, sizeof(sin));if (!listener) {fprintf(stderr, "Could not create a listener!\n");return 1;}event_base_dispatch(base);evconnlistener_free(listener);event_base_free(base);return 0;
}

这个示例程序的逻辑如下:

  1. 首先初始化libevent的event_base,它是libevent的核心组件,负责管理各种事件。
  2. 然后创建一个evconnlistener,用于监听指定端口的TCP连接请求。当有新的客户端连接到来时,会自动调用accept_conn_cb回调函数。
  3. 在accept_conn_cb函数中,为每个新的客户端连接创建一个bufferevent对象,并设置读写回调函数。当客户端发送数据时会触发echo_read_cb,当发生异常时会触发echo_event_cb。
  4. 在echo_read_cb函数中,从bufferevent的输入缓冲区读取客户端发送的数据,然后将其复制到输出缓冲区,这就实现了echo的功能。
  5. 最后在main函数中调用event_base_dispatch启动事件循环,监听并处理各种事件,直到程序退出。

这个示例虽然简单,但展示了libevent异步I/O编程的基本流程:初始化event_base、创建事件监听器、定义事件回调函数,以及启动事件循环等。可以看到,借助libevent提供的接口,编写高性能的网络应用变得简洁明了。

2. libevent原理
2.1 事件处理流程

创建事件event,添加到event_base,调用event_base_loop/event_base_dispatch进入分发,调用event_base_loopexit/event_base_loopbreak 退出事件循环,调用event_del+event_free删除事件并回收资源,调用event_base_free回收event_base资源。

对于fork的子进程,需要使用event_reinit重新初始化。

在这里插入图片描述

事件处理流程图,引用自C++网络库:Libevent网络库的原理及使用方法。

2.2 事件状态

Libevent 的事件对象(struct event)主要有三个状态:非未决未决激活

包括API函数:event_new、event_set、event_add、event_del、event_free。

在这里插入图片描述

事件状态流程图,引用自C++网络库:Libevent网络库的原理及使用方法。

2.3 缓冲区事件(bufferevent)

在libevent中,bufferevent是一个高级的I/O抽象,它在普通的event基础上提供了更多的功能和便利性。bufferevent内部维护了两个缓冲区(用户区)和一个文件描述符(通常是网络socket)。这两个缓冲区分别用于读写数据,起到了在用户代码和内核之间缓存数据的作用。

通过使用bufferevent,我们可以方便地读写数据,而不必直接操作底层的文件描述符。当数据可读时,内核会自动将数据读取到bufferevent的输入缓冲区;当我们往bufferevent的输出缓冲区写入数据时,内核也会自动将数据发送出去。这种机制大大简化了I/O操作,使得编程更加高效和便捷。

除了数据缓冲功能,bufferevent还支持设置三个回调函数:读回调、写回调和事件回调。这三个回调函数分别在不同的事件发生时被调用:

  • 读回调:当输入缓冲区有可读数据时触发,我们可以在这个回调函数中从输入缓冲区读取数据进行处理。
  • 写回调:当输出缓冲区的数据被发送完毕后触发,通常在这个回调函数中继续往输出缓冲区写入新的数据。
  • 事件回调:当发生一些特殊的事件时触发,如连接关闭、发生错误等,我们可以在这个回调函数中进行相应的处理。

通过设置这三个回调函数,我们就可以在不同的事件发生时执行相应的逻辑,从而实现灵活的I/O控制。

2.4 连接监听器(evconnlistener)

libevent中的连接监听器(evconnlistener)是一个用于接受传入连接的专用对象。它封装了底层socket通信的细节,使得监听和接受新连接变得简单和直观。

evconnlistener内部隐藏了socket编程中一系列繁琐的步骤,包括创建socket、绑定地址和端口(bind)、开始监听(listen)等。当我们创建一个evconnlistener时,实际上相当于一次性完成了这些底层操作。

一旦evconnlistener创建成功,它就开始监听指定的地址和端口,等待新的客户端连接到来。这里的监听过程是完全异步和非阻塞的,不会影响事件循环中其他事件的处理。

当有新的客户端连接到达时,evconnlistener会自动调用accept函数接受这个连接,并创建一个与之对应的文件描述符(fd)。接着,它会调用我们预先设置好的回调函数,将这个新的连接交给用户代码进一步处理。在回调函数中,我们可以根据需要为这个新连接创建bufferevent等对象,以便读写数据。

使用evconnlistener接受连接的优势在于,它将底层的socket操作完全封装起来,暴露给用户一个简洁的回调接口。我们只需要定义一个回调函数,在其中处理新连接即可,不必关心底层的socket细节。这种方式不仅简化了编程,也提高了代码的可读性和可维护性。

此外,evconnlistener还提供了一些有用的特性,比如设置监听队列大小、绑定多个地址、设置超时时间等。这些特性进一步增强了其灵活性和实用性。

2.5 libevent数据结构

在libevent的内部实现中,为了高效地管理各种事件,它使用了几个关键的数据结构。

libevent使用三个双向链表来分别存储不同类型的事件:

  • I/O事件链表:这个链表存储了所有注册的I/O事件(如读写事件),每个节点表示一个事件,包含事件类型、文件描述符、回调函数等信息。
  • 信号事件链表:这个链表存储了所有注册的信号事件,每个节点表示一个信号事件,包含信号值、回调函数等信息。
  • 活动事件链表:这个链表存储了所有已经就绪(激活)的事件,这些事件可能来自I/O事件、信号事件或定时器事件。libevent会依次处理这个链表中的事件。

使用双向链表的好处是插入、删除操作的时间复杂度为O(1),而且可以方便地在表头表尾操作,非常适合事件管理的场景。

其次,对于定时器事件,libevent使用了一个小根堆(最小堆)来存储。小根堆是一种常见的优先队列数据结构,它能够在O(logN)的时间内完成插入和删除操作,并且能够在O(1)时间内获取最小值(即最近的定时器事件)。

libevent在每次事件循环中,都会检查小根堆的堆顶元素,如果其超时时间已到,就将其激活,并移入到活动事件链表中等待处理。

除了这些主要的数据结构,libevent还使用了其他一些辅助的数据结构,如哈希表用于快速查找事件,数组用于保存不同优先级的事件等。

2.6 事件抽象原理

libevent的核心就是对各种事件的抽象和统一管理,使得开发者可以用一致的方式处理不同类型的事件。

对于I/O事件,libevent主要是对系统提供的I/O多路复用机制进行了封装,比如select、poll、epoll、kqueue等。这些机制允许同时监视多个文件描述符的状态变化,当其中某些文件描述符就绪时(如可读、可写),就会通知应用程序进行相应的处理。libevent将这些底层接口封装起来,提供了统一的事件注册、派发接口,简化了I/O事件的处理。

信号事件的处理则相对复杂一些。为了将信号事件集成到libevent的事件管理中,libevent创建了一个socketpair,即一对相互连接的匿名socket。其中一个socket用于监听信号事件,另一个socket用于写入信号通知。当信号发生时,信号处理函数会往写socket中写入数据,这样监听socket上就会产生可读事件,从而触发相应的事件回调。这种方法巧妙地将信号事件转化为了I/O事件,使得信号事件也能被libevent统一管理起来。

对于定时器事件,libevent利用了I/O多路复用机制的超时等待特性。像select和epoll_wait等函数都允许指定一个最长等待时间,即使没有事件发生,它们也会在超时后返回。libevent根据所有定时器事件的最小超时时间来设置系统I/O的超时值,当I/O函数返回时,再检查和激活所有已经到期的定时器事件。这样,定时器事件就巧妙地融入到了整个事件驱动框架中,与I/O事件一起被libevent统一调度。

libevent的事件抽象原理体现了软件设计中的一些重要思想,如封装、抽象、分层等。通过对底层复杂性的封装,向上提供简单统一的接口,极大地提高了开发效率和代码质量。这也是libevent能够成为广受欢迎的高性能网络库的重要原因。

2.7 多线程处理

在使用libevent进行多线程编程时,需要特别注意线程安全问题。默认情况下,libevent的API是非线程安全的,如果多个线程同时操作同一个event_base实例,就可能导致数据竞争和不可预期的行为。

一个常见的问题是多个线程同时修改event_base,例如注册新事件、删除事件等。这些操作会修改event_base内部的数据结构,如果没有适当的同步机制, 就会引发竞态条件和数据破坏。

为了在多线程环境中安全地使用libevent,一种常见的方法是为每个线程创建独立的event_base实例。每个线程只操作自己的event_base,这样就避免了线程间的相互干扰。线程可以各自管理自己的事件和任务队列,独立地进行事件派发和处理。

在Memcached中,主线程负责接受新的客户端连接,并将其分配给工作线程处理。每个工作线程都有自己独立的event_base和任务队列,负责处理分配给自己的客户端请求。这种设计避免了线程间的竞争,同时也提高了并发处理的效率。

在多线程环境中使用libevent需要仔细考虑线程安全问题,最安全和简单的方法是为每个线程使用独立的event_base实例,避免线程间的直接竞争。如果必须在线程间共享event_base,就需要使用libevent提供的线程安全API,并且小心地进行线程同步。







Alt

Once Day

也信美人终作土,不堪幽梦太匆匆......

如果这篇文章为您带来了帮助或启发,不妨点个赞👍和关注,再加上一个小小的收藏⭐!

(。◕‿◕。)感谢您的阅读与支持~~~

相关文章:

  • 为拟建设的网站申请一个域名国内做网站比较好的公司
  • 成都网站制作公司有哪些seo网站优化方案案例
  • 服务器用来做网站空间seo顾问服务 乐云践新专家
  • 南昌网站建设公司特色广州网站优化软件
  • 西安网站建设seo怎样上百度做广告
  • wordpress启用静态东莞网站推广及优化
  • 网站公安网安备案查询API集成指南
  • 元宇宙时代实物建模新趋势:动态纹理映射与实时渲染方案
  • 【驱动设计的硬件基础】PCI和PCI-E
  • TongWeb替换tomcat
  • 【机器学习深度学习】多层神经网络的构成
  • MySQL深分页性能瓶颈:问题分析与解决方案
  • Linux SPI核心驱动spidev.c深度解析
  • svn域名更换,批量修改项目svn地址(linux)
  • FineBI(二)- 数据导入
  • AI时代工具:AIGC导航——AI工具集合
  • day041-web集群架构搭建
  • 阿里最新开源:Mnn3dAvatar 3D数字人框架, 无需联网,本地部署可离线运行,支持多模态实时交互
  • Docker 报错“x509: certificate signed by unknown authority”的排查与解决实录
  • CentOS下安装JDK17
  • CentOS 7 编译安装Nginx 1.27.5完整指南及负载均衡配置
  • Luckysheet Excel xlsx 导入导出互相转换
  • RSS解析并转换为JSON的API集成指南
  • 关键领域软件工厂的安全中枢如何全面升级供应链检测能力
  • CentOS 7 通过YUM安装MySQL 8.0完整指南
  • Redis的渐进式hash和缓存时间戳深入学习