1.概述
前面我们已经分析了event
,event_callback
,event_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
个固定尺寸块。
此时buffer_len
为1000
,misalign
为0
,off
为0
,整个数据区域均可用于接收来自recv
获得的套接字数据。
b. 处理可读事件,执行recv
向其中放入600
字节数据
此时buffer_len
为1000
,misalign
为0
,off
为600
,整个数据区域还剩400
字节空间可用于继续接收来自recv
获得的套接字数据。
c. 执行了上层回调,假设600
个字节由一个尺寸为400
字节的包,和一个尺寸300
字节的包构成。
上层回调执行中对数据区域构成一个完成包的部分会触发包的处理逻辑,处理完毕后会将此部分消耗掉。
此时buffer_len
为1000
,misalign
为400
,off
为200
,整个数据区域还剩400
字节空间可用于继续接收来自recv
获得的套接字数据。
这样我们就分析了作为发送缓存区组成单元时,块内三类区域的变迁过程。
(2). 发送缓存区
a. 初始时刻–分配了一个容量为sizeof(evbuffer_chain)+1000
个固定尺寸块。
此时buffer_len
为1000
,misalign
为0
,off
为0
,整个数据区域均可用于接收来自send
调用中的待发送数据。
b. 用户通过send
向发送缓存区写入一个尺寸为600
的包
此时buffer_len
为1000
,misalign
为0
,off
为600
,整个数据区域还剩400
字节空间可用于继续接收来自send
提供的应用数据。
c. libevent
执行可写事件处理,将发送缓存区内尺寸为400
的有效数据写入到了套接字内核缓存区。
此时buffer_len
为1000
,misalign
为400
,off
为200
,整个数据区域还剩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_cbs
为0
,deferred
不需要设置。
b. 一种是在对缓存区操作后异步引发。此时deferred_cbs
为1
,deferred
需要提前设置。借助手动分发deferred
实现event_base
事件循环后续执行这些异步引发的上层回调。
作为套接字发送缓存区使用时,可以借助这类机制实现通信对象的可写监控在发送缓存区存在有效数据时注册到event_base
,在发送缓存区无有效数据时从event_base
移除。
作为套接字接收缓存区使用时,暂时没发现需使用这个机制的地方。
(9). flags
标志信息。
(10). cb_queue
隶属的通信对象所关联到的event_base
(11). parent
隶属的通信对象
(12). callbacks
用于收集上层回调。
2.3.bufferevent
前面的evbuffer_chain
,evbuffer
用于实现发送缓存区,接收缓存区,而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_lock
为1
。
(3). readcb_pending
,writecb_pending
,eventcb_pending
,errno_pending
bufferevent
中允许使用者提供readcb
,writecb
,errorcb
三类回调。
在通信对象内部需要触发上述三类回调的场景,就我们分析的服务于套接字通信的bufferevent_private
而言,总是在需要时,直接触发回调即可。
但libevent
出于灵活及可扩展考量,还是提供了另外一种异步延迟触发回调的方式。
要使用异步延迟触发回调必须:
a. bufferevent_private
的options
包含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
用于一次read
,write
系统调用最大可操作数据尺寸进行限制。
(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_read
,ev_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
返回负值,但errno
为EINPROGRESS
,可认为连接请求正常发出,但尚未获得结果.
c. 若connect
返回负值,但errno
非EINPROGRESS
,可算作失败.
(3). 针对上述,立即失败的场景,直接返回-1
结束.
(4). 这里需要建立新的套接字和通信对象的关联.
所谓建立关联就是,将通信对象的两个event
分别从关联的event_base
取消注册.再重新用新的fd
去初始化两个event
,并按enabled
字段去向event_base
重新建立关联的过程.
(5). 设置connecting
为1
,表示通信对象此时处于连接中,并返回0
.表示连接已经正常发起.
注意的是:
a. 连接立即成功时,由于注册了对可写事件的监控.此时也会触发可写事件处理.可在可写处理中完成连接建立动作.
b. 连接正常发起时,无论后续失败还是成功,在结果达到时均会产生可写事件.在可写事件处理中分别处理异步成功,异步失败的动作.
libevent
连接发起有两处看着是有问题的:
a. 连接立即成功时,没保证一定注册可写事件监控是不对的,此时再手动引发一次上层设置的可写回调也是不对的.
b. connect
返回EINTR
算作连接已经正常发起也是不对的.此时应该再次connect
,直到返回值表示连接成功,连接失败,连接进行中才行.
3.1.5.断开连接
libevent
中通信对象没提供主动断开的接口,只提供了通信对象释放的接口.
释放通信对象前会从关联的event_base
移除此通信对象上所有关联到其的event
及event_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
移除此通信对象上所有关联到其的event
及event_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). 写入成功,会判断发送缓存区有效数据若此时为0
,则会从关联event_base
移除可写event
.
(6). 写入成功,也会立即引发一个关于写入的回调.不过一般而言,通信对象没必要需要这样的回调.
3.2.作为服务端被动连接的通信对象使用流程
服务端的通信对象完全可参考客户端的通信对象使用.唯一的区别只是,不用再针对通信对象发起连接.直接基于accept
得到的套接字产生新的通信对线下.设置其应用层回调.使能其可写,可读事件后,即可正常使用这样的对象收取包,发送包,处理其事件.