yuyi
Libevent 事件通知库
基本概念
可以基于不同的 I/O 通知方式(epoll, kqueue, select, poll等)来调度事件。
其通过事件循环的方式来不断地等待事件的发生,在事件发生时调用相应的回调函数。
下面直接 show code 不浪费时间
- 使用 libevent 创建一个事件循环
#include <event2/event.h>
int main() {
struct event_base *base;
base = event_base_new(); // 创建新的事件基础结构
if (!base) {
fprintf(stderr, "Could not initialize libevent!\n");
return 1;
}
event_base_dispatch(base); // 启动事件循环
event_base_free(base); // 释放事件基础结构
return 0;
}
由上面代码可知,使用 libevent
库时的必要代码。
- 创建事件基础
base
- 启动事件循环
event_base_dispatch
- 结束后释放事件基础
event_base_free
简单定时器事件示例
接下来写一个简单的定时器事件代码示例,每 2 秒会触发一次回调函数
#include <event2/event.h>
#include <stdio.h>
#include <unistd.h>
// 回调函数
void on_timeout(evutil_socket_t fd, short what, void *arg) {
printf("Timeout!\n");
}
int main() {
struct event_base *base = event_base_new();
struct event *timeout_event;
struct timeval tv = {2, 0}; // 2秒超时
// 创建定时器事件
timeout_event = event_new(base, -1, EV_TIMEOUT, on_timeout, NULL);
// evtimer_add 函数将定时器事件添加到事件循环后,libevent 会在 2 秒后触发该事件。 之后,定时器会自动重置,再次等待 2 秒后再次触发事件,以此循环。
evtimer_add(timeout_event, &tv);
event_base_dispatch(base);
event_free(timeout_event);
event_base_free(base);
return 0;
}
事件创建函数各参数释义
base
:libevent
的事件基础 (event_base),用来管理事件循环。- -1: 文件描述符。 因为定时器事件不依赖于文件描述符,所以这里设置为 -1。
EV_TIMEOUT
: 事件类型,表明这是一个定时器事件。on_timeout
: 回调函数,当定时器事件触发时会调用这个函数。NULL
: 用户数据指针,可以用来传递额外信息给回调函数。
传入不同的事件类型可以创建其他事件:
libevent
支持多种事件类型, 除了定时器事件(EV_TIMEOUT
),常见的还有:EV_READ
: 当文件描述符可读时触发。EV_WRITE
: 当文件描述符可写时触发。EV_SIGNAL
: 当接收到指定信号时触发。
总的来说,需要经过下面简单的几步。
1·创建事件基础 base
2·创建定时器事件 3·添加事件到 base
4·启动事件循环,并在结束后释放相关资源
简易 TCP 服务器
接着试着用 libevent 创建简单的 TCP 服务器。和之前的操作步骤类似,
#include <event2/event.h>
int main() {
struct event_base *base = event_base_new();
// sockaddr_in 结构体用于存储 IPv4 地址和端口信息
struct sockaddr_in sin = {0}; // 声明 sockaddr_in 结构体变量,将其初始化为全 0。
sin.sin_family = AF_INET;
// 设置 IP 地址为 0.0.0.0 (INADDR_ANY) 监听 0.0.0.0 表示监听所有可用的网络接口
sin.sin_addr.s_addr = htonl(0);
// htonl() 函数将主机字节序的无符号长整型转换为网络字节序
sin.sin_port = htons(5555); // 设置端口号为 5555
// 创建套接字: 使用 socket() 系统调用创建一个套接字,并设置套接字选项。
int listener_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listener_fd < 0) {
perror("socket");
return 1;
}
// 绑定地址: 使用 bind() 系统调用将套接字绑定到指定的地址和端口。
if (bind(listener_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0) {
perror("bind");
return 1;
}
// 监听连接: 使用 listen() 系统调用将套接字设置为监听模式
if (listen(listener_fd, 5) < 0) { // 监听套接字的监听队列设置为 5
perror("listen");
return 1;
}
// 有新连接到监听套接字时,操作系统会将监听套接字标记为 可读
struct event * listener_event = event_new(base, listener_fd, EV_READ | EV_PERSIST, on_accept, base);
event_add(listener_event, NULL);
event_base_dispatch(base);
// 释放资源
event_free(listener_event);
close(listener_fd); // 关闭监听套接字
event_base_free(base);
return 0;
}
当连接建立后,libevent 监听到 EV_READ 事件后,会调用回调函数 on_accept
处理。
bind() 和 listen() 函数是操作系统提供的系统调用,它们在内核中实现。 虽然具体的实现细节会因操作系统而异,但总体流程是类似的。
- bind() 函数:
参数:
sockfd: 要绑定的套接字描述符。
my_addr: 指向 sockaddr 结构体的指针,包含要绑定的地址和端口信息。
addrlen: my_addr 结构体的长度。
功能: 将一个本地地址与套接字绑定。
内部实现:
检查参数: 内核检查套接字描述符是否有效,以及地址信息是否合法。
查找可用端口: 如果 my_addr 中的端口号为 0,内核会自动分配一个可用的端口号。
绑定地址: 内核将 my_addr 中的地址和端口号与套接字关联起来。
更新套接字状态: 内核将套接字的状态更新为已绑定。- listen() 函数:
参数:
sockfd: 要监听的套接字描述符。
backlog: 监听队列的最大长度。
功能: 将一个已绑定的套接字设置为监听模式,准备接受连接请求。
内部实现:
检查参数: 内核检查套接字描述符是否有效,以及是否已绑定地址。
创建监听队列: 内核为套接字创建一个监听队列,用于存放等待接受的连接请求。
设置队列长度: 内核根据 backlog 参数设置监听队列的最大长度。
更新套接字状态: 内核将套接字的状态更新为监听模式。
总结下,创建监听事件需要先创建 sockaddr_in
结构题存储 IP 地址和端口信息,接着创建套接字并用bind
绑定套接字和地址端口。再接着,还需要使用 listen
来监听套接字。最后,还需要再对套接字创建 libevent
可用的监听事件,并添加到事件循环中。
好在 libevent
库提供了 evconnlistener
相关的方法可以简化这个过程,它封装了底层套接字和事件的操作。
#include <event2/event.h>
#include <event2/listener.h>
#include <event2/buffer.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
struct event_base *base = event_base_new();
// sockaddr_in 结构体用于存储 IPv4 地址和端口信息
struct sockaddr_in sin = {0}; // 声明 sockaddr_in 结构体变量,将其初始化为全 0。
sin.sin_family = AF_INET;
// 设置 IP 地址为 0.0.0.0 (INADDR_ANY) 监听 0.0.0.0 表示监听所有可用的网络接口
sin.sin_addr.s_addr = htonl(0);
// htonl() 函数将主机字节序的无符号长整型转换为网络字节序
sin.sin_port = htons(5555); // 设置端口号为 5555
struct evconnlistener *listener = evconnlistener_new_bind(base, on_accept, NULL,
LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, -1, (struct sockaddr*)&sin, sizeof(sin));
event_base_dispatch(base);
evconnlistener_free(listener);
event_base_free(base);
return 0;
}
监听队列的长度参数 backlog
,即内核可以容纳的等待接受的连接请求的最大数量。 -1 表示使用系统默认值。
evconnlistener_new_bind
: 创建并绑定一个 TCP 连接监听器。它会创建一个套接字,将其绑定到指定的地址和端口,并开始监听连接请求。当有新的连接请求到达时,它会调用用户提供的回调函数来处理连接。evconnlistener_new
: 创建一个未绑定的 TCP 连接监听器。这允许在创建监听器后手动设置套接字选项,然后使用 evconnlistener_bind
将其绑定到地址和端口。evconnlistener_bind
: 将一个未绑定的连接监听器绑定到指定的地址和端口。evconnlistener_set_error_cb
: 设置连接监听器发生错误时的回调函数。evconnlistener_free
: 释放连接监听器,并关闭底层套接字
下面是相应回调函数的代码
// 触发读事件时回调函数
void on_read(struct bufferevent *bev, void *ctx) {
char buffer[256];
int n;
// 从缓冲区中读数据
while ((n = bufferevent_read(bev, buffer, sizeof(buffer))) > 0) {
fwrite(buffer, 1, n, stdout);
// 写入数据到缓冲区
bufferevent_write(bev, buffer, n); // Echo back to client
}
}
// 连接成功时触发的回调函数
void on_accept(evconnlistener *listener, evutil_socket_t fd, struct sockaddr *address, int socklen, void *ctx) {
struct event_base *base = evconnlistener_get_base(listener);
// 创建 bufferevent 内置缓冲区的结构
struct bufferevent *bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
// 设置回调函数
bufferevent_setcb(bev, on_read, NULL, NULL, NULL);
// 开启读写事件触发回调
bufferevent_enable(bev, EV_READ | EV_WRITE);
}
...
libevent
会将网络套接字到达的数据存入到其建立的读缓冲区中并触发读会回调函数
当调用 bufferevent_write
时将尝试把数据写入写缓冲区,libevent
会先检查写入缓冲区是否有足够空间,接着尝试将数据发送到网络套接字,发送数据到系统为套接字创建的发送缓冲区成功后会再次检查写缓冲区是否有空间,如果有空间就会触发写回调函数。
🦧 知识扩展
在操作系统层面,网络套接字也有自己的缓冲区,用于处理数据的传输:
- 接收缓冲区: 操作系统为每个套接字维护一个接收缓冲区。当数据从网络到达时,首先被存储在这个缓冲区中。然后,libevent 将数据从接收缓冲区读取到 bufferevent 的读缓冲区中。
- 发送缓冲区: 操作系统为每个套接字维护一个发送缓冲区。当应用程序通过 bufferevent_write 写入数据时,数据首先被存储在 bufferevent 的写缓冲区中,然后 libevent 会尝试将这些数据写入操作系统的发送缓冲区。