1.什么是libevent
libevent 是一个开源的事件通知库,主要用于开发高性能的事件驱动应用程序,特别适用于网络服务器的开发。它提供了跨平台的抽象接口,使得开发人员可以利用事件驱动的方式处理网络和文件描述符的异步 I/O 操作。
主要特点和功能:
-
事件驱动的编程模型:
- libevent 提供了一个基于事件的编程模型,开发人员可以注册事件处理函数来响应不同类型的事件,如网络连接就绪、数据可读写、定时器超时等。
-
跨平台支持:
- libevent 可以在多种操作系统上运行,包括 Unix-like 系统(如 Linux、FreeBSD、MacOS 等)和 Windows。它通过底层的事件通知机制(如 epoll、kqueue、select 等)来实现高效的事件处理。
-
支持多种 I/O 模型:
- 支持边缘触发(ET)和水平触发(LT)两种 I/O 模型,可以根据应用程序的需求选择合适的模型来处理事件。
-
丰富的事件类型:
- 支持多种事件类型,包括网络套接字事件(如连接、接收、发送)、信号事件、定时器事件等,使得开发人员能够灵活处理各种异步 I/O 操作和定时任务。
-
高效的性能和资源管理:
- libevent 设计目标是提供高性能和低延迟的事件驱动处理,通过有效地利用操作系统提供的事件通知机制,尽可能地减少系统调用和资源消耗。
安装libevent:
在 Linux 系统中,可以通过包管理器安装 libevent。以下是一些常见的发行版安装 libevent 的命令:
Ubuntu/Debian:
bash
sudo apt-get install libevent-dev
CentOS/RHEL:
bash
sudo yum install libevent-devel
如果系统中没有预编译的包或者需要安装特定版本的 libevent,可以从官方网站下载源代码进行编译安装:
- 下载最新稳定版或指定版本的 libevent 源代码:
bash
wget https://github.com/libevent/libevent/releases/download/release-x.x.x-stable/libevent-x.x.x-stable.tar.gz
tar -zxvf libevent-x.x.x-stable.tar.gz
cd libevent-x.x.x-stable
#编译和安装:
./configure
make
sudo make install
2.相关操作函数
libevent 提供了一些常用的操作函数,用于注册事件、处理事件、管理事件循环等。以下是一些常用的函数及其详细说明:
2.1 event_base_new
cpp
struct event_base *event_base_new(void);
功能:创建一个新的事件处理器对象。
参数:无。
返回值:返回一个指向 struct event_base 的指针,表示创建的事件处理器对象。如果创建失败,返回 NULL。
注意事项:
该函数会根据系统平台选择合适的事件通知机制,如 epoll、kqueue、select 等。
创建的事件处理器对象是整个 libevent 应用程序的核心,负责管理事件、驱动事件循环等操作。
2.2 event_base_dispatch
cpp
int event_base_dispatch(struct event_base *base);
功能:进入事件循环,处理注册到事件处理器的所有事件。
参数:base 是通过 event_base_new 创建的事件处理器对象。
返回值:函数执行成功时返回 0,失败时返回 -1。
注意事项:
调用该函数后,libevent 将开始监听注册的事件,并调用相应的事件处理回调函数。
事件循环会一直运行,直到所有事件处理完成或者显式地退出事件循环。
2.3 event_new
cpp
struct event *event_new(struct event_base *base, evutil_socket_t fd, short events, event_callback_fn cb, void *arg);
功能:创建一个新的事件对象,并初始化事件的相关参数。
参数:
base:事件处理器对象,通过 event_base_new 创建。
fd:关联的文件描述符或套接字。
events:事件类型,可以是 EV_READ、EV_WRITE、EV_SIGNAL 等,用 | 运算符组合多个事件类型。
cb:事件触发时调用的回调函数。
arg:传递给回调函数的参数。
返回值:返回一个指向 struct event 的指针,表示创建的事件对象。如果创建失败,返回 NULL。
注意事项:
事件对象创建后,需要使用 event_add 函数将其注册到事件处理器中才能生效。
回调函数 cb 的类型为 void callback(evutil_socket_t fd, short events, void *arg),其中 fd 是触发事件的文件描述符或套接字,events 是触发的事件类型,arg 是传递的参数。
2.4 event_add
cpp
int event_add(struct event *ev, const struct timeval *timeout);
功能:将事件对象添加到事件处理器中,使其可以被监听和触发。
参数:
ev:要添加的事件对象,通过 event_new 创建。
timeout:可选参数,指定事件的超时时间,如果为 NULL 则表示无超时限制。
返回值:函数执行成功时返回 0,失败时返回 -1。
注意事项:
在调用 event_add 前,必须先通过 event_new 创建事件对象,并设置好事件的相关参数和回调函数。
可以使用 timeout 参数来设置事件的超时时间,超时后事件处理器会调用事件的回调函数并传递超时事件类型。
2.5 event_del
cpp
int event_del(struct event *ev);
功能:从事件处理器中删除事件对象,停止监听和触发该事件。
参数:ev 是要删除的事件对象。
返回值:函数执行成功时返回 0,失败时返回 -1。
注意事项:
调用 event_del 后,事件对象将不再被监听和触发。
删除事件对象后,如果不再需要,应当释放其内存资源,避免内存泄漏。
2.6 event_free()
cpp
void event_free(struct event *ev);
功能:释放由 event_new() 创建的事件对象,并释放其相关资源。
参数:ev:要释放的事件对象指针。
返回值:无返回值。
具体含义:
event_free() 函数用于释放事件对象 ev 所占用的内存资源,并取消与事件对象相关联的任何操作,包括注册的事件、回调函数等。
注意事项:
在释放事件对象之前,确保事件对象不再需要使用。
如果事件对象关联了定时器或者是持久性事件,event_free() 会取消这些事件,并释放相关的资源。
不应在事件处理回调函数中调用 event_free(),因为这可能导致未定义的行为或者内存访问问题。事件处理函数执行完毕后,由事件处理器负责释放相关资源。
2.7 event_base_free()
cpp
void event_base_free(struct event_base *base);
功能:释放由 event_base_new() 创建的事件处理器对象,并释放其相关资源。
参数:base:要释放的事件处理器对象指针。
返回值:无返回值。
具体含义:
event_base_free() 函数用于释放事件处理器对象 base 所占用的内存资源,并取消与事件处理器相关联的任何操作。
注意事项:
在释放事件处理器之前,确保事件处理器不再需要使用。
调用 event_base_free() 后,事件处理器对象 base 及其管理的所有事件对象和资源都将被释放。
应确保在程序退出前,调用 event_base_free() 释放事件处理器,避免内存泄漏和资源浪费。
3.libevent库的一般使用流程
1. 初始化 libevent
首先需要初始化 libevent 库,创建一个事件处理器(event base)。事件处理器负责管理和调度所有事件,包括网络 I/O、定时器等。初始化步骤通常包括:
- 调用
event_base_new()
创建一个新的事件处理器对象。 - 可以通过
event_base_config
进行配置,如设置日志、多线程支持等。
2. 创建并注册事件
一旦有了事件处理器,接下来是创建和注册各种事件。主要包括:
- 套接字事件:如 TCP 或 UDP 的读写事件。
- 信号事件:处理操作系统信号,如 SIGINT、SIGTERM 等。
- 定时器事件:用于定时执行某些操作。
事件的创建通常包括:
- 使用
event_new()
创建事件对象,并设置回调函数和事件类型。 - 调用
event_assign()
或类似函数将事件对象与事件处理器关联。
3. 设置和注册回调函数
每个事件都关联着特定的回调函数,用于事件发生时的处理逻辑。主要回调函数包括:
- 读写回调:处理套接字读写事件。
- 事件回调:处理特定事件发生时的逻辑,如连接建立、连接关闭等。
通过 event_set
或类似函数设置事件的回调函数,并将事件注册到事件处理器中。
4. 启动事件循环
一旦所有事件和回调函数都设置好并注册到事件处理器中,可以通过以下步骤启动事件循环:
- 调用
event_base_dispatch()
或event_base_loop()
启动事件处理循环。 - 事件处理器会不断监听注册的事件,一旦事件发生,就调用相应的回调函数处理事件。
5. 处理事件和清理资源
在事件循环中,处理器会根据事件类型不断调用相应的回调函数处理事件。处理的主要工作包括:
- 处理套接字数据读写。
- 处理信号事件,如处理程序终止信号。
- 处理定时器事件,执行定时任务。
同时,需要确保在程序退出时释放资源,包括:
- 调用
event_free()
释放事件对象。 - 调用
event_base_free()
释放事件处理器对象。
4.示例代码
使用libevent实现简单的TCP回显服务器
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <event2/event.h>
struct event *connev = NULL;
// 读取数据的回调函数
void readcb(evutil_socket_t fd, short events, void *arg)
{
int n;
char buf[1024];
memset(buf, 0x00, sizeof(buf));
n = read(fd, buf, sizeof(buf));
if (n <= 0)
{
close(fd);
if (connev)
{
// 将通信文件描述符对应的事件从base地基上删除
event_del(connev);
event_free(connev);
connev = NULL;
}
}
else
{
write(fd, buf, n);
}
}
// 新连接的回调函数
void conncb(evutil_socket_t fd, short events, void *arg)
{
struct event_base *base = (struct event_base *)arg;
// 接受新的客户端连接
struct sockaddr_storage client_addr;
socklen_t client_len = sizeof(client_addr);
int cfd = accept(fd, (struct sockaddr *)&client_addr, &client_len);
if (cfd > 0)
{
// 打印客户端连接信息
char client_ip[INET6_ADDRSTRLEN];
int client_port;
if (client_addr.ss_family == AF_INET)
{
struct sockaddr_in *s = (struct sockaddr_in *)&client_addr;
inet_ntop(AF_INET, &s->sin_addr, client_ip, sizeof(client_ip));
client_port = ntohs(s->sin_port);
}
else
{
struct sockaddr_in6 *s = (struct sockaddr_in6 *)&client_addr;
inet_ntop(AF_INET6, &s->sin6_addr, client_ip, sizeof(client_ip));
client_port = ntohs(s->sin6_port);
}
printf("Accepted connection from %s:%d\n", client_ip, client_port);
// 创建通信文件描述符对应的事件并设置回调函数为readcb
connev = event_new(base, cfd, EV_READ | EV_PERSIST, readcb, NULL);
if (connev == NULL)
{
fprintf(stderr, "Failed to create new event for client\n");
// 退出循环
event_base_loopexit(base, NULL);
return;
}
// 将通信文件描述符对应的事件添加到base事件处理器中
if (event_add(connev, NULL) == -1)
{
fprintf(stderr, "Failed to add event for client\n");
event_free(connev);
connev = NULL;
return;
}
}
}
int main()
{
// 创建socket
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1)
{
perror("socket");
return 1;
}
// 设置端口复用
int opt = 1;
if (setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1)
{
perror("setsockopt");
close(lfd);
return 1;
}
// 绑定
struct sockaddr_in serv;
memset(&serv, 0, sizeof(serv));
serv.sin_addr.s_addr = htonl(INADDR_ANY);
serv.sin_port = htons(8888);
serv.sin_family = AF_INET;
if (bind(lfd, (struct sockaddr *)&serv, sizeof(serv)) == -1)
{
perror("bind");
close(lfd);
return 1;
}
// 监听
if (listen(lfd, 120) == -1)
{
perror("listen");
close(lfd);
return 1;
}
// 创建事件处理器
struct event_base *base = event_base_new();
if (base == NULL)
{
fprintf(stderr, "Could not initialize libevent!\n");
close(lfd);
return 1;
}
// 创建监听文件描述符对应的事件
struct event *ev = event_new(base, lfd, EV_READ | EV_PERSIST, conncb, base);
if (ev == NULL)
{
fprintf(stderr, "Failed to create event for listening socket\n");
event_base_free(base);
close(lfd);
return 1;
}
// 将新创建的事件添加到base事件处理器中
if (event_add(ev, NULL) == -1)
{
fprintf(stderr, "Failed to add event for listening socket\n");
event_free(ev);
event_base_free(base);
close(lfd);
return 1;
}
// 进入事件循环
if (event_base_dispatch(base) == -1)
{
fprintf(stderr, "Failed to run event dispatch loop\n");
}
// 释放资源
event_free(ev);
event_base_free(base);
close(lfd);
return 0;
}
使用 telnet 或 nc 命令进行连接测试
bash
telnet <server_ip> <server_port>
nc <server_ip> <server_port>
nc localhost 8888
<server_ip>:服务器的 IP 地址,如果是本地可以使用 localhost 或 127.0.0.1。
<server_port>:服务器监听的端口号,例如 8888。