1. libevent库介绍

1.1 什么是libevent

libevent也称为事件通知库,即所见皆事件,是一个用C语言实现的、基于事件驱动(event-driven)的轻量级高性能开源网络库,适用于Windows、Linux、bsd等多种平台,内部使用select、epoll、kqueue等系统调用管理事件机制。libevent不一定是用到网络当中,本地的文件描述符都可以用。

1.2 libevent特点

  • 事件驱动(event-driven),高性能

  • 轻量级,专注于网络,不如 NGINX 那么臃肿庞大

  • 源代码相当精炼、易读

  • 跨平台,支持 Windows、Linux、*BSD和Mac OS,但Windows支持不怎么好

  • 支持多种I/O多路复用技术,select、epoll、poll、dev/poll、select、kqueue、evports等

  • 支持I/O,定时器和信号等事件

  • 采用Reactor设计模式

  • 支持HTTP(S),DNS解析

libevent是用于编写高速可移植非阻塞IO应用的库,其设计目标是:可移植性、高性能、便携和可扩展性

2. libevent框架

2.1 步骤流程

1.创建event_base

  • struct event_base *event_base_new(void);

  • struct event_base *base = event_base_new();

2.创建事件event

  • 常规事件event:event_new();

  • bufferevent事件:bufferevent_socket_new();

3.将事件添加到base上

  • int event_add(struct event *ev, const struct timeval *tv);

4.循环监听事件满足

  • int event_base_dispatch(struct event_base *base);

5.释放event_base

  • event_base_free(base);

2.2 event事件

1.struct event *event_new(struct event_base *base, evutil_socket_t fd, short what, event_callback_fn cb, void *arg);

  • 参1:event_base_new()返回值

  • 参2:绑定到event上的文件描述符,也是回调函数参数

  • 参3:对应的事件(r,w,e),也是回调函数参数

    • EV_READ:一次读事件
    • EV_WRITE:一次写事件
    • EV_PERSIST: 持续触发,结合event_base_dispatch函数使用
  • 参4:一旦事件满足监听条件,就回调的函数

    • typedef void(*event_callback_fn)(evutil_socket_t fd, short, void*);
  • 参5:回调函数的参数;

    • 返回值:成功创建的event;

2.添加事件到event_base

int event_add(struct event *ev, const struct timeval *tv);

  • 参1:event_new()的返回值

  • 参2:NULL

3.从event_base上摘下事件[了解即可]

int event_del(struct event *ev);

  • 参数:event_new()的返回值

4.销毁事件

int event_free(struct event *ev);

  • 参数:event_new()的返回值

注意:使用到libevent库的事件,编译时后面要加-levent

补:常规事件event的过程状态

  • 未决:有资格被处理,但还没有被处理(事件没有到来)

  • 非未决:没有资格被处理

案例:通过event事件对管道写内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void write_cb(evutil_socket_t fd, short what, void *arg){
//写管道
char buf[1024] = {0};
static int num = 0;
//下面没有设置持续写的话,在这里加一个while(1)也可以
sprintf(buf,"hello,world_%d\n",num++); //将要写的内容存到buf
write(fd,buf,strlen(buf)+1); //向管道发送buf数据
sleep(1);
}
int main(){
//由于读操作那边已经创建管道了,所以这边不用创建了,它们是用同一个管道
//打开管道
int fd = open("myfifo",O_WRONLY | O_NONBLOCK); //以只写方式和非阻塞方式打开管道
if(fd == -1){
perror("open error");
exit(1);
}
//创建一个event_base ---> 相当于底座
struct event_base* base = NULL;
base = event_base_new();
//创建event事件ev
struct event* ev = NULL;
//ev = event_new(base,fd,EV_WRITE,write_cb,NULL); //只写一次,如果没有持续触发,那么下面的event_base_dispatch没有用
ev = event_new(base,fd,EV_WRITE | EV_PERSIST,write_cb,NULL); // 持续写。参数:底座;需要监听的fd;对fd做什么样的监听(写和持续触发);回调函数;回调函数的参数
//添加事件
event_add(ev,NULL); //将事件添加到base(底座)上
//事件循环
event_base_dispatch(base); //接下来由内核处理 -->相当于while(1){ epoll(); }; --->事件满足,自动回调
//释放资源
event_free(ev);
event_base_free(base);
close(fd);
return 0;
}

案例:通过event事件对管道读内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void read_cb(evutil_socket_t fd, short what, void *arg){
//读管道
char buf[1024] = {0};
int len = read(fd,buf,sizeof(buf)); //从管道将读到的内容存放到buf
printf("read event:%s\n",(what & EV_READ?"Yes":"No"));
printf("data len = %d, buf = %s\n",len,buf);
sleep(1);
}
int main(){
unlink("myfifo");
//创建命名管道---->无血缘关系的进程间也可以通信
mkfifo("myfifo",0664);
//打开管道
int fd = open("myfifo",O_RDONLY | O_NONBLOCK); //以只读方式和非阻塞方式打开管道
if(fd == -1){
perror("open error");
exit(1);
}
//创建一个event_base ---> 相当于底座
struct event_base* base = NULL;
base = event_base_new();
//创建事件
struct event* ev = NULL;
ev = event_new(base,fd,EV_READ | EV_PERSIST,read_cb,NULL); //底座;需要监听的fd;对fd做什么样的监听(读和持续触发);回调函数;回调函数的参数
//添加事件
event_add(ev,NULL); //将事件添加到base(底座)上
//事件循环
event_base_dispatch(base); //接下来由内核处理 -->相当于while(1){ epoll(); }; --->事件满足,自动回调
//释放资源
event_free(ev);
event_base_free(base);
close(fd);
return 0;
}

2.3 bufferevent缓冲事件

原理:bufferevent有两个缓冲区,也是队列实现,读走就没了,先进先出

读过程:有数据—–>读回调函数被调用—–>使用bufferevent_read()—–>有数据。

写过程:使用bufferevent_write()—–>向写缓冲中写数据—–>该缓冲区有数据就自动写出—–>写完,回调函数被调用(鸡肋)

1.创建bufferevent事件

struct bufferevenet *bufferevent_socket_new(struct event_base *base, evutil_socket_t fd, enum bufferevent_options options);

  • 参1:event_base

  • 参2:封装到bufferevent内的fd

  • 参3:BEV_OPT_CLOSE_ON_FREE—>释放bufferevent时关闭底层传输端口,这将关闭底层套接字(网络套接字),释放底层bufferevent等

  • 返回值:成功创建的bufferevent事件对象

2.销毁bufferevent

void bufferevent_socket_free(struct bufferevent *ev);

3.给bufferevent设置回调

  • 区别:event事件设置回调 event_new(fd,callback),一个函数就可以,而bufferevent事件设置回调则麻烦一点

bufferevent_socket_new(fd); bufferevent_setcb(callback);

4.void bufferevent_setcb(struct bufferevent* bufev, bufferevent_data_cb readcb, bufferevent_data_cb writecb, bufferevent_event_cb eventcb, void *cbarg);

  • 参1:bufferevent_socket_new()返回值

  • 参2:设置bufferevent读缓冲,对应回调read_cb{bufferevent_read()读数据}

  • 参3:设置bufferevent写缓冲,对应回调write_cb{ }–>给调用者,发送写成功通知,可以写NULL

  • 参4:设置事件回调,也可以传NULL

  • 参5:上述回调函数使用的参数

5.启动、关闭bufferevent的缓冲区

void bufferevent_enbale(struct bufferevent *bufev, short events);

  • events:EV_READ、EV_WRITE、EV_READ|EV_WRITE

注意:默认write缓冲是enable、read缓冲是disable,所以需要开启都缓冲

6.客户端实现

socket(); connect();

int bufferevent_socket_connect(struct bufferevent *bev, struct sockaddr *address, int addrlen);

  • 参1:bufferevent事件对象(封装了fd)

  • 参2和参3:等同于connect()的参2和参3

7.服务端实现

socket(); bind(); listen(); accept();

struct evconnlistener *evconnlistener_new_bind(struct event_base *base, evconnlistener_cb cb, void *ptr, unsigned flags, int backlog, const struct sockaddr *sa, int socklen);

  • 参1:event_base

  • 参2:回调函数,一旦被回调,说明在其内部应该与客户端完成,数据读写操作,进行通信。

  • 参3:回调函数的参数

  • 参4:LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE

  • 参5:listen的参2,-1表示最大值

  • 参6:服务器自身的地址结构体

  • 参7服务器自己的地址结构体大小

  • 返回值:成功创建的监听器

8.bufferevent的读事件回调触发时机:

当数据由内核的读缓冲区到bufferevent的读缓冲区的时候,会触发bufferevent()的读事件回调。需要注意的是,数据由内核到bufferevent的过程不是由用户程序执行的,是由bufferevent内部操作的

被触发的回调函数是自己写的,可以在里面读bufferevent的数据了,也可以向bufferevent写缓冲区写数据

9.bufferevent的写事件回调触发时机:

当用户程序将数据写到bufferevent的写缓冲区之后,bufferevent会自动将数据写到内核的写缓冲区,最终由内核程序将数据发送出去。此时回调函数起到的是通知作用,向我们传达数据已经发送出去的信息。

案例:用bufferevent来实现网络通信(服务端)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
//读缓冲区回调--->当数据从内核缓冲区到bufferevent的读缓冲区时,触发该事件
void read_cb(struct bufferevent *bev,void *arg){
char buf[1024] = {0};
//只能用下面这个函数(bufferevent包里面的)代替read,因为没有文件描述符
bufferevent_read(bev,buf,sizeof(buf)); //从bufferevent读缓冲区中读数据到buf中
printf("client say: %s\n",buf); //打印buf中的内容,也就是客户端发的内容
char *p = "我是服务器,已经成功收到你发送的数据!\n";
//因为写入bufferevent写缓冲区的数据会自动发送到内核缓冲区,则读回调函数里面直接向bufferevent写数据相当于回应客户端
bufferevent_write(bev,p,strlen(p)+1); //向bufferevent写缓冲区写入数据,系统就会将信息发送给客户端
sleep(1);
}
//写缓冲区回调--->当bufferevent的写缓冲区的数据发送到内核缓冲区时,触发该函数
void write_cb(struct bufferevent *bev,void *arg){
//因为读回调向bufferevent写缓冲区写入了数据,并且由内核处理,数据发送给了内核缓冲区,最后客户端收到信息,这些事情写回调都不用做
printf("成功写数据给客户端,写缓冲区回调函数被回调.....\n"); //写回调起到的作用就是通知,表示信息已经发送出去
}
//事件回调
void event_cb(struct bufferevent *bev,short events,void *arg){
if(events & BEV_EVENT_EOF){
printf("connection closed\n");
}else if(events & BEV_EVENT_ERROR){
printf("some other error\n");
}
bufferevent_free(bev); //出现上面两种情况都释放bev(bufferevent)
printf("buffevent 资源已经被释放...\n");
}
//监听回调 --->当客户端连上,会执行这个函数
void cb_listener(struct evconnlistener *listener,evutil_socket_t fd,struct sockaddr *addr,int len,void *ptr){
printf("connect new client\n");
struct event_base* base = (struct event_base*)ptr; //将传过来的参数进行转换
//创建bufferevent事件--->然后就有了bufferevent的读写缓冲区
struct bufferevent *bev;
bev = bufferevent_socket_new(base,fd,BEV_OPT_CLOSE_ON_FREE);
//给bufferevent缓冲区设置回调
bufferevent_setcb(bev,read_cb,write_cb,event_cb,NULL); //设置了读回调函数、写回调函数和事件回调;最后NULL是传给三个的回调函数的参数,表示无
//启用bufferevent的读缓冲,默认是disable的
bufferevent_enable(bev,EV_READ); //bufferevent的读缓存区默认是关闭的,所以需要打开
}
int main(){
struct sockaddr_in serv; //定义一个存放服务端信息的结构体
memset(&serv,0,sizeof(serv)); //将改结构体清0
//初始化服务端结构体的信息
serv.sin_family = AF_INET;
serv.sin_port = htons(9527);
serv.sin_addr.s_addr = htonl(INADDR_ANY);
//创建event_base--->插座
struct event_base* base = event_base_new();
//创建套接字、绑定、接收连接请求
struct evconnlistener* listener; //监听器,下面函数第二个参数是创建监听回调函数
listener = evconnlistener_new_bind(base,cb_listener,base,LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE,36,(struct sockaddr*)&serv,sizeof(serv)); //当客户端连接上的时候,cb_listener会被调用,其中需要传入参数base,里面会用到
//启动循环监听
event_base_dispatch(base); //该函数循环监听那些事件对象(有回调函数的),满足条件就激活对应的事件,进行回调
evconnlistener_free(listener);
event_base_free(base);
return 0;
}

案例:用bufferevent来实现网络通信(客户端)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
//读缓冲区回调--->当数据从内核缓冲区到bufferevent的读缓冲区时,触发该事件
void read_cb(struct bufferevent *bev,void *arg){
char buf[1024] = {0};
bufferevent_read(bev,buf,sizeof(buf)); //从bufferevent读缓冲区中读数据到buf中
printf("server say: %s\n",buf); //打印buf中的内容,也就是服务端发的内容
//bufferevent_write(bev,buf,strlen(buf)+1); //向bufferevent写缓冲区写入数据,系统就会将信息发送给对端
sleep(1);
}
//写缓冲区回调--->当bufferevent的写缓冲区的数据发送到内核缓冲区时,触发该函数
void write_cb(struct bufferevent *bev,void *arg){
//因为读回调向bufferevent写缓冲区写入了数据,并且由内核处理,数据发送给了内核缓冲区,最后客户端收到信息,这些事情写回调都不用做
printf("成功写入数据给服务端,客户端回调起通知作用.....\n"); //写回调起到的作用就是通知,表示信息已经发送出去
}
//事件回调
void event_cb(struct bufferevent *bev,short events,void *arg){
if(events & BEV_EVENT_EOF){
printf("connection closed\n");
}else if(events & BEV_EVENT_ERROR){
printf("some other error\n");
}else if(events & BEV_EVENT_CONNECTED){
printf("已经连接到服务器.......\n");
return;
}
bufferevent_free(bev); //最上面两种情况就释放bev(bufferevent)
printf("buffevent 资源已经被释放...\n");
}
//客户端与用户交互,从终端读取数据写给服务器
void read_terminal(evutil_socket_t fd, short what, void *arg){
//读数据
char buf[1024] = {0};
int len = read(fd,buf,sizeof(buf)); //此时的fd是标准输入,即从标准输入数据到buf中
struct bufferevent* bev = (struct bufferevent*)arg; //将传过来的参数进行转换
//发送数据
bufferevent_write(bev,buf,len+1); //向服务端发送数据
}
int main(){
//创建一个event_base,底座
struct event_base* base = NULL;
base = event_base_new();
int fd = socket(AF_INET,SOCK_STREAM,0); //通信描述符
//通信的fd放到bufferevent中
struct bufferevent* bev = NULL;
bev = bufferevent_socket_new(base,fd,BEV_OPT_CLOSE_ON_FREE); //创建了bufferevent事件,将其的bufferevent读写缓冲区与内核文件描述符建立关系
//定义服务端信息结构体
struct sockaddr_in serv;
memset(&serv,0,sizeof(serv));
serv.sin_family = AF_INET;
serv.sin_port = htons(9527);
inet_pton(AF_INET,"192.168.88.93",&serv.sin_addr.s_addr);
//连接服务器
bufferevent_socket_connect(bev,(struct sockaddr*)&serv,sizeof(serv));
//设置回调
bufferevent_setcb(bev,read_cb,write_cb,event_cb,NULL); //设置了读回调、写回调和事件回调
//设置读回调生效
bufferevent_enable(bev,EV_READ); //打开读缓冲区,不打开的话,服务器发信息来,这边不会执行读回调
//创建事件 ---> 有了bufferevent事件只是有了它的读写缓冲区与内核缓冲区建立关系,但作为客户端,需要主动给服务器发信息,就可以创建event事件来从终端向对端发信息
struct event* ev = event_new(base,STDIN_FILENO,EV_READ | EV_PERSIST,read_terminal,bev); //监听标准输入终端,读事件,持续触发,read_terminal是回调函数
//添加事件
event_add(ev,NULL);
//启动循环监听
event_base_dispatch(base);
event_free(ev);
event_base_free(base);
return 0;
}

在上面代码中,需要注意的是,客户端在创建了bufferevent事件后,只是将其的读写缓冲区与内核的作了联系,若后面不创建event事件来主动向服务端发信息,将运行不起来程序,因为服务器和客户端的读缓冲区被激活的前提是有信息到自己的bufferevent读缓冲区,但没有event事件让客户端从终端的输入信息发送到对端,这个程序就不会执行下去。