如何快速学习libevent源码

    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<<"时间事件触发"<<std::endl;
	
    timeval tv;
    evutil_timerclear(&tv);
    tv.tv_sec = 2;
	
    event *timeEvent = (event *)arg;        
	//注意的是,只要事件不是标记为PERSIST 所有事件都只触发一次,想要它重复执行就要重新添加时间。        
	//由于事件已经注册过了,所以不用在调用event_set,只用event_add直接加入到激活列表中。      
	event_add(timeEvent, &tv);
}

int _tmain(int argc, _TCHAR* argv[])
{        
    //定义一个时间事件timeval tv; 
    event timeout_test;       
	
    //定义一个时间,在libevent中都是通过timeval这个结构来表示时间。
    evutil_timerclear(&tv);    //一定要记得初始化
    tv.tv_sec = 2;            //设置时间为2秒,这个时间会在2秒后触发。
	
    //libevnt的初始化,里面会初始化一个全局变量event_base 这个是整个库的核心,所有的东西都是围绕着它在运行。
    event_init();     
	
    //将这个事件注册到event_base中,第一个参数就是事件本身,第二个参数文件描述符(时间事件就-1),第三个参数,第四个参数为事件触发时的        
    //调用函数,第五个参数为回调函数的参数(可为空)
    event_set(&timeout_test, -1, 0, time_cb, NULL);

    //将事件注册好之后,该函数就是将它添加到激活列表中,使得event_base在可以检测它的时间是否触发。tv就代表它在多少时间后背触发。
    event_add(&timeout_test, &tv);
    
    //该函数就是让整个框架开始运行,event_base就开始循环的去检测所有事件是否发生。      
    event_dispatch();
    return 0;
}

(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走一次,就能大概理解一下运行流程了。有些函数实现没有去讲解,因为那些函数细节对整理流程没有多大影响。现在我们来对整个流程对一个梳理。

    

一、buffereventevbuffer

这两个结构体在libevent使用中可有可无。
1.他就是对socket的I/O操作再进行了一次封装,作用是等到他将I/O数据复制到缓存区之后再执行用户的回调,这是线性执行的所以没有什么性能上的改善,但对代码的简洁和用户对库的使用上还是有帮助的。
2.可以控制消息的处理粒度(buffer的高低水位)等
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;   //回调函数的参数  
}; 


持续更新中.....有什么需要问题 可在评论区下留言,我好加入在文章中

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值