libevent从入门到掌握<四>
数据缓冲Bufferevent
很多时候,除了响应事件之外,应用还希望做一定的数据缓冲。比如说,写入数据的时候 ,通常的运行模式是:
- 决定要向连接写入一些数据,把数据放入到缓冲区中
- 等待连接可以写入
- 写入尽量多的数据
- 记住写入了多少数据,如果还有更多数据要写入,等待连接再次可以写入
这种缓冲 IO 模式很通用,libevent 为此提供了一种通用机制,即bufferevent。
bufferevent 由一个底层的传输端口(如套接字 ),一个读取缓冲区和一个写入缓冲区组成。与通常的事件在底层传输端口已经就绪,可以读取或者写入的时候执行回调不同的是,bufferevent 在读取或者写入了足够量的数据之后调用用户提供的回调。
有多种共享公用接口的 bufferevent 类型,编写本文时已存在以下类型:
-
基于套接字的 bufferevent
:使用 event_*接口作为后端,通过底层流式套接字发送或者接收数据的 bufferevent -
异步 IO bufferevent
:使用 Windows IOCP 接口,通过底层流式套接字发送或者接收数据的 bufferevent(仅用于 Windows,试验中) -
过滤型 bufferevent
:将数据传输到底层 bufferevent 对象之前,处理输入或者输出数据的 bufferevent:比如说,为了压缩或者转换数据。 -
成对的 bufferevent
:相互传输数据的两个 bufferevent。
注意
:截止2.0.2-alpha 版,这里列出的 bufferevent 接口还没有完全正交于所有 的 bufferevent 类型。也就是说,下面将要介绍的接口不是都能用于所有bufferevent 类型。libevent 开发 者在未来版本中将修正这个问题。
也请注意
:当前 bufferevent 只能用于像 TCP 这样的面向流的协议,将来才可能会支持 像 UDP 这样的面向数据报的协议。
bufferevent和evbuffer
每个 bufferevent 都有一个输入缓冲区和一个输出缓冲区 ,它们的类型都是“struct evbuffer”。 有数据要写入到 bufferevent 时,添加数据到输出缓冲区 ;bufferevent 中有数据供读取的时候,从输入缓冲区抽取(drain)数据。
回调和水位
每个 bufferevent 有两个数据相关的回调:一个读取回调和一个写入回调。
默认情况下,从底层传输端口读取了任意量的数据之后会调用读取回调 ;
输出缓冲区中足够量的数据被清空到底层传输端口后写入回调会被调用。
通过调整 bufferevent 的读取和写入 “水位 (watermarks )”可以覆盖这些函数的默认行为。
每个 bufferevent 有四个水位:
-
读取低水位
:读取操作使得输入缓冲区的数据量在此级别或者更高时 ,读取回调将被调用。默认值为 0,所以每个读取操作都会导致读取回调被调用。 -
读取高水位
:输入缓冲区中的数据量达到此级别后, bufferevent 将停止读取,直到输入缓冲区中足够量的数据被抽取 ,使得数据量低于此级别 。默认值是无限 ,所以永远不会因为输入缓冲区的大小而停止读取。 -
写入低水位
:写入操作使得输出缓冲区的数据量达到或者低于此级别时 ,写入回调将被调用。默认值是 0,所以只有输出缓冲区空的时候才会调用写入回调。 -
写入高水位
:bufferevent 没有直接使用这个水位。它在 bufferevent 用作另外一 个 bufferevent 的底层传输端口时有特殊意义。请看后面关于过滤型 bufferevent 的介绍。
bufferevent 接口汇总
1.bufferevent_socket_new()
struct bufferevent *bufferevent_socket_new(
struct event_base *base,
evutil_socket_t fd,
enum bufferevent_options options);
enum bufferevent_options options:
-
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。
2.bufferevent_socket_connect()
int bufferevent_socket_connect(struct bufferevent *bev,
struct sockaddr *address, int addrlen);
address 和 addrlen 参数跟标准调用 connect()的参数相同。如果还没有为 bufferevent 设置套接字,调用函数将为其分配一个新的流套接字,并且设置为非阻塞的。
3.bufferevent_free()
void bufferevent_free(struct bufferevent *bev);
这个函数释放 bufferevent。bufferevent 内部具有引用计数,所以,如果释放 时还有未决的延迟回调,则在回调完成之前 bufferevent 不会被删除。
如果设置了 BEV_OPT_CLOSE_ON_FREE 标志,并且 bufferevent 有一个套接字或者底层 bufferevent 作为其传输端口,则释放 bufferevent 将关闭这个传输端口。
4.bufferevent_setcb()
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 参数是一个表示事件标志的位掩码
- bufferevent_enable()
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得到evbuffer
如果只是通过网络读取或者写入数据 ,而不能观察操作过程,是没什么好处的。bufferevent 提供了下列函数用于观察要写入或者读取的数据。
struct evbuffer *bufferevent_get_input(struct bufferevent *bufev);
struct evbuffer *bufferevent_get_output(struct bufferevent *bufev);
这两个函数提供了非常强大的基础 :它们分别返回输入和输出缓冲区 。关于可以对 evbuffer 类型进行的所有操作的完整信息,请看下一章。
如果写入操作因为数据量太少而停止(或者读取操作因为太多数据而停止 ),则向输出缓冲 区添加数据(或者从输入缓冲区移除数据)将自动重启操作。
向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_read(),data 处的内存块必须有足够的空间容纳 size 字节数据。
bufferevent的清空操作
int bufferevent_flush(struct bufferevent *bufev,
short iotype, enum bufferevent_flush_mode state);
清空 bufferevent 要求 bufferevent 强制从底层传输端口读取或者写入尽可能多的数据 ,而忽略其他可能保持数据不被写入的限制条件 。函数的细节功能依赖于 bufferevent 的具体类型。
otype 参数应该是 EV_READ、EV_WRITE 或者 EV_READ | EV_WRITE,用于指示应该处 理读取、写入,还是二者都处理。 state 参数可以是 BEV_NORMAL、BEV_FLUSH 或者 BEV_FINISHED。BEV_FINISHED 指示应该告知另一端,没有更多数据需要发送了; 而 BEV_NORMAL 和 BEV_FLUSH 的区别依赖于具体的 bufferevent 类型。
失败时 bufferevent_flush()返回-1,如果没有数据被清空则返回 0,有数据被清空则返回 1。
一般操作流程:(服务端)
socket->bind->listen->event_base_new()->event_new()->accept->bufferevent_socket_new()->bufferevent_setcb()->bufferevent_enable()->event_add->event_base_dispatch
提供一个网上的案例:
cli.cpp:
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<errno.h>
#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<event.h>
#include<event2/bufferevent.h>
#include<event2/buffer.h>
#include<event2/util.h>
int tcp_connect_server(const char* server_ip, int port);
void cmd_msg_cb(int fd, short events, void* arg);
void server_msg_cb(struct bufferevent* bev, void* arg);
void event_cb(struct bufferevent *bev, short event, void *arg);
int main(int argc, char** argv)
{
if( argc < 3 )
{
printf("please input 2 parameter\n");
return -1;
}
//两个参数依次是服务器端的IP地址、端口号
int sockfd = tcp_connect_server(argv[1], atoi(argv[2]));
if( sockfd == -1)
{
perror("tcp_connect error ");
return -1;
}
printf("connect to server successful\n");
struct event_base* base = event_base_new();
struct bufferevent* bev = bufferevent_socket_new(base, sockfd,
BEV_OPT_CLOSE_ON_FREE);
//监听终端输入事件
struct event* ev_cmd = event_new(base, STDIN_FILENO,
EV_READ | EV_PERSIST, cmd_msg_cb,
(void*)bev);
event_add(ev_cmd, NULL);
//当socket关闭时会用到回调参数
bufferevent_setcb(bev, server_msg_cb, NULL, event_cb, (void*)ev_cmd);
bufferevent_enable(bev, EV_READ | EV_PERSIST);
event_base_dispatch(base);
printf("finished \n");
return 0;
}
void cmd_msg_cb(int fd, short events, void* arg)
{
char msg[1024];
int ret = read(fd, msg, sizeof(msg));
if( ret < 0 )
{
perror("read fail ");
exit(1);
}
struct bufferevent* bev = (struct bufferevent*)arg;
//把终端的消息发送给服务器端
bufferevent_write(bev, msg, ret);
}
void server_msg_cb(struct bufferevent* bev, void* arg)
{
char msg[1024];
size_t len = bufferevent_read(bev, msg, sizeof(msg));
msg[len] = '\0';
printf("recv %s from server\n", msg);
}
void event_cb(struct bufferevent *bev, short event, void *arg)
{
if (event & BEV_EVENT_EOF)
printf("connection closed\n");
else if (event & BEV_EVENT_ERROR)
printf("some other error\n");
//这将自动close套接字和free读写缓冲区
bufferevent_free(bev);
struct event *ev = (struct event*)arg;
//因为socket已经没有,所以这个event也没有存在的必要了
event_free(ev);
}
typedef struct sockaddr SA;
int tcp_connect_server(const char* server_ip, int port)
{
int sockfd, status, save_errno;
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr) );
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
status = inet_aton(server_ip, &server_addr.sin_addr);
if( status == 0 ) //the server_ip is not valid value
{
errno = EINVAL;
return -1;
}
sockfd = ::socket(PF_INET, SOCK_STREAM, 0);
if( sockfd == -1 )
return sockfd;
status = ::connect(sockfd, (SA*)&server_addr, sizeof(server_addr) );
if( status == -1 )
{
save_errno = errno;
::close(sockfd);
errno = save_errno; //the close may be error
return -1;
}
evutil_make_socket_nonblocking(sockfd);
return sockfd;
}
ser.cpp:
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<event.h>
#include<event2/bufferevent.h>
void accept_cb(int fd, short events, void* arg);
void socket_read_cb(bufferevent* bev, void* arg);
void event_cb(struct bufferevent *bev, short event, void *arg);
int tcp_server_init(int port, int listen_num);
int main(int argc, char** argv)
{
int listener = tcp_server_init(9999, 10);
if( listener == -1 )
{
perror(" tcp_server_init error ");
return -1;
}
struct event_base* base = event_base_new();
//添加监听客户端请求连接事件
struct event* ev_listen = event_new(base, listener, EV_READ | EV_PERSIST,
accept_cb, base);
event_add(ev_listen, NULL);
event_base_dispatch(base);
event_base_free(base);
return 0;
}
void accept_cb(int fd, short events, void* arg)
{
evutil_socket_t sockfd;
struct sockaddr_in client;
socklen_t len = sizeof(client);
sockfd = ::accept(fd, (struct sockaddr*)&client, &len );
evutil_make_socket_nonblocking(sockfd);
printf("accept a client %d\n", sockfd);
struct event_base* base = (event_base*)arg;
bufferevent* bev = bufferevent_socket_new(base, sockfd, BEV_OPT_CLOSE_ON_FREE);
bufferevent_setcb(bev, socket_read_cb, NULL, event_cb, arg);
bufferevent_enable(bev, EV_READ | EV_PERSIST);
}
void socket_read_cb(bufferevent* bev, void* arg)
{
char msg[4096];
size_t len = bufferevent_read(bev, msg, sizeof(msg));
msg[len] = '\0';
printf("recv the client msg: %s", msg);
char reply_msg[4096] = "I have recvieced the msg: ";
strcat(reply_msg + strlen(reply_msg), msg);
bufferevent_write(bev, reply_msg, strlen(reply_msg));
}
void event_cb(struct bufferevent *bev, short event, void *arg)
{
if (event & BEV_EVENT_EOF)
printf("connection closed\n");
else if (event & BEV_EVENT_ERROR)
printf("some other error\n");
//这将自动close套接字和free读写缓冲区
bufferevent_free(bev);
}
typedef struct sockaddr SA;
int tcp_server_init(int port, int listen_num)
{
int errno_save;
evutil_socket_t listener;
listener = ::socket(AF_INET, SOCK_STREAM, 0);
if( listener == -1 )
return -1;
//允许多次绑定同一个地址。要用在socket和bind之间
evutil_make_listen_socket_reuseable(listener);
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = 0;
sin.sin_port = htons(port);
if( ::bind(listener, (SA*)&sin, sizeof(sin)) < 0 )
goto error;
if( ::listen(listener, listen_num) < 0)
goto error;
//跨平台统一接口,将套接字设置为非阻塞状态
evutil_make_socket_nonblocking(listener);
return listener;
error:
errno_save = errno;
evutil_closesocket(listener);
errno = errno_save;
return -1;
}
想了解学习更多C++后台服务器方面的知识,请关注:
微信公众号:C++后台服务器开发