libevent源码解析--evbuffer_chain,evbuffer,bufferevent,bufferevent_private

1.概述
前面我们已经分析了eventevent_callbackevent_base及监听套接字处理。
有了event_base我们便可实现事件监控,事件分发处理。
有了监听套接字处理,我们便可实现服务端监听,通过accept得到服务端通信套接字。

一个网络库核心功能由客户端,服务端组成。
我们要实现网络库,除了上述设施,还需通信对象,客户端。
围绕通信对象主要有以下功能:
(1). 创建通信对象并对其初始化。
(2). 通信对象可以用来实现套接字上io事件的管理,io事件的处理。
(3). 借助通信对象提供的接口,我们可以实现发送数据。
(4). 借助通信对象提供的接口设置和获取其属性信息。
(5). 借助通信对象提供的接口关闭连接。
(6). 释放通信对象。
上述功能更多是通信对象通用的能力。

为了借助通信对象实现具体的逻辑功能。我们还需要能够为其指定事件处理函数,收包回调函数。
有了这些回调函数,内部再使得在连接建立,连接断开,连接错误时触发我们的事件处理函数,收取完整数据包时触发我们的收包回调函数,我们便可借助此通信对象实现具体的逻辑处理。

针对客户端,除了要具备一个通信对象的能力,还得具有主动发起连接,获取连接状态的能力。
这里,我们分析libevent中的通信对象与客户端。

2.结构
2.1.evbuffer_chain

struct evbuffer_chain {
	struct evbuffer_chain *next;
	size_t buffer_len;
	ev_misalign_t misalign;
	size_t off;
	unsigned flags;
#define EVBUFFER_REFERENCE	0x0004	
#define EVBUFFER_DANGLING	0x0040
	int refcnt;
	unsigned char *buffer;
};

在这里插入图片描述
我们这里讨论通信对象,libevent中每个服务于套接字的通信对象:
(1). 持有一个发送缓存区用于暂时缓存用户执行send发送的数据,在可写事件处理中会执行异步发送完成数据的实际发送。
(2). 持有一个接收缓存区用于暂时缓存可读事件处理中执行recv收取的套接字上的数据,在可读回调中提供给上层以便供其处理,处理后再消耗掉。

无论是发送缓存区,还是接收缓存区基本的组成单元是上图所示结构。可以将上述结构看成固定尺寸的内存块,代表一片连续可用内存空间。由管理区域,数据区域两部分组成。管理区域就是一个evbuffer_chain 类型实例。参考上图其结构各个字段含义如下:
(1). next
我们说上述只是构成缓存区的一个单元,多个这样的单元构成的链式结构组成缓存区。通过next形成链式结构。
(2). buffer_len
数据区域容量。
(3). misalign
无效部分尺寸。
(4). off
有效数据部分尺寸。
(5). flags
标志信息。
(6). buffer
指向数据区域起始位置。

由于上述组成单元要么用于实现发送缓存区,要么用于实现接收缓存区。
我们分别讨论用于接收缓存区和发送缓存区下数据区域的一个典型变迁过程:
(1). 接收缓存区
假设我们的发送缓存区只含有一个evbuffer_chain 代表的固定尺寸块。
a. 初始时刻–我们分配一个容量为sizeof(evbuffer_chain)+1000缓存区,这样此缓存区构成我们上述由管理区域和容量为1000的数据区描述的结构。
在这里插入图片描述
此时buffer_len1000misalign0off0,整个数据区域均可用于接收来自recv获得的套接字数据。
b. 处理可读事件,执行recv向其中放入600字节数据
在这里插入图片描述
此时buffer_len1000misalign0off600,整个数据区域还剩400字节空间可用于继续接收来自recv获得的套接字数据。
c. 执行了上层回调,假设600个字节由一个尺寸为400字节的包,和一个尺寸300字节的包构成。
上层回调执行中对数据区域构成一个完整包的部分会触发包的处理逻辑,处理完毕后会将此部分消耗掉。
在这里插入图片描述
此时buffer_len1000misalign400off200,整个数据区域还剩400字节空间可用于继续接收来自recv获得的套接字数据。
这样我们就分析了作为发送缓存区组成单元时,数据区中三类区域的变迁过程。

(2). 发送缓存区
a. 初始时刻–我们分配了一个容量为sizeof(evbuffer_chain)+1000的缓存区,这样此缓存区构成我们上述由管理区域和容量为1000的数据区描述的结构。。
在这里插入图片描述
此时buffer_len1000misalign0off0,整个数据区域均可用于接收来自send调用中的待发送数据。
b. 用户通过send向发送缓存区写入一个尺寸为600的包
在这里插入图片描述
此时buffer_len1000misalign0off600,整个数据区域还剩400字节空间可用于继续接收来自send提供的应用数据。
c. libevent执行可写事件处理,将发送缓存区内尺寸为400的有效数据写入到了套接字内核缓存区。
在这里插入图片描述
此时buffer_len1000misalign400off200,整个数据区域还剩400字节空间可用于继续接收来自send提供的应用数据。

2.2.evbuffer

struct evbuffer {
	struct evbuffer_chain *first;
	struct evbuffer_chain *last;
	struct evbuffer_chain **last_with_datap;
	size_t total_len;
	size_t max_read;
	size_t n_add_for_cb;
	size_t n_del_for_cb;
#ifndef EVENT__DISABLE_THREAD_SUPPORT
	void *lock;
#endif
	unsigned own_lock : 1;
	unsigned deferred_cbs : 1;
	ev_uint32_t flags;
	struct event_base *cb_queue;
	int refcnt;
	struct event_callback deferred;
	LIST_HEAD(evbuffer_cb_queue, evbuffer_cb_entry) callbacks;
	struct bufferevent *parent;
};

libevent中的每个通信对象持有一个发送缓存区,一个接收缓存区。
每个缓存区用一个evbuffer 实例来描述。
其各个字段含义如下:
(1). first
指向组成缓存区的首个固定尺寸块。
(2). last
指向组成缓存区的末个固定尺寸块。
(3). last_with_datap
libevent中出于灵活性考量,设想了很多由固定尺寸块构成的链式结构场景。
比如发送缓存区下,随着用户陆续执行send,我们分配了10个块构成的链式结构来容纳数据,此后,可写事件处理中,执行异步发送将前3个块内的数据发送出去了,但块并未从链式结构移除,此时我们又希望能快速找到链式结构上首个含有效数据的块。此时需借助last_with_datap
接收缓存区下,随着陆续执行recv,我们分配了10个块构成的链式结构来容纳数据,此后,上层回调处理中,将前3个块内的数据消耗掉了,但块并未从链式结构移除,此时我们又希望能快速找到链式结构上首个含有效数据的块。此时需借助last_with_datap

这个字段是对最后含有效数据的块的前一块的next字段取地址后的结果。
(4). total_len
由于缓存区有多个块组成,所以我们需要一个额外字段记录构成缓存区的所有块内有效数据尺寸之和。
(5). max_read
用于接收缓存区时,用于限制一次recv最多可向缓存区放入的字节尺寸。
(6). n_add_for_cb,n_del_for_cb
libevent支持每次我们向缓存区写入新数据或对缓存区执行消耗动作后,借助event_callback,在event_base的事件循环处理中集中触发一次外部提供的应用层回调。
n_add_for_cb,n_del_for_cb将作为应用层回调的参数。用于告知两次回调间缓存区内数据增加量,减少量。
(7). lock,own_lock
当我们讨论通信对象持有的发送缓存区,接收缓存区时,缓存区对象总是借助隶属的通信对象的可递归互斥锁实现互斥保护。
所以,这里lock将指向隶属的通信对象持有的可递归的互斥锁。own_lock将为0
(8). deferred_cbs,deferred
前面说了,libevent中允许在我们向缓存区放入新数据,从缓存区消耗了数据时,引发指定的上层回调。
引发方式有两种:
a. 一种是在对缓存区操作后立即引发。此时deferred_cbs0deferred不需要设置。
b. 一种是在对缓存区操作后异步引发。此时deferred_cbs1deferred需要提前设置。借助手动分发deferred实现event_base事件循环后续执行这些异步引发的上层回调。

作为套接字发送缓存区使用时,可以借助这类机制实现通信对象的可写监控在发送缓存区存在有效数据时自动注册到event_base,在发送缓存区无有效数据时自动从event_base移除。来避免不必要的事件通知.
(9). flags
标志信息。
(10). cb_queue
隶属的通信对象所关联到的event_base
(11). parent
隶属的通信对象.
(12). callbacks
用于收集上层回调。上层回调会在每次事件处理后,缓存区内有效数据变化时被触发.触发方式参考对deferred_cbs,deferred的分析.

2.3.bufferevent
前面的evbuffer_chainevbuffer用于实现发送缓存区,接收缓存区,而bufferevent则用于实现通信对象。

struct bufferevent {
	struct event_base *ev_base;
	const struct bufferevent_ops *be_ops;
	struct event ev_read;
	struct event ev_write;
	struct evbuffer *input;
	struct evbuffer *output;
	bufferevent_data_cb readcb;
	bufferevent_data_cb writecb;
	bufferevent_event_cb errorcb;
	void *cbarg;
	short enabled;
};

其各个字段含义如下:
(1). ev_base
此通信对象所关联到的event_base。每个event_base会独占一个线程执行事件循环。通信对象的事件监控,事件处理,异步回调均放在关联event_base的事件循环中进行。
(2). be_ops
包含为通信对象提供支持的一组操作集合。

struct bufferevent_ops {
	// 名称
	const char *type;
	// bufferevent 可视为通信对象基础类型,一般作为更高层次类型的字段。
	// 这里表示作为某类型字段存在时,bufferevent字段距离依附类型实例起始地址的偏移量。
	off_t mem_offset;
	// 通过此方法向关联event_base注册指定类型事件
	int (*enable)(struct bufferevent *, short);
	// 通过此方法向关联event_base取消注册指定类型事件
	int (*disable)(struct bufferevent *, short);
	// 在通信对象释放阶段1执行此操作--异步释放发起
	void (*unlink)(struct bufferevent *);
	// 在通信对象释放阶段2执行此操作--异步回调中
	void (*destruct)(struct bufferevent *);
	// 允许通过此方法来控制通信对象,比如获取或设置其属性
	int (*ctrl)(struct bufferevent *, enum bufferevent_ctrl_op, union bufferevent_ctrl_data *);
};

(3). ev_read
针对服务于套接字的通信对象,这个event用于代表可读事件对应的event
前面介绍event时,可知event是外部向event_base注册事件监控的载体。依附其的event_callback则提供了如何对事件执行处理的回调函数。
(4). ev_write
针对服务于套接字的通信对象,这个event用于代表可写事件对应的event
(5). input
代表了隶属于通信对象的接收缓存区。
(6). output
代表了隶属于通信对象的发送缓存区。
(7). readcb
前面讨论通信对象时候说了,通信对象需要在收取到套接字上数据后触发上层回调。以便应用层对已经接收数据进行反向序列化,获得数据包,对数据包进行逻辑处理,以便实现网络逻辑功能。
这个readcb就是上层提供的收包回调。
(8). writecb
libevent允许提供此回调,以便发送缓存区有效数据不足时,给上层一个通知处理时机。一般而言,不需要提供此回调。我们可直接通过通信对象的send接口实现数据发送。
(9). errorcb
允许用户提供此回调。在异步连接建立成功,连接建立失败,连接断开,产生错误事件时均触发此回调。以便应用层可针对性进行事件处理。
(10). cbarg
允许提供自定义数据。每次触发readcb,writecb,errorcb回调时会原样提供此自定义数据。
为了保证回调期间自定义数据有效性,提供者需保证,先释放通信对象,再释放自定义数据。
通信对象异步释放下,只有在异步回调之后释放自定义数据才被认为是安全的。
(11). enabled
用于表示通信对象上此时支持的事件类型。目前只能是EV_READ,EV_WRITE
只有一个事件类型先被支持,才有可能被注册到关联的event_base

2.4.bufferevent_private

struct bufferevent_private {
	struct bufferevent bev;
	unsigned own_lock : 1;
	unsigned readcb_pending : 1;
	unsigned writecb_pending : 1;
	short eventcb_pending;
	int errno_pending;
	struct event_callback deferred;
	enum bufferevent_options options;
	
	bufferevent_suspend_flags read_suspended;
	bufferevent_suspend_flags write_suspended;
	
	unsigned connecting : 1;
	unsigned connection_refused : 1;
	
	int refcnt;
	void *lock;
	ev_ssize_t max_single_read;
	ev_ssize_t max_single_write;
	union {
		struct sockaddr_in6 in6;
		struct sockaddr_in in;
	} conn_address;
};

前面说了bufferevent一般看成通信对象基础类型。
bufferevent_private 则构成了libevent实现套接字通信的通信对象完全体。
其各个字段含义:
(1). bev
依附于其的bufferevent对象,用于实现通信对象基础功能。
(2). own_lock
表示此对象是否拥有互斥锁。通信对象可自己拥有,也可采用隶属的更高层对象的互斥锁。
对我们分析的服务于套接字通信的bufferevent_private,其拥有互斥锁。故own_lock1
(3). readcb_pendingwritecb_pendingeventcb_pendingerrno_pending
bufferevent中允许使用者提供readcbwritecberrorcb三类回调。
在通信对象内部需要触发上述三类回调的场景,就我们分析的服务于套接字通信的bufferevent_private 而言,总是在需要时,直接触发回调即可。

libevent出于灵活及可扩展考量,还是提供了另外一种异步延迟触发回调的方式。
要使用异步延迟触发回调必须:
a. bufferevent_privateoptions包含BEV_OPT_DEFER_CALLBACKS。表示在对象层面支持这种行为。
b. libevent内部需触发应用回调处,必须执行触发函数时通过参数的options包含BEV_OPT_DEFER_CALLBACKS,来表示希望以异步延迟方式触发回调。

readcb_pending,writecb_pending ,eventcb_pending为延迟异步回调机制提供支持。来在引发阶段记录下那些异步回调被引发了。
错误类型事件回调异步引发时,需要在回调参数提供套接字错误码,errno_pending在引发阶段记录了错误码信息。
(4). deferred
异步回调机制使用时,deferred为其提供支持。
引发异步回调,即手动分发此event_callback对象到event_base,以便event_base事件循环中后续执行其处理函数。
其处理函数中再依据readcb_pending,writecb_pending ,eventcb_pending的设置,决定引发何种类型的回调。
(5). read_suspended
通信对象提供了临时禁止从套接字继续读取新数据及产生新的可读事件的机制。
当读取被禁止时,read_suspended里包含了禁止的原因信息。当这些原因解除后,就可解除禁止。
(6). write_suspended
通信对象提供了临时禁止向套接字内核发送缓存区继续写入新数据及产生新的可写事件的机制。
当写入被禁止时,write_suspended里包含了禁止的原因信息。当这些原因解除后,就可解除禁止。
(7). connecting
当我们通过connect接口发出连接请求,但连接过程尚未结束期间。connecting 将为1
(8). connection_refused
异步连接中收到对端拒绝提示时被设置为1。这样异步连接可写处理中将知道对端拒绝我们的连接请求。
(9). lock
指向持有的互斥锁对象。
(10). max_single_read,max_single_write
用于一次readwrite系统调用最大可操作数据尺寸进行限制。
(11). conn_address
这里是通信对象所连接的另一端的地址信息。

2.5.服务于套接字通信的bufferevent使用的be_ops

const struct bufferevent_ops bufferevent_ops_socket = {
	// 名称
	"socket",
	// bufferevent字段在依附的bufferevent_private中距离实例起始地址偏移量
	evutil_offsetof(struct bufferevent_private, bev),
	// 用于实现向关联的event_base注册指定类型event
	be_socket_enable,
	// 用于实现向关联的event_base取消注册指定类型event
	be_socket_disable,
	// 通信对象释放阶段1操作--异步释放发起时
	NULL, /* unlink */
	// 通信对象释放阶段2操作--如关闭关联的套接字
	be_socket_destruct,
	// 用于设置或获取通信对象的属性
	be_socket_ctrl,
};

3.功能
3.1.作为客户端的通信对象使用流程
3.1.1.创建通信对象
此步骤执行的关键步骤为:
(1). 为客户端分配一个bufferevent_private实例对象.
(2). 为此实例对象执行初始化.初始化参考上述对其各个字段含义的分析.
初始化过程指的注意的是:
a. 会为bufferevent_private动态分配一个evbuffer用于其发送缓存区,动态分配一个evbuffer用于其接收缓存区.
b. 会初始化其ev_readev_write两个event.这两个event的标志为EV_READ|EV_PERSIST|EV_FINALIZE.表示分别服务于可读事件,可写事件.且是持久的.EV_FINALIZE使得默认下执行event_del不会阻塞.
c.初始化过程为服务于发送缓存区的outbuf添加了一个回调对象.用于在发送缓存区内容从无变有时,检测若可写事件是支持的且未被禁止时,自动向关联的event_base添加可写event注册.
d. 对象初始化是enabled字段设置的是EV_WRITE.因为客户端异步连接依赖可写事件监控来完成异步连接结束处理.

3.1.2.设置通信对象上层回调
客户端要实现事件处理,收包处理,发送缓存区有效内容不足时处理,需设置好相应的上层回调函数,及回调时所用的自定义参数.

3.1.3.向关联event_base注册事件监控
为了使得关联的event_base可以在其事件循环里帮我们监控套接字上的事件,及在事件产生时分发对应的event以便后续执行依附其的event_callback上的回调处理.我们需通过接口让通信对象向关联event_base注册指定类型的event.对客户端,一般同时需要注册可读,可写类型的event.但如果此阶段bufferevent_private所关联的套接字为-1,注册时会忽略.

3.1.4.发起连接
我们可以通过接口使得bufferevent发起到某个地址对象的连接.其执行过程如下:
(1). 确定连接基于的套接字.
若关联的fd此时为-1,则会创建一个新的非阻塞套接字.
若关联的fd并非-1,我们应保证此套接字并未连接.
(2). 通过套接字发出连接请求.
a. 若connect返回值非负值,表示连接已经建立.
b. 若connect返回负值,但errnoEINPROGRESS,可认为连接请求正常发出,但尚未获得结果.
c. 若connect返回负值,但errnoEINPROGRESS,可算作失败.
(3). 针对上述,立即失败的场景,直接返回-1结束.
(4). 这里需要建立新的套接字和通信对象的关联.
所谓建立关联就是,将通信对象的两个event分别从关联的event_base取消注册.再重新用新的fd去初始化两个event,并按enabled字段去向event_base重新建立关联的过程.
(5). 设置connecting,表示通信对象此时处于连接中,并返回0.表示连接已经正常发起.

注意的是:
a. 连接正常发起时,无论后续失败还是成功,在结果达到时均会产生可写事件.在可写事件处理中分别处理异步成功,异步失败的动作.对应evutil_socket_connect_返回0
b. 针对connect返回EINTR按连接请求正常发出处理,依赖后续可写事件中执行异步的连接建立或连接失败处理.
d. 连接立即成功下,没有强制向关联event_base添加一次可写event.客户端创建时enable中含EV_WRITEbufferevent_setfd调用中会对enable了的event执行移除再添加.所以,通常不会有问题.对应evutil_socket_connect_返回
e. 发起连接立即收到连接拒绝错误的,按立即成功类别处理.后续可写event处理中会作为异步失败处理.对应evutil_socket_connect_返回2
f. 其他情况的错误,对应evutil_socket_connect_返回-1.此时bufferevent_socket_connect对外返回-1.外部此时应视为连接已经失败.执行连接失败处理.

3.1.5.断开连接
libevent中通信对象没提供主动断开的接口,只提供了通信对象释放的接口.
释放通信对象前会从关联的event_base移除此通信对象上所有关联到其的eventevent_callback,并通过event_base提供的异步释放机制在异步回调中执行实际的释放操作.

3.1.6.主动发送数据
参考上述关于evbuffer的描述.
向其写入新数据简要描述为:
(1). 若最后一个持有有效数据块内尚可放入,先将数据放入此块.
(2). 若数据还有剩余,分配新块,剩余数据放入新快.新快插入链式结构.
(3). 更新数据增量,立即触发一次发送缓存区上挂着的回调.此回调用于在需要时自动向关联event_base注册通信对象可写事件.

3.1.7.实现收取数据包处理
每次处理可读事件收到新的数据后会自动触发一次用户提供的收包回调.
在此回调里面应该,分析现有收取内容是否构成一个完整包.
若是,则应取出新包,反序列化后,处理包的逻辑.将包尺寸从缓存区对象上消耗掉.
若剩余部分,不足一个包应结束回调(后续再次可读并读取新的内容时会再次触发回调).

3.1.8.实现事件处理
事件处理一般划分为两类:
(1). 连接建立
此时作相应的连接建立处理即可.
(2). 超时或错误
针对此类情形一般直接释放连接对象即可.

3.1.11.释放通信对象
释放通信对象前会从关联的event_base移除此通信对象上所有关联到其的eventevent_callback,并通过event_base提供的异步释放机制在异步回调中执行实际的释放操作.

3.2.通信对象的io事件处理
3.2.1.实现可读事件处理
可简要描述为:
(1). 先计算本次执行一次recv最多可收取的数据量.
(2). 分析接收缓存区最后一块是否存在足够空间来完成数据接收,若存在在最后一块上作数据接收.
(3). 若不存在,分配一个新的块.基于新的块完成数据接收.新块插入链式结构.
(4). 若接收出错,则立即引发事件处理.若接收成功,则立即引发收包回调.

3.2.2.实现可写事件处理
可简要描述为:
(1). 若连接中收到可写事件,此时需进一步判断是属于异步连接成功,还是异步连接失败.
异步连接失败时,需向关联event_base移除可写,可读event,并立即触发事件处理.结束.
异步连接成功时,会立即触发事件处理.并判断若此时enabled不含EV_WRITE则向关联event_base取消可写event
(2). 计算本次允许向write写入的数据量.
(3). 会对发送缓存区各个块进行规整处理,规整的目的是得到一块连续的包含指定尺寸的待发送区域.规整过程可能涉及块间数据转移,产生新块,释放块等.
(4). 写入过程遭遇错误,会立即引发上层事件回调.
(5). 写入成功,会判断发送缓存区有效数据若此时为,则会从关联event_base移除可写event
(6). 写入成功,也会立即引发一个关于写入的回调.不过一般而言,通信对象没必要需要这样的回调.

3.2.作为服务端被动连接的通信对象使用流程
服务端的通信对象完全可参考客户端的通信对象使用.唯一的区别只是,不用再针对通信对象发起连接.直接基于accept得到的套接字产生新的通信对象下.设置其应用层回调.使能其可写,可读事件后,即可正常使用这样的对象收取包,发送包,处理其事件.

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

raindayinrain

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值