<span style="font-family: 微软雅黑; font-size: 14px; orphans: 2; widows: 2; background-color: rgb(255, 255, 255);"> 在libevent的官方文档中指出,bufferevent有四种,分别如下:</span>
(1)基于套接字的bufferevent
(2)异步IObufferevent
(3)过滤型bufferevent
(4)成对的bufferevent
在本文中,我们只介绍基于套接字的bufferevent,这种类型的bufferevent是使用最多的。剩下的三种类型我们将在后面的文章中陆续介绍。
另外我们还需提到一点,在官方文档中提到当前版本的libevent的bufferevent只支持面向流的协议。至于什么是面向流的协议在此处也不提及,后面的章节会介绍使用bufferevent机制读写串口,到时我们再正式的谈谈流到底是什么。
言归正传,在这里为什么我们要先介绍怎样创建一个基于套接字的bufferevent实例呢?是因为这种类型的bufferevent使用最多,并且libevent提供了很多的支持。
1、创建套接字bufferevent
创建套接字bufferevent的函数bufferevent_socket_new。该函数在event2/bufferevent.h中声明。
struct bufferevent *bufferevent_socket_new(struct event_base *base,evutil_socket_t fd,enum bufferevent_options options);
base是event_base,options是表示bufferevent选项的位掩码,fd表示一个用户指定的套接字描述符。函数成功返回一个bufferevent,失败返回NULL。
刚才提到的options选项是一个枚举类型,其成员如下:
BEV_OPT_CLOSE_ON_FREE:释放buffervent并关闭底层套接口。
BEV_OPT_THREADSAFE:自动为bufferevent分配锁,主要目的是为了在多线程环境下安全的使用bufferevent。这里需要注意,libevent不是线程安全的,这是其设计上的一个缺陷,但并不影响我们在多线程环境下使用它,前提是要了解libevent中的那些结构是全局的,需要我们枷锁处理的。至于linevent哪些结构是全局的以及其在多线程环境下的使用,我们在后面的章节会介绍。
BEV_OPT_DEFER_CALLBACKS:设置这个标志时,bufferevent延迟所有回调。
BEV_OPT_UNLOCK_CALLBACKS:默认情况下,如果设置bufferevent为线程安全的,则bufferevent会在调用用户提供的回调函数时进行锁定。如果设置了这个选项会让libevent在执行回调的时候不进行加锁。
2、为bufferevent设置回调函数
一个bufferevent只可以设置三个回调函数,一个写入回调,一个读取回调,一个事件回调。当bufferevent的输出buffer满足某个条件时,调用写回调函数。当bufferevent的输入buffer满足某个条件时,调用读回调函数。当套接口上某个事件发生时,调用事件回调函数。
该函数在event2/bufferevent.h中声明。其原型如下:
void bufferevent_setcb(struct bufferevent *bufev,bufferevent_data_cb readcb,bufferevent_data_cb writecb,bufferevent_event_cb eventcb,void *cbarg);
其中函数指针类型bufferevent_data_cb和bufferevent_event_cb eventcb的定义如下:
typedef void (*bufferevent_data_cb) (struct bufferevent *bev,void *ctx);
typedef void (*bufferevent_event_cb)(struct bufferevent *bev,short events,void *ctx);
通过bufferevent_setcb可以设置用户自己编写的读/写/事件回调函数,但是必须遵循其相应的接口类型。比如说读/写回调函数必须是返回类型为void,参数列表为(struct bufferevent *bev,void *ctx)的形式。函数bufferevent_setcb的最后一个参数是用户传递的(如果不想设置,可以传递NULL),对于所有的回调函数共享的一个指针变量,如果其中某一个回调函数修改了,那么在其它函数中也生效。
2.1 启/停bufferevent上的读写事件
void bufferevent_enable(struct bufferevent *bufev,short events);
void bufferevent_disable(strcut bufferevent *bufev,short events);
short bufferevent_get_enabled(short bufferevent *bufev);
bufferevent_enable用来启用bufferevent上的读/写事件。bufferevent_disable用来禁用bufferevent上的读/写事件。bufferevent_get_enabled用来返回该bufferevent上启用的事件。
2.2 设置读写事件的“水位”
所谓的“水位”是针对bufferevent的输入buffer,输出buffer而言的。“水位”是触发读写事件回调函数的触发器。当上面提到的某个buffer达到某个“水位”时,相应的回调函数将会触发。
2.2.1 设置读事件“水位”
![]()
 |
| 读事件水位设置示意图 |
读取低水位线是相对于bufferevent的输入buffer而言的,数据源输入buffer中的数据不断的被写入到bufferevent的输入缓冲区中。当bufferevent的输入buffer中的数据的个数(字节数)>低水位线时才触发读事件回调函数。其目的是只有当攒够了一定量的数据的时候才进行读取操作,减少了读bufferevent输入buffer的次数。默认情况下读取低水位线为0,也就是说bufferevent的输入buffer只要有一个字节的数据也会触发读事件的回调函数。读取高水位线是相对于bufferevent的输入buffer而言的,数据源输入buffer中的数据不断地被写入到bufferevent的输入缓冲区中,当bufferevent的输入缓冲区中数据的个数(字节数)超过了指定的高水位线后,数据源输入buffer中的数据将不会被写入到bufferevent中,直到bufferevent中的数据被抽取降到高水位线以下。默认情况下,此值为无限大,也就是说数据源中的数据一直被不断地写入到bufferevent的输入buffer中。
2.2.2 设置写事件“水位”
![]()
 |
| 写事件水位设置示意图 |
写入低水位线是相对于bufferevent的输出buffer而言的。bufferevent的输出buffer中的数据不断被写入到数据源的输出buffer中。当bufferevent的输出buffer中数据的个数(字节数)< 低水位线时触发用户设定的写回调函数,此时用户可以将自己定义的缓冲区的数据写入到bufferevent的输出buffer中。默认情况下,低水位线为0,表示只有当bufferevent的输出buffer为空时才调用用户的写回调函数。
写入高水位线对于用户而言是没有意义的。它在bufferevent用作另外一个bufferevent的底层传输端口时有特殊意义。我们在成对的bufferevent中介绍。
说了这么多,那到底怎么设置bufferevent读写的高低水位呢?libevent提供了函数bufferevent_setwatermark。其原型定义在event2/bufferevent.h中。
void bufferevent_setwatermark(struct bufferevent *bufev,short events,size_t lowmark,size_t highmark);
通过bufferevent_setwatermark的参数events可以单独设置读取水位,或者写入水位,或者俩者都同时进行设置。对于高水位用0表示无限。
下面我们使用基于套接口bufferevent创建一个回显程序echo。
echo由两部分组成:客户端和服务器端。客户端通过标准输入(stdin)输入一行文本发送到服务器端,然后服务器端将接收到的文本再返回给客户端。
服务器端tcpserv程序代码清单如图1所示:
#include "../unp.h"
int main(int argc, char **argv) {
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
void sig_chld(int);
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
Signal(SIGCHLD, sig_chld); /* must call waitpid() */
for ( ; ; ) {
clilen = sizeof(cliaddr);
if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) {
if (errno == EINTR)
continue; /* back to for() */
else
err_sys("accept error");
}
if ( (childpid = Fork()) == 0) { /* child process */
Close(listenfd); /* close listening socket */
str_echo(connfd); /* process the request */
exit(0);
}
Close(connfd); /* parent closes connected socket */
}
}
|
|
图2 服务器端tcpserv程序代码清单
|
服务器端程序没有使用libevent框架,它的功能是监听套接口,如果一旦有连接请求到来,就创建一个子进程然后回射用户发送过来的数据。我们主要介绍客户端tcpcli程序。
服务器端程序运行如下:
客户端tcpcli程序代码清单如图2所示:
#include <event2/event.h>
#include <event2/bufferevent.h>
#include <event2/buffer.h>
#include <event2/util.h>
#include "../unp.h"
struct info {
const char *name;
size_t total_drained;
};
char recvline[1024] = {'a','b','\n','\0'};
void read_callback(struct bufferevent *bev,void *ctx) {
struct info *inf =(struct info *) ctx;
struct evbuffer *input = bufferevent_get_input(bev);
size_t len = evbuffer_get_length(input);
if(len) {
inf->total_drained += len;
evbuffer_drain(input,len);
fprintf(stderr,"we received [%lu] bytes from %s\n",(unsigned long)len,inf->name);
}
}
void write_callback(struct bufferevent *bev,void *ctx) {
char sendline[MAXLINE];
struct evbuffer *output = bufferevent_get_output(bev);
if(Fgets(sendline, MAXLINE, stdin) != NULL) {
fprintf(stderr,"we sent [%s]\n", sendline);
bufferevent_write(bev,sendline,strlen(sendline));
}
}
void event_callback(struct bufferevent *bev,short events,void *ctx) {
struct info *inf = (struct info *)ctx;
struct evbuffer *input = bufferevent_get_input(bev);
int finished = 0;
if(events & BEV_EVENT_EOF) {
if(events & BEV_EVENT_READING) {
size_t len = evbuffer_get_length(input);
fprintf(stderr,"Got a close from %s.we drained %lu bytes from it,and have %lu left.\n",inf->name,(unsigned long)inf->total_drained,(unsigned long)len);
} else if(events & BEV_EVENT_WRITING) {
fprintf(stderr,"writing error\n");
}
finished = 1;
}
if(events & BEV_EVENT_ERROR) {
if(events & BEV_EVENT_READING) {
fprintf(stderr,"Got an error from %s:%s\n",inf->name,evutil_socket_error_to_string(EVUTIL_SOCKET_ERROR()));
} else if(events & BEV_EVENT_WRITING) {
fprintf(stderr,"writing error\n");
}
finished = 1;
}
if(events & BEV_EVENT_CONNETTED) {
bufferevent_write(bev,recvline,strlen(recvline));//向服务器写入一行
}
if(finished) {
free(ctx);
bufferevent_free(bev);
}
}
int main(int argc, char **argv) {
int sockfd;
struct sockaddr_in servaddr;
struct event_base *base;
struct bufferevent *bev;
struct info* info1;
info1 = (struct info*)malloc(sizeof(struct info));
info1->name="buffer1";
info1->total_drained = 0;
if (argc != 2)
err_quit("usage: tcpcli <IPaddress>");
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
base = event_base_new();
bev = bufferevent_socket_new(base,-1,BEV_OPT_CLOSE_ON_FREE|BEV_OPT_DEFER_CALLBACKS);//创建一个套接口类型的bufferevent
bufferevent_setwatermark(bev,EV_READ|EV_WRITE,0,0);//设置读写的数位
bufferevent_setcb(bev, read_callback,write_callback,event_callback,info1);//设置bufferevent的读/写/事件回调函数
bufferevent_enable(bev,EV_READ|EV_WRITE);//使能读写
if(bufferevent_socket_connect(bev,(SA*)&servaddr,sizeof(servaddr)) < 0) {//连接到服务器127.0.0.1
bufferevent_free(bev);
return -1;
}
fprintf(stderr,"connec ok!\n");
event_base_dispatch(base);
exit(0);
}
|
| 图2 客户端tcpcli程序代码清单 |
我们可以这样运行tcpcli客户端程序:
./tcpcli 127.0.0.1
tcpcli程序中像创建套接口的Socket函数是在系统提供的socket函数的基础上进行了封装,其定义在unp.h中。
下面我们来分析一下这段代码。
结构体struct info用来记录我们从bufferevent的输入buffer中读取多少数据。可以通过name给bufferevent指定名称。total_drained表明读取的数据的个数(字节数)。
函数read_callback是我们定义的读回调函数。首先我们通过函数bufferevent_get_input获取bufferevent的输入buffer。函数bufferevent_get_input定义在event2/bufferevent.h中。其原型如下:
struct evbuffer *bufferevent_get_input(struct bufferevent *bufev);
struct evbuffer *bufferevent_get_output(struct bufferevent *bufev);
然后我们调用函数evbuffer_get_length获取输入buffer中数据的个数(字节数)。函数evbuffer_get_length定义在event2/buffer.h中,其原型如下:
size_t evbuffer_get_length(const struct evbuffer *buf);
如果bufferevent的输入buffer中有数据我们就将其数据的个数(字节数)累加到total_drained。
函数write_callback是我们定义的写回调函数。首先我们获取bufferevent的输出缓冲区。然后从标准输入读取一行,发送到服务器端。
函数event_callback是我们定义的事件回调函数。首先读取bufferevent的输入缓冲区,然后检测某个事件是否发生。其中参数events是libevent初始化的,如果发生了某个事件那么就会被设置到events中。bufferevent事件分为两类:读操作事件和写操作事件。每一类又定义了具体的事件。
BEV_EVENT_READING:读取操作时发生了某事件。
BEV_EVENT_ERITING:写入操作时发生某事件。
BEV_EVENT_ERROR:操作时发生错误。
BEV_EVENT_TIMEOUT:发生超时。
BEV_EVENT_EOF:遇到文件结束符。
BEV_EVENT_CONNECTED:请求的连接过程已经完成。
比如说当读取操作时发生了超时,那么events参数将被设置为BEV_EVENT_READING|BEV_EVENT_TIMEOUT。如果写操作发生了超时,那么events参数将被设置为BEV_EVENT_WRITING|BEV_EVENT_TIMEOUT。
在main函数中我们首先初始化了struct info,然后创建了套接字sockfd和bufferevent,调用bufferevent_socket_connect连接到服务器端。在上面的介绍中我们提到,基于套接口的bufferevent是使用最多的,并且livevent提供了在其上提供了很多的现成的封装。函数bufferevent_socket_connect就是libevent提供的对系统函数connect的封装,调用此函数后如果套接口连接到服务器,libevent将会设置BEV_EVENT_CONNECTED事件,然后调用事件回调函数。如果不使用此函数,而是使用系统的connect函数,连接上服务器后libevent将会设置写入事件和BEV_EVENT_CONNECTED事件,将会触发写回调函数。
一旦连接建立,也就是说BEV_EVENT_CONNECTED事件产生将会调用函数bufferevent_write向套接口发送a,b,\n三个字符。函数bufferevent_write定义在event2/bufferevent.h中,其原型如下:
int bufferevent_write(struct bufferevent *bufev,const void *data,size_t size);
服务器端接收到a,b,\n三个字符,然后回射到客户端。
但是此时客户端程序并没有打印出a,b,\n而是阻塞在写回调函数的Fgets,等待用户输入。那么现在我们在客户端输入一行。
我们输入了mill\n后,在屏幕上打印出we received [3] bytes from buffer1。不对啊,我们输入的是mill\n一共5个字符,怎么说收到了三个字符呢?我们来分析一下出现这个情况的原因。
![]()
 |
| 图3 客户端和服务器端交互图 |
首先是客户端发起连接,三路握手结束之后,发送a,b,\n三个字符。发送完之后首先触发的是写回调函数?为什么不是先触发读回调函数呢?因为发送完数据之后,bufferevent的输出buffer被清空了,变成了0,写回调函数被触发。读回调函数的触发条件是bufferevent的输入buffer有数据,因为发送数据有延时,所以读回调函数的触发要滞后于写回调函数。这就导致了数据的不同步。
那既然是先触发的写回调函数,那么我们在写回调进行读操作,在读回调进行写操作是不是就能同步了呢?我们更改代码如下:
void read_callback(struct bufferevent *bev,void *ctx) {
fprintf(stderr,"in read call back\n");
char sendline[MAXLINE];
struct evbuffer *output = bufferevent_get_output(bev);
if(Fgets(sendline, MAXLINE, stdin) != NULL) {
fprintf(stderr,"we sent [%s]\n", sendline);
bufferevent_write(bev,sendline,strlen(sendline));
}
}
void write_callback(struct bufferevent *bev,void *ctx) {
fprintf(stderr,"in write call back");
struct info *inf =(struct info *) ctx;
struct evbuffer *input = bufferevent_get_input(bev);
size_t len = evbuffer_get_length(input);
if(len) {
inf->total_drained += len;
evbuffer_drain(input,len);
fprintf(stderr,"we received [%lu] bytes from %s\n",(unsigned long)len,inf->name);
}
}
|
| 图4 交换了功能的读写回调函数 |
在更改的代码中我们让写回调函数读取bufferevent的输入buffer,在读回调函数进行写入bufferevent的输出buffer。这样我们期望的结果是libevent首先调用写回调函数,执行完相应的代码,再执行读回调函数。然而执行的结果让我们大出意外。执行结果如下:
这是为什么呢?其实上面的这种想法是错误。因为写回调的触发条件时bufferevent的输出buffer空了。但是并不一定输入buffer中有数据了。所以你在写回调触发的时候去读取数据并不一定成功。所以写回调函数首先被触发但是输入buffer中没有数据,返回。随后写回调触发,调用Fgets函数阻塞在用户输入。
还有什么好的办法吗?或许这种方法可行。我们定义一个全局的变量static int is_read_ok,按如图5所示方式操作它:
 |
| 图5 echo读写同步时序图 |
程序代码清单如下:
static int is_read_ok = 0;
void read_callback(struct bufferevent *bev,void *ctx) {
fprintf(stderr,"in read call back\n");
struct info *inf =(struct info *) ctx;
struct evbuffer *input = bufferevent_get_input(bev);
size_t len = evbuffer_get_length(input);
if(len) {
is_read_ok = 1;
inf->total_drained += len;
evbuffer_drain(input,len);
fprintf(stderr,"we received [%lu] bytes from %s\n",(unsigned long)len,inf->name);
}
}
void write_callback(struct bufferevent *bev,void *ctx) {
fprintf(stderr,"in write call back");
char sendline[MAXLINE];
struct evbuffer *output = bufferevent_get_output(bev);
if(is_read_ok == 1) {
if(Fgets(sendline, MAXLINE, stdin) != NULL) {
fprintf(stderr,"we sent [%s]\n", sendline);
bufferevent_write(bev,sendline,strlen(sendline));
}
is_read_ok = 0;
}
}
|
|
图6 使用全局变量控制读写的代码
|
运行结果如下:

从运行结果中我们可以看出,写回调函数被触发了,但是没有执行任何动作(因为is_read_ok=0)。读回调函数读取了bufferevent的输入bufer并将is_read_ok设置为1,但是为什么写回调函数不会再被执行了呢?因为上面的代码违背了事件驱动程序的基本流程。事件驱动程序是事件驱动的,一个事件的发生会调用相应的处理函数,那么下一次驱动的发生要么是系统设置,要么是在处理函数中设置。这也是为什么我们在connet一连接上就调用bufferevent_write函数先进行一次写入,该操作启动了整个事件迭代的开始。而上面的代码中由于我们在一开始的写回调中没有进行任何操作,没有为下一次读事件的发生设定条件,就导致了事件链的中断。
看来这种方法还是不行。好好想想echo程序的本质就是写-读-写-读。既然把读写分开存在写回调函数先触发的问题,那干脆不用写回调函数,直接在写入函数中处理,按照读-写-读的方式进行处理。修改后的代码如下:
#include <event2/event.h>
#include <event2/bufferevent.h>
#include <event2/buffer.h>
#include <event2/util.h>
#include "../unp.h"
struct info {
const char *name;
size_t total_drained;
};
char recvline[1024] = {'a','b','\n','\0'};
void read_callback(struct bufferevent *bev,void *ctx) {
struct info *inf =(struct info *) ctx;
char sendline[MAXLINE];
struct evbuffer *input = bufferevent_get_input(bev);
struct evbuffer *output = bufferevent_get_output(bev);
size_t len = evbuffer_get_length(input);
if(len) {
inf->total_drained += len;
evbuffer_drain(input,len);
fprintf(stderr,"we received [%lu] bytes from %s\n",(unsigned long)len,inf->name);
}
if(Fgets(sendline, MAXLINE, stdin) != NULL) {
fprintf(stderr,"we sent [%s]\n", sendline);
bufferevent_write(bev,sendline,strlen(sendline));
}
}
void event_callback(struct bufferevent *bev,short events,void *ctx) {
struct info *inf = (struct info *)ctx;
struct evbuffer *input = bufferevent_get_input(bev);
int finished = 0;
if(events & BEV_EVENT_EOF) {
if(events & BEV_EVENT_READING) {
size_t len = evbuffer_get_length(input);
fprintf(stderr,"Got a close from %s.we drained %lu bytes from it,and have %lu left.\n",inf->name,(unsigned long)inf->total_drained,(unsigned long)len);
} else if(events & BEV_EVENT_WRITING) {
fprintf(stderr,"writing error\n");
}
finished = 1;
}
if(events & BEV_EVENT_ERROR) {
if(events & BEV_EVENT_READING) {
fprintf(stderr,"Got an error from %s:%s\n",inf->name,evutil_socket_error_to_string(EVUTIL_SOCKET_ERROR()));
} else if(events & BEV_EVENT_WRITING) {
fprintf(stderr,"writing error\n");
}
finished = 1;
}
if(events & BEV_EVENT_CONNETTED) {
bufferevent_write(bev,recvline,strlen(recvline));//向服务器写入一行
}
if(finished) {
free(ctx);
bufferevent_free(bev);
}
}
int main(int argc, char **argv) {
int sockfd;
struct sockaddr_in servaddr;
struct event_base *base;
struct bufferevent *bev;
struct info* info1;
info1 = (struct info*)malloc(sizeof(struct info));
info1->name="buffer1";
info1->total_drained = 0;
if (argc != 2)
err_quit("usage: tcpcli <IPaddress>");
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
base = event_base_new();
bev = bufferevent_socket_new(base,-1,BEV_OPT_CLOSE_ON_FREE|BEV_OPT_DEFER_CALLBACKS);//创建一个套接口类型的bufferevent
bufferevent_setwatermark(bev,EV_READ|EV_WRITE,0,0);//设置读写的数位
bufferevent_setcb(bev, read_callback,NULL,event_callback,info1);//设置bufferevent的读/写/事件回调函数
bufferevent_enable(bev,EV_READ|EV_WRITE);//使能读写
if(bufferevent_socket_connect(bev,(SA*)&servaddr,sizeof(servaddr)) < 0) {//连接到服务器127.0.0.1
bufferevent_free(bev);
return -1;
}
fprintf(stderr,"connec ok!\n");
event_base_dispatch(base);
exit(0);
}
|
| 图7 只使用读回调操作的echo客户端代码 |
客户端执行结果如下:
服务器端执行结果如下:
一个简单的echo程序被我搞的这么复杂,真是越来越觉得自己在程序设计方面缺少灵性了。一开始看到bufferevent上的例子,就怀疑怎么这些例子怎么没有同时使用读写回调的?那我就写个同时使用读写回调的回显程序吧,没想到会有这样的陷阱。其实想想也是,有的东西你不去探索是不知道它会出现什么问题的。如果我一开始就只使用读回调去做的话,这其中的奥妙我也不能知道。或许这就是阿Q精神吧。
在下一节我们看看如何用用bufferevent操作串口。