Libevent 编程指南
Libevent 编程指南
事件 基本概念
Libevent是基于 Reactor 模式的网络库,在 Reactor 模式中,通常都有一个事件循环(Event Loop),在 Libevent 中,这个事件循环就是 event_base
结构体:
struct event_base *event_base_new(void); // 创建事件循环
void event_base_free(struct event_base *base); // 销毁事件循环
int event_base_dispatch(struct event_base *base); // 运行事件循环
通常来说,事件循环主要有两个作用:
- 用来管理事件,比如说添加我们感兴趣的事件,修改事件或删除事件。
- 用来轮询它管理的所有事件,如果发现有事件活跃 (avtive),就调用相应的回调函数去处理事件。
Libevent 使用 event
结构体来代表事件,可以使用 event_new()
创建一个事件
struct event *event_new(struct event_base *base, // 事件循环
evutil_socket_t fd, // 文件描述符
short what, // 事件类型
event_callback_fn cb, // 回调函数
void *arg); // 传递给回调函数的参数
创建一个事件之后,要怎么把事件加入到事件循环呢?可以使用 event_add()
函数
int event_add(struct event *ev, // 事件
const struct timeval *tv); // 超时时间
默认情况下,当一个事件变得活跃时,Libevent 会执行这个事件的回调函数,但同时也会将这个事件从事件循环中移除,
例如,下面的程序,定时器只会触发一次:
#include <event2/event.h>
#include <iostream>
#include <string>
void timer_cb(evutil_socket_t fd, short what, void *arg)
{
auto str = static_cast<std::string *>(arg);
std::cout << *str << std::endl;
}
int main()
{
std::string str = "Hello, World!";
auto *base = event_base_new(); // 事件循环
struct timeval five_seconds = {1, 0};
auto *ev = event_new(base, -1, EV_TIMEOUT, timer_cb, (void *)&str); // 事件
event_add(ev, &five_seconds); // 事件加入到事件循环
event_base_dispatch(base);
event_free(ev);
event_base_free(base);
return 0;
}
那要怎么样才能让事件不被移除呢?当创建事件时,在事件类型加上 EV_PERSIST
就可以。让我们修改上面的程序,让定时器每秒就触发一次
auto *ev = event_new(base, -1, EV_TIMEOUT|EV_PERSIST, timer_cb, (void *)&str);
TCP Server
Libevent 使用 evconnlistener
结构来表示 TCP Server,创建 TCP Server 的做法很简单:
struct evconnlistener *evconnlistener_new_bind(
struct event_base *base, // 事件循环
evconnlistener_cb cb, // 回调函数,当 accept() 成功时会被调用
void *arg, // 传递给回调函数的参数
unsigned flags, // 选项
int backlog, // tcp backlog 参数
const struct sockaddr *sa, // 地址
int socklen
);
void evconnlistener_free(struct evconnlistener *lev);
调用 evconnlistener_new_bind()
函数之后,listening socket 会自动被设置成非阻塞的。我们还通过 flags
参数设置一些有用的选项,例如:
LEV_OPT_CLOSE_ON_FREE
表示当调用evconnlistener_free()
时,相应的 listening socket 也会被close()
掉。LEV_OPT_REUSEABLE
表示会自动对 listening socket 设置SO_REUSEADDR
这个 TCP 选项。
下面的程序创建了一个简单的 TCP Server:
#include <event2/listener.h>
#include <arpa/inet.h>
#include <string.h>
#include <iostream>
void accept_conn_cb(struct evconnlistener *listener, evutil_socket_t fd,
struct sockaddr *address, int socklen, void *arg)
{
char addr[INET_ADDRSTRLEN];
auto *sin = reinterpret_cast<sockaddr_in *>(address);
inet_ntop(AF_INET, &sin->sin_addr, addr, INET_ADDRSTRLEN);
std::cout << "Accept TCP connection from: " << addr << std::endl;
}
void accept_error_cb(struct evconnlistener *listener, void *arg)
{
auto *base = evconnlistener_get_base(listener);
// 跨平台的错误处理
int err = EVUTIL_SOCKET_ERROR();
std::cerr << "Got an error on the listener: "
<< evutil_socket_error_to_string(err)
<< std::endl;
event_base_loopexit(base, NULL);
}
int main()
{
short port = 8000;
struct sockaddr_in sin;
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = htonl(INADDR_ANY);
sin.sin_port = htons(port);
auto *base = event_base_new();
auto *listener = evconnlistener_new_bind(
base, accept_conn_cb, nullptr,
LEV_OPT_CLOSE_ON_FREE|LEV_OPT_REUSEABLE, -1,
reinterpret_cast<struct sockaddr *>(&sin), sizeof(sin)
);
if (listener == nullptr) {
std::cerr << "Couldn't create listener" << std::endl;
return 1;
}
evconnlistener_set_error_cb(listener, accept_error_cb);
event_base_dispatch(base);
return 0;
}
Libevent 深入浅出
参考:【Gitbook】libevent深入浅出,本书要求有一定的服务并发编程基础
本教程要求有一定的服务并发编程基础,了解select和epoll等多路I/O复用机制。
教程目的主要是快速建立libevent的认知,了解libevent的常用数据结构和编程方法。
达到可以使用libevent写出自己的高并发服务器处理模型。
epoll
流 - IO操作 - 阻塞
Page not found
解决阻塞死等待的方法
Page not found
什么是epoll
Page not found
epollAPI
创建EPOLL
/**
* @param size 告诉内核监听的数目
*
* @returns 返回一个epoll句柄(即一个文件描述符)
*/
int epoll_create(int size);
...
int epfd = epoll_create(1000);

控制EPOLL
/**
* @param epfd 用epoll_create所创建的epoll句柄
* @param op 表示对epoll监控描述符控制的动作
*
* EPOLL_CTL_ADD(注册新的fd到epfd)
* EPOLL_CTL_MOD(修改已经注册的fd的监听事件)
* EPOLL_CTL_DEL(epfd删除一个fd)
*
* @param fd 需要监听的文件描述符
* @param event 告诉内核需要监听的事件
*
* @returns 成功返回0,失败返回-1, errno查看错误信息
*/
int epoll_ctl(int epfd, int op, int fd,
struct epoll_event *event);
struct epoll_event {
__uint32_t events; /* epoll 事件 */
epoll_data_t data; /* 用户传递的数据 */
}
/*
* events : {EPOLLIN, EPOLLOUT, EPOLLPRI,
EPOLLHUP, EPOLLET, EPOLLONESHOT}
*/
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event new_event;
new_event.events = EPOLLIN | EPOLLOUT;
new_event.data.fd = 5;
epoll_ctl(epfd, EPOLL_CTL_ADD, 5, &new_event);

触发模式
水平触发
水平触发的主要特点是,如果用户在监听epoll事件,当内核有事件的时候,会拷贝给用户态事件,但是如果用户只处理了一次,那么剩下没有处理的会在下一次epoll_wait再次返回该事件。
这样如果用户永远不处理这个事件,就导致每次都会有该事件从内核到用户的拷贝,耗费性能,但是水平触发相对安全,最起码事件不会丢掉,除非用户处理完毕。
边缘触发
边缘触发,相对跟水平触发相反,当内核有事件到达, 只会通知用户一次,至于用户处理还是不处理,以后将不会再通知。这样减少了拷贝过程,增加了性能,但是相对来说,如果用户马虎忘记处理,将会产生事件丢的情况。
简单的epoll服务器
epoll 和 reactor
event_base
创建默认的event_base
event_base_new() 函数分配并且返回一个新的具有默认设置的 event_base。函数会检测环境变量,返回一个到 event_base 的指针。如果发生错误,则返回 NULL。选择各种方法时,函数会选择 OS 支持的最快方法。
struct event_base *event_base_new(void);
大多数程序使用这个函数就够了。
event_base_new()函数声明在中,首次出现在 libevent 1.4.3版。
创建复杂的event_base
略
事件循环 event_loop
运行循环
一旦有了一个已经注册了某些事件的 event_base(关于如何创建和注册事件请看下一节 ), 就需要让 libevent 等待事件并且通知事件的发生。
#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);
……
libevent - 视频
参考: libevent 解决了网络编程中哪些痛点?|libevent 是什么?libevent 解决问题的逻辑?io 和事件的关系?libevent 实战中使...
libevent 解决了网络编程中的哪些痛点
FAQ
- libevent 是什么?
- libevent 解决问题的逻辑?io 和事件的关系
- 将一些东西当作事件来处理,如:
- C--connect-->S
- C--send-->S
- S--connect-->DB
- C<--send-->S<--send-->DB
- 将一些东西当作事件来处理,如:
- libevent 实战使用层次
- 代码编写:连接处理,数据发送,接收处理,连接断开处理
FAQ
- Q libevent 解决问题的逻辑?
- A libevent 是一个事件通知库,事件指的是:网络事件、超时时间、信号处理
- Q 网络io 和 事件的关系
- A libevent 也需要IO操作,分阻塞io 和 非阻塞io两种情况
- 阻塞IO:
int clientfd = accept(listenfd, addr, sz); // 通常看这里的fd,如果fd是阻塞的则这里会阻塞 int n = read(clientfd, buf, sz); connect(fd, addr, sz); int n = write(clientfd, buf, sz);
- 非阻塞IO:
libevent使用的非阻塞IO - 两者的区别
如果IO就绪,行为一样。非阻塞IO如果未就绪,立即返回。
阻塞IO会阻塞一个线程,一个线程只能监听一个。而非阻塞IO一个线程能多个。
至于如何监听事件,由libevent的IO多路复用
技术来做 - 事件处理流程
- 注册事件
- 检测事件,通过
IO多路复用
检测 - 触发执行事件,通过 callback,来处理 IO
- 经常要处理的是种fd:listenFd、clientFd、connectFd
代码
无事件demo
int main() {
struct event_base *base = event_base_new(); // 事件
event_base_loop(base, EVLOOP_NO_EXIT_ON_EMPTY); // 事件循环 (没有事件)
event_base_free(base); // 释放事件
return 0;
}
加个事件,经常要处理3种fd
- listenfd
- clientfd
- connectfd
# include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <event.h>
#include <event2/listener.h>
void listener_cb(struct evconnlistener * lis,
evutil_socket_t fd, struct sockaddr * sock, int socklen, void * ctx) {
printf("recv a connection\n");
}
int main() {
struct event_base *base = event_base_new(); // 事件管理器
// socket -> bind -> listen -> 注册读事件 提供回调函数
struct sockaddr_in sin = {0};
sin.sin_ family = AF_INET;
sin.sin_port = htons(8888);
struct evconnlistener *listener = evconnlistener_new_bind( // listenfd的事件
base,
listener_cb,
base,
LEV_OPT_REUSEABLE|LEV_OPT_CLOSE_ON_FREE,
256,
(struct sockaddr*)&sin,
sizeof(struct sockaddr_in)
);
event_base_loop(base, EVLOOP_NO_EXIT_ON_EMPTY); // 事件循环
event_base_free(base); // 释放事件
return 0;
}
视频2
参考:【B站】高并发+高可用+高负载 C++企业级网络开发实战 Libevent C++跨平台高级实战技术[2022]
前面
略
select、epoll、icop 区别
- 多路复用
- select
- 平台:跨平台
- 每次都要从用户空间拷贝到内核空间
- 遍历整个fd_set, 0(n)
- 最大可监听的fd数量不能超过FD_SETSIZE
- poll
- 平台:跨平台
- 类似于select,无FD_SETSISE限制
- epoll
- 平台:Windows不支持
- 内核态、红黑树
- 内存共享交互mmap
- 不需要全部复制、返回双向链表
- LT(level triggered) 水平触发。事件没有处理,一直触发。epoll_wiat
- ET(edge triggered) 边缘触发。每当状态变化时触发一个事件
- icop
- 平台:Linux不支持
- 线程池
- select
libevent是根据优先级来选用的,epoll -> poll -> select -> iocp
服务端接收连接的代码示例
视频3
【B站】【阶段四:Linux高并发服务器开发】2-5 libevent
event_base
/**
* @param flags:
* EVLOOP_ONCE 0x01,只触发一次,阻塞等待
* EVLOOP_NOBLOCK 0x02,非阻塞检测,立即返回
*/
int event_base_loop(struct event_base *base, int flags);
/**
* 更常用的一个api,相当于没有标志位的event_base_loop,相当于 `while(1){epoll_wait}`
* 程序会一直执行直到没有要检测的api
*/
int event_base_dispatch(struct event_base *base);
/**
* 退出循环监听(等待固定时间后)
*/
int event_base_loopexit(struct event_base *base, const struct timeval *tv*);
/**
* 退出循环监听(立即)
*/
int event_base_loopbreak(struct event_base *base);
libevent
libeventAPI
详见 看源码 一章
实战 - tcp服务器
这个是简化版,难一点的可以看官方sample文件夹里的demo event_tcp_server_array.c
简化版的流程一览:
- 套接字准备
- 创建套接字
- 绑定
- 监听
- 使用libevent
- 初始化event_base根节点
- 初始化上树节点
- 节点上树
- 监听
这个视频老师太喜欢简写了,我个人其实不是太喜欢简写过度
- l: listen 迎接套接字对应的
- c: client 服务套接字对应的(不是客户端套接字)
- fd:file descriptor
- cb:callback
- lfdcb: "l fd cb"
void cfdcb(int cfd, short event, void* arg){
char buf[1500] = "";
int n = Read(cfd, buf, sizeof(buf));
if(n<=0){
perror("err close\n");
// event_del(); // 下树,但这里会有问题,不知道树节点
}
else {
printf("");
}
}
void lfdcb(int lfd, short event, void* arg){ // 回调
struct event_base* base = (struct evnet_base*)arg;
int cfd = Accept(lfd, NULL, NULL); // 提取新的cfd
struct event *ev = event_new(base, lfd, EV_READ|EV_PERSIS, lfdcb, NULL); // 初始化上树节点
event_add(ev, NULL); // 将cfd上树
}
int main(int argc, char* argv[]){
// 创建套接字
// 绑定
int lfd = tcp4bind(8000, NULL);
// 监听
Listen(lfd, 128);
// 创建event_base根节点
struct event_base* base = event_base_new();
// 初始化lfd上树节点
struct event* ev = event_new(base, lfd, EV_READ|EV_PERSIST, lfd);
// 上树
event_add(ev, NULL);
// 循环监听
event_base_dispatch(base); // 阻塞
// 收尾
close(lfd);
event_free(ev); // 这行在视频代码里是没有的,但GPT说要这一步,而且官方demo也是有这一步的
event_base_free(base);
return 0;
}
实战 - tcp服务器(官方数组版)
下树对应的客户fd的问题,在官方demo里是这样实现的:
// 下树时的操作
event_del(getEventByFd(fd)); // 从数组中找到对应的
// 添加时的操作
struct event* readev = event_mew(base, cfd, EV_READ|EV_PERSIST, readcb, base);
event_add(readev, NULL);
addEvent(cfd, readev); // 添加到数组,该数组长度为 [_MAX_CLIENT_]
buffevent
事件介绍
普通event事件、高级event事件(buffevent)
核心:
- 一个文件描述符
- 两个缓冲区
- 三个回调
- 事件回调
- 出错
- 断开连接
读回调:底层的读缓存区数据拷贝到应用层缓冲区,会触发读回调
写数据:从应用层缓冲区将数据写入底层缓冲区会出发写回调
事件监听流程
buffeventAPI
新
/**
* 新增buffer事件
* @param base,根节点
* @param fd,文件描述符
* @param options, bufferevent的选项
* BEV_OPT_CLOSE_ON_FREE,释放bufferevent自动关闭底层接口 (即close fd)
* BEV_OPT_THREADSAFE,使bufferevent能够在多线程下是安全的 (即自动加锁)
*/
struct bufferevent* bufferevent_socket_new(struct event_base* base, evutil_socket_t fd, int options);
回调函数原型:
typedef
wath代表的事件:
BEV_EVENT_EOF,对方关闭连接
BEV_EVENT_ERROR,错误
BEV_EVENT_TIMEOUT,超时
BEV_EVENT_CONNECTED,建立连接成功
有三个回调函数:
- 读回调,当bufferevent将底层缓冲区的数据读到自身读缓冲区时触发
- 写回调,当bufferevent将自身读缓冲区的数据写到底层缓冲区时触发
- 事件回调,当bufferevent绑定的socket连接,断开或异常时触发
链接监听器
作用:创建、绑定、监听、提取 套接字
/**
* @param base,base 根节点
* @param cb,提取cfd后调用的回调
* @param ptr,传给回调的参数
* @param flags,标志,一般第一和第四个比较常用
* LEV_OPT_LEAVE_SOCKETS_BLOCKING,文件描述符为阻塞的
* LEV_OPT_CLOSE_ON_FREE,关闭时自动释放
* LEV_OPT_REUSEABLE,端口复用
* LEV_OPT_THREADSAFE,分配锁 线程安全
* @param backlog,-1
* @param sa,绑定的地址信息
* @param socklen,sa的大小
* @return 连接侦听器的地址
*/
struct evconnlistener* evconnlistener_new_bind(
struct event_base* base,
evconnlistener_cb cb,
void* ptr,
unsigned flags,
int backlog,
const struct sockaddr*si sa,
int socklen);
/**
* 与前一个函数的不同点在后两个参数,使用本函数时认为socket已经初始化、bind、listen好了
*/
struct evconnlistener* evconnlistener_new(
struct event_base* base,
evconnlistener_cb cb,
void* ptr,
unsigned flags,
int backlog,
evutil_socket_t fd);
/**
* 回调函数
* @param evl,链接侦听器的地址
* @param fd,cfd
* @param cliaddr,客户端的地址信息
* @param ptr,evconnlistener_new_bind 传过来的参数
*/
typedef void(*evconnlistener_cb)(
struct evconnlistener* evl,
evutil_socket_t fd,
struct sockaddr* cliaddr,
int socklen,
void* ptr);