浅谈libevent的使用--事件和数据缓冲
Posted 流年十维
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈libevent的使用--事件和数据缓冲相关的知识,希望对你有一定的参考价值。
首先在学习libevent库的使用前,我们还要从基本的了解开始,已经熟悉了epoll以及reactor,然后从event_base学习,依次学习事件event、数据缓冲Bufferevent和数据封装evBuffer等,再结合具体的几个实例来了解libevent库的一些基本使用,有助于我们理解它的一些内部实现(由于之前我已经写过一篇epoll反应堆模型的,所以这里就不再介绍,直接从event_base开始介绍)。
libevent下载与安装:
在官网上找到 libevent-2.0.22-stable.tar.gz 下载地址。(版本可以自己选择,下面介绍的是Ubuntu下安装方法,其他系统参考)
tar -zxvf libevent-2.0.22-stable.tar.gz
cd libevent-2.0.22-stable/
./configure
make
sudo make install
libevent特点:
事件驱动,高性能;
轻量级,专注于网络;
跨平台,支持 Windows、Linux、Mac Os等;
支持多种 I/O多路复用技术, epoll、poll、dev/poll、select 和kqueue 等;
支持 I/O,定时器和信号等事件;
libevent组件:
evutil:用于抽象不同平台网络实现差异的通用功能。
event和event_base: libevent的核心,为各种平台特定的、基于事件的非阻塞 IO后端提供抽象API,让程序可以知道套接字何时已经准备好,可以读或者写,并且处理基本的超时功能,检测OS信号。
bufferevent: 为libevent基于事件的核心提供使用更方便的封装。除了通知程序套接字已经准备好读写之外,还让程序可以请求缓冲的读写操作,可以知道何时IO已经真正发生。( bufferevent接口有多个后端, 可以采用系统能够提供的更快的非阻塞 IO方式,如Windows中的IOCP。)
evbuffer:在bufferevent层之下实现了缓冲功能,并且提供了方便有效的访问函数
*****************************************************华丽的分隔线*************************************************************
event_base:
使用 libevent 函数之前需要分配一个或者多个 event_base 结构体。每个event_base 结构 体持有一个事件集合,可以检测以确定哪个事件是激活的。如果设置 event_base 使用锁,则可以安全地在多个线程中访问它 。然而,其事件循环只能 运行在一个线程中。如果需要用多个线程检测 IO,则需要为每个线程使用一个 event_base。
event_base_new()函数分配并且返回一个新的具有默认设置的 event_base。函数会检测环境变量,返回一个到 event_base 的指针。如果发生错误,则返回 NULL。选择各种方法时,函数会选择 OS 支持的最快方法。
struct event_base *event_base_new(void);
使用完 event_base 之后,使用 event_base_free()进行释放。
void event_base_free(struct event_base *base);
注意:这个函数不会释放当前与 event_base 关联的任何事件,或者关闭他们的套接字 ,或 者释放任何指针
一旦有了一个已经注册了某些事件的 event_base, 就需要让 libevent 等待事件并且通知事件的发生。
#define EVLOOP_ONCE 0x01
#define EVLOOP_NONBLOCK 0x02
#define EVLOOP_NO_EXIT_ON_EMPTY 0x04
int event_base_loop(struct event_base *base, int flags);
默认情况下,event_base_loop()函数运行 event_base 直到其中没有已经注册的事件为止。执行循环的时候 ,函数重复地检查是否有任何已经注册的事件被触发 (比如说,读事件 的文件描述符已经就绪,可以读取了;或者超时事件的超时时间即将到达)。如果有事件被触发,函数标记被触发的事件为 “激活的”,并且执行这些事件。
在 flags 参数中设置一个或者多个标志就可以改变 event_base_loop()的行为。如果设置了 EVLOOP_ONCE ,循环将等待某些事件成为激活的 ,执行激活的事件直到没有更多的事件可以执行,然会返回。如果设置了 EVLOOP_NONBLOCK,循环不会等待事件被触发: 循环将仅仅检测是否有事件已经就绪,可以立即触发,如果有,则执行事件的回调。
完成工作后,如果正常退出, event_base_loop()返回0;如果因为后端中的某些未处理 错误而退出,则返回 -1。
int event_base_dispatch(struct event_base *base);
event_base_dispatch ()等同于没有设置标志的 event_base_loop ( )。所以event_base_dispatch ()将一直运行,直到没有已经注册的事件了,或者调用 event_base_loopbreak()或者 event_base_loopexit()为止。
如果想在移除所有已注册的事件之前停止活动的事件循环,可以调用两个稍有不同的函数 。
int event_base_loopexit(struct event_base *base,const struct timeval *tv);
int event_base_loopbreak(struct event_base *base);
event_base_loopexit()让 event_base 在给定时间之后停止循环。如果 tv 参数为NULL, event_base 会立即停止循环,没有延时。
如果 event_base 当前正在执行任何激活事件的回调,则回调会继续运行,直到运行完所有激活事件的回调之才退出。
event_base_loopbreak ()让 event_base 立即退出循环。它与event_base_loopexit (base,NULL)的不同在于,如果 event_base 当前正在执行激活事件的回调 ,它将在执行完当前正在处理的事件后立即退出。
综上所述event_base处理过程主要如下:
1.调用event_base_new()创建一个event_base
2.注册了某些事件的 event_base
3.调用event_base_loop()或者event_base_dispatch()函数,循环等待事件并且通知事件的发生
4.调用event_base_loopexit()或者event_base_loopbreak()移除所有已注册的事件之前停止活动的事件循环
5.使用完 event_base 之后,使用event_base_free()进行释放
******************************************华丽的分割线*******************************************************
事件event:
libevent 的基本操作单元是事件。每个事件代表一组条件的集合,这些条件包括:
文件描述符已经就绪,可以读取或者写入
文件描述符变为就绪状态,可以读取或者写入(仅对于边沿触发 IO)
超时事件
发生某信号
用户触发事件
所有事件具有相似的生命周期。调用 libevent 函数设置事件并且关联到event_base 之后, 事件进入“已初始化(initialized)”状态。此时可以将事件添加到event_base 中,这使之进入“未决(pending)”状态。在未决状态下,如果触发事件的条件发生(比如说,文件描述 符的状态改变,或者超时时间到达 ),则事件进入“激活(active)”状态,(用户提供的)事件回调函数将被执行。如果配置为“持久的(persistent)”,事件将保持为未决状态。否则, 执行完回调后,事件不再是未决的。删除操作可以让未决事件成为非未决(已初始化)的 ; 添加操作可以让非未决事件再次成为未决的。
创建事件
使用 event_new()接口创建事件。
#define EV_TIMEOUT 0x01
#define EV_READ 0x02
#define EV_WRITE 0x04
#define EV_SIGNAL 0x08
#define EV_PERSIST 0x10
#define EV_ET 0x20
typedef void (*event_callback_fn)(evutil_socket_t, short, void *);
struct event *event_new(struct event_base *base, evutil_socket_t fd,short what, event_callback_fn cb,void *arg);
void event_free(struct event *event);
event_new()试图分配和构造一个用于 base 的新的事件。
what 参数是上述标志的集合。
如果 fd 非负,则它是将被观察其读写事件的文件。
事件被激活时, libevent 将调用 cb 函数,
传递这些参数:文件描述符 fd,表示所有被触发事件的位字段 ,以及构造事件时的 arg参数。
发生内部错误,或者传入无效参数时, event_new()将返回 NULL。
所有新创建的事件都处于已初始化和非未决状态 ,调用 event_add()可以使其成为未决的。
释放事件
要释放事件,调用 event_free()。对未决或者激活状态的事件调用 event_free()是安全的:在释放事件之前,函数将会使事件成为非激活和非未决的。
事件标志
EV_TIMEOUT:这个标志表示某超时时间流逝后事件成为激活的。构造事件的时候,EV_TIMEOUT标志是 被忽略的:可以在添加事件的时候设置超时 ,也可以不设置。超时发生时,回调函数的 what 参数将带有这个标志。
EV_READ:表示指定的文件描述符已经就绪,可以读取的时候,事件将成为激活的。
EV_WRITE:表示指定的文件描述符已经就绪,可以写入的时候,事件将成为激活的。
EV_SIGNAL:用于实现信号检测,请看下面的 “构造信号事件”节。
EV_PERSIST:表示事件是“持久的”
EV_ET:表示如果底层的 event_base 后端支持边沿触发事件,则事件应该是边沿触发的。
事件的未决和非未决
设置未决事件:在非未决的事件上调用 event_add()将使其在配置的 event_base 中成为未决的。
int event_add(struct event *ev, const struct timeval *tv);
如果 tv 为 NULL,添加的事件不会超时。否则, tv 以秒和微秒指定超时值。
如果对已经未决的事件调用 event_add(),事件将保持未决状态,并在指定的超时时间被重新调度。
设置非未决事件:对已经初始化的事件调用 event_del()将使其成为非未决和非激活的。如果事件不是未决的或者激活的,调用将没有效果。
int event_del(struct event *ev);
如果在事件激活后,其回调被执行前删除事件,回调将不会执行。
一次触发事件
如果不需要多次添加一个事件,或者要在添加后立即删除事件,而事件又不需要是持久的 , 则可以使用 event_base_once()
int event_base_once(struct event_base *, evutil_socket_t, short,void (*)(evutil_socket_t, short, void *), void *, const struct timeval *);
除了不支持 EV_SIGNAL 或者 EV_PERSIST 之外,这个函数的接口与 event_new()相同。 安排的事件将以默认的优先级加入到 event_base并执行。回调被执行后,libevent内部将 会释放 event 结构。
事件总结:1.创建事件
2.事件的未决和非未决 设置未决事件调用event_add() 设置非未决事件调用event_del()
3.触发事件 一次触发事件调用event_base_once()
event代码示例
server.c
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <unistd.h> 5 #include <sys/types.h> 6 #include <sys/stat.h> 7 #include <fcntl.h> 8 #include <event2/event.h> 9 10 //typedef void (*event_callback_fn)(evutil_socket_t, short, void *) 11 void callback_func(evutil_socket_t fd, short event, void *arg) 12 { 13 char buf[256] = {0}; 14 int len = 0; 15 16 printf("fd = %d, event = %d", fd, event); 17 18 len = read(fd, buf, sizeof(buf)); 19 20 if (len == -1) { 21 perror("read"); 22 return; 23 } else if (len == 0) { 24 perror("remote close fd"); 25 return; 26 } else { 27 buf[len] = \'\\0\'; 28 printf("read buf=[%s]\\n", buf); 29 } 30 return; 31 } 32 33 int main(int argc, char *argv[]) 34 { 35 int fd; 36 struct event_base * base = NULL; 37 struct event *evfifo = NULL; 38 const char *fifo = "event.fifo"; 39 40 unlink(fifo); 41 42 if (mkfifo(fifo, 0644) == -1) { 43 perror("mkfifo"); 44 exit(1); 45 } 46 47 fd = open(fifo, O_RDONLY); 48 if (fd == -1) { 49 perror("open socket error"); 50 exit(1); 51 } 52 53 //创建一个event_base 54 base = event_base_new(); 55 56 //将fd 绑定到一个事件中 同时绑定一个读的回调函数 57 //此时evfifo事件是一个初始化状态的事件 58 evfifo = event_new(base, fd, EV_READ|EV_PERSIST, callback_func, NULL); 59 60 //将此事件从 初始化--->未决 61 event_add(evfifo, NULL); 62 63 //循环等待事件监控 64 event_base_dispatch(base); 65 66 event_free(evfifo); 67 event_base_free(base); 68 69 return 0; 70 }
client.c
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <unistd.h> 5 #include <fcntl.h> 6 7 int main(int argc, char *argv[]) 8 { 9 int fd = 0; 10 11 const char *str = "hello event"; 12 13 fd = open("event.fifo", O_RDWR); 14 if (fd == -1) { 15 perror("open event.fifo error"); 16 exit(1); 17 } 18 19 while (1) { 20 write(fd, str, strlen(str)); 21 sleep(1); 22 } 23 24 close(fd); 25 return 0; 26 }
执行如下命令进行编译(前提已经安装libevent库)
gcc server.c -l event -o server
gcc client.c -l event -o client
编译好后启动server和client,结果如下:
*****************************************************华丽的分隔线*************************************************************
数据缓冲Bufferevent
很多时候,除了响应事件之外,应用还希望做一定的数据缓冲。比如说,写入数据的时候 ,通常的运行模式是:
1.决定要向连接写入一些数据,把数据放入到缓冲区中
2.等待连接可以写入
3.写入尽量多的数据
4.记住写入了多少数据,如果还有更多数据要写入,等待连接再次可以写入
bufferevent 由一个底层的传输端口(如套接字 ),一个读取缓冲区和一个写入缓冲区组成。与通常的事件在底层传输端口已经就绪,可以读取或者写入的时候执行回调不同的是,bufferevent 在读取或者写入了足够量的数据之后调用用户提供的回调。
bufferevent和evbuffer
每个 bufferevent 都有一个输入缓冲区和一个输出缓冲区 ,它们的类型都是“structevbuffer”。 有数据要写入到 bufferevent 时,添加数据到输出缓冲区 ;bufferevent 中有数据供读取的时候,从输入缓冲区抽取(drain)数据。 evbuffer 接口支持很多种操作。
水位
每个 bufferevent 有两个数据相关的回调:一个读取回调和一个写入回调。默认情况下,从底层传输端口读取了任意量的数据之后会调用读取回调 ;输出缓冲区中足够量的数据被清空到底层传输端口后写入回调会被调用。通过调整 bufferevent 的读取和写入 “水位 (watermarks )”可以覆盖这些函数的默认行为。
每个 bufferevent 有四个水位:
读取低水位 : 读取操作使得输入缓冲区的数据量在此级别或者更高时 ,读取回调将被调用。默认值为 0,所以每个读取操作都会导致读取回调被调用。
读取高水位 : 输入缓冲区中的数据量达到此级别后, bufferevent 将停止读取,直到输入缓冲区中足够量的数据被抽取 ,使得数据量低于此级别 。默认值是无限 ,所以永远不会因为输入缓冲区的大小而停止读取。
写入低水位 : 写入操作使得输出缓冲区的数据量达到或者低于此级别时 ,写入回调将被调用。默认值是 0,所以只有输出缓冲区空的时候才会调用写入回调。
写入高水位 : bufferevent 没有直接使用这个水位。它在 bufferevent 用作另外一 个 bufferevent 的底层传输端口时有特殊意义。
回调
默认情况下,bufferevent 的回调在相应的条件发生时立即被执行 。在依赖关系复杂的情况下 ,这种立即调用会制造麻烦 。比如说,假如某个回调在 evbuffer A 空的时候向其中移入数据 ,而另一个回调在
evbuffer A 满的时候从中取出数据。这些调用都是在栈上发生的,在依赖关系足够复杂的时候,有栈溢出的风险。
要解决此问题,可以请求 bufferevent(或者 evbuffer)延迟其回调。条件满足时,延迟回调不会立即调用,而是在 event_loop()调用中被排队,然后在通常的事件回调之后执行.
bufferevent 选项标志
BEV_OPT_CLOSE_ON_FREE : 释放 bufferevent 时关闭底层传输端口。这将关闭底层套接字,释放底层 bufferevent 等
BEV_OPT_THREADSAFE : 自动为 bufferevent 分配锁,这样就可以安全地在多个线程中使用 bufferevent。
BEV_OPT_DEFER_CALLBACKS : 设置这个标志时, bufferevent 延迟所有回调.
BEV_OPT_UNLOCK_CALLBACKS : 默认情况下,如果设置 bufferevent 为线程安全 的,则 bufferevent 会在调用用户提供的回调时进行锁定。设置这个选项会让 libevent 在执行回调的时候不进行锁定。
创建基于套接字的bufferevent
可以使用 bufferevent_socket_new()创建基于套接字的 bufferevent。
struct bufferevent *bufferevent_socket_new(struct event_base *base,evutil_socket_t fd,enum bufferevent_options options);
base 是 event_base,options 是表示 bufferevent 选项(BEV_OPT_CLOSE_ON_FREE 等) 的位掩码, fd是一个可选的表示套接字的文件描述符。如果想以后设置文件描述符,可以设置fd为-1。成功时函数返回一个 bufferevent,失败则返回 NULL。
在bufferevent上启动链接
int bufferevent_socket_connect(struct bufferevent *bev,struct sockaddr *address, int addrlen);
address 和 addrlen 参数跟标准调用 connect()的参数相同。如果还没有为bufferevent 设置套接字,调用函数将为其分配一个新的流套接字,并且设置为非阻塞的。
如果已经为 bufferevent 设置套接字,调用bufferevent_socket_connect() 将告知libevent 套接字还未连接,直到连接成功之前不应该对其进行读取或者写入操作。连接完成之前可以向输出缓冲区添加数据。如果连接成功启动,函数返回 0;如果发生错误则返回 -1。
释放bufferevent操作
void bufferevent_free(struct bufferevent *bev);
这个函数释放 bufferevent。bufferevent 内部具有引用计数,所以,如果释放 时还有未决的延迟回调,则在回调完成之前 bufferevent 不会被删除。
如果设置了 BEV_OPT_CLOSE_ON_FREE 标志,并且 bufferevent 有一个套接字或者底层 bufferevent 作为其传输端口,则释放 bufferevent 将关闭这个传输端口。
操作回调和启用/禁用
typedef void (*bufferevent_data_cb)(struct bufferevent *bev, void *ctx);
typedef void (*bufferevent_event_cb)(struct bufferevent *bev,short events, void *ctx);
void bufferevent_setcb(struct bufferevent *bufev,bufferevent_data_cb readcb, bufferevent_data_cb writecb,bufferevent_event_cb eventcb, void *cbarg);
void bufferevent_getcb(struct bufferevent *bufev,bufferevent_data_cb *readcb_ptr,bufferevent_data_cb *writecb_ptr,bufferevent_event_cb *eventcb_ptr,void **cbarg_ptr);
bufferevent_setcb()函数修改 bufferevent 的一个或者多个回调 。readcb、writecb和eventcb函数将分别在已经读取足够的数据 、已经写入足够的数据 ,或者发生错误时被调用 。每个回调函数的第一个参数都是发生了事件的bufferevent ,最后一个参数都是调用bufferevent_setcb()时用户提供的 cbarg 参数:可以通过它向回调传递数据。事件回调 的 events 参数是一个表示事件标志的位掩码:请看前面的 “回调和水位”节。要禁用回调,传递 NULL 而不是回调函数 。注意:bufferevent 的所有回调函数共享单个 cbarg, 所以修改它将影响所有回调函数。
接口
void bufferevent_enable(struct bufferevent *bufev, short events);
void bufferevent_disable(struct bufferevent *bufev, short events);
short bufferevent_get_enabled(struct bufferevent *bufev);
可以启用或者禁用 bufferevent 上的 EV_READ、EV_WRITE 或者 EV_READ | EV_WRITE 事件。没有启用读取或者写入事件时, bufferevent 将不会试图进行数据读取或者写入。没有必要在输出缓冲区空时禁用写入事件: bufferevent 将自动停止写入,然后在有数据等 待写入时重新开始。类似地,没有必要在输入缓冲区高于高水位时禁用读取事件 :bufferevent 将自动停止读取, 然后在有空间用于读取时重新开始读取。默认情况下,新创建的 bufferevent 的写入是启用的,但是读取没有启用。 可以调用bufferevent_get_enabled()确定 bufferevent 上当前启用的事件。
操作bufferevent中的数据
通过bufferevent得到evbuffer
struct evbuffer *bufferevent_get_input(struct bufferevent *bufev);
struct evbuffer *bufferevent_get_output(struct bufferevent *bufev);
这两个函数提供了非常强大的基础 :它们分别返回输入和输出缓冲区 。
如果写入操作因为数据量太少而停止(或者读取操作因为太多数据而停止 ),则向输出缓冲 区添加数据(或者从输入缓冲区移除数据)将自动重启操作。
向bufferevent的输出缓冲区添加数据
int bufferevent_write(struct bufferevent *bufev,const void *data, size_t size);
int bufferevent_write_buffer(struct bufferevent *bufev,struct evbuffer *buf);
这些函数向 bufferevent 的输出缓冲区添加数据。 bufferevent_write()将内存中从data 处开 始的 size 字节数据添加到输出缓冲区的末尾 。
bufferevent_write_buffer()移除 buf 的所有内 容,将其放置到输出缓冲区的末尾。成功时这些函数都返回 0,发生错误时则返回-1。
从bufferevent的输入缓冲区移除数据
size_t bufferevent_read(struct bufferevent *bufev, void *data, size_t size);
int bufferevent_read_buffer(struct bufferevent *bufev,struct evbuffer *buf);
这些函数从 bufferevent 的输入缓冲区移除数据。bufferevent_read()至多从输入缓冲区移除 size 字节的数据,将其存储到内存中 data 处。函数返回实际移除的字节数。 bufferevent_read_buffer()函数抽空输入缓冲区的所有内容,将其放置到 buf 中,成功时返 回0,失败时返回 -1。
bufferevent的清空操作
int bufferevent_flush(struct bufferevent *bufev,short iotype, enum bufferevent_flush_mode state);
数据缓冲Bufferevent总结:1.读取写入的高低水位
2.调用bufferevent_socket_new()创建基于套接字的 bufferevent
3.调用bufferevent_socket_connect启动链接
4.操作回调和启用/禁用
5.操作bufferevent中的数据
6.bufferevent的清空操作
7.调用bufferevent_free()释放bufferevent操作
*************************************************华丽的分割线*****************************************************************
数据封装evBuffer
libevent 的 evbuffer 实现了为向后面添加数据和从前面移除数据而优化的字节队列。evbuffer 用于处理缓冲网络 IO 的“缓冲”部分。它不提供调度 IO 或者当 IO 就绪时触发 IO 的 功能:这是 bufferevent 的工作。
创建和释放evbuffer
struct evbuffer *evbuffer_new(void);
void evbuffer_free(struct evbuffer *buf);
这两个函数的功能很简明: evbuffer_new() 分配和返回一个新的空 evbuffer ; 而evbuffer_free()释放 evbuffer 和其内容。
向evbuffer添加数据
int evbuffer_add(struct evbuffer *buf, const void *data, size_tdatlen);
这个函数添加 data 处的 datalen 字节到 buf 的末尾,
成功时返回0,失败时返回-1。
evbuffer数据移动
int evbuffer_add_buffer(struct evbuffer *dst, struct evbuffer *src);
int evbuffer_remove_buffer(struct evbuffer *src,struct evbuffer *dst,size_t datlen);
evbuffer_add_buffer()将 src 中的所有数据移动到 dst 末尾,成功时返回0,失败时返回-1。
evbuffer_remove_buffer()函数从 src 中移动 datlen 字节到 dst 末尾,尽量少进行复制。如果字节数小于 datlen,所有字节被移动。函数返回移动的字节数。
*************************************************华丽的分割线*****************************************************************
结合以上介绍,写一个关于数据缓冲Bufferevent和evbuffer一些操作的案例:使用套接字基于TCP协议,将客户端将小写字母发送到服务端,转换成大写字母,再回写给客户端输出到屏幕上
sever.c如下
1 #include<event2/listener.h> 2 #include<event2/bufferevent.h> 3 #include<event2/buffer.h> 4 #include<ctype.h> 5 #include<arpa/inet.h> 6 #include<string.h> 7 #include<strings.h> 8 #include<stdlib.h> 9 #include<stdio.h> 10 #include<errno.h> 11 #define SERV_PORT 9999 12 13 //如果客户端有数据写过来,那么会触发当前的回调函数 14 static void echo_read_cb(struct bufferevent *bev,void *ctx) 15 { 16 //input 就是当前bufferevent的输入缓冲区地址,如果想得到用户数据就从input中获取 17 struct evbuffer *input = bufferevent_get_input(bev); 18 19 //output 就是当前bufferevent的输出缓冲区地址,如果客户端写数据 就将数据写到output中就可以了 20 struct evbuffer *output = bufferevent_get_output(bev); 21 22 //将input缓冲区中数据取出来进行大小写转换 23 int i,ret; 24 char *buf = malloc(BUFSIZ); 25 bzero(buf,BUFSIZ); 26 void* ptr = (void*)buf; 27 evbuffer_remove(input,ptr,BUFSIZ);//将input缓冲区中数据移除,并复制一份到ptr所指向的空间中 28 buf = (char*)ptr; 29 for(i = 0;i < strlen(buf); i++ ) 30 { 31 buf[i] = toupper(buf[i]);//大小写转换 32 } 使用 libevent 在 C 中的 TCP 端口事件回调