目录
(1) 初始化 libevent:(类似Reactor机制)
一. 什么是libevent库
Libevent 是一个轻量级的开源高性能网络库,主要用于简化事件驱动的网络编程。它封装了底层操作系统的 I/O 多路复用机制(如 epoll、kqueue、select 等),提供统一的 API,帮助开发者高效处理网络事件、定时器和信号。它支持/dev/poll、kqueue、event ports、select、poll和epoll事件机制,也因此它是一个跨操作系统的库(支持Linux、*BSD、Mac OS X、Solaris、Windows等)。
二. 安装libevent库
libevent官网:https://libevent.org/
我下载的是libevent-2.1.11-stable.tar.gz (一般推荐下载稳定版本Stable Releases,不推荐下最新的一版可能会有一点小bug)
如果用的Linux系统,安装方式如下:
(1)下载时,如果有sudo权限可以把下载的libevent库放到系统路径下(如/usr/bin),如果没有权限可以把下载的libevent库放到用户路径下(如/$HOME/local 自己文件路径)
(2) 在Linux系统上下载压缩包
wget https://github.com/libevent/libevent/releases/download/release-2.1.11-stable/libevent-2.1.11-stable.tar.gz
(3)解压
tar -xzf libevent-2.1.11-stable.tar.gz
(4)cd 到解压完成后的文件中
cd libevent-2.1.11-stable
(5)编译安装
没有sudo权限(需要改环境配置,因为你安装在自己的用户文件里,是不在默认的库路径下的,你需要自己添加,否则系统在使用库的时候只会去默认路径下找,会找不到你安装的库)
注意:我这里的环境配置是临时生效的,只在本机可以,如果别人也登录这个Linux系统是没有办法生效的,想永久生效用后面的(~/.bashrc
是用户级的 shell 配置文件)
./configure --prefix=$HOME/local # 指定用户目录
make
make install # 无需sudo
#环境配置(临时生效)
export PATH="$HOME/local/bin:$PATH"
export LD_LIBRARY_PATH="$HOME/local/lib:$LD_LIBRARY_PATH"
#环境配置(永久生效)
echo 'export LD_LIBRARY_PATH="$HOME/local/lib:$LD_LIBRARY_PATH"' >> ~/.bashrc
有sudo权限
sudo ./configure --prefix=/usr/local # 默认系统路径
sudo make
sudo make install # 需要sudo
三. ibevent 核心流程
(1) 初始化 libevent:(类似Reactor机制)
事件初始化:创建一个event_base
对象,这是 libevent 的基础,用于管理事件集合。
// 创建
struct event_base * event_base_new(void);
// 销毁释放
event_base_free(struct event_base *)
// 如果fork出子进程,想在子进程继续使用event_base,那么子进程需要对event_base重新初始化
int event_reinit(struct event_base *base);
底层实现:event_base
会自动检测当前系统支持的最佳 I/O 多路复用机制(如 epoll、kqueue 等),并初始化事件循环的上下文。Linux下一般会用epoll模型。
用下面这个命令可以获取IO模型的名称。
const char *event_base_get_method(const struct event_base *base);
什么是 Reactor?
定义:一种事件驱动的设计模式,用于处理并发 I/O 操作。
核心思想:
“不要主动等待事件,而是等事件通知你”。
将事件监听和事件处理分离,通过回调机制实现异步响应。
(2)事件注册:
event_base是事件的集合,负责事件的循环,以及集合的销毁。而event就是event_base中的基本单元:事件。
创建新的 event 结构,指定事件对应的文件描述符(fd)、事件类型(如读事件EV_READ
、写事件EV_WRITE
)、回调函数以及回调函数的参数等。然后通过event_add()
函数将事件添加到event_base
的事件队列中,此时事件进入等待激活状态。event_new生成的事件是在堆上分配的内存。
//创建一个事件event
struct event *event_new(struct event_base *base, evutil_socket_t fd,short what, event_callback_fn cb,void *arg);
//将事件添加到 event_base 的事件集合中
int event_add(struct event *ev, const struct timeval *tv);
当一个事件通过event_add被注册到event_base上的时候,这个事件处于pending(等待状态)
当只有有事件进来的时候,event才会被激活active状态,相关的回调函数就会被调用。
事件类型:
EV_READ
:监听 fd 可读事件(如客户端发送数据)。
EV_WRITE
:监听 fd 可写事件(如发送缓冲区可写入数据)。
EV_PERSIST
:持续监听事件(触发后不自动删除)。
- base:即event_base
- fd:文件描述符。
- what:event关心的各种条件。
- cb:回调函数。
- arg:用户自定义的数据,可以传递到回调函数中去。
event_assign ( ) 函数
event_new每次都会在堆上分配内存。有些场景下并不是每次都需要在堆上分配内存的,这个时候我们就可以用到event_assign方法。
已经初始化或者处于 pending 的 event,首先需要调用 event_del()删除这个事件,后再调用 event_assign()。这个时候就可以重用这个event了。
//函数会将事件删除,确保不再监听这个事件后续才能用event_assign()重新分配参数
int event_del(struct event *event);
// 此函数用于初始化 event(包括可以初始化栈上和静态存储区中的 event)
// event_assign() 和 event_new() 除了 event 参数之外,使用了一样的参数
// event 参数用于指定一个未初始化的且需要初始化的 event
// 函数成功返回 0 失败返回 -1
int event_assign(struct event *event, struct event_base *base,evutil_socket_t fd, short what,void (*callback)(evutil_socket_t, short, void *), void *arg);
// 类似上面的函数,此函数被信号 event 使用
event_assign(event, base, signum, EV_SIGNAL|EV_PERSIST, callback, arg)
(3)启动事件循环:循环监听
调用event_base_dispatch()函数启动事件循环,该函数内部实际调用event_base_loop()函数。在event_base_loop()中,会先检查是否有信号标记,如果有则调用信号的回调函数。接着根据定时器最小时间,设置 I/O 多路复用的最大等待时间,然后调用选定的 I/O 多路复用机制的dispatch函数(如 epoll 的epoll_dispatch)监听事件,将活跃事件添加到活跃事件链表中。同时,检查定时事件,将就绪的定时事件从小根堆中删除,插入到活跃事件链表中。
简单来说他类似于while(1)的功能,去循环监视事件的发生。
//调用该函数,相当于(相当于while(1) {epoll_wait})没有设置标志位的event_base_loop。
//程序将会一直运行,直到没有需要检测的事件了,或者被结束循环的api终止。
int event_base_dispatch(struct event_base *base);
//或者用
#define EVLOOP_ONCE 0x01
#define EVLOOP_NONBLOCK 0x02
#define EVLOOP_NO_EXIT_ON_EMPTY 0x04
int event_base_loop(struct event_base *base, int flags);
如果参数填了0,则只有事件进来的时候才会调用一次事件的回调函数,比较常用
EVLOOP_ONCE(0x01):执行单次事件循环:
- 处理所有就绪事件
- 若设置了超时事件,则等待到第一个超时事件触发
- 处理完所有就绪事件或超时事件后立即返回
EVLOOP_NONBLOCK(0x02):非阻塞模式:
- 检查是否有就绪事件,若无则立即返回
- 不等待任何超时事件,即使已设置超时
EVLOOP_NO_EXIT_ON_EMPTY(0x04):循环不退出:
- 即使没有活动事件(如无监听的 fd 或超时事件),也不退出事件循环
- 需手动调用
event_base_loopexit()
终止循环
退出循环监听
//两个函数的区别是如果正在执行激活事件的回调函数
//将在事件回调执行结束后终止循环(如果tv时间非NULL,那么将等待tv设置的时间
后立即结束循环)
int event_base_loopexit(struct event_base *base, const struct timeval *tv);
//立即终止循环
int event_base_loopbreak(struct event_base *base);
struct timeval {
long tv_sec;
long tv_usec;
(4)事件处理阶段
对于每个被激活的事件,libevent 会调用用户注册的回调函
回调函数执行完成后,根据事件类型决定是否重新添加该事件:
1.持久化事件(EV_PERSIST):不会自动移除
如果event_new中的what参数选择了EV_PERSIST,则是持久的类型。持久的类型调用完回调函数后,会继续转为pending状态,就会继续等待事件进来,后续相同类型的事件会继续触发回调。大部分情况下会选择持久类型的事件。
2. 非持久化事件:继续保持监听
事件触发(回调执行)后,就会变成初始化的状态。,会自动变成 non-pending(非等待)状态,相当于被 event_del()
了。 如果希望继续监听该事件,需要调用event_add 继续将事件注册到event_base上之后才能使用。
(5)资源释放阶段
最后结束时一定要释放资源,释放事件资源 和 event_base 资源
void event_free(struct event *event);
注意:
- 实际上,
write_cb()
是用来响应数据发送完成事件的,真正写入数据的操作是通过bufferevent_write()
完成的。
四. bufferevent
bufferevent 是 libevent 中提供的一种高级 I/O 抽象,建立在 evbuffer 之上,用于管理 带缓冲的事件驱动 I/O。它简化了异步 I/O 的管理,使得开发者在处理网络数据时可以更加方便地进行读取、写入和处理错误。bufferevent 背后通过结合eventloop事件循环、evbuffer 以及底层 I/O 操作,形成一个结构化的 I/O 流管理方式。
1.bufferevent功能:
- 异步 I/O 操作:支持异步读写操作,不会阻塞主线程。
- 双缓冲机制:分别维护读缓冲区和写缓冲区,读写操作使用独立的缓冲区 (evbuffer) 来管理数据。
- 自动管理事件:自动处理 I/O 事件,如数据可读、可写、错误处理等,无需手动管理底层事件。
- 读写超时:支持超时机制,用户可以设置读取或写入超时。
- 高级控制:提供读写暂停、调整缓冲区大小等高级功能。
2.bufferevent核心结构
(1)创建bufferevent
struct bufferevent *bufferevent_socket_new(struct event_base *base, evutil_socket_t fd, enum bufferevent_options options);
参数:
- base:即event_base
- fd:文件描述符。如果是socket的方法,则socket需要设置为非阻塞的模式。
- options:行为选项,下面是行为选项内容:
- BEV_OPT_CLOSE_ON_FREE :当 bufferevent 被释放同时关闭底层(socket 被关闭等) 一般用这个选项
- BEV_OPT_THREADSAFE :为 bufferevent 自动分配锁,这样能够在多线程环境中安全使用
- BEV_OPT_DEFER_CALLBACKS : 当设置了此标志,bufferevent 会延迟它的所有回调(参考前面说的延时回调)
- BEV_OPT_UNLOCK_CALLBACKS : 如果 bufferevent 被设置为线程安全的,用户提供的回调被调用时 bufferevent 的锁会被持有。如果设置了此选项,Libevent 将在调用你的回调时释放 bufferevent 的锁
(2)释放Bufferevent
void bufferevent_free(struct bufferevent *bev);
如果设置了延时回调BEV_OPT_DEFER_CALLBACKS,则释放会在延时回调调用了回调函数之后,才会真正释放。
(3)禁用或启动回调函数
可以启用或者禁用 bufferevent 上的 EV_READ(读)、EV_WRITE(写) 或者 EV_READ | EV_WRITE 事件。没有启用读取或者写入事件时, bufferevent 将不会试图进行数据读取或者写入。
//开启监听事件(读,写,)
void bufferevent_enable(
struct bufferevent *bufev,
short events
);
//禁用, 对应的回调就不会被调用
void bufferevent_disable(
struct bufferevent *bufev,
short events
);
short bufferevent_get_enabled(
struct bufferevent *bufev
);
例如像下面这样使用:
// 启用读取和写入事件
bufferevent_enable(bev, EV_READ | EV_WRITE);
// 禁用写入事件(仅读取)
bufferevent_disable(bev, EV_WRITE);
(4)设置bufferevent回调函数和相关设置
使用bufferevent之后,libevent会帮我们托管三种事件:
1、读取事件
2、写入事件
3、处理事件
这三种事件都在回调的函数结构中体现!!
//用于设置对应的具体的回调函数
void bufferevent_setcb(
struct bufferevent *bev,
bufferevent_data_cb readcb, // 读回调函数,这些都是自己定义的函数
bufferevent_data_cb writecb, // 写回调(缓冲区清空时触发)
bufferevent_event_cb eventcb, // 错误事件回调(如连接关闭、错误)
void *cbarg // 公用传输的传递
);
//可以用下面这个检查当前设置了哪些回调函数
void bufferevent_getcb(
struct bufferevent *bufev,
bufferevent_data_cb *readcb_ptr,
bufferevent_data_cb *writecb_ptr,
bufferevent_event_cb *eventcb_ptr,
void **cbarg_ptr
);
水位设置
相当于bufferevent内部缓冲区是一个2米深的水池,假设这个水池里已经有1米深的水了,低水位lowmark相当于进水口,高水位highmark相当于出水口,我现在想要达到1.5米深的水后进去游泳,1.5米就是低水位,游泳事件就是达到低水位后我执行的回调函数,水池加水最多只能加到2米深,超过2米会溢出水池,2米就是高水位标记。
可以批量处理数据:避免频繁回调,等数据积累到一定量再处理,防止缓冲区溢出
void bufferevent_setwatermark(
struct bufferevent *bufev, // bufferevent指针
short events, // 事件类型
size_t lowmark, // 低水位标记
size_t highmark // 高水位标记
);
(5)在基于套接字的bufferevent上启动连接
如果bufferevent的套接字还没有连接上,可以启动新的连接。
//如果连接启动成功,函数返回0;发生错误则返回-1
int bufferevent_socket_connect(
struct bufferevent *bev,
struct sockaddr *address,
int addrlen);
(6)操控bufferevent中的数据
注意:下面涉及到的evbuffer后面会讲
-
获取缓冲区函数:
//获取 bufferevent 的输入缓冲区(用于读取接收到的数据)
struct evbuffer *bufferevent_get_input(struct bufferevent *bufev);
//获取 bufferevent 的输出缓冲区(用于写入待发送的数据)
struct evbuffer *bufferevent_get_output(struct bufferevent *bufev);
-
写入函数:
bufferevent_write()从bufferevent的输出缓冲区添加(写)数据
将内存中从data处开始的size字节数据添加到输出缓冲区的末尾
bufferevent_write_buffer() 移除buf的所有内容,将其放置到输出缓冲区的末尾。
//都是返回值:成功0,失败-1
//向 bufferevent 的输出缓冲区写入原始数据。
int bufferevent_write(struct bufferevent *bufev,const void *data, size_t size);
//将数据从 evbuffer 写入到 bufferevent 的输出缓冲区。
int bufferevent_write_buffer(struct bufferevent* bufev,struct evbuffer* buf);
-
输出函数:
//从 bufferevent 的输入缓冲区读取数据。
size_t bufferevent_read(struct bufferevent *bufev, void *data, size_t size);
//将 bufferevent 输入缓冲区的数据读取到指定的 evbuffer中。
int bufferevent_read_buffer(struct bufferevent *bufev, struct evbuffer *buf);
五.Evbuffer
Evbuffer是 libevent 提供的高效内存缓冲区管理结构,用于网络编程中的数据缓存和流式处理,Evbuffer给我们提供了非常实用的IO缓存工具。
struct evbuffer
是 libevent 内部用于管理动态缓冲区的核心数据结构。
struct evbuffer{
u_char *buffer; // 当前有效缓冲区的内存起始地址
u_char *orig_buffer; // 整个分配(realloc)用来缓冲的内存起始地址
size_t misalign; // origin_buffer和buffer之间的字节数
size_t totallen; // 整个分配用来缓冲的内存字节数
size_t off; // 当前有效缓冲区的长度(字节数)
void (*cb)(struct evbuffer *, size_t, size_t, void *); //回到函数,当缓冲区有变化的时候会被调用
void *cbarg; //回调函数的参数
};
(1)创建和销毁Evbuffer
//创建新的 evbuffer
struct evbuffer *evbuffer_new(void)
//释放 evbuffer
void evbuffer_free(struct evbuffer *buf)
(2)向buffer中添加数据
libevent的缓冲是一个连续的内存区域,其处理数据的方式(写数据和读数据)更像一个队列操作方式:从后写入,从前读出。
//追加数据
//这个函数添加data处的datalen字节到buf的末尾,成功时返回0,失败时返回-1
int evbuffer_add(struct evbuffer *buf, const void *data, size_t datlen)
//格式化写入(类似 sprintf)
int evbuffer_add_printf(struct evbuffer *buf, const char *fmt, ...)
//(移动数据)将一个 evbuffer(源缓冲区)的全部内容移动到另一个 evbuffer(目标缓冲区),并清空源缓冲区。
//dst:目标缓冲区,src:源缓冲区
int evbuffer_add_buffer(struct evbuffer *dst, struct evbuffer *src)
//将外部内存区域的引用添加到 evbuffer,而非复制数据.当 evbuffer 不再需要该数据时,会调用清理函数释放资源。
//buf:目标缓冲区,将添加数据引用 data:指向外部数据的指针
int evbuffer_add_reference(struct evbuffer *buf, const void *data, size_t datlen, evbuffer_ref_cleanup_cb cleanupfn, void *arg)
下面这两个函数用于向 evbuffer
的头部插入数据
//将指定数据 (data) 插入到 evbuffer 的头部(首部)
//逆向evbuffer_add()
int evbuffer_prepend(struct evbuffer *buf, const void *data, size_t size);
//将 src 缓冲区的数据全部插入到 dst 缓冲区的头部
//逆向evbuffer_add_buffer
int evbuffer_prepend_buffer(struct evbuffer *dst, struct evbuffer* src);
(3)读取数据
//读取数据(并移除)
size_t evbuffer_remove(struct evbuffer *buf, void *data, size_t datlen)
//移动数据到另一个 buffer
int evbuffer_remove_buffer(struct evbuffer *src, struct evbuffer *dst, size_t datlen)
//零拷贝访问数据(直接获取指针)
evbuffer_peek(struct evbuffer *buffer, ev_ssize_t len, struct evbuffer_ptr *start_at, struct evbuffer_iovec *vec_out, int n_vec)
(4)缓冲区管理
//返回缓冲区当前数据量,返回的是buffer中的字节数
size_t evbuffer_get_length(const struct evbuffer *buf)
//丢弃前 len 字节
int evbuffer_drain(struct evbuffer *buf, size_t len)
//预分配内存(避免多次扩容)
//1.计算所需总空间:当前已用空间 + datlen。
//2.检查现有容量:如果当前容量已足够,则不进行任何操作。
//3.扩展缓冲区:
//若容量不足,尝试分配更大的内存块。
//通常会按指数方式增加容量(例如翻倍),以减少未来重新分配的次数。
//将原有数据复制到新的内存区域。
int evbuffer_expand(struct evbuffer *buf, size_t datlen)
(5)删除和移动buffer中的内容
//从buf前面复制和移除datlen字节到data处的内存中
//如果可用字节少于datlen,函数复制所有字节。失败时返回-1,否则返回复制了的字节数
int evbuffer_remove(struct evbuffer *buf, void *data, size_t datlen);
//将src中的所有数据移动到dst末尾,成功时返回0,失败时返回-1
int evbuffer_add_buffer(struct evbuffer *dst, struct evbuffer *src);
//从src中移动datlen字节到dst末尾,尽量少进行复制。如果字节数小于datlen,所有字节被移动。函数返回移动的字节数。
int evbuffer_remove_buffer(struct evbuffer *src, struct evbuffer *dst,
size_t datlen);
(6)搜索buffer中的内容
//是 libevent 提供的 缓冲区位置指针,用于在 evbuffer 中随机访问数据
//pos为偏移量,如果为-1则没查询到,大于-1,则搜索到了匹配的位置
//函数返回包含字符串位置,或者在没有找到字符串时包含-1的evbuffer_ptr结构体。
struct evbuffer_ptr {
ev_ssize_t pos;
struct {
/* internal fields */
} _internal;
};
//查找,在缓冲区中查找含有len个字符的字符串what
//如果提供了start参数,则从指定的位置开始搜索;否则,从开始处进行搜索。
struct evbuffer_ptr evbuffer_search(struct evbuffer *buffer, const char *what, size_t len, const struct evbuffer_ptr *start);
//和evbuffer_search差不多,只是它只考虑在end之前出现的what。
struct evbuffer_ptr evbuffer_search_range(struct evbuffer *buffer, const char *what, size_t len, const struct evbuffer_ptr *start, const struct evbuffer_ptr *end);
//用于在evbuffer中查找行终止符
struct evbuffer_ptr evbuffer_search_eol(struct evbuffer *buffer, struct evbuffer_ptr *start, size_t *eol_len_out, enum evbuffer_eol_style eol_style);
(7)面向行的读取
//从evbuffer前面取出一行,用一个新分配的空字符结束的字符串返回这一行
char *evbuffer_readln(struct evbuffer *buffer, size_t *n_read_out, enum evbuffer_eol_style eol_style);
enum evbuffer_eol_style {
EVBUFFER_EOL_ANY, //通用行结束符,支持 \n(LF)、\r\n(CRLF)、\r(CR)
EVBUFFER_EOL_CRLF, // \r\n 或者\n 视为行结束符,
EVBUFFER_EOL_CRLF_STRICT, //必须为 \r\n,否则视为无效
EVBUFFER_EOL_LF, //也就是\n,ASCII值是0x0A
EVBUFFER_EOL_NUL //将 \0(NULL 字符)视为行结束符。
};
(8)复制数据
//从缓冲区 头部 拷贝指定长度的数据到目标内存,不修改缓冲区内容。
ev_ssize_t evbuffer_copyout(struct evbuffer *buf, void *data, size_t datlen)
//从 指定位置(pos)拷贝数据到目标内存,不修改缓冲区内容
ev_ssize_t evbuffer_copyout_from(struct evbuffer *buf,const struct evbuffer_ptr *pos,void *data_out,size_t datlen);
正在学习中,如果有错敬请指出