一般通过libevent进行网络编程,都是将一个socket的fd与一个event进行绑定,并自行维护一个buffer用于存储从socket上接收的数据,同时可能也用于待发送数据的缓存。然后通过可读可写事件从socket上收取数据写入缓存并进行相应处理,或者将缓存中的数据通过socket发送。
libevent为这种带缓存的IO模式提供了一种通用的机制,那就是bufferevent。一个bufferevent包含了一个底层传输的fd(通常为socket),一个输入buffer和一个输出buffer,并且bufferevent已经帮我们完成了从socket上接收数据写入输入buffer,同时从输出buffer中取出数据通过socket发送,当输入输出缓存中的数据达到一定量时调用我们设置的回调函数。这样使得我们可以更加关注数据的处理。
bufferevent的简单使用
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#include "arpa/inet.h"
#include "event.h"
//读回调处理
void read_callback(struct bufferevent * pBufEv, void * pArg)
{
//获取输入缓存
struct evbuffer * pInput = bufferevent_get_input(pBufEv);
//获取输入缓存数据的长度
int nLen = evbuffer_get_length(pInput);
//获取数据的地址
const char * pBody = (const char *)evbuffer_pullup(pInput, nLen);
//进行数据处理
//写到输出缓存,由bufferevent的可写事件读取并通过fd发送
//bufferevent_write(pBufEv, pResponse, nResLen);
return ;
}
//写回调处理
void write_callback( struct bufferevent * pBufEv, void * pArg )
{
return ;
}
//事件回调处理
void event_callback(struct bufferevent * pBufEv, short sEvent, void * pArg)
{
//成功连接通知事件
if(BEV_EVENT_CONNECTED == sEvent)
{
bufferevent_enable(pBufEv, EV_READ);
}
return ;
}
int main( void )
{
struct event_base * pEventBase = NULL;
struct bufferevent * pBufEv = NULL;
//创建事件驱动句柄
pEventBase = event_base_new();
//创建socket类型的bufferevent
pBufEv = bufferevent_socket_new(pEventBase, -1, 0);
//设置回调函数, 及回调函数的参数
bufferevent_setcb(pBufEv, read_callback, write_callback, event_callback, NULL);
struct sockaddr_in tSockAddr;
memset(&tSockAddr, 0, sizeof(tSockAddr));
tSockAddr.sin_family = AF_INET;
tSockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
tSockAddr.sin_port = htons(50000);
//连接服务器
if( bufferevent_socket_connect(pBufEv, (struct sockaddr*)&tSockAddr, sizeof(tSockAddr)) < 0)
{
return 0;
}
//开始事件循环
event_base_dispatch(pEventBase);
//事件循环结束 资源清理
bufferevent_free(pBufEv);
event_base_free(pEventBase);
return 0;
}
使用细节
tcp连接断开处理
对于客户端来说,如果仅有bufferevent这么一个事件,那么当tcp连接断开时,调用回调函数后会退出事件循环(event_base_loop)。因为bufferevent感知tcp连接断开后会删除相关的事件,这个时候事件循环中没有任何事件,于是退出循环。
在官网的教程中看到可以对event_base设置选项EVLOOP_NO_EXIT_ON_EMPTY保证没有等待事件时也不会退出事件循环,但是在最新稳定版本中(libevent-2.0.21-stable)没有该选项设置,在2.1.x-alph中才有该选项。当然我们可以采用增加定时器事件的方式来处理断链后不退出事件循环,甚至进一步实现断链重连的功能。这个定时器事件可以是断链后在回调函数中动态增加,也可以是一开始就增加一个持久的定时器事件,检测连接状态并触发向服务器重连。例如:
int g_nState;
//定时器事件回调函数
void handle_timeout(int nSock, short sWhat, void * pArg)
{
if( 0 == g_nState )
{
struct bufferevent * pBufferEvent = (struct bufferevent *)pArg;
struct sockaddr_in tSockAddr;
memset(&tSockAddr, 0, sizeof(tSockAddr));
tSockAddr.sin_family = AF_INET;
tSockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
tSockAddr.sin_port = htons(50000);
bufferevent_socket_connect(pBufferEvent, (struct sockaddr*)&tSockAddr, sizeof(tSockAddr));
}
}
void event_callback(struct bufferevent * pBufEv, short sEvent, void * pArg)
{
//成功连接 状态变更
if( BEV_EVENT_CONNECTED == sEvent )
{
bufferevent_enable( pBufEv, EV_READ );
g_nState = 1;
}
//出现错误
if( 0 != (sEvent & (BEV_EVENT_ERROR)) )
{
//关闭fd 并更改状态
int fd = bufferevent_getfd(pBufEv);
if( fd > 0 )
{
evutil_closesocket(fd);
}
bufferevent_setfd(pBufEv, -1);
g_nState = 0;
}
}
int main( void )
{
...
//增加PERSIST的定时器事件
struct event eTimeout;
struct timeval tTimeout = {10, 0};
//回调函数的参数为bufferevent
event_assign(&eTimeout, pEventBase, -1, EV_PERSIST, handle_timeout, pBufferEvent);
evtimer_add(&eTimeout, &tTimeout);
...
}
这里需要注意的是:重连之前最好先关闭bufferevent中的fd,或者直接对bufferevent进行释放并重新创建一个新的bufferevent。如果是直接释放bufferevent再次新建,那么在创建bufferevent时记得设置BEV_OPT_CLOSE_ON_FREE参数,这样在释放bufferevent时会对fd进行关闭,从而不会出现fd泄漏。(不设置该参数,通过bufferevent_setfd传入fd,释放bufferevent后自行关闭fd也是一种处理方式)
心跳处理
通常,客户端与服务端之间都有心跳检测,以检测tcp链路是否正常。那么通过bufferevent开发的客户端或者服务端完成心跳检测功能可以有这么几种实现方式:
(1)增加定时器事件:前面提到了可以增加持久的定时器事件来检测状态并触发断链重连,当然我们也可以利用这个定时器事件来完成定时发送心跳包的功能。个人觉得这种方式不太好的一点是:需要有一种机制让定时器事件的回调处理函数获取bufferevent的句柄,例如作为定时器事件回调函数的参数,这样才能将心跳包的数据写入该bufferevent并通过fd发送,这两种事件搅合在一起会有些混乱。
(2)利用bufferevent的超时机制:bufferevent可以为读写设置超时时间,我们可以利用读超时来完成定时发送心跳包的功能。在事件的回调处理函数中处理BEV_EVENT_TIMEOUT|BEV_EVENT_READING事件,然后将心跳包写入输出缓存。这种方式有一点需要注意:bufferevent触发超时事件后会将对应的可读/可写事件删除,我们在处理完超时事件后需要重新注册一下对应的事件(bufferevent_enable)。
void event_callback(struct bufferevent * pBufEv, short sEvent, void * pArg)
{
if( BEV_EVENT_CONNECTED == sEvent )
{
bufferevent_enable( pBufEv, EV_READ );
//设置读超时时间 10s
struct timeval tTimeout = {10, 0};
bufferevent_set_timeouts( pBufEv, &tTimeout, NULL);
}
if(0 != (sEvent & (BEV_EVENT_TIMEOUT|BEV_EVENT_READING)) )
{
//发送心跳包
...
//重新注册可读事件
bufferevent_enable(pBufEv, EV_READ);
}
...
return;
}
高低水位的使用
默认情况下,bufferevent从fd上接收到任何数据并写入输入缓存区时,就会回调交给我们进行处理。而我们的客户端和服务端通信时都会遵循一定的协议(数据包格式),比如固定长度的包头,然后从包头中获取包体的数据长度,等包体的数据都接收完成后再进行实际处理。在这种情况下,我们可以设置读的低水位减少回调的次数。bufferevent会等输入缓存区中的数据长度超过最低水位时,才回调我们的函数进行业务处理。
实现细节
整体概况
从bufferevent的结构体中我们可以看到,bufferevent中包含了读,写两个事件,这两个事件的回调函数分别为bufferevent_readcb和bufferevent_writecb。bufferevent同时还包含了输入输出两个缓存区,以及读、写、事件回调函数的指针,高低水位的设置,事件驱动的句柄等。当触发可读可写事件后,回调bufferevent_readcb或bufferevent_writecb,在这里完成从fd上的数据收发,然后根据收发结果及高低水位的设置等来进行不同的回调处理。
evbuffer与bufferevent
bufferevent采用evbuffer作为输入输出缓存。evbuffer像是一个字节队列,在队列的末尾写入数据,在队列的头部读取数据。evbuffer具体实现则是一个链表,链表中的每个节点都是一块连续的内存块,往evbuffer写数据时(调用evbuffer_add/evbuffer_add_printf等函数),evbuffer内部动态创建链表节点,并紧凑的写入数据(一个节点写满后,再写另外一个节点);从evbuffer中删除数据时(调用evbuffer_remove/evbuffer_drain),从链表头部节点开始读取,当一个节点的数据被全部读取后删除该节点,如果未读取完,则用标示记录数据已读取(删除)的位置。对于这种头部有数据被标示为读取(删除)的节点,再次写入数据时,可能会进行调整,即将数据部分整体往前拷贝移动,然后再继续写入数据。
defer callback
在创建bufferevent时,可以设置不同的选项,其中一个是BEV_OPT_DEFER_CALLBACKS,这意味着延迟进行回调。所谓延迟回调,是将该事件延迟等到本次事件循环中所有active事件都处理完成后再进行该事件的处理。在event_base中,有一个active事件队列,一个defer事件队列,事件循环时,遍历active事件队列并进行相应的处理,当发现某个事件是需要延迟处理时,将该事件放到defer事件队列中,继续后续active事件的处理,等active事件队列中的事件都处理完成后,再处理defer队列中的事件。
对于bufferevent来说,当fd上有数据可读时,其实是先进行了一次回调(bufferevent_readcb),这个回调函数中判断是否需要延迟处理,如果不需要延迟处理则直接回调我们设置的回调函数,如果需要延迟处理,则等libevent处理完其他的active事件后再次调用bufferevent的回调函数,然后在这个回调函数中再调用我们设置的回调函数。
==============================
bufferevent总结到此,如有不正确之处,欢迎吐槽交流。
最后,学习bufferevent时在mailing list中看到这么一句话,个人非常喜欢,与大家分享。
I just don't want to do mistakes/bad design decisions without fully understanding how it works。