libevent框架
之前有做过分析,这次是谈谈如何将libevent搭建在vs工作环境下
,并且编写一个demo进行测试。测试过程中会再一次带大家分析消息是怎么传递的。我的libevent版本libevent-2.0.22-stable
,用对应的vs命令工具进入该目录
我的是Visual Studio 2008版本
的Command Prompt
执行成功后在libevent目录下生成三个lib
之后用vs创建控制台项目
生成成功后在项目目录里创建Include和Lib两个文件夹
分别进入libevent这两个目录里边
将内部的所有文件拷贝到Include文件夹里,event内容重复可以合并我们项目目录Include文件夹下的内容为
将libevent库中的三个lib拷贝到项目的Lib文件夹里
下一步配置项目属性,完成编译1、配置头文件包含路径,C++/General/Additional Include Directories 配置为相对路径的Include(因配置的路径不同而异)
2、配置代码生成
C/C++ /Code Generation RuntimeLibrary 设置为MTD,因为库的生成是按照这个MTD模式生成的,所以要匹配3、配置 C/C++ /Advanced/Compile As Compile as C++ Code (/TP)
(因为我的工程用到C++的函数所以配置这个)网上有人推荐配置成TC的也可以,自己根据项目需要4、配置库目录
Linker/General/Additional Library Directories ..\Lib(根据自己的Lib文件夹和项目相对位置填写)
5配置 Linker\Input\AdditionalLibraries
ws2_32.lib;wsock32.lib;libevent.lib;libevent_core.lib;libevent_extras.lib;
6 配置忽略项,可以不配置
输入\忽略特定默认库 libc.lib;msvcrt.lib;libcd.lib;libcmtd.lib;msvcrtd.lib;%(IgnoreSpecificDefaultLibraries)
生成lib后,不带调试信息,无法单步进函数里,所以要修改脚本:Makefile.nmake第二行CFLAGS=$(CFLAGS) /Od /W3 /wd4996 /nologo /Zi
到此为止项目配置好了,我们来写相关的demo代码
主函数
1 | int main(int argc, char **argv) |
listener回调函数
1 | static void listener_cb(struct evconnlistener *listener, evutil_socket_t fd, |
一些基本参数
static const char MESSAGE[] = “Hello, NewConnection!\n”;
static const int PORT = 9995;
bufferevent的写回调函数
1 | static void conn_writecb(struct bufferevent *bev, void *user_data) |
bufferevent的事件回调函数
1 | //仅仅作为事件回调函数,写自己想要做的功能就行 |
信号终止函数
1 | //程序捕捉到信号后就让baseloop终止 |
整个demo完成了。
下面分析下libevent如何做的消息传递和回调注册函数从main函数中的evconnlistener_new_bind入手
1 | struct evconnlistener * |
evconnlistener_new_bind 完成了socket生成和绑定,并且内部调用evconnlistener_new
生成了evconnlistener* listener,将listener和socket绑定在一起。
1 | struct evconnlistener * |
该结构包含一个evconnlistener和event事件结构体
evconnlistener结构如下
1 | struct evconnlistener { |
lev->base.ops = &evconnlistener_event_ops;这句就是对这个结构体指针赋值,evconnlistener_event_ops是一个实例化的结构体对象,里面包含定义好的操作函数
1 | static const struct evconnlistener_ops evconnlistener_event_ops = { |
对lev->base其余参数的赋值就不一一解释了。接下来看一下event_assign函数内部实现
1 | { |
event_assign
内部实现可以看出该函数仅仅对event
的属性进行设置
event_assign
主要对event设置了listener_read_cb
回调函数,这是
很重要的一个细节,我们看下listener_read_cb内部实现
1 | static void |
在evconnlistener_new中
//evconnlistener_cb 设置为listener_cb
lev->base.cb = cb;
lev->base
就是evconnlistener
对象
listener_read_cb
内部回调用绑定在evconnlistener
的listener_cb
。
记得之前我所说的绑定么?evconnlistener_new
这个函数里生成的lev
,
之后对lev,这里的lev就是 evconnlistener_event对象,lev->listener是
event对象,通过调用event_assign(&lev->listener, base, fd, EV_READ|EV_PERSIST, listener_read_cb, lev);
lev->listener绑定的就是listener_read_cb
。也就是是说
listener_read_cb
调用后,从而调用了绑定在evconnlistener
的listener_cb
。
那么我们只要知道lev->listener(event对象)的读事件是如何派发的就可以梳理此
流程了。
之前我们梳理过event_dispatch
里进行的事件派发,调用不同模型的dispatch,
稍后再梳理一遍。因为调用event_assign
仅仅对event设置了属性,还没有加到
事件队列里。
在evconnlistener_new
函数里调用完event_assign
,之后调用的是
evconnlistener_enable
,evconnlistener_enable这个函数完成了事件
添加到事件队列的功能。
1 | int |
上面有evconnlistener_event_ops结构体,那几个函数也列出来了。
我们看下event_listener_enable函数
1 | static int |
里面调用了event_add
1 | //添加事件操作 |
对于event_add
内部判断是否枷锁,进行加锁,然后调用
event_add_internal完成事件添加
1 | static inline int |
由于lev->listener(event)类型的事件是I/O 读事件,所以会进入evmap_io_add完成读事件的添加
1 | int |
该函数内部的一些函数就不展开,挺好理解的。到此为止我们了解了listen描述符的回调函数和读事件的绑定。
回到main函数,看下event_base_dispatch就知道绑定在lev->listener(event)类型的读事件listener_read_cb
是如何派发的,进而在读事件里完成了evconnlistener的listener_cb的调用。
1 | int |
int event_base_loop(struct event_base *base, int flags)这个函数内部
我们只看关键代码
1 | int |
之前有说过evsel初始化和模型选择的代码,这里重新梳理下const struct eventop *evsel
;
event_base_new或者event_init内部调用了event_base_new_with_config,
event_base_new_with_config函数调用了base->evsel = eventops[i]
;
eventops属主封装了几种模型结构体的指针
1 | static const struct eventop *eventops[] = { |
举个例子,看下epollops
1 | const struct eventop epollops = { |
eventop 类型
1 | struct eventop { |
epollops是eventop类型的变量,实现了增加删除,初始化,销毁,派发等功能
。
所以当模型选择epoll时,res = evsel->dispatch(base, tv_p)
;实际调用的是epoll的派发函数
1 | static int epoll_dispatch(struct event_base *base, struct timeval *tv) |
`evmap_io_active函数内部调用event_active_nolock,event_active_nolock中调用
event_queue_insert(base, ev, EVLIST_ACTIVE);负责将event放入激活队列里,
并且更新event在evmap中的 标记状态。`
到目前为止我们了解了事件派发的流程,event_base_loop循环执行网络模型的dispatch,内核返回就绪事件,dispatch内部调用evmap_io_active将就绪事件放入激活队列里
。
在event_base_loop中调用event_process_active处理就绪队列中的event
。比如内核返回listen描述符读就绪事件,那么就会将listen的event放入就绪队列中,
在event_process_active处理event的读事件
,调用了之前绑定的listener_read_cb回调函数。
下面看下
1 | static int |
循环调用了event_process_active_single_queue
1 | switch (ev->ev_closure) { |
(*ev->ev_callback)(ev->ev_fd, ev->ev_res, ev->ev_arg);就是调用绑定在event上的回调函数。
比如绑定在lev->listener(event)类型的读事件listener_read_cb;
从而调用了绑定在evconnlistener的listener_cb。
这样整个流程就跑通了。最上面有listener_cb函数的实现,整个消息传递流程不跟踪了,读者可以模仿上面的方式去跟踪消息。
这里简单表述下在bufferevent创建的时候调用了这个函数
1 | struct bufferevent * |
函数内部为ev_read和ev_write 设置默认的回调函数bufferevent_readcb和bufferevent_writecb。
接着为输出的 evbuffer绑定bufferevent_socket_outbuf_cb函数。
bufferevent_socket_outbuf_cb函数如果发现我们有可写的东西,并且没开始写,那么 将ev_
write事件加入event队列,跟上面的轮询一样,有可写就绪事件就会触发绑定在ev_write上的bufferevent-writecb函数。如果没有添加写的数据,就跳出函数。之后调用。由于这时处于bufferevent刚创建状态,那么说明没有数据写入bufferevent,所以这时是不会将ev_write加入event队列的。回到listener_cb函数。接着调用bufferevent_setcb 函数设置bufferevent的读,写,事件,
回调函数。
调用bufferevent_enable使写事件生效,
内部调用
bufev->be_ops->enable(bufev, impl_events);
bufferevent注册好的回调函数如下
1 | const struct bufferevent_ops bufferevent_ops_socket = { |
1 | static int |
eventbuffer读写事件加入到event队列里,此处为添加ev_write写事件,当写事件就绪,
轮询可以出发绑定的bufferevent_writecb回调函数。
当调用bufferevent_writecb
这个函数时,我们把内部代码简化分析
1 | static void |
_bufferevent_run_writecb内部调用了bufev->writecb(bufev, bufev->cbarg);也就是说我们自己实现的
1 | static void |
到此为止整个bufferevent消息走向梳理出来。最后有一点需要陈述,在listener_cb中最后调用bufferevent_write(bev, MESSAGE, strlen(MESSAGE));
`其内部调用evbuffer_add,该函数内部evbuffer_invoke_callbacks(buf);会调用bufferevent_socket_outbuf_cb,进而调用
bufferevent_write。`
所以我认为是调用evbuffer_add向outbuf中添加数据后,调用了evbuffer_invoke_callbacks,触发bufferevent_write, 或者ev_write先检测到写就绪事件,然后调用buffervent_write.这两者先后并不清楚
。
整个流程就是这样,需要继续研究然后梳理。