libevent杂谈
libevent是一个优秀的跨平台异步事件驱动库.当然,使用libevent的所有主品中,最有代表性的就是Memcached了.
本文主要讲是libevent的在应用上的一些基础知识,大湿们莫笑.
最简情形
最简单的libevent示例在libevent官方首页可下载.
地址:http://www.monkey.org/~provos/libevent/event-test.c
本示例中使用的是UNIX 管道fifo.具体流程是创建一个fifo文件,然后用libevent监听文件EV_READ事件,这里的EV_READ他哥是EV_WRITE,他们不是文件被读或者被写时的事件,而是一个可读或者可写的状态,说白了其实应该叫readable或者writeable.创建时管道是空的,当随便用shell 的echo 命令向里面写点东西时,libevent就会检测到这个状态,然后调用注册的回调函数处理这个事件.
整个代码也很简单,略去出错检验部分,对libevent的应用代码大致如下:
1 2 3 4 5 |
struct event evfifo; event_init(); event_set(&evfifo, socket, EV_READ, fifo_read, &evfifo); event_add(&evfifo, NULL); event_dispatch(); |
下面说一下上面的流程:
- 首先是申明一个event结构体对象.
- 然后调用event_init()初始化全局的event_base.
- 再调用event_set初始化第一步申明的evfifo对象.这个函数的第一个参数相当于函数返回值的功能了,C语言经常这么用.把evfifo的地址传进去,然后在函数返回时他就会被初始化成我们想要的类型.后面都是初始化它的参数.第二个socket当然是对哪一个socket上的事件进行监测,第三个是上面说过的事件类型,是一个short类型,第四个是回调函数名,当fifo可读时就调用这个函数,最后一个是传给这个函数的第三个参数(为什么是第三个,下面我们看了这个函数的实现就明白了).
- 然后调用event_add将初始化完成的evfifo放入事件队列.后面的NULL参数是一个time_out值.是libevent作为定时器时的设置,我们这里不需要,所以设置为NULL.
- 最后调用event_dispatch()驱动事件监测器就完成了.
这时候,当这个fifo文件有可读信息时,fifo_read函数就会被调用.而上面这个通过event_add加进去的事件会从集合中删除,以免重复监测到EV_READ状态.当然会有某些时候你不希望事件被通知一次就删除,你可以设置数据类型为EV_READ|EV_PERSIST.后一个参数可以使回调函数被调用时保持原事件不删除,依然留在事件队列中.
下面我们看一看fifo_read相关的实现.
1 2 3 4 5 6 |
fifo_read(int fd, short event, void *arg) { struct event *ev = arg; event_add(ev, NULL); //do something....; } |
首先是函数的参数,这个参数不是自己可以控制的,由于它是event的回调函数,前两个参数是默认的,而第三个参数就是我们上面的event_set函数的最后一个参数.我们可以看到这个参数传过来有什么用,他的用处就是让我们再将这个事件添加到全局的事件监测队列中去(第4行).
最简单的示例就讲完了.还有更复杂的吗?当然有,可以说上面的示例其实过于简单了,以至于我们随便个程序都能办到,而不必非得用libevnt.杀鸡用牛刀,是为了给没用过刀的人演示刀是怎么用的.
libevent结合socket编程
稍微复杂一点的例子是将管道换成socket连接,其实这里面大同小异,不过可能层次上要多一层,且听细细道来.
我们都非常清楚socket连接中的socket(),bind(),listen(),accept(),recv(),send(),close()这个最规范的流程,好,我们看一下其中哪些在正常情况下会导致同步地阻塞.accept在没有连接过来的时候,是会阻塞的,这是第一个,accept后如果socket中没有可读数据,调用recv是会阻塞的,这是第二个.好了,就他们俩.
所以这时候不能向上面那样一个main函数一个fifo_read就搞定了.这时候通常需要三个函数.
main()
sock_accept();
sock_read();
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
void sock_read(int fd, short event, void *arg){ char buf[1024]; int len; struct event *ev = arg; if((len = recv(fd, buf, sizeof(buf)-1,0)) == -1)& send(fd, buf, strlen(buf)+1, 0); event_add(ev, NULL); }
static void sock_accept(int fd, short event, void *arg){ struct event *ev = arg; struct sockaddr addr; int s; socklen_t len = sizeof(addr); struct event *rev = (struct event *)malloc(sizeof(*rev));
if((s = accept(fd, &addr, &len)) == -1){ fprintf(stderr, "Sock Accept Failed! "); exit(0); } event_set(rev, s, EV_READ, sock_read, rev); event_add(rev, NULL); event_add(ev, NULL); }
int main(){ struct event ev; int fd; struct sockaddr_in addr; if ((fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { fprintf(stderr, "Sock Create Failed! "); exit(1); }
bzero(&addr, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(1234); addr.sin_addr.s_addr = 0; if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) { fprintf(stderr, "Sock Bind Failed! "); exit(1); }
if (listen(fd, 10) == -1) { fprintf(stderr,"Sock Listen Failed! "); exit(1); }
event_init(); event_set(&ev, fd, EV_READ, sock_accept, &ev); event_add(&ev, NULL); event_dispatch(); } |
main用来做socket初始化的工作,一直执行到listen()函数,这时候将sock的fd加入到event事件队列中,设置回调函数sock_accept()
当有连接到来时,再调用sock_accept();sock_accept()执行accept函数产生一个新的socket fd,构造一个新的event对象,其fd为accept函数返回的fd,其监测状态为EV_READ,其回调函数为sock_read,将这个新event对象加入到event事件队列中.同时将main函数中的主event事件加入到event事件队列中,让我们设置的端口依然可以响应外部连接.
当通过accept新创建的这个 socket fd状态为可读时,sock_read函数被调用,他用函数recv从socket中读取信息,处理完业务逻辑后再调用send()返回结果给客户端..由于客户端通常是采用connect一次而请求多次的形式,所以在处理完成后,不能关闭这个socket,而且还要将在sock_accept中创建的这个监测是否有可读信息的event对象再将添加到event事件队列中.当然,你可以自己实现一个当recv到的数据是exit或者quit时就关闭连接的逻辑.
到此完成了整个调用.
上面有一个细节,就是在main函数中我们对event_set的调用是取了ev的地址,而ev的类型也不是一个指针,而在sock_accept里是申明了一个指针,然后用malloc来初始化的.当然,对evnet_set函数来说,只要是一个传入的地址就可以,我们这么做的原因是,在main函数中申明的ev会随着main的结束而被释放.当然main结束就是程序终止,释放是应该的,而我们不能让sock_accept中的对象在sock_accept执行完后就释放,因为sock_read还得用它呢.所以我们用了malloc.
实际情形
好了,上面其实也很简单.甚至可以说在真实环境下根本不够用,下面我们谈一下真正高并发请求下如何使用libevent编程.
首先考虑多线程的情况,多线程与libevent相结合的方式已经成为很多产品的选择,但是在libevent官方文档中指出,event_add函数是非线程安全的,如果我们使用多线程编程,必须做到的一点就是保证event_add函数不能出现在多个线程的执行流程中.
要实现这一点有两个方法:
- 后端应用逻辑的处理与libevent网络层处理分别用不同的线程.也就是网络层和具体的逻辑层分开,网络层将接收到的请求送入一个任务池就算完成任务,比如上面的只要accept创建了新的socket并将其加入任务池中就可以将主event重新添加到事件队列中.而后端启用一个独立的线程从任务池中取任务单元独立完成,这时所有任务是串行的,其实这根本就不是多线程处理.当然,这里说的是采用两个线程,网上也有人把网络层和逻辑层分布在不同机器通过网络通信实现.当然也是一种实现方式.
- 另一个方法比较直接.不是说event_add函数并不保证线程安全吗,那好,我把这个函数的处理放到一个池子里.通过单一的串行处理来不就行了.这时候我们的逻辑层完全可以是真正的多线程,每一个任务执行完后要将原事件再添加到event队列中时,不再调用event_add函数,而是将其放到一个池子中,由一单一线程将其一个个取出并串行的调用event_add添加到event队列中.
当然,还有人用每一个线程一个event_base的方法来实现,我个人是不太能接受这种山寨的方式.而且实现也不难想象,这里就不说了.
本人水平有限,如有错误,欢迎指正.
相关链接:
http://www.monkey.org/~provos/libevent/
http://hi.chinaunix.net/?20660017/viewspace-41444