libevent 梳理

C10K 背景

C10K问题 :如何在一台物理机上同时服务 10000 个用户?这里 C 表示并发,10K 等于 10000?

C10K 问题本质上是一个操作系统问题,需要考虑:

1.文件句柄数:每个客户连接都代表一个文件描述符,一旦文件描述符不够用了,新的连接就会被放弃(可以用 root 权限修改 /etc/sysctl.conf 文件);

2.系统内存:假设每个连接需要 128K 的缓冲区,那么 1 万个链接就需要大约 1.2G 的应用层缓冲;

3.网络带宽:假设 1 万个连接,每个连接每秒传输大约 1KB 的数据,那么带宽需要 10000 x 1KB/s x8 = 80 Mbps。

C10K 问题解决之道

要想解决 C10K 问题,就需要从两个层面上来统筹考虑:

1.应用程序如何和操作系统配合,感知 I/O 事件发生,并调度处理在上万个套接字上的 I/O 操作?

2.应用程序如何分配进程、线程资源来服务上万个连接?

这两个层面的组合就形成了解决 C10K 问题的几种解法方案:
1.阻塞 I/O + 进程

每个连接通过 fork 派生一个子进程进行处理,因为一个独立的子进程负责处理了该连接所有的 I/O,所以即便是阻塞 I/O,多个连接之间也不会互相影响。但是这种方法效率不高,扩展性差,资源占用率高。
2.阻塞 I/O + 线程

进程模型占用的资源太大,还有一种轻量级的资源模型,这就是线程。通过为每个连接调用 pthread_create 创建一个单独的线程,也可以达到上面使用进程的效果。

因为线程的创建是比较消耗资源的,况且不是每个连接在每个时刻都需要服务,因此,可以预先通过创建一个线程池,并在多个连接中复用线程池来获得某种效率上的提升。
3.非阻塞 I/O + readiness notification + 单线程

应用程序其实可以采取轮询的方式来对保存的套接字集合进行挨个询问,从而找出需要进行 I/O 处理的套接字。(select/poll 需要每次 dispatch 之后,对所有注册的套接字进行逐个排查,效率并不是最高的,epoll 在 dispatch 调用返回之后只提供有 I/O 事件或者 I/O 变化的套接字)
4.非阻塞 I/O + readiness notification + 多线程

前面的做法是所有的 I/O 事件都在一个线程里分发,如果我们把线程引入进来,可以利用现代 CPU 多核的能力,让每个核都可以作为一个 I/O 分发器进行 I/O 事件的分发。这就是所谓的主从 reactor 模式。基于 epoll/poll/select 的 I/O 事件分发器可以叫做 reactor,也可以叫做事件驱动,或者事件轮询(eventloop)。

5.异步 I/O+ 多线程

异步非阻塞 I/O 模型是一种更为高效的方式,当调用结束之后,请求立即返回,由操作系统后台完成对应的操作,当最终操作完成,就会产生一个信号,或者执行一个回调函数来完成 I/O 处理。这就涉及到了 Linux 下的 aio 机制。

Linux网络编程 - C10K问题:高并发模型的设计初篇

libevent 简介

libevent 是一个轻量级的开源的高性能的事件触发的网络库,适用于 windows、linux、bsd 等多种平台,内部使用 select、epoll、kqueue 等系统调用管理事件机制。

1.跨平台支持,支持Windows、Linux、BSD和Mac OS;

2.统一事件源,libevent 对 I/O 事件、信号和定时事件提供统一的处理;

3.线程安全,libevent 使用 libevent_pthreads 库来提供线程安全支持。

4.基于 reactor 模式的实现。

libevent API 提供了一种机制,用于在文件描述符上发生特定事件或达到超时后执行回调函数。此外,libevent还支持由于信号或常规超时而导致的回调。

libevent 旨在替换在事件驱动的网络服务器中找到的事件循环。应用程序只需要调用 event_dispatch(),然后动态添加或删除事件,而无需更改事件循环。

libevent 功能

Libevent 提供了事件通知,io 缓存事件,定时器,超时,异步解析 dns,事件驱动的 http server 以及一个 rpc 框架。

  • 事件通知:当文件描述符可读可写时将执行回调函数。
  • IO缓存:缓存事件提供了输入输出缓存,能自动的读入和写入,用户不必直接操作IO。
  • 定时器:libevent 提供了定时器的机制,能够在一定的时间间隔之后调用回调函数。
  • 信号:触发信号,执行回调。
  • 异步的 dns 解析:libevent 提供了异步解析 dns 服务器的 dns 解析函数集。
  • 事件驱动的 http 服务器:libevent 提供了一个简单的,可集成到应用程序中的 HTTP 服务器。
  • RPC 客户端服务器框架:libevent 为创建 RPC 服务器和客户端创建了一个 RPC 框架,能自动的封装和解封数据结构。

总体架构

事件处理框架 - event_base

使用 libevent函数之前需要分配一个或者多个 event_base 结构体。每个event_base 结构体持有一个事件集合,可以检测以确定哪个事件是激活的。每个 event_base 都有一种用于检测哪种事件已经就绪的 "方法"。

event_base API函数

cpp 复制代码
// 头文件
#include <event2/event.h>
// 操作函数
struct event_base * event_base_new(void);          //创建事件处理框架
void event_base_free(struct event_base * base);    //释放事件处理框架

// 检查event_base的后端方法
const char** event_get_supported_methods(void);
const char *event_base_get_method(const struct event_base *base);

event_base 和 fork(进程)关系:

1.子进程创建成功之后,父进程可以继续使用 event_base;

2.子进程中 event_base 也会被复制,使用时需要用下面函数重新初始化:

cpp 复制代码
int event_reinit(struct event_base* base);

事件循环

event_base 不停的检测委托的检测是实际是不是发生了,如果发生了,event_base 会调用对应的回调函数,这个回调函数的用户委托检测事件的时候给的。

设置事件循环

如果委托了 event_base 检测某些事件,不停的进行循环检测;

结束检测时间:所有要检测的事件都被触发,并且处理完毕。

cpp 复制代码
// 头文件
#include <event2/event.h>

// 操作函数
#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);
	参数:
		- base: 通过 event_base_new(void)得到的
		- flags:
			- EVLOOP_ONCE: 一直检测某个事件, 当事件被触发了, 停止事件循环
			- EVLOOP_NONBLOCK: 非阻塞的方式检测, 当事件被触发了, 停止事件循环
			- EVLOOP_NO_EXIT_ON_EMPTY: 一直进行事件检测, 如果没有要检测的事件, 不退出

int event_base_dispatch(struct event_base* base); 	// 一般使用这个函数
	参数:
		- base: 通过 event_base_new(void)得到的

终止事件循环

cpp 复制代码
// 头文件
#include <event2/event.h>

struct timeval {
	long    tv_sec;                    
	long    tv_usec;    // 微秒        
};

// 在 tv 时长之后退出循环, 如果这个参数为空NULL, 直接退出事件循环
// 事件循环: 检测对应的事件是否被触发了
// 如果事件处理函数正在被执行, 执行完毕之后才终止
int event_base_loopexit(struct event_base * base, const struct timeval * tv);

// 马上终止
int event_base_loopbreak(struct event_base * base);

事件

事件基本操作

事件的创建 event_new

cpp 复制代码
//要检测事件   what:
#define EV_TIMEOUT 	0x01
#define EV_READ 	0x02
#define EV_WRITE 	0x04
#define EV_SIGNAL 	0x08
#define EV_PERSIST 	0x10	// 修饰某个事件是持续触发的
#define EV_ET 		0x20	// 边沿模式

//回调函数格式:
typedef void (*event_callback_fn)(evutil_socket_t,short,void *);
参数:
	- 第一个参数: event_new的第二个参数
	- 第二个参数: 实际触发的事件
	- 第三个参数: event_new的最后一个参数

// 创建事件
struct event* event_new(struct event_base * base,evutil_socket_t fd,
     					 short what,event_callback_fn cb,void * arg);
参数:
	- base: event_base_new得到的
	- fd: 文件描述符, 检测这个fd对应的事件
	- what: 监测fd的什么事件 
	- cb: 回调函数, 当前检测的事件被触发, 这个函数被调用
	- arg: 给回调函数传参

事件的释放

cpp 复制代码
// 释放事件资源
void event_free(struct event * event);

事件的添加、删除

事件被 new 出之后, 不能直接被 event_base 进行检测,event_add 之后 event_base 就可以对事件进行检测

cpp 复制代码
int  event_add(struct event * ev,const  struct timeval * tv);
参数: tv-> 超时时间, 
      如果这个值> 0, 比如 == 3
      检测fd的读事件, 在三秒之内没有触发该事件 -> 超时-> 超时之后, 事件对应的回调函数会被强制调用
      如果该参数为NULL, 不会做超时检测
  
// 删除要检测的事件
int  event_del(struct event * ev);

事件的优先级设置

cpp 复制代码
// EVENT_MAX_PRIORITIES == 256     最大的初始化事件优先级
int event_base_priority_init(struct event_base * base,int n_priorities);
参数:
	- n_priorities: 等级的个数, 假设 == 6
      也就是说有6个等级: 0,1,2,3,4,5, 0优先级最高
// 获取当前可用的等的个数
int event_base_get_npriorities(struct event_base * base);
// 给事件设置等级
int event_priority_set(struct event *event, int priority);
参数:
	- event: 创建的事件
	- priority: 要设置的等级

带缓冲区的事件

  1. bufferevent 理解:

    (1)是 libevent 为 IO 缓冲区操作提供的一种通用机制;

    (2)bufferevent 由一个底层的传输端口(如套接字),一个读取缓冲区和一个写入缓冲区组成。

    (3)与通常的事件在底层传输端口已经就绪,可以读取或者写入的时候执行回调不同的是,bufferevent 在读取或者写入了足够量的数据之后调用用户提供的回调。

  2. 每个 bufferevent 有两个数据相关的回调

    (1)读取回调 :从底层传输端口读取了任意量的数据之后会调用读取回调(默认);

    (2)写入回调:输出缓冲区中足够量的数据被清空到底层传输端口后写入回调会被调用(默认)。

  • 创建/释放基于套接字的 bufferevent bufferevent_socket_new
cpp 复制代码
struct bufferevent *bufferevent_socket_new(
  	struct event_base *base,
  	evutil_socket_t fd,
  	enum bufferevent_options options
  ); 
参数:
	- base: 处理事件的
	- fd: 通信的文件描述符
	- options: BEV_OPT_CLOSE_ON_FREE -> 自动释放底层资源
返回值: 得到带缓冲区的事件变量
  
// 释放资源
void bufferevent_free(struct bufferevent *bev);
  • bufferevent 上启动连接服务器函数 bufferevent_socket_connect
    (1)如果还没有为bufferevent 设置套接字,调用函数将为其分配一个新的流套接字,并且设置为非阻塞的;
    (2)如果已经为 bufferevent 设置套接字,调用bufferevent_socket_connect() 将告知 libevent 套接字还未连接,直到连接成功之前不应该对其进行读取或者写入操作;
    (3)连接完成之前可以向输出缓冲区添加数据。
cpp 复制代码
int bufferevent_socket_connect(struct bufferevent *bev, struct sockaddr *address, int addrlen); 
参数:
	- bev: 带缓冲区的事件, 里边封装 fd
	- address: 要连接的服务器的IP和端口
	- addrlen: address结构体的内存大小

bufferevent 读写缓冲区回调操作 bufferevent_setcb

cpp 复制代码
//读、写事件触发之后的回调函数格式
typedef void (*bufferevent_data_cb)(struct bufferevent *bev, void *ctx);
参数:
	- bev: 从bufferevent_setcb函数中的第一个参数传入的
	- ctx: 从bufferevent_setcb函数中的最后第一个参数传入的
 
//特殊事件的回调函数格式 		
typedef void (*bufferevent_event_cb)(struct bufferevent *bev, short events, void *ctx);
参数:
	- bev: 从bufferevent_setcb函数中的第一个参数传入的
	- events: 可以检测到的事件
		EV_EVENT_READING:读取操作时发生某事件,具体是哪种事件请看其他标志。
		BEV_EVENT_WRITING:写入操作时发生某事件,具体是哪种事件请看其他标志。
		BEV_EVENT_ERROR:操作时发生错误。关于错误的更多信息,请调用 EVUTIL_SOCKET_ERROR()。
		BEV_EVENT_TIMEOUT:发生超时。
		BEV_EVENT_EOF:遇到文件结束指示。
		BEV_EVENT_CONNECTED:请求的连接过程已经完成 
 
void bufferevent_setcb(struct bufferevent *bufev, 
                       bufferevent_data_cb readcb, 		
                       bufferevent_data_cb writecb, 
                       bufferevent_event_cb eventcb, void *cbarg
);
参数:
	- bufev: 带缓冲区的事件
	- readcb: 读事件触发之后的回调函数
	- writecb: 写事件触发之后的回调函数
	- eventcb: 特殊事件的回调函数
	- cbarg: 给回调函数传参
  • 禁用、启用缓冲区

可以启用或者禁用 bufferevent 上的 EV_READ、EV_WRITE 或者 EV_READ | EV_WRITE 事件。 没有启用读取或者写入事件时,bufferevent 将不会试图进行数据读取或者写入。

  • 写缓冲区默认是有效的,读缓冲区默认无效
cpp 复制代码
// 设置某个事件有效
void bufferevent_enable(struct bufferevent *bufev, short events); 

// 设置某个事件无效
void bufferevent_disable(struct bufferevent *bufev, short events);

// 获取缓冲区对应的有效事件
short bufferevent_get_enabled(struct bufferevent *bufev);
  • 操作bufferevent中的数据 bufferevent_write bufferevent_read
cpp 复制代码
// 向bufferevent的输出缓冲区添加数据
  int bufferevent_write(struct bufferevent *bufev, const void *data, size_t size);
  
// 从bufferevent的输入缓冲区移除数据
  size_t bufferevent_read(struct bufferevent *bufev, void *data, size_t size);

链接监听器

  • 创建和释放 evconnlistener
cpp 复制代码
#include <event2/listener.h> 
 
//回调函数格式
 typedef void (*evconnlistener_cb)(
 			struct evconnlistener *listener,   
 			evutil_socket_t sock,   
 			struct sockaddr *addr, 
 			int len, 
 			void *ptr
 ); 
参数:
	- listener: evconnlistener_new_bind 返回的地址
	- sock: 用于通信的fd
	- addr: 客户端的地址信息
	- ptr: 外部传进来的参数, evconnlistener_new_bind的第三个参数
  		
 
// 创建监听的套接字, 绑定, 设置监听, 等待并接受连接请求
struct evconnlistener *evconnlistener_new_bind(
			struct event_base *base,    
			evconnlistener_cb cb,	        // 接受新连接之后的回调函数
			void *ptr,                      // 回调函数参数
			unsigned flags, 
			int backlog,                   // listen()中的第二参数,最多的监听数量,小于128的整数
			const struct sockaddr *sa,     // 本地的IP和端口
			int socklen				   // struct sockaddr结构体大小
);
参数:
	- flags:
		LEV_OPT_CLOSE_ON_FREE: 自动关闭底层套接字
		LEV_OPT_REUSEABLE: 设置端口复用
// 释放
void evconnlistener_free(struct evconnlistener *lev); 
  • 启用和禁用 evconnlistener

设置无效之后, 就不监听连接请求了

cpp 复制代码
  #include <event2/listener.h> 
  int evconnlistener_disable(struct evconnlistener *lev);
  int evconnlistener_enable(struct evconnlistener *lev);
  • 调整 evconnlistener 的回调函数
cpp 复制代码
#include <event2/listener.h>
void evconnlistener_set_cb(struct evconnlistener *lev, evconnlistener_cb cb, void *arg);

总结

处理不带缓冲区的事件:

  • 创建事件处理框架event_base event_base_new()
  • 创建新事件event event_new()
  • 将事件添加到事件处理框架event_baseevent_add()
  • 启动事件循环检测 event_base_dispatch()
  • 循环结束之后释放资源 event_base_free()event_free()

处理带缓冲区的事件:

1、创建事件处理框架event_base event_base_new()

2、服务器端:

  • 创建连接监听器(在回调函数得到fdevconnlistener_new_bind()
  • 将通信fd包装 bufferevent_socket_new()
  • 使用bufferevent通信:给bufferevent读写缓冲区设置回调函数 bufferevent_setcb()
  • 设置读缓冲区可用 bufferevent_enable()
  • 对缓冲区数据操作 bufferevent_write()bufferevent_read()

3、客户端:

  • 创建通信用的fd并且使用 bufferevent 包装 bufferevent_socket_new()
  • 连接服务器 bufferevent_socket_connect()
  • 使用bufferevent通信:给 bufferevent 读写缓冲区设置回调函数 bufferevent_setcb()
  • 设置读缓冲区可用 bufferevent_enable()
  • 对缓冲区数据操作 bufferevent_write()bufferevent_read()

一般流程

创建event 注册到 event base 中 监听事件的发生 将event添加到激活队列中 遍历激活队列,调用event的回调函数

libevent 使用步骤

  1. 创建socket
  2. 创建事件集 event_base
  3. 创建event(socket, EV_READ, callback1) / event(socket, EV_WRITE, callback2)
  4. event添加到事件集event_base
  5. event_base_dispatch(evnet_base); event_base_loop();

注 意:

程序的最后调用event_base_dispatch(base);实现事件的循环处理

cpp 复制代码
while()
{
	//调用多路复用
	event_base_dispatch(base);
    ...
}

一篇文章搞懂Libevent网络库的原理与应用l
Linux c 开发 - libevent
libevent源码解读------简单工作流程

libevent 解决了网络编程哪些痛点?

1.高效的网络缓冲区

在内核中有读缓冲区和写缓冲区,减少用户态和内核态的切换。

用户态读缓冲区的存在是为了处理粘包的问题,因为网络协议栈是不知道用户界定数据包的格式,没法确定一个完整的数据包。

用户态写缓冲区的存在是因为用户根本不清楚内核写缓存区的状态,需要把没有写出去的数据缓存起来等待下次写事件时把数据写出去。

buffer 的设计有三种类型:

(1)固定数组,固定长度。限定了处理数据包的能力,没有动态伸缩的能力;需要频繁挪动数据。

(2)ring buffer。可伸缩性差。

(3)chain buffer。解决可伸缩性差的问题,避免频繁挪动数据;同时也引进了新的问题,一个数据可能在多个buffer 中都有,即数据分割,这会导致多次系统调用,从而引起中断上下文的切换。解决办法是使用 readv() 将内核中连续的 buffer 读到用户态不连续的 buffer 中,writev() 把用户态不连续的 buffer 写到内核连续的 buffer 中;从而减少系统调用。

2.IO函数使用与网络原理

(1)有了 libevent 可以不使用IO函数。因为如果使用IO函数,既需要知道这些IO函数里面的系统调用返回值的含义。

(2)有了 libevent 就可以不清楚数据拷贝原理。

(3)有了 libeven t就可以不清楚网络原理以及网络编程流程。

(4)有了 libevent 只需要知道事件处理,IO操作完全交由 libevent 处理。

3.多线程

加锁的效果比较好。

一个线程尽量只处理一个 reactor 的事件。

(1)buffer 加锁时,读要读出一个完整的数据包。

(2)buffer 加锁时,写要写一个完整的数据包。

深入理解libevent事件库的原理与实践技巧

相关推荐
小皮侠2 分钟前
nginx的使用
java·运维·服务器·前端·git·nginx·github
Maki Winster33 分钟前
在 Ubuntu 下配置 oh-my-posh —— 普通用户 + root 各自使用独立主题(共享可执行)
linux·运维·ubuntu
守望时空3335 分钟前
Linux下KDE桌面创建自定义右键菜单
linux
l0sgAi1 小时前
vLLM在RTX50系显卡上部署大模型-使用wsl2
linux·人工智能
ddfa12342 小时前
XML 笔记
xml·服务器
海外空间恒创科技2 小时前
一台香港原生ip站群服务器多少钱?
服务器·网络协议·tcp/ip
Charlene Fung2 小时前
vs code远程自动登录服务器,无需手动输入密码的终极方案(windows版)
运维·服务器·vscode·ssh
麟城Lincoln2 小时前
【RHCSA-Linux考试题目笔记(自用)】servera的题目
linux·笔记·考试·rhcsa
碣石潇湘无限路2 小时前
【部署与总结】从本地运行到公网服务器的全过程
运维·服务器
linux修理工2 小时前
ipmitool 使用简介(ipmitool sel list & ipmitool sensor list)
运维·服务器