Libevent源码剖析之bufferevent-CSDN博客
Libevent源码剖析之evbuffer-CSDN博客
Libevent源码剖析-event-CSDN博客
Libevent源码剖析之reactor-CSDN博客
Libevent源码剖析之iocp_iocp 源码-CSDN博客
Libevent源码剖析之epoll-CSDN博客
Libevent源码剖析之poll-CSDN博客
Libevent源码剖析之select-CSDN博客
关于libevent,曾在项目中直接或间接使用过,360开源的evpp便是对libevent的c++封装,窃以为简单好用,但裸用的话本人在最近开源的缓存库C缓存库Github地址里使用过,感受是比较累。累的原因,一则是C语言所写,一则是对libevent源码还不够熟,踩坑较多。
因此,从本文开始,拟对libevent写系列文章,目的是通过对libevent的源码分析,来加深理解。
后文均基于libevent2.1.18版本来剖析。
这里简要介绍下文章结构,算是提纲挈领。
- 首先,介绍libevent的核心部分,包括各平台IO多路复用,reactor原理,事件注册&收集&分发&处理,以及一些重要数据结构的实现原理,比如小根堆。
- 其次,会详细分析evbuffer的实现原理,以及为何需要evbuffer、它解决了什么问题。
- 再则,进一步探讨bufferevent的实现原理,以及相关api、为何需要bufferevent、它解决了什么问题;
- 最后,有了以上核心特性之后,我们便可以利用libevent开发任何基于tcp/udp的应用程序了,这里会以http server为例来介绍。
此处简要介绍下libevent的各层次,及其职责:
- libevent的核心层,是基于各平台IO多路复用的reactor实现,其中iocp是proactor模型,libevent亦将其融入进核心层。
- 中间层,则是引入了evbuffer,本层是bufferevent外围及调用方和libevent的核心层的一个数据缓冲层。
- 最外层,便是bufferevent,本层是libevent库对外的模块,也就是直接面向库的使用者。
2.1.1 reactor
reactor 模式 是一种设计模式,常用于处理多并发的 I/O 事件,尤其在高性能服务器或网络框架中广泛应用。它通过事件驱动机制和非阻塞 I/O 实现多个客户端请求的并发处理,而无需为每个连接分配一个独立的线程或进程。
核心组件
reactor 模式通常包含以下几个主要组件:
1. reactor (反应器):
- 负责等待并检测 I/O 事件(如读、写、连接等)。当事件发生时,Reactor 将事件分派给合适的事件处理器(或回调函数)进行处理。
- 一般通过事件多路复用机制(如 select、poll、epoll 在 Linux 上,kqueue 在 BSD 上,或 Windows 的 IOCP)来监视多个 I/O 操作是否准备就绪。
2. handlers (事件处理器):
- 每个事件处理器都与某一类事件(如读、写、错误事件)关联。当 Reactor 检测到某类事件时,会调用相应的事件处理器来处理该事件。
3. demultiplexer (事件多路分离器):
- 这是底层的操作系统功能,用来将多个事件(I/O 操作)组合并检测哪些事件已经准备就绪。常见的多路分离器有 select、poll、epoll 等。
4. event loop (事件循环):
- 事件循环是 reactor 模式的核心机制,它持续循环等待新的事件并处理。整个 reactor 的流程是基于事件循环的不断运行。
工作流程
1. 注册事件:
- 服务器在启动时,将需要监听的 I/O 事件(如客户端连接请求、读写请求等)注册到 reactor 中。
2. 等待事件发生:
- reactor 通过事件多路分离器(如 epoll 或 select)持续监听多个 I/O 事件,当有事件就绪时,会通知 reactor。
3. 事件分派:
- 当有 I/O 事件就绪时,reactor 会调用对应的事件处理器(handler)进行处理。每个事件类型都会有其对应的处理器。
4. 处理事件:
- 事件处理器对具体的事件(如读写数据、处理请求)进行处理,完成后返回到 reactor 继续监听新的事件。
2.1.2 reactor与proactor的区别
reactor 模式与 proactor 模式 是两种不同的 I/O 模型:
• reactor:
- I/O 操作是由应用程序发起的,当 I/O 事件(如数据可读、可写)准备好时,reactor 通知应用程序进行处理。应用程序在事件触发时执行实际的 I/O 操作。
- 是同步IO模型;
• proactor:
- I/O 操作由操作系统发起,应用程序只需提供完成后的处理逻辑。操作系统在 I/O 操作完成后通知应用程序进行处理。
- 是异步IO模型;
在 reactor 模式下,事件的处理是由应用程序主动执行的;而在 proactor 模式下,事件处理通常由操作系统完成。
2.1.3 IO多路复用
在以往的同步阻塞模式下,对IO的检测和读写操作是由类似read()/write()系统调用来完成的,而在reactor模型里,对IO的检测则是通过IO多路复用系统调用来完成,read()/write()则具体执行读写操作。
换言之,IO多路复用的职责是IO检测,而由read()/write()来完成读写操作。
2.1.3.1 select
select 多路复用是一种用于监视多个文件描述符(如网络套接字、文件等)的 I/O 操作状态的系统调用。通过 select,程序可以同时等待多个 I/O 操作的就绪(如读、写或异常事件),从而避免为每个 I/O 操作创建独立的线程或进程。
select 最常用于高并发场景下的事件驱动编程,例如网络服务器或实时通信系统。通过 select,应用可以一次性监视大量连接,处理 I/O 操作时不会阻塞。
注册文件描述符:程序将感兴趣的文件描述符(如网络连接的 socket 文件描述符)注册给 select。
监视文件描述符:select 进入阻塞状态,等待文件描述符上的 I/O 事件(如可读、可写或出现异常)发生。
事件就绪时返回:当某个文件描述符的 I/O 操作准备就绪时,select 返回,通知应用程序哪些文件描述符可以执行 I/O 操作。
处理事件:应用程序根据 select 返回的结果对已准备就绪的文件描述符执行相应的操作。
2.1.3.2 poll
poll 多路复用是与 select 类似的系统调用,用于监控多个文件描述符上的 I/O 操作,允许程序同时等待多个事件(如可读、可写、或异常)。与 select 相比,poll 去除了文件描述符数量的限制,并提供了更灵活的接口。它是 select 的改进版本,适用于高并发场景。
监控多个文件描述符:poll 可以监控任意数量的文件描述符(不限于 select 的 1024 个限制)。
阻塞等待事件:程序调用 poll,阻塞等待指定的文件描述符变为就绪状态(如可读、可写或出现异常)。
事件返回:一旦文件描述符有事件发生,poll 返回,程序可以处理这些事件。
2.1.3.3 epoll
epoll 是 Linux 提供的一种高效的多路复用机制,用于处理大量并发连接时的 I/O 事件。与传统的 select 和 poll 相比,epoll 的性能大幅提高,尤其在监控大量文件描述符时具有显著优势。
事件驱动:epoll 采用事件驱动模式(edge-triggered 和 level-triggered),只在文件描述符的状态发生变化时才通知应用程序,而不像 select 和 poll 需要每次调用时轮询所有文件描述符。
O(1) 复杂度:epoll 的性能不随着监控文件描述符数量的增加而下降,在管理大量连接时表现出近乎恒定的时间复杂度。
内核事件表:epoll 通过内核维护的事件表,避免每次调用时重新遍历文件描述符列表。
2.1.3.4 iocp
I/O Completion Ports (IOCP) 是 Windows 上的一种高效 I/O 多路复用机制,用于处理大量并发连接和 I/O 操作。与 select、poll 等轮询机制不同,IOCP 采用了完成端口和回调机制来高效地处理异步 I/O 操作。
异步 I/O:IOCP 允许程序在执行 I/O 操作时不阻塞主线程,I/O 操作会在后台进行,完成后通过完成端口通知主线程。
线程池:IOCP 使用线程池来处理并发 I/O 事件,根据负载动态分配工作线程,从而最大化 CPU 和 I/O 资源的利用。
完成端口(Completion Ports):完成端口是 IOCP 的核心,它是一个操作系统提供的队列,异步 I/O 操作的结果会被推送到这个队列中,工作线程从中获取 I/O 结果并进行处理。
句柄绑定:每个 I/O 操作的文件句柄(socket、文件、管道等)可以绑定到一个完成端口,当操作完成后,系统将事件通知到相应的完成端口。
2.1.3.5 devpoll
/dev/poll 是 Solaris 操作系统上的一种 I/O 多路复用机制,类似于 Linux 上的 poll 和 epoll,但在性能和可扩展性上进行了优化。它为需要监视大量文件描述符(如网络服务器等高并发场景)的应用程序提供了一种高效的事件通知机制。
文件描述符监控:与传统的 poll 不同,/dev/poll 通过一个持久的设备文件(/dev/poll)来跟踪文件描述符的状态,而不是每次调用都需要重新传入一组文件描述符。程序只需一次性注册需要监控的文件描述符,之后可以多次调用等待事件,极大地减少了系统调用的开销。
持久性:注册的文件描述符是持久的,直到被手动移除或文件关闭,因此不需要像传统 poll 那样在每次事件检测时都传递整个文件描述符集。
事件通知:当文件描述符的状态发生变化时,/dev/poll 会通知应用程序,允许其处理相应的 I/O 操作。
2.1.3.6 devport
dev/port 是 Solaris 操作系统中的一种设备接口,主要用于直接访问 I/O 端口,但它不是一种多路复用机制。与多路复用相关的机制主要包括 select、poll、epoll、/dev/poll、IOCP 等。因为 /dev/port 的主要用途与 I/O 端口的直接访问有关,而不是事件驱动的 I/O 多路复用,所以它与多路复用没有直接的联系。
因此,本书不表,感兴趣的读者可查询libevent2.1.18相关源码。
2.1.3.7 kqueue
2.1.3.8 win32select
TODO : 后续补充
2.1.4 evbuffer
evbuffer模块,在libevent库中,是介于reactor模块和bufferevent模块之间,属于中间层。
在非阻塞的网络程序里,网络库应该能够缓存业务app的数据,以便在网络可用时再由网络库将数据发送出去(或者将数据接收后缓存起来再通知业务app),如此便不至于阻塞业务程序。
这就是evbuffer的职责,也就是evbuffer要解决的问题。
evbuffer的原理,一言以蔽之,就是用若干段堆上分配的非连续空间,以链条的方式来存储连续数据。
libevent对evbuffer的实现,其功能是比较丰富的:
- 接收栈上内存数据;
- 接收堆上内存数据;
- 接收evbuffer;
- 接收可变参数数据;
- 接收整个文件;
- 接收片段文件;
2.1.5 bufferevent
bufferevent模块,是libevent的最外层,是直接面向调用方的。换言之,若要用libevent开发程序,bufferevent是程序员最常打交道的。
bufferevent,在libevent库的使用方和evbuffer之间,起着承上启下的作用。
2.1.6 evconnlistener
在基于libevent实现tcp的服务端程序时,如果裸用系统调用的话,需要每次都要通过socket()来创建fd,再通过bind()来绑定,再通过listen()来监听,然后通过accept()来接收客户端连接,最后再通过send()/recv()来收发数据,完了再通过close()关闭fd。
这些都是例行程序,libevent通过evconnlistener来完成此功能。换言之,当我们基于libevent实现tcp的服务端app时,直接用evconnlistener来实现一个acceptor即可,几行代码即可完成。
值得一提的是,若选择的是epoll,则缺省用ET边缘模式来提高效率。
2.1.7 重要数据结构
然后,来介绍下libevent的重要数据结构,比如event_base,event,eventop,evconnlistener,iocp相关等等。
http server
在libevent实现了reactor和evbuffer以及bufferevent特性之后,这就是一个完整的网络库了,我们就可以利用libevent库来开发任何tcp或udp程序了。
libevent的官方,利用libevent实现了3个例子:http客户端和服务器,dns和rpc。本文以http为例来介绍,其他例子可类比理解。
值得一提的是,libevent实现的http,是基于http1.2协议以下,只实现了http1.1标准部分,未实现http的流式传输。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/bian-cheng-ri-ji/49619.html