libevent库函数的简单介绍【奇牛学院】
1.前言
阅读下文需要一定的网络编程知识,应熟悉基本套接字的使用与原理,对IO多路复用有一定的了解。
2.什么是libevent
摘自搜狗百科
Libevent 是一个用C语言编写的、轻量级的开源高性能事件通知库,主要有以下几个亮点:事件驱动( event-driven),高性能;轻量级,专注于网络,不如 ACE 那么臃肿庞大;源代码相当精炼、易读;跨平台,支持 Windows、 Linux、 *BSD 和 Mac Os;支持多种 I/O 多路复用技术, epoll、 poll、 dev/poll、 select 和 kqueue 等;支持 I/O,定时器和信号等事件;注册事件优先级。
Libevent 已经被广泛的应用,作为底层的网络库;比如 memcached、 Vomit、 Nylon、 Netchat等等。
我的理解是libevent是一个高性能的网络库,其提供了网络编程下的很多有用功能,它内部封装了IO多路复用的大部分接口,支持select,poll,epoll。因此在高并发环境下使用它是很适合的,因为它既能保证效率支持网络高并发又能极大的精简代码还支持跨平台。
libevent是事件触发的网络库,事件触发这一概念很重要,无论是学习IO多路复用,如select,poll,epoll,还是libevent,这都是很重要的概念。不理解这一概念,就不能真正理解libevent的使用。
我们知道在网络编程中,服务器端套接字创建后,我们需要调用listen函数对其进行监听,一但客户端连上了服务器端,服务器端就会可读,这是我们就可以使用accept函数接受客户端套接字。在这一过程中,可以理解为服务器端可读,发生了读事件。
结合libevent库理解,libevent库中我们可以创建事件集,用来存放多个事件,每个事件可以设置要监听的套接字以及监听的事件,如读事件或写事件。一旦被监听的套接字监听的事件发生,就会执行我们预先设置好的函数来处理,也叫回调函数。这就是事件触发的整个过程。
3.响应事件
当文件描述符可读可写时将执行回调函数。
创建事件集:
struct event_base *event_base_new(void);
/* 函数分配并且返回一个新的具有默认设置的event_base。函数会检测环境变量,
返回一个到event_base的指针。如果发生错误,则返回NULL。选择各种方法时,
函数会选择OS支持的最快方法。 */
创建事件:
struct event event_new
(struct event_base* base ,evutil_socket_t fd,short what ,
event_callback_fn cb, void* arg)
/* event_new()试图分配和构造一个用于base的新的事件。
what参数是上述标志的集合。如果fd非负,则它是将被观察其读写事件的文件。
事件被激活时,libevent将调用cb函数,传递这些参数:文件描述符fd,表示所有被触发事件的位字段,以及构造事件时的arg参数。
发生内部错误,或者传入无效参数时,event_new()将返回NULL。
所有新创建的事件都处于已初始化和非未决状态,调用event_add()可以使其成为未决的。
*/
添加事件:
int event_add(struct event * ev,const struct timeval* tv)
/* 在非未决的事件上调用event_add()将使其在配置的event_base中成为未决的。
成功时函数返回0,失败时返回-1。如果tv为NULL,添加的事件不会超时。
否则,tv以秒和微秒指定超时值。*/
删除事件:
int event_del(struct event* ev)
/* 对已经初始化的事件调用event_del()将使其成为非未决和非激活的。
如果事件不是未决的或者激活的,调用将没有效果。成功时函数返回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()为止。 */
实例:使用libevent实现的一个简单的回声服务器,客户端输入字符,发送给服务器端,服务器端收到后输出该字符串,并发回给客户端。
server.c
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <event.h>
#include <event2/event.h>
#include <assert.h>
#define BUFLEN 128
struct ConnectData{ //用来保存事件的结构体
char buff[BUFLEN]; //用来保存字符串的字符数组
struct event* ev; //事件指针
};
void accept_connection(int fd,short events,void* arg); //接收到客户端请求后,把客户端套接字加入到事件集中并进行监听
void do_echo_request(int fd,short events,void* arg); //读出客户端写入的数据,并监听写事件
void do_echo_response(int fd,short events,void* arg); //向客户端写入数据并监听读事件
int tcp_server_init(int port,int listen_num); //接受客户端连接函数,进行服务器端的套接字创建
struct event_base* base; //事件集
int main(int argc,char** argv){
int listener=tcp_server_init(666,666);
if(listener==-1){
printf("服务端套接字创建失败!\n");
exit(1);
}
base=event_base_new(); //创建事件集
struct event* event_listen=event_new(base,listener,EV_READ | EV_PERSIST,accept_connection,base); //设置事件,第一个参数为事件集,第二个为监听的套接字,第三个为事件行为,EV_READ | EV_PERSIST 表示一旦可读就执行绑定函数,第四个参数为事件发生时执行的函数,第五个为传入执行函数的参数。
event_add(event_listen,NULL); //把事件加入到事件集中
event_base_dispatch(base); //循化处理事件
return 0;
}
int tcp_server_init(int port,int listen_num){
evutil_socket_t listener; //为跨平台定义的类型,在这里可以等同为int
listener=socket(AF_INET,SOCK_STREAM,0);
if(listener==-1){
return -1;
}
evutil_make_listen_socket_reuseable(listener); //设置套接字可重复绑定
struct sockaddr_in server;
server.sin_family=AF_INET;
server.sin_addr.s_addr=0;
server.sin_port=htons(port);
if(bind(listener,(struct sockaddr*)&server,sizeof(server))<0){
printf("服务器端绑定失败!\n");
exit(2);
}
if(listen(listener,listen_num)<0){
printf("服务器端监听失败!\n");
exit(3);
}
evutil_make_socket_nonblocking(listener); //设置非阻塞
return listener;
}
void accept_connection(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("收到一个新的连接!\n");
struct event_base* base=(struct event_base*)arg;
struct ConnectData* tmp=(struct ConnectData*)malloc(sizeof(struct ConnectData)); //ConnectData结构体,存放事件与字符串
struct event *ev = event_new(NULL, -1, 0, NULL, NULL); //创建一个事件,为了存放到上述结构体中
tmp->ev=ev;
event_assign(ev, base, sockfd, EV_READ,do_echo_request, (void*)tmp); //设置事件参数
event_add(ev,NULL); //加入到事件集中
return;
}
void do_echo_request(int fd,short events,void* arg){
struct ConnectData* tmp=(struct ConnectData*)arg;
int ret=read(fd,tmp->buff,BUFLEN-1); //客户端套接字可读,读出数据
if(ret<0){
printf("读取失败!\n");
exit(4);
}
char *str=tmp->buff;
printf("-------%s-------\n",str);
struct event* ev=tmp->ev;
event_set(ev,fd,EV_WRITE,do_echo_response,(void*)tmp); //修改事件,监听事件可写
ev->ev_base=base;
event_add(ev,NULL);
return;
}
void do_echo_response(int fd,short events,void* arg){
struct ConnectData* tmp=(struct ConnectData*)arg;
int ret=write(fd,tmp->buff,sizeof(tmp->buff)); //将上个函数中读出的数据发回到客户端
if(ret<0){
printf("数据写入失败!\n");
exit(5);
}
struct event* ev=tmp->ev;
event_set(ev,fd,EV_READ,do_echo_request,(void*)tmp); //修改事件监听写端
ev->ev_base=base;
event_add(ev,NULL);
bzero(tmp->buff,sizeof(tmp->buff)); //清空字符数组
return;
}
~
client.c
#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/util.h>
int connect_server(const char* server_ip, int port);
void cmd_read_data(int fd, short events, void* arg);
void socket_read_data(int fd, short events, void *arg);
int main(int argc, char** argv)
{
if( argc < 3 )
{
printf("please input 2 parameters\n");
return -1;
}
//两个参数依次是服务器端的IP地址、端口号
int sockfd = connect_server(argv[1], atoi(argv[2]));
if( sockfd == -1)
{
perror("tcp_connect error ");
return -1;
}
printf("connect to server successfully\n");
struct event_base* base = event_base_new();
struct event *ev_sockfd = event_new(base, sockfd, //监视客户端套接字,一旦可读就读出
EV_READ | EV_PERSIST,
socket_read_data, NULL);
event_add(ev_sockfd, NULL);
//监听终端输入事件
struct event* ev_cmd = event_new(base, STDIN_FILENO, //监视标准输入,等待输入字符串
EV_READ | EV_PERSIST, cmd_read_data,
(void*)&sockfd);
event_add(ev_cmd, NULL);
event_base_dispatch(base);
printf("finished \n");
return 0;
}
void cmd_read_data(int fd, short events, void* arg)
{
char msg[1024];
int ret = read(fd, msg, sizeof(msg)-1);
if( ret <= 0 )
{
perror("read fail ");
exit(1);
}
int sockfd = *((int*)arg);
if(msg[ret - 1]=='\n') msg[ret - 1] = '\0';
else msg[ret] = '\0';
//把终端的消息发送给服务器端,客户端忽略性能考虑,直接利用阻塞方式发送
printf("write to server>>> [%s]\n", msg);
write(sockfd, msg, ret);
}
void socket_read_data(int fd, short events, void *arg)
{
char msg[1024];
//为了简单起见,不考虑读一半数据的情况
int len = read(fd, msg, sizeof(msg)-1);
if( len == 0 )
{
printf("connection close. exit~\n");
exit(1);
}else if(len < 0){
perror("read fail ");
return ;
}
msg[len] = '\0';
printf("recv from server<<<<< [%s] \n", msg);
}
typedef struct sockaddr SA;
int 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;
}
4.利用缓存事件
很多时候,除了响应事件之外,应用还希望做一定的数据缓冲。比如说,写入数据的时候,通常的运行模式是:
决定要向连接写入一些数据,把数据放入到缓冲区中
等待连接可以写入
写入尽量多的数据
记住写入了多少数据,如果还有更多数据要写入,等连接再次可以写入
这种缓冲IO模式很通用,libevent为此提供了一种通用机制,即bufferevent。bufferevent由一个底层的传输端口(如套接字),一个读取缓冲区和一个写入缓冲区组成。与通常的事件在底层传输端口已经就绪,可以读取或者写入的时候执行回调不同的是,bufferevent在读取或者写入了足够量的数据之后调用用户提供的回调。
响应事件在事件发生后执行回调函数需要我们自己再进行读或写,而缓存事件在事件发生后,如读事件发生,会自动将数据读到一个缓存区域,他帮我们完成了读取数据这一步骤。其实这是很自然的逻辑,有了数据,当然要读取出来。
创建bufferevent:
struct bufferevent* bev = bufferevent_socket_new(struct event_base* base,evutil_socket_t sockfd,enum bufferevent_options options);
/* base是event_base,options是表示bufferevent选项(BEV_OPT_CLOSE_ON_FREE等)的位掩码,fd是一个可选的表示套接字的文件描述符。如果想以后设置文件描述符,可以设置fd为-1。
成功时函数返回一个bufferevent,失败则返回NULL。
*/
修改bufferevent:
void bufferevent_ setcb (struct bufferevent *bufev,bufferevent_ data_ cb readcb, bufferevent_ data_ cb writecb,bufferevent event_ cb eventcb, void *cbarg) ;
/* bufferevent_setcb()函数修改bufferevent的一个或者多个回调。
readcb、writecb和eventcb函数将分别在已经读取足够的数据、已经写入足够的数据,或者发生错误时被调用。
每个回调函数的第一个参数都是发生了事件的bufferevent,最后一个参数都是调用bufferevent_setcb()时用户提供的cbarg参数:可以通过它向回调传递数据。
事件回调的events参数是一个表示事件标志的位掩码:*/
使bufferevent 生效:
void bufferevent enable (struct bufferevent *bufev, short events) ;
server1.c
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <event.h>
#include <event2/event.h>
#include <assert.h>
#define BUFLEN 128
struct ConnectData{ //用来保存事件的结构体
char buff[BUFLEN]; //用来保存字符串的字符数组
// struct event* ev; //事件指针
struct bufferevent* bev; //缓冲事件
};
void accept_connection(int fd,short events,void* arg); //接收到客户端请求后,把客户端套接字加入到事件集中并进行监听
void do_echo_request(struct bufferevent* bev,void* arg); //读出客户端写入的数据,并监听写事件
int tcp_server_init(int port,int listen_num); //接受客户端连接函数,进行服务器端的套接字创建
struct event_base* base; //事件集
int main(int argc,char** argv){
int listener=tcp_server_init(666,666);
if(listener==-1){
printf("服务端套接字创建失败!\n");
exit(1);
}
base=event_base_new(); //创建事件集
struct event* event_listen=event_new(base,listener,EV_READ | EV_PERSIST,accept_connection,base); //设置事件,第一个参数为事件集,第二个为监听的套接字,第三个为事件行为,EV_READ | EV_PERSIST 表示一旦可读就执行绑定函数,第四个参数为事件发生时执行的函数,第五个为传入执行函数的参数。
event_add(event_listen,NULL); //把事件加入到事件集中
event_base_dispatch(base); //循化处理
return 0;
}
int tcp_server_init(int port,int listen_num){
evutil_socket_t listener; //为快跨平台定义的类型,在这里可以等同为int
listener=socket(AF_INET,SOCK_STREAM,0);
if(listener==-1){
return -1;
}
evutil_make_listen_socket_reuseable(listener); //设置套接字可重复绑定
struct sockaddr_in server;
server.sin_family=AF_INET;
server.sin_addr.s_addr=0;
server.sin_port=htons(port);
if(bind(listener,(struct sockaddr*)&server,sizeof(server))<0){
printf("服务器端绑定失败!\n");
exit(2);
}
if(listen(listener,listen_num)<0){
printf("服务器端监听失败!\n");
exit(3);
}
evutil_make_socket_nonblocking(listener); //设置非阻塞
return listener;
}
void accept_connection(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("收到一个新的连接!\n");
struct event_base* base=(struct event_base*)arg;
struct ConnectData* tmp=(struct ConnectData*)malloc(sizeof(struct ConnectData)); //ConnectData结构体,存放事件与字符串
struct bufferevent* bev=bufferevent_socket_new(base,sockfd,BEV_OPT_CLOSE_ON_FREE); //创建缓存事件
tmp->bev=bev;
bufferevent_setcb(bev,do_echo_request,NULL,NULL,tmp); //设置缓存事件属性
bufferevent_enable(bev,EV_READ | EV_PERSIST); //使缓存事件生效
return;
}
void do_echo_request(struct bufferevent* bev,void* arg){
struct ConnectData* tmp=(struct ConnectData*)arg;
char *str=tmp->buff;
bufferevent_read(bev,str,BUFLEN); //往缓存区域读出数据
printf("-------%s-------\n",str);
bufferevent_write(bev,str,BUFLEN); //往缓存区域写入数据,客户端套接准备好后会自动写入
return;
}
~
~
client.c:
与响应事件一样,见上文。
注:
执行时客户端要带上ip地址与端口号666。
5.libevent 中文文档
最后,附上libevent中文文档以供参考