一、Libevent概述
1、简介
-
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等等。
2、安装
-
官网:官网连接
源码安装:libevent-2.1.11-stable.tar.gz -
Ubuntu安装:
apt-get install libevent-dev
二、两个重要的结构体
1、struct event
2、struct event_base
三、libevent常用接口
1、event_init( )函数:初始化事件集合
(1)函数原型
原型:struct event_base *event_init(void)
(2)函数实现
struct event_base *event_init(void)
{
struct event_base *base = event_base_new_with_config(NULL);
if (base == NULL) {
event_errx(1, "%s: Unable to construct event_base", __func__);
return NULL;
}
current_base = base;
return (base);
}
(3)函数作用
初始化事件集合,其实就是调用了event_base_new_with_config( )
函数,创建event_base
对象,并且赋值给了全局变量struct evdns_base *current_base
。
2、event_set( )函数:初始化事件
(1) 函数原型
原型:void event_set(struct event *ev, evutil_socket_t fd, short events, void (*callback)(evutil_socket_t, short, void *), void *arg);
参数:
1)事件
2)关联的文件描述符
3)事件类型
4)回调函数
5)回调函数的参数
(2)函数实现:
void event_set(struct event *ev, evutil_socket_t fd, short events, void (*callback)(evutil_socket_t, short, void *), void *arg)
{
int r;
r = event_assign(ev, current_base, fd, events, callback, arg);
EVUTIL_ASSERT(r == 0);
}
(3)函数作用:
初始化event事件(其实就是给结构体ev的成员赋值)
fd
表示事件对应的文件描述符,events
表示事件的类型,callback
是回调函数(即当fd满足条件时调用该函数),arg
表示给回调函数传递的参数。
3、event_add( )函数:把事件添加到集合
(1)函数原型
原型:int event_add(struct event *ev, const struct timeval *tv);
(2)函数实现
int event_add(struct event *ev, const struct timeval *tv)
{
int res;
if (EVUTIL_FAILURE_CHECK(!ev->ev_base)) {
event_warnx("%s: event has no event_base set.", __func__);
return -1;
}
EVBASE_ACQUIRE_LOCK(ev->ev_base, th_base_lock);
res = event_add_nolock_(ev, tv, 0);
EVBASE_RELEASE_LOCK(ev->ev_base, th_base_lock);
return (res);
}
(3)函数作用
- 1.将event注册到event_base的I/O多路复用要监听的事件中;
- 2.将event注册到event_base的已注册事件链表中;
- 3.如果传入了超时时间,则删除旧的超时时间,重新设置,并将event添加到event_base的小根堆中;
- 如果没有传入超时时间,则不会添加到小根堆中。
- 只有步骤1成功,才会执行步骤2和3;否则什么都没做,直接返回,保证不会改变event的状态。
4、event_dispatch( )函数:监听事件
这个函数会让程序陷入死循环, 如果集合中没有事件可以监听,则返回
例子1:IO事件
- 1-fifo.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <event.h>
#include <fcntl.h>
/*
当监听的事件满足条件的时候,会触发回调函数,通过回调函数读取数据
*/
void fifo_read(evutil_socket_t fd, short events, void *arg) {
char buf[32] = {0};
int ret = read(fd, buf, sizeof(buf));
if (-1 == ret) {
perror("read");
exit(1);
}
printf("从管道读取: %s\n", buf);
}
int main() {
int ret = mkfifo("fifo.tmp", 00700);
if (-1 == ret) {
perror("mkfifo");
exit(1);
}
int fd = open("fifo.tmp", O_RDONLY);
if (-1 == fd) {
perror("open");
exit(1);
}
// 创建事件
struct event ev;
// 初始化事件集合
event_init();
// 初始化事件(把fd和事件ev绑定)
// 参数:事件、关联的文件描述符、事件类型、回调函数、回调函数参数
event_set(&ev, fd, EV_READ | EV_PERSIST, fifo_read, NULL);
//把事件添加到集合
event_add(&ev, NULL);
//开始监吿
event_dispatch(); //死循玿 如果集合中没有事件可以监听,则返囿
exit(0);
}
- 1-write_fifo.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd = open("fifo.tmp", O_WRONLY);
if (-1 == fd)
{
perror("open");
exit(2);
}
char buf[128] = {0};
while (1)
{
scanf("%s", buf);
if (write(fd, buf, strlen(buf)) == -1)
{
perror("write");
break;
}
if (!strcmp(buf, "bye"))
break;
memset(buf, 0, 128);
}
close(fd);
return 0;
}
- 运行结果:
5、event_base_new( )函数:创建事件集合
(1)函数原型
原型:struct event_base *event_base_new(void);
(2)函数实现
struct event_base *event_base_new(void)
{
struct event_base *base = NULL;
struct event_config *cfg = event_config_new();
if (cfg) {
base = event_base_new_with_config(cfg);
event_config_free(cfg);
}
return base;
}
(3)函数作用
创建event_base对象
注意:采用event_base_new( )创建出来的事件集合,最后要用 event_base_free(base)
释放掉,因为event_base_new( )是在堆空间上进行创建的。
(4)event_base_new( )与event_init( )的区别
- 这两个函数都会创建一个事件集合
- event_init( )创建的是一个全局的事件集合
- event_base_new( )创建的是一个局部的事件集合
- event_init( )函数实现是由event_base_new( )封装而成的
6、event_base_free( )函数:释放事件集合
用于释放event_base_new( )
函数创建的集合
7、event_assign( )函数:初始化事件
(1)函数原型:
原型:int event_assign(struct event *ev, struct event_base *base, evutil_socket_t fd, short events, void (*callback)(evutil_socket_t, short, void *), void *arg)
(2)参数介绍
1)事件
2)事件集合
3)关联的文件描述符
4)事件类型
5)回调函数
6)回调函数的参数
(3)函数作用
将指定事件集合中的事件与某一文件描述符进行关联
(4)event_assign( )与event_set( )的区别
- event_assign( )可以指定事件集合;
- event_set( )不能指定事件集合,默认采用event_init( )创建的全局的事件集合;
8、event_base_dispatch( )函数:监听事件
可监听事件集合当中的事件,和event_dispatch( )的作用相同
(1)event_base_dispatch( )与event_dispatch( )的区别
- event_base_dispatch( )可以指定监听哪个事件集合;
- event_dispatch( )不能指定事件集合,默认监听event_init( )创建的全局的事件集合;
9、event_del( )函数:把事件从集合中删除
原型:event_del(struct event *ev);
例子2:信号事件
- 2-signal.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <event.h>
int signal_count = 0;
void signal_handler(evutil_socket_t fd, short events, void *arg) {
struct event *ev = (struct event *)arg;
printf("收到信号 %d\n", fd);
signal_count++;
if (signal_count >= 5) {
// 把事件从集合中删除
event_del(ev);
}
}
int main() {
// 创建事件集合
struct event_base *base = event_base_new();
// 创建事件
struct event ev;
// 把事件和信号进行绑定
event_assign(&ev, base, SIGINT, EV_SIGNAL | EV_PERSIST, signal_handler, &ev);
// 把事件添加到集合中
event_add(&ev, NULL);
// 监听集合
event_base_dispatch(base);
// 释放集合
event_base_free(base);
exit(0);
}
运行结果(收到一次Ctrl+C就打印信息,收到五次之后退出):
四、libevent高并发服务器
常见步骤:
- 创建socket对象
- 绑定 bind
- 监听 listen
- 接受连接 accept
1、evconnlistener_new_bind( )函数
(1)作用:用于创建一个evconnlistener对象的函数之一,用于监听指定地址和端口上的连接请求。
(2)函数原型:
struct evconnlistener *evconnlistener_new_bind(struct event_base *base,
evconnlistener_cb cb,
void *ptr,
unsigned flags,
int backlog,
const struct sockaddr *sa,
int socklen);
(3)参数介绍
- 1)base
struct event_base类型的指针,表示事件集合 - 2)cb
evconnlistener_cb类型的回调函数指针,用于处理新连接的事件。其中:evconnlistener_cb类型
- 3)ptr
传递给回调函数的参数指针 - 4)flags
标志位,可以是LEV_OPT_REUSEABLE或LEV_OPT_CLOSE_ON_FREE等选项
其中:
- 5)backlog:监听队列的长度。
- 6)sa:指向struct sockaddr类型的指针,表示要监听的地址和端口。
- 7)socklen:sa指向的地址结构体的长度。
(4)例子
2、evconnlistener_free( )函数
(1)作用:释放 TCP 监听器
- 具体来说,evconnlistener_free() 函数用于释放由 evconnlistener_new() 或 evconnlistener_new_bind() 函数创建的 TCP 监听器。在释放监听器之前,应该确保已经停止了监听器上的事件循环,并且不再有任何活动的连接。
(2)函数原型
void evconnlistener_free(struct evconnlistener *listener);
参数 listener 是指向要释放的 TCP 监听器的指针
(3)注意事项
-
使用 evconnlistener_free() 函数释放监听器时,libevent 库将自动关闭监听器的文件描述符,并释放监听器所占用的内存。在释放监听器之后,应该将指向监听器的指针设置为 NULL,以避免出现悬空指针的问题。
-
需要注意的是,如果在释放监听器之前仍然有活动的连接,则这些连接将被关闭,并且可能会因为连接未正常关闭而导致数据丢失。因此,在释放监听器之前,应该确保已经停止了所有连接的事件循环,并且数据已经被正确地处理和发送。
3、bufferevent_socket_new( )函数
(1)作用:对新出现的socket创建一个bufferevent
- bufferevent_socket_new() 是 libevent 库中用于创建基于套接字的缓冲区事件处理器的函数。
- 套接字缓冲区事件处理器是一种基于 libevent 库的高级 I/O 抽象,它可以将套接字的读写操作转换为事件驱动的方式。使用套接字缓冲区事件处理器,应用程序可以方便地进行非阻塞 I/O 操作,避免了使用底层的 socket API 进行复杂的事件循环编程。
(2)函数原型
struct bufferevent *bufferevent_socket_new(
struct event_base *base,
evutil_socket_t fd,
enum bufferevent_options options
);
(3)参数介绍
- 1)base
事件集合 - 2)fd
表示要创建缓冲区事件处理器的套接字文件描述符 - 3)options
表示创建缓冲区事件处理器的选项,可以是多个选项的组合,例如 BEV_OPT_CLOSE_ON_FREE、BEV_OPT_THREADSAFE 等
(4)注意事项
- bufferevent_socket_new() 函数将返回一个指向新创建的套接字缓冲区事件处理器的指针。应用程序可以使用返回的指针来操作缓冲区事件处理器,例如注册事件回调函数、发送和接收数据等。
- 套接字缓冲区事件处理器提供了一些高级功能,例如数据读写缓冲区、自动调整缓冲区大小、高效的数据传输等。开发人员可以通过设置选项来控制这些功能的行为,从而满足不同的应用场景需求
(5)例子
4、bufferevent_setcb( )函数
(1)改变bufferevent的回调函数
bufferevent_setcb() 函数是 libevent 库中用于设置套接字缓冲区事件处理器回调函数的函数。
(2)函数原型
void bufferevent_setcb(
struct bufferevent *bev,
bufferevent_data_cb readcb,
bufferevent_data_cb writecb,
bufferevent_event_cb eventcb,
void *cbarg
);
(3)参数介绍
- 1)bev:指向要设置回调函数的套接字缓冲区事件处理器的指针。
- 2)readcb:表示在缓冲区有数据可读时调用的回调函数。
- 3)writecb:表示在缓冲区可写时调用的回调函数。
- 4)eventcb:表示在事件处理器发生错误或状态变化时调用的回调函数。
- 5)cbarg:表示传递给回调函数的参数。
(4)注意
- 使用 bufferevent_setcb() 函数,应用程序可以指定回调函数来处理套接字缓冲区事件处理器上的事件,例如接收数据、发送数据、处理错误等。回调函数应该是线程安全的,并且需要尽快返回,以避免阻塞事件循环。
- 需要注意的是,bufferevent_setcb() 函数必须在事件处理器开始运行之前调用,否则设置的回调函数可能无法生效。而且,在设置回调函数之后,应用程序应该尽快启动事件处理器的事件循环,以便及时处理事件。
(5)例子
5、bufferevent_read( )函数
(1)作用:读取套接字缓冲区事件处理器中的数据
- bufferevent_read() 是 libevent 库中的一个函数,用于读取套接字缓冲区事件处理器中的数据。
(2)函数原型
int bufferevent_read(struct bufferevent *bev, void *data, size_t size);
(3)参数介绍
- 1)bev:指向要读取数据的套接字缓冲区事件处理器的指针;
- 2)data:指向存储读取数据的缓冲区的指针;
- 3)size:表示要读取的数据的最大字节数。
(4)注意事项
- 使用 bufferevent_read() 函数,应用程序可以从套接字缓冲区事件处理器中读取数据到指定的缓冲区中,并返回实际读取的字节数。如果没有数据可读,函数将立即返回,并返回 0。如果读取时发生错误,函数将返回 -1,并设置 errno 变量来指示错误的原因。
- 需要注意的是,使用 bufferevent_read() 函数只会从套接字缓冲区中读取数据,并不会阻塞等待数据到达。如果没有数据可读,函数将立即返回,因此需要在事件处理器的读回调函数中多次调用该函数,以确保读取所有可用的数据。
(5)例子
6、bufferevent_free( )函数
(1)作用:释放套接字缓冲区事件处理器的内存资源
(2)函数原型:
void bufferevent_free(struct bufferevent *bev);
(3)注意事项
- 使用 bufferevent_free() 函数,应用程序可以释放套接字缓冲区事件处理器占用的内存资源,包括套接字缓冲区、读写缓冲区、回调函数等。在释放套接字缓冲区事件处理器之前,应用程序应该确保已经停止所有的 I/O 操作,并且没有剩余的事件需要处理。
- 需要注意的是,如果套接字缓冲区事件处理器是使用 bufferevent_socket_new() 函数创建的,则在释放套接字缓冲区事件处理器之前,应用程序通常需要先关闭套接字。可以使用 bufferevent_free() 函数的前一个参数 bev 来访问套接字缓冲区事件处理器的套接字描述符,然后使用 close() 函数来关闭套接字。
完整的例子
服务端
#include <stdio.h>
#include <stdlib.h>
#include <event.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <event2/listener.h>
//读取数据
void read_cb(struct bufferevent *bev, void *ctx) {
char buf[128] = {0};
size_t ret = bufferevent_read(bev, buf, sizeof(buf));
if (ret < 0) {
printf("bufferevent_read error!\n");
} else {
printf("read %s\n", buf);
}
}
void event_cb(struct bufferevent *bev, short what, void *ctx) {
if (what & BEV_EVENT_EOF) {
printf("客户端下线\n");
bufferevent_free(bev); //释放bufferevent对象
} else {
printf("未知错误\n");
}
}
void listener_cb(struct evconnlistener *listener, evutil_socket_t fd, struct sockaddr *addr, int socklen, void *arg) {
printf("接受%d的连接\n", fd);
struct event_base *base = arg;
//针对已经存在的socket创建bufferevent对象
//事件集合(从主函数传递来)、fd(代表TCP连接)、BEV_OPT_CLOSE_ON_FREE(如果释放bufferevent对象则关闭连接)
struct bufferevent *bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
if (NULL == bev) {
printf("bufferevent_socket_new error!\n");
exit(1);
}
//给bufferevent设置回调函数
//bufferevent对象、读事件回调函数、写事件回调函数、其他事件回调函数、参数
bufferevent_setcb(bev, read_cb, NULL, event_cb, NULL);
//使能bufferevent对象
bufferevent_enable(bev, EV_READ);
}
// 常见步骤:
/*
socket
bind
listen
accept
*/
int main() {
//创建一个事件集合
struct event_base *base = event_base_new();
if (NULL == base) {
printf("event_base_new error\n");
exit(1);
}
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8000);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
//创建socket、绑定、监听、接受连接
//创建监听对象,在指定的地址上监听接下来的TCP连接
//事件集合、当有连接时调用的函数、回调函数参数、释放监听对象关闭socket|端口重复使用、监听队列长度、绑定信息
//返回值:监听对象
struct evconnlistener *listener = evconnlistener_new_bind(base, listener_cb, base,
LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, 10, (struct sockaddr *)&server_addr,sizeof(server_addr));
if (NULL == listener) {
printf("evconnlistener_new_bind error\n");
exit(1);
}
//监听集合中的事件
event_base_dispatch(base);
//释放两个对象
evconnlistener_free(listener);
event_base_free(base);
exit(0);
}
客户端:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
int main()
{
//创建socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("socket");
exit(1);
}
//发起连接请求
struct sockaddr_in server_info; //保存服务器的信息
bzero(&server_info, sizeof(server_info));
server_info.sin_family = AF_INET;
server_info.sin_port = htons(8000);
server_info.sin_addr.s_addr = inet_addr("127.0.0.1");
if (connect(sockfd, (struct sockaddr *)&server_info, sizeof(server_info)) == -1)
{
perror("connect");
exit(2);
}
char buf[1024] = {0};
while (1)
{
scanf("%s", buf);
if (send(sockfd, buf, strlen(buf), 0) == -1)
{
perror("send");
break;
}
if (!strcmp(buf, "bye"))
break;
bzero(buf, 1024);
}
close(sockfd);
return 0;
}
运行结果: