libevent算是我看的第一份开源库源码。在大学期间看过STL的vector容器的源码,因为当时vector是使用最多的一个容器。现在刚工作做的游戏服务器开发,就像看点网络库的源码,所以就选择了libevent1.4 相对容易学习的开源库。
http://mp.blog.csdn.net/postedit/79415586 点击打开链接
这个是一个怎样在vs下调试libevent的教程。调试时我们可以跟快速的去了解数据结构和框架流程。我当然是一个菜鸟,可以先去搜索一下其他博主的libevnt的讲解,有点基础了解后可能才看得懂我说的(毕竟水平在这里)。
以下是一个定时触发事件的代码,可以在event_int这里开始调式跟着走一遍,了解大致流程。
一、简单示例
void tine_cb(int fd/*文件描述符*/, short what, void *arg/*传递的参数*/)
{
std::cout<<"时间事件触发"<
(csdn这个代码编辑器有点恶心,格式对齐老是有问题。)
上面代码的运行结果是,每两秒就会输出一句。
一、event结构
struct event {
//指向下一个event事件,event都是储存在event_list双向列表中的。因为有很多evnet_list所以元素中也储存了下一个元素的指针方便使用。
TAILQ_ENTRY (event) ev_next;
//这是储存在event_base中(event_list*)activequeues二维数组中的。
TAILQ_ENTRY (event) ev_active_next;
//这是储存在event_base中sig结构体中的信号事件列表。
TAILQ_ENTRY (event) ev_signal_next;
//如果是定时事件,那么这个就记录了他在时间小根堆中的下标
unsigned int min_heap_idx;
//记录它所属的event_base
struct event_base *ev_base;
//如果是定时事件就是-1,I/O事件就是文件描述符,signal事件,是绑定的信号
int ev_fd;
//事件类型 I/O事件(EV_WRITE、EV_READ) 定时事件(EV_TIMEOUT) 信号(EV_SIGNAL)
short ev_events;
//如果触发,将调用回调函数的次数
short ev_ncalls;
//通常指向ev_ncalls或者为NULL
short *ev_pncalls;
//如果是定时事件,那么这个就记录了定时时间
struct timeval ev_timeout;
//事件优先级 event_base中(event_list*)activequeues 数组所在下标
int ev_pri;
//回调函数
void (*ev_callback)(int, short, void *arg);
//回调参数
void *ev_arg;
//回调结果
int ev_res;
//标记事件当时所处状态,在哪一个状态链表中,一共有6个
//#define EVLIST_TIMEOUT 0x01 //event在time堆中
//#define EVLIST_INSERTED 0x02 // event在已注册事件链表中
//#define EVLIST_SIGNAL 0x04 // 未见使用
//#define EVLIST_ACTIVE 0x08 // event在激活链表中
//#define EVLIST_INTERNAL 0x10 // 内部使用标记
//#define EVLIST_INIT 0x80 // event已被初始化
int ev_flags;
};
首先看event_init,这个是对libevent的初始化,也就是对event_base进行初始化。函数一进去就直接调用event_base_new()
一、event_bse_new
struct event_base * event_base_new(void)
{
int i;
struct event_base *base; //相当于一个reactor,声明后分配空间
if ((base = calloc(1, sizeof(struct event_base))) == NULL)
event_err(1, "%s: calloc", __func__);
detect_monotonic(); //通过调用clock_gettime()来检测系统是否支持monotonic时钟类型
gettime(base, &base->event_tv);//将当前时间赋值给event_tv
min_heap_ctor(&base->timeheap);//初始化小根堆 ,这个就是用来管理时间,最先要发生的事件就会在堆顶
TAILQ_INIT(&base->eventqueue);//初始化链表 这是用来存放所有事件的列表。
base->sig.ev_signal_pair[0] = -1;//初始化socket对 这两个先不管,是用来处理信号事件的两个socket
base->sig.ev_signal_pair[1] = -1;
base->evbase = NULL; //一个指向底层实现的指针,所有事件通知(除了时间事件)都是从它这里发出来的。而libevent就将这些事件通知统一到一起。
for (i = 0; eventops[i] && !base->evbase; i++) {//选择统一事件的方法,如epoll
base->evsel = eventops[i];
base->evbase = base->evsel->init(base);//选择完之后调用 window下就看win2.c文件,对select模式的一个封装
}
if (base->evbase == NULL)
event_errx(1, "%s: no event mechanism available", __func__);
if (evutil_getenv("EVENT_SHOW_METHOD")) //获得系统环境变量看是否需要打印一当前使用的evbase
event_msgx("libevent using: %s\n",
base->evsel->name);
event_base_priority_init(base, 1); //设置优先级的最高级,这里只有1个优先级 ,event_base还有一个事件的优先级列表,后面会讲
return (base);
}
可能现在对event_base这玩意还有点不熟悉,接下来看下这个结构的组成
一、event_base
struct event_base {
const struct eventop *evsel; ///<事件驱动引擎 前面所说的对select的一个封装,也可以指向epoll等模型
void *evbase; ///<事件驱动引擎的全局数据,在每一个事件引擎文件中定义,在了解基础的框架时,evsel和evbase暂时先不管
int event_count; //全部事件的数量,包括未激活的
int event_count_active; //当前处于激活状态的数量
int event_gotterm; //用来标记是否停止检测所有事件
int event_break; //是否立即停止检测
/* active event management */
struct event_list **activequeues; ///<激活优先级队列,二维数组下标越高的优先级越低,所以有点优先级低的事件可能会延迟响应
int nactivequeues; ///<激活队列数目 表示有多少个优先级。
/* signal handling info */
struct evsignal_info sig; ///<信号 处理信号事件的结构
struct event_list eventqueue; ///<全部事件队列
struct timeval event_tv; //记录时间的,在主循环中使用
struct min_heap timeheap; ///<这里libevent将定时器队列实现为一个最小堆,也就是为了每次都把时间最晚的定时器能取出来,然后实现超时。更其实算法很简单,想要详细的了解可以去看下算法导论的第六章的Priority queues.
struct timeval tv_cache; //这是一个时间缓存,在一个循环中不必多次去获得系统时间。
};
在evnet_base 初始化后,对event事件进行初始化
一、event_set
void event_set( struct event *ev, int fd /*文件描述符或者信号,对于定时事件,设为-1即可*/,
short events/*事件类型,比如EV_READ|EV_PERSIST, EV_WRITE, EV_SIGNAL等*/,
void (*callback)(int, short, void *)/*回调函数*/,
void *arg/*回调函数的参数*/)
{
//(event_base)current_base 这个全局变量,在evnet_base_new中初始化
ev->ev_base = current_base;
ev->ev_callback = callback;
ev->ev_arg = arg;
ev->ev_fd = fd;
ev->ev_events = events;
//对event的成员进行初始化
ev->ev_res = 0;
ev->ev_flags = EVLIST_INIT; //标记为已经初始化
ev->ev_ncalls = 0;
ev->ev_pncalls = NULL;
//e->min_heap_idx = -1; 初始化他在小根堆中的位置为-1;表示没有
min_heap_elem_init(ev);
//默认该事件的优先级为中
if(current_base)
ev->ev_pri = current_base->nactivequeues/2;
}
在event初始化时候,该event处于一个初始化状态。想要它被event_base检测到就需要将它加入到激活列表中。
一、event_add
int event_add(struct event *ev, const struct timeval *tv/*如果不是定时事件则为NULL*/)
{
struct event_base *base = ev->ev_base;//event_base
const struct eventop *evsel = base->evsel;//evsel为底层的操作
void *evbase = base->evbase;
int res = 0;
//为timer事件分配空间
if (tv != NULL && !(ev->ev_flags & EVLIST_TIMEOUT)) {
if (min_heap_reserve(&base->timeheap, //为下一个即将储存的timeval分配内存空间,但还没有赋值初始化。
1 + min_heap_size(&base->timeheap)) == -1)
return (-1);
}
//添加事件 注意定时事件没有在这儿添加哦 定时事件在外层框架就可以出了哦,不用去调用那些seclet什么的
if ((ev->ev_events & (EV_READ|EV_WRITE|EV_SIGNAL)) &&
!(ev->ev_flags & (EVLIST_INSERTED|EVLIST_ACTIVE))) {
res = evsel->add(evbase, ev);//底层回调函数,外层框架到这就结束了先不要跟进去。就把他当做是另一个库在执行
if (res != -1)
event_queue_insert(base, ev, EVLIST_INSERTED);//event插入队列
}
//如果是timer事件将调整timer堆
if (res != -1 && tv != NULL) {
struct timeval now;
//表明event已经在timer堆中了,删除旧的
if (ev->ev_flags & EVLIST_TIMEOUT)
event_queue_remove(base, ev, EVLIST_TIMEOUT);
//表明event已经在激活列表中了,删除旧的
if ((ev->ev_flags & EVLIST_ACTIVE) &&
(ev->ev_res & EV_TIMEOUT)) {
if (ev->ev_ncalls && ev->ev_pncalls) {
/* Abort loop */
*ev->ev_pncalls = 0;
}
event_queue_remove(base, ev, EVLIST_ACTIVE);
}
gettime(base, &now);//计算时间并插入到timer的小根堆中
evutil_timeradd(&now, tv, &ev->ev_timeout);//宏定义:timeout= now + tv
event_queue_insert(base, ev, EVLIST_TIMEOUT);
}
return (res);
}
将event成功加入到激活列表后,我们只需开始event_base的事件检测主循环了。来检测那些时间发生了,并按事件的优先级来执行他们的回调。
在示例中调用的event_dispatch,在函数里面也是直接调用event_base_loop。
一、event_base_loop
{
const struct eventop *evsel = base->evsel;
void *evbase = base->evbase;
struct timeval tv;
struct timeval *tv_p;
int res, done;
//清空时间缓存,该变量的作用就是避免每次都去获得系统时间
base->tv_cache.tv_sec = 0;
//第一次添加Signal事件时,指定信号所属的event_base
if (base->sig.ev_signal_added)
evsignal_base = base;
done = 0;
while (!done) { //进入事件主循环
//设置event_base的标记,以表明是否需要跳出循环
if (base->event_gotterm) { //event_loopexit_cb()可设置
base->event_gotterm = 0;
break;
}
if (base->event_break) { //event_base_loopbreak()可设置
base->event_break = 0;
break;
}
timeout_correct(base, &tv); //校准时间,防止用户自己调时间,可能会导致某些定时事件就不会被触发了.//所以这里要修正小根堆的所有时间
tv_p = &tv;
//根据定时器堆中最小超时时间计算I/O多路复用的最大等待时间tv_p
//当I/O多路复用是堵塞时,如果等待时间太久的话,某些定时事件就不能及时触发。所以设置一个I/O的最长等待时间。
//如果不是堵塞时,则这个TV就没啥用了,直接清空
if (!base->event_count_active && !(flags & EVLOOP_NONBLOCK)) {
timeout_next(base, &tv_p);
}
else
{
evutil_timerclear(&tv);
}
//没有注册事件,则退出。所以要保证一直有监听事件,不然自己又要再启动一次loop
if (!event_haveevents(base)) {
event_debug(("%s: no events registered.", __func__));
return (1);
}
//获得当前的系统时间
gettime(base, &base->event_tv);
//当缓存时间不为0时,gettime函数会直接将缓存时间当作系统时间去赋值,为了下次gettime能获得跟精准的系统时间则需要将缓存时间为0
base->tv_cache.tv_sec = 0;
//调用I/O多路复用,底层回调函数,外层框架到这就结束了先不要跟进去。就把他当做是另一个库在执行
res = evsel->dispatch(base, evbase, tv_p);
if (res == -1)
return (-1);
//将time cache赋值为当前系统时间
gettime(base, &base->tv_cache);
//检查定时事件是否有事件触发,将触发的定时事件从小根堆中删除,插入到活跃事件链表中
timeout_process(base);
//当有事件触发时 什么事件类型都可以
if (base->event_count_active) {
//处理event_base的活跃链表中的事件
//调用event的回调函数,优先级高的event_list先处理
//因为有个优先级,所以优先级低的evnet_list可能要在下几次循环中执行
event_process_active(base);
//如果没有已经触发的事件了,且标记会只循环一次,则结束这次循环
if (!base->event_count_active && (flags & EVLOOP_ONCE))
done = 1;
} else if (flags & EVLOOP_NONBLOCK)
done = 1;
}
//循环结束,清空时间缓存
base->tv_cache.tv_sec = 0;
event_debug(("%s: asked to terminate loop.", __func__));
return (0);
}
多跟着event_base_loop走一次,就能大概理解一下运行流程了。有些函数实现没有去讲解,因为那些函数细节对整理流程没有多大影响。现在我们来对整个流程对一个梳理。
一、bufferevent 和 evbuffer
struct bufferevent {
struct event_base *ev_base; //
struct event ev_read;
struct event ev_write;
struct evbuffer *input; //输入缓存区,接受数据的缓存,用于将sokcet的数据缓存到这里来。
struct evbuffer *output; //输出缓存区,把将要发送的数据放到这里,当socket可写的时候,再去send
struct event_watermark wm_read; //读操作的 高低水位
struct event_watermark wm_write; //写操作的 高低水位
evbuffercb readcb; //当数据已经从I/O读到缓存区后,调用的回调
evbuffercb writecb; //当数据已经发送到I/O后才调用的,如果程序没有对低水位的处理,这个没有必要去设置
everrorcb errorcb; //错误回调
void *cbarg; //三个回调的参数
int timeout_read; /* in seconds */
int timeout_write; /* in seconds */
short enabled; //设置执行那些回调 ev_read和ev_write 当不需要write_cb时就不用填ev_write,然后这个回调就不会执行
};
struct evbuffer
{
u_char *buffer; //当前存放有效数据的缓冲区的内存起始地址
u_char *orig_buffer; //整个分配(realloc)用来缓冲的内存起始地址
size_t misalign; //origin_buffer和buffer之间的字节数
size_t totallen; //整个分配用来缓冲的内存字节数
size_t off; //当前存放有效数据的缓冲区的长度(字节数)
void (*cb)(struct evbuffer *, size_t, size_t, void *); //缓冲区有变化时调用的回调函数,可以不设置
void *cbarg; //回调函数的参数
};
持续更新中.....有什么需要问题 可在评论区下留言,我好加入在文章中