libevent之bufferevents
cnblogs 2024-06-26 16:09:00 阅读 53
目录
- Bufferevents:概念和基础知识
- Bufferevents 和 evbuffers
- 回调和水印
- 延迟回调
- 缓冲区事件的选项标志
- 使用基于套接字的缓冲区事件
- 创建基于套接字的缓冲区事件
- 在基于套接字的缓冲区事件上启动连接
- 按主机名启动连接
- 通用 bufferevent 操作
- 释放缓冲区事件
- 操作回调、水印和启用的操作
- 操作缓冲区事件中的数据
- 读写超时
- 在缓冲区事件上启动刷新
- 特定于类型的 bufferevent 函数
- 手动锁定和解锁缓冲区事件
- 过时的缓冲区事件功能
- Bufferevents:高级主题
- 配对缓冲区事件
- 筛选缓冲区事件
- 限制最大单次读/写大小
- 缓冲事件和速率限制
- 速率限制模型
- 设置缓冲事件的速率限制
- 为一组缓冲区事件设置速率限制
- 检查当前速率限制值
- 手动调整速率限制
- 在速率受限的组中设置尽可能小的份额
- 速率限制实现的局限性
- Bufferevents 和 SSL
- 设置和使用基于 OpenSSL 的缓冲区事件
- 关于线程和 OpenSSL 的一些说明
Bufferevents:概念和基础知识
大多数情况下,应用程序希望执行一定数量的数据 除了仅响应事件之外,还进行缓冲。当我们想要的时候 写入数据,例如,通常的模式运行如下:
- 决定我们要将一些数据写入连接;把那个 缓冲区中的数据。
- 等待连接变为可写
- 尽可能多地写入数据
- 记住我们写了多少,如果我们还有更多的数据要写, 等待连接再次变为可写。
这种缓冲 IO 模式很常见,以至于 Libevent 提供了一个 它的通用机制。“缓冲事件”由 底层传输(如套接字)、读取缓冲区和写入 缓冲区。而不是常规事件,这些事件在 基础传输已准备好读取或写入,即 BufferEvent 在读取或写入足够多的内容时调用其用户提供的回调 数据。
有多种类型的缓冲事件,它们都共享一个共同点 接口。在撰写本文时,存在以下类型:
基于套接字的缓冲事件
从基础发送和接收数据的缓冲事件 stream socket,使用 event_* 接口作为其后端。
异步 IO 缓冲区事件
使用 Windows IOCP 接口发送和 将数据接收到基础流套接字。(仅限 Windows; 实验性的。
筛选缓冲区事件
之前处理传入和传出数据的缓冲事件 将其传递给基础 BufferEvent 对象,例如,传递给 压缩或翻译数据。
配对缓冲事件
两个相互传输数据的缓冲事件。
注意
从 Libevent 2.0.2-alpha 开始,这里的 bufferevents 接口仍然是 在所有 BufferEvent 类型中不完全正交。换言之, 并非下面描述的每个接口都适用于所有 BufferEvent 类型。 Libevent 开发人员打算在未来的版本中纠正此问题。
另请注意
缓冲区事件目前仅适用于面向流的协议,如 TCP。 将来可能会支持面向数据报的协议,如 UDP。
本节中的所有函数和类型都在 event2/bufferevent.h文件。与 evbuffers 特别相关的函数包括 在 event2/buffer.h 中声明;有关以下方面的信息,请参阅下一章 那些。
Bufferevents 和 evbuffers
每个缓冲区事件都有一个输入缓冲区和一个输出缓冲区。这些是 类型为“struct evbuffer”。当您有数据要写入时 buffer事件,将其添加到输出缓冲区;当 bufferevent 具有 数据供您读取,请将其从输入缓冲区中排出。
evbuffer 接口支持多种操作;我们讨论它们 后面的部分。
回调和水印
每个 bufferevent 都有两个与数据相关的回调:一个读取回调 和写入回调。默认情况下,读取回调被调用 每当从基础传输中读取任何数据时,写入 每当输出缓冲区中的足够数据被清空时,就会调用回调 基础传输。您可以覆盖这些函数的行为 通过调整 bufferevent 的读写“水印”。
每个缓冲事件都有四个水印:
读取低水位线
每当发生离开 bufferevent 的输入缓冲区的读取时 在此级别或更高级别,将调用 BufferEvent 的 Read 回调。 默认为 0,因此每次读取都会产生读取回调被调用。
读取高水位线
如果 bufferevent 的输入缓冲区达到此级别,则 buffer事件停止读取,直到从输缓冲区,再次将我们带到它下方。默认为无限制,所以 我们永远不会因为输入缓冲区的大小而停止读取。
写下低水位线
每当发生将我们带到此级别或更低级别的写入时,我们 调用写入回调。默认值为 0,因此写入回调 除非清空输出缓冲区,否则不会调用。
写高水位线
不直接由缓冲区事件使用,此水印可以具有特殊的 这意味着当 bufferevent 用作 另一个 BufferEvent。请参阅下面有关筛选缓冲区事件的说明。
bufferevent 还具有 “error” 或 “event” 回调,该回调将 调用以告知应用程序有关非面向数据的事件,例如 当连接关闭或发生错误时。以下事件 标志的定义如下:
BEV_EVENT_READING
在对 bufferevent 执行读取操作期间发生事件。看 其他标志是哪个事件。
BEV_EVENT_WRITING
在对 bufferevent 执行写入操作期间发生事件。看 其他标志是哪个事件。
BEV_EVENT_ERROR
bufferevent 操作期间发生错误。查看更多 有关错误的信息,请调用 EVUTIL_SOCKET_ERROR()。
BEV_EVENT_TIMEOUT
缓冲事件的超时已过期。
BEV_EVENT_EOF
我们在 bufferevent 上得到了文件结束指示。
BEV_EVENT_CONNECTED
我们在 bufferevent 上完成了请求的连接。
(上述事件名称在 Libevent 2.0.2-alpha 中是新增的。
延迟回调
默认情况下,在以下情况下会立即执行 bufferevent 回调 发生相应的情况。(evbuffer 也是如此 回调也是如此;我们稍后会谈到这些。此即时调用 当依赖关系变得复杂时,可能会造成麻烦。例如,假设 有一个回调,当数据增长时,它会将数据移动到 evbuffer A 中 empty,以及另一个从 evbuffer A 处理数据的回调 它长得很饱满。由于这些调用都发生在堆栈上,因此 如果依赖项变得足够讨厌,则可能会面临堆栈溢出的风险。
为了解决这个问题,你可以告诉 bufferevent(或 evbuffer)它的 回调应该被推迟。当满足条件时 延迟回调,而不是立即调用它,而是排队 作为 event_loop() 调用的一部分,并在常规事件之后调用。 回调。
(延迟回调是在 Libevent 2.0.1-alpha 中引入的。
缓冲区事件的选项标志
在创建 bufferevent 时,可以使用一个或多个标志来更改其 行为。识别的标志包括:
BEV_OPT_CLOSE_ON_FREE
释放缓冲事件后,关闭基础传输。 这将关闭底层套接字,释放底层 bufferevent 等。
BEV_OPT_THREADSAFE
自动为 bufferevent 分配锁,以便它 可从多个线程安全使用。
BEV_OPT_DEFER_CALLBACKS
设置此标志后,bufferevent 会延迟其所有回调, 如上所述。
BEV_OPT_UNLOCK_CALLBACKS
默认情况下,当 bufferevent 设置为线程安全时, 每当任何用户提供 调用回调。设置此选项可使 Libevent 发布 bufferEvent 调用回调时的锁定。
(Libevent 2.0.5-beta 于 BEV_OPT_UNLOCK_CALLBACKS 年推出。其他选项 以上是 Libevent 2.0.1-alpha 中的新功能。
使用基于套接字的缓冲区事件
要使用的最简单的缓冲区事件是基于套接字的类型。一个 基于 socket 的 bufferevent 使用 Libevent 的底层事件机制来 检测底层网络套接字何时准备好读取和/或写入 操作,并使用底层网络调用(如 readv、writev、 WSASend 或 WSARecv) 来传输和接收数据。
创建基于套接字的缓冲区事件
可以使用以下方法创建基于套接字的缓冲区事件 bufferevent_socket_new():
接口
struct bufferevent *bufferevent_socket_new(
struct event_base *base,
evutil_socket_t fd,
enum bufferevent_options options);
base 是 event_base,options 是 bufferevent 的位掩码 选项(BEV_OPT_CLOSE_ON_FREE等)。fd 参数是一个 套接字的可选文件描述符。如果出现以下情况,您可以将 fd 设置为 -1 您希望稍后设置文件描述符。
提示 | [确保您提供给bufferevent_socket_new的套接字是 在非阻塞模式下。Libevent 提供了方便的方法 evutil_make_socket_nonblocking为此。 |
---|---|
此函数在成功时返回 bufferevent,在失败时返回 NULL。
bufferevent_socket_new() 函数是在 Libevent 2.0.1-alpha 中引入的。
在基于套接字的缓冲区事件上启动连接
如果 bufferevent 的套接字尚未连接,则可以启动新的 连接。
接口
int bufferevent_socket_connect(struct bufferevent *bev,
struct sockaddr *address, int addrlen);
address 和 addrlen 参数与标准调用相同 connect() 中。如果 bufferevent 尚未设置套接字, 调用此函数会为其分配一个新的流套接字,并使 它不阻塞。
如果 bufferevent 确实已经有套接字,则调用 bufferevent_socket_connect() 告诉 Libevent 套接字不是 连接,并且在套接字上不执行任何读取或写入操作,直到 连接操作已成功。
在连接之前将数据添加到输出缓冲区是可以的 做。
如果连接已成功启动,则此函数返回 0,并且 -1 如果发生错误。
例
#include <event2/event.h>
#include <event2/bufferevent.h>
#include <sys/socket.h>
#include <string.h>
void eventcb(struct bufferevent *bev, short events, void *ptr)
{
if (events & BEV_EVENT_CONNECTED) {
/* We're connected to 127.0.0.1:8080. Ordinarily we'd do
something here, like start reading or writing. */
} else if (events & BEV_EVENT_ERROR) {
/* An error occured while connecting. */
}
}
int main_loop(void)
{
struct event_base *base;
struct bufferevent *bev;
struct sockaddr_in sin;
base = event_base_new();
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = htonl(0x7f000001); /* 127.0.0.1 */
sin.sin_port = htons(8080); /* Port 8080 */
bev = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE);
bufferevent_setcb(bev, NULL, NULL, eventcb, NULL);
if (bufferevent_socket_connect(bev,
(struct sockaddr *)&sin, sizeof(sin)) < 0) {
/* Error starting connection */
bufferevent_free(bev);
return -1;
}
event_base_dispatch(base);
return 0;
}
bufferevent_socket_connect() 函数是在 libevent-2.0.2-alpha。 在此之前,您必须手动调用 connect() 在您的套接字上,以及连接的时间 完成后,BufferEvent 会将其报告为写入。
请注意BEV_EVENT_CONNECTED,只有在启动 使用 bufferevent_socket_connect() 尝试 connect()。如果您调用 connect() 时,连接会报告为写入。
如果您想自己调用 connect(),但仍然收到一个 BEV_EVENT_CONNECTED 事件 连接成功时,调用 bufferevent_socket_connect(bev, NULL, 0) 后 connect() 返回 -1 和 errno 等于 EAGAIN 或 EINPROGRESS。
此函数是在 Libevent 2.0.2-alpha 中引入的。
按主机名启动连接
很多时候,您希望将解析主机名和连接到主机名结合起来 变成一个操作。有一个接口:
接口
int bufferevent_socket_connect_hostname(struct bufferevent *bev,
struct evdns_base *dns_base, int family, const char *hostname,
int port);
int bufferevent_socket_get_dns_error(struct bufferevent *bev);
此函数解析 DNS 名称主机名,查找 family 类型的地址。(允许的家庭类型为 AF_INET、AF_INET6 和 AF_UNSPEC。如果 名称解析失败,它会使用错误事件调用事件回调。 如果成功,它会像bufferevent_connect一样启动连接尝试 愿意。
dns_base 参数是可选的。如果它为 NULL,则 Libevent 会阻塞 等待名称查找完成,这通常不是您想要的。如果 它提供了,然后 Libevent 使用它异步查找主机名。 有关 DNS 的更多信息,请参阅第 R9 章。
与 bufferevent_socket_connect() 一样,此函数告诉 Libevent 任何 BufferEvent 上的现有套接字未连接,并且没有读取或写入 应该在套接字上完成,直到解析完成并连接 操作成功。
如果发生错误,则可能是 DNS 主机名查找错误。你可以找到 通过调用找出最近的错误是什么 bufferevent_socket_get_dns_error()。如果返回的错误代码为 0,则没有 DNS 检测到错误。
示例:简单的 HTTP v0 客户端。
/* Don't actually copy this code: it is a poor way to implement an
HTTP client. Have a look at evhttp instead.
*/
#include <event2/dns.h>
#include <event2/bufferevent.h>
#include <event2/buffer.h>
#include <event2/util.h>
#include <event2/event.h>
#include <stdio.h>
void readcb(struct bufferevent *bev, void *ptr)
{
char buf[1024];
int n;
struct evbuffer *input = bufferevent_get_input(bev);
while ((n = evbuffer_remove(input, buf, sizeof(buf))) > 0) {
fwrite(buf, 1, n, stdout);
}
}
void eventcb(struct bufferevent *bev, short events, void *ptr)
{
if (events & BEV_EVENT_CONNECTED) {
printf("Connect okay.\n");
} else if (events & (BEV_EVENT_ERROR|BEV_EVENT_EOF)) {
struct event_base *base = ptr;
if (events & BEV_EVENT_ERROR) {
int err = bufferevent_socket_get_dns_error(bev);
if (err)
printf("DNS error: %s\n", evutil_gai_strerror(err));
}
printf("Closing\n");
bufferevent_free(bev);
event_base_loopexit(base, NULL);
}
}
int main(int argc, char **argv)
{
struct event_base *base;
struct evdns_base *dns_base;
struct bufferevent *bev;
if (argc != 3) {
printf("Trivial HTTP 0.x client\n"
"Syntax: %s [hostname] [resource]\n"
"Example: %s www.google.com /\n",argv[0],argv[0]);
return 1;
}
base = event_base_new();
dns_base = evdns_base_new(base, 1);
bev = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE);
bufferevent_setcb(bev, readcb, NULL, eventcb, base);
bufferevent_enable(bev, EV_READ|EV_WRITE);
evbuffer_add_printf(bufferevent_get_output(bev), "GET %s\r\n", argv[2]);
bufferevent_socket_connect_hostname(
bev, dns_base, AF_UNSPEC, argv[1], 80);
event_base_dispatch(base);
return 0;
}
bufferevent_socket_connect_hostname() 函数是 Libevent 中的新功能 2.0.3-阿尔法;bufferevent_socket_get_dns_error() 是 2.0.5-beta 中的新功能。
通用 bufferevent 操作
本节中的函数适用于多个缓冲区事件 实现。
释放缓冲区事件
接口
void bufferevent_free(struct bufferevent *bev);
此函数释放缓冲区事件。缓冲区事件在内部 reference-counted,因此如果 bufferevent 有待处理的延迟 回调 当你释放它时,它不会被删除,直到回调 都完成了。
但是,bufferevent_free() 函数会尝试释放 Buffer事件。如果有待处理的数据要写入 BufferEvent,它可能不会在 BufferEvent 之前刷新 释放。
如果设置了 BEV_OPT_CLOSE_ON_FREE 标志,并且此 bufferevent 具有 套接字或与之关联的底层缓冲事件作为其传输, 释放 BufferEvent 时,该传输将关闭。
此函数是在 Libevent 0.8 中引入的。
操作回调、水印和启用的操作
接口
typedef void (*bufferevent_data_cb)(struct bufferevent *bev, void *ctx);
typedef void (*bufferevent_event_cb)(struct bufferevent *bev,
short events, void *ctx);
void bufferevent_setcb(struct bufferevent *bufev,
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_setcb() 函数更改一个或多个回调 缓冲事件。readcb、writecb 和 eventcb 函数是 当读取足够多的数据时,调用(分别),当读取足够的数据时 写入,或事件发生时。每个参数的第一个参数是 发生事件的 bufferEvent。最后一个参数是 用户在 cbarg 参数中提供的值 bufferevent_callcb():你可以用它来将数据传递给你的 回调。事件回调的 events 参数是一个位掩码 事件标志:请参阅上面的“回调和水印”。
您可以通过传递 NULL 而不是回调来禁用回调 功能。请注意,bufferevent 上的所有回调函数都共享 单个 cbarg 值,因此更改它将影响所有值。
您可以通过传送 bufferevent 来检索当前设置的回调 指向 bufferevent_getcb() 的指针,该指针将 readcb_ptr 设置为当前读取 回调,writecb_ptr当前写入回调,*eventcb_ptr 当前事件回调,以及 *cbarg_ptr 当前回调参数 田。任何设置为 NULL 的指针都将被忽略。
bufferevent_setcb() 函数是在 Libevent 1.4.4 中引入的。类型 名称“bufferevent_data_cb”和“bufferevent_event_cb”在 Libevent 中是新的 2.0.2-阿尔法。bufferevent_getcb() 函数是在 2.1.1-alpha 中添加的。
接口
void bufferevent_enable(struct bufferevent *bufev, short events);
void bufferevent_disable(struct bufferevent *bufev, short events);
short bufferevent_get_enabled(struct bufferevent *bufev);
您可以启用或禁用事件 EV_READ、EV_WRITE 或 EV_READ|EV_WRITE在缓冲事件上。当阅读或写作不是 启用后,BufferEvent 将不会尝试读取或写入数据。
当输出缓冲区为空时,无需禁用写入: BufferEvent 自动停止写入,然后再次重新启动 当有数据要写入时。
同样,当输入缓冲区 达到其高水位线:缓冲事件自动停止 读取,并在有阅读空间时重新启动。
默认情况下,新创建的 bufferevent 已启用写入功能,但未启用 读数。
您可以调用 bufferevent_get_enabled() 来查看当前正在发生的事件 在 BufferEvent 上启用。
这些函数是在 Libevent 0.8 中引入的,但 bufferevent_get_enabled(),在 2.0.3-alpha 版本中引入。
接口
void bufferevent_setwatermark(struct bufferevent *bufev, short events,
size_t lowmark, size_t highmark);
bufferevent_setwatermark() 函数调整读取水印, 写入单个 BufferEvent 的水印,或同时写入水印。(如果EV_READ 在事件字段中设置,则调整读取的水印。如果 EV_WRITE在事件字段中设置,则会调整写入水印。
高水位线 0 等同于“无限”。
此函数在 Libevent 1.4.4 中首次公开。
例
#include <event2/event.h>
#include <event2/bufferevent.h>
#include <event2/buffer.h>
#include <event2/util.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
struct info {
const char *name;
size_t total_drained;
};
void read_callback(struct bufferevent *bev, void *ctx)
{
struct info *inf = ctx;
struct evbuffer *input = bufferevent_get_input(bev);
size_t len = evbuffer_get_length(input);
if (len) {
inf->total_drained += len;
evbuffer_drain(input, len);
printf("Drained %lu bytes from %s\n",
(unsigned long) len, inf->name);
}
}
void event_callback(struct bufferevent *bev, short events, void *ctx)
{
struct info *inf = ctx;
struct evbuffer *input = bufferevent_get_input(bev);
int finished = 0;
if (events & BEV_EVENT_EOF) {
size_t len = evbuffer_get_length(input);
printf("Got a close from %s. We drained %lu bytes from it, "
"and have %lu left.\n", inf->name,
(unsigned long)inf->total_drained, (unsigned long)len);
finished = 1;
}
if (events & BEV_EVENT_ERROR) {
printf("Got an error from %s: %s\n",
inf->name, evutil_socket_error_to_string(EVUTIL_SOCKET_ERROR()));
finished = 1;
}
if (finished) {
free(ctx);
bufferevent_free(bev);
}
}
struct bufferevent *setup_bufferevent(void)
{
struct bufferevent *b1 = NULL;
struct info *info1;
info1 = malloc(sizeof(struct info));
info1->name = "buffer 1";
info1->total_drained = 0;
/* ... Here we should set up the bufferevent and make sure it gets
connected... */
/* Trigger the read callback only whenever there is at least 128 bytes
of data in the buffer. */
bufferevent_setwatermark(b1, EV_READ, 128, 0);
bufferevent_setcb(b1, read_callback, NULL, event_callback, info1);
bufferevent_enable(b1, EV_READ); /* Start reading. */
return b1;
}
操作缓冲区事件中的数据
从网络读取和写入数据对您没有好处,如果您不能看它。Bufferevents 为您提供了这些方法来提供它们 要写入的数据,以及要读取的数据:
接口
struct evbuffer *bufferevent_get_input(struct bufferevent *bufev);
struct evbuffer *bufferevent_get_output(struct bufferevent *bufev);
这两个函数是非常强大的基本函数:它们返回 分别输入和输出缓冲器。有关所有的完整信息 可以对 EVPud 类型执行的操作,请参见下一篇 章。
请注意,应用程序只能从输入中删除(而不是添加)数据 缓冲区,并且只能向输出缓冲区添加(而不是删除)数据。
如果写入缓冲事件由于数据太少而停止 (或者如果读取因太多而停滞不前),然后将数据添加到 输出缓冲区(或从输入缓冲区中删除数据)将 自动重新启动它。
这些函数是在 Libevent 2.0.1-alpha 中引入的。
接口
int bufferevent_write(struct bufferevent *bufev,
const void *data, size_t size);
int bufferevent_write_buffer(struct bufferevent *bufev,
struct evbuffer *buf);
这些函数将数据添加到 bufferevent 的输出缓冲区。叫 bufferevent_write() 将内存中的**大小字节添加到 输出缓冲区的末尾。调用 bufferevent_write_buffer() 删除 BUF 的全部内容,并将它们放在输出的末尾 缓冲区。如果成功,两者都返回 0,如果发生错误,则返回 -1。
这些函数从 Libevent 0.8 开始就已经存在了。
接口
size_t bufferevent_read(struct bufferevent *bufev, void *data, size_t size);
int bufferevent_read_buffer(struct bufferevent *bufev,
struct evbuffer *buf);
这些函数从 bufferevent 的输入缓冲区中删除数据。这 bufferevent_read() 函数从输入中删除最大size的字节 缓冲区,将它们存储在data存储器中。它返回数字 实际删除的字节数。bufferevent_read_buffer() 函数 排出输入缓冲区的全部内容并将它们放入 BUF;成功时返回 0,失败时返回 -1。
请注意,使用 bufferevent_read(),数据中的内存块必须 实际上有足够的空间来容纳大小字节的数据。
bufferevent_read() 函数自 Libevent 0.8 以来就已存在; bufferevent_read_buffer() 是在 Libevent 2.0.1-alpha 中引入的。
例
#include <event2/bufferevent.h>
#include <event2/buffer.h>
#include <ctype.h>
void
read_callback_uppercase(struct bufferevent *bev, void *ctx)
{
/* This callback removes the data from bev's input buffer 128
bytes at a time, uppercases it, and starts sending it
back.
(Watch out! In practice, you shouldn't use toupper to implement
a network protocol, unless you know for a fact that the current
locale is the one you want to be using.)
*/
char tmp[128];
size_t n;
int i;
while (1) {
n = bufferevent_read(bev, tmp, sizeof(tmp));
if (n <= 0)
break; /* No more data. */
for (i=0; i<n; ++i)
tmp[i] = toupper(tmp[i]);
bufferevent_write(bev, tmp, n);
}
}
struct proxy_info {
struct bufferevent *other_bev;
};
void
read_callback_proxy(struct bufferevent *bev, void *ctx)
{
/* You might use a function like this if you're implementing
a simple proxy: it will take data from one connection (on
bev), and write it to another, copying as little as
possible. */
struct proxy_info *inf = ctx;
bufferevent_read_buffer(bev,
bufferevent_get_output(inf->other_bev));
}
struct count {
unsigned long last_fib[2];
};
void
write_callback_fibonacci(struct bufferevent *bev, void *ctx)
{
/* Here's a callback that adds some Fibonacci numbers to the
output buffer of bev. It stops once we have added 1k of
data; once this data is drained, we'll add more. */
struct count *c = ctx;
struct evbuffer *tmp = evbuffer_new();
while (evbuffer_get_length(tmp) < 1024) {
unsigned long next = c->last_fib[0] + c->last_fib[1];
c->last_fib[0] = c->last_fib[1];
c->last_fib[1] = next;
evbuffer_add_printf(tmp, "%lu", next);
}
/* Now we add the whole contents of tmp to bev. */
bufferevent_write_buffer(bev, tmp);
/* We don't need tmp any longer. */
evbuffer_free(tmp);
}
读写超时
与其他事件一样,如果某个事件,则可以调用超时 时间过去了,没有任何数据成功 由 bufferevent 写入或读取。
接口
void bufferevent_set_timeouts(struct bufferevent *bufev,
const struct timeval *timeout_read, const struct timeval *timeout_write);
将超时设置为 NULL 应该会删除它;然而在 Libevent 之前 2.1.2-alpha 这不适用于所有事件类型。(作为解决方法 旧版本,您可以尝试将超时设置为多天间隔 和/或让你的 eventCB 函数忽略BEV_TIMEOUT事件,而你不这样做 想要他们。
如果 bufferevent 在尝试读取数据时至少等待 timeout_read 秒,则将触发读取超时。写入 如果 bufferevent 在尝试写入数据时至少等待 timeout_write 秒,则将触发超时。
请注意,只有当 bufferevent 想要时,超时才会计算在内 读取或写入。换言之,如果出现以下情况,则不会启用读取超时 在 bufferevent 上禁用读取,或者如果输入缓冲区已满,则读取被禁用 (在其高水位线)。同样,未启用写入超时 如果写入被禁用,或者没有要写入的数据。
当发生读写超时时,相应的读写 在 BufferEvent 上禁用操作。然后事件回调是 使用任一 BEV_EVENT_TIMEOUT|BEV_EVENT_READING 或 BEV_EVENT_TIMEOUT|BEV_EVENT_WRITING。
此函数自 Libevent 2.0.1-alpha 以来一直存在。它没有表现 在 Libevent 2.0.4-alpha 之前,跨 bufferevent 类型保持一致。
在缓冲区事件上启动刷新
接口
int bufferevent_flush(struct bufferevent *bufev,
short iotype, enum bufferevent_flush_mode state);
刷新 bufferevent 会告知 bufferevent 强制执行尽可能多的字节数 尽可能从基础传输中读取或写入, 忽略其他可能阻止他们的限制 正在写。它的详细功能取决于 buffer事件。
iotype 参数应为 EV_READ、EV_WRITE 或 EV_READ|EV_WRITE 指示是否应读取、写入或同时读取和/或写入的字节 处理。状态参数可能是BEV_NORMAL之一, BEV_FLUSH,或BEV_FINISHED。BEV_FINISHED表示另一个 应告知方不会再发送数据;区别 BEV_NORMAL 和 BEV_FLUSH 之间取决于 buffer事件。
bufferevent_flush() 函数在失败时返回 -1,如果没有数据,则返回 0 已刷新,如果某些数据已刷新,则为 1。
目前(从 Libevent 2.0.5-beta 开始),bufferevent_flush() 只有 为某些 BufferEvent 类型实现。特别是,基于套接字 bufferEvents 没有它。
特定于类型的 bufferevent 函数
并非所有 bufferevent 都支持这些 bufferevent 函数 类型。
接口
int bufferevent_priority_set(struct bufferevent *bufev, int pri);
int bufferevent_get_priority(struct bufferevent *bufev);
此函数将用于实现 bufev 的事件的优先级调整为 pri。有关以下内容的更多信息,请参见 event_priority_set() 优先 级。
此函数在成功时返回 0,在失败时返回 -1。它适用于 仅限基于套接字的缓冲事件。
bufferevent_priority_set() 函数是在 Libevent 1.0 中引入的; bufferevent_get_priority() 直到 Libevent 2.1.2-alpha 才出现。
接口
int bufferevent_setfd(struct bufferevent *bufev, evutil_socket_t fd);
evutil_socket_t bufferevent_getfd(struct bufferevent *bufev);
这些函数设置或返回基于 fd 的文件描述符 事件。只有基于套接字的 bufferevents 支持 setfd()。两者都返回 -1 失败;setfd() 在成功时返回 0。
bufferevent_setfd() 函数是在 Libevent 1.4.4 中引入的; bufferevent_getfd() 函数是在 Libevent 2.0.2-alpha 中引入的。
接口
struct event_base *bufferevent_get_base(struct bufferevent *bev);
此函数返回缓冲区事件的event_base。它被引入 2.0.9-rc。
接口
struct bufferevent *bufferevent_get_underlying(struct bufferevent *bufev);
此函数返回另一个 bufferevent 所在的 bufferevent 用作交通工具(如果有)。有关何时出现这种情况的信息 ,请参阅有关筛选缓冲区事件的说明。
此函数是在 Libevent 2.0.2-alpha 中引入的。
手动锁定和解锁缓冲区事件
与 evbuffers 一样,有时您希望确保许多操作 在缓冲区上,事件都是以原子方式执行的。Libevent 公开函数 可用于手动锁定和解锁 BufferEvent。
接口
void bufferevent_lock(struct bufferevent *bufev);
void bufferevent_unlock(struct bufferevent *bufev);
请注意,如果 bufferevent 不是 给定创建时的BEV_OPT_THREADSAFE线程,或者如果 Libevent 的线程 未激活支持。
使用此函数锁定 bufferevent 将锁定其关联的 evbuffers 也。这些函数是递归的:锁定 bufferevent 是安全的 你已经拿着锁了。当然,您必须调用一次解锁 每次锁定 BufferEvent。
这些函数是在 Libevent 2.0.6-rc 中引入的。
过时的缓冲区事件功能
bufferevent 后端代码在 Libevent 1.4 和 Libevent 2.0。在旧界面中,有时是 正常构建并访问结构的内部结构 buffer事件,并使用依赖于此访问的宏。
为了使事情变得混乱,旧代码有时会使用 以“evbuffer”为前缀的 bufferEvent 功能。
以下是以前称呼事物的简要指南 Libevent 2.0:
现用名 | 旧名称 |
---|---|
bufferevent_data_cb | evbuffercb |
bufferevent_event_cb | everrorcb |
BEV_EVENT_READING | EVBUFFER_READ |
BEV_EVENT_WRITE | EVBUFFER_WRITE |
BEV_EVENT_EOF | EVBUFFER_EOF |
BEV_EVENT_ERROR | EVBUFFER_ERROR |
BEV_EVENT_TIMEOUT | EVBUFFER_TIMEOUT |
bufferevent_get_input(二) | EVBUFFER_INPUT(二) |
bufferevent_get_output(二) | EVBUFFER_OUTPUT(二) |
旧函数是在 event.h 中定义的,而不是在 event2/bufferevent.h 中定义的。
如果您仍然需要访问公共部件的内部 bufferEvent 结构,可以包含 event2/bufferevent_struct.h。我们 建议不要这样做:struct bufferevent 的内容将在 Libevent 的版本。在以下情况下,本节中的宏和名称可用 包括 event2/bufferevent_compat.h。
设置缓冲区事件的界面在旧版本中有所不同:
接口
struct bufferevent *bufferevent_new(evutil_socket_t fd,
evbuffercb readcb, evbuffercb writecb, everrorcb errorcb, void *cbarg);
int bufferevent_base_set(struct event_base *base, struct bufferevent *bufev);
bufferevent_new() 函数仅创建套接字缓冲区事件,并这样做 在已弃用的“默认”event_base上。呼叫bufferevent_base_set调整 仅套接字 Buffer 事件的event_base。
没有将超时设置为结构时间,而是将其设置为 秒数:
接口
void bufferevent_settimeout(struct bufferevent *bufev,
int timeout_read, int timeout_write);
最后,请注意 Libevent 的底层 evbuffer 实现 2.0 之前的版本效率非常低,以至于使用 高性能应用程序的 BufferEvents 有点值得怀疑。
最后更新 世界协调时 2024-02-18 20:10:44
Bufferevents:高级主题
这些文件版权所有 (c) 2009-2012 由 Nick Mathewson 制作 可在知识共享署名-非商业性使用-相同方式共享下使用 许可证,版本 3.0。未来的版本可能会在 限制性许可证。
此外,这些文档中的源代码示例也已获得许可 在所谓的“3-Clause”或“Modified”BSD许可证下。请参阅随这些文档一起分发的 license_bsd 文件 对于完整的条款。
若要获取本文档最新版本的源代码,请安装 git 并运行“git clone git://github.com/libevent/libevent-book.git”
本章介绍了 Libevent 缓冲事件的一些高级功能 对于典型用途不是必需的实现。如果你只是 学习如何使用 BufferEvents,您应该暂时跳过本章 并继续阅读 EVBUFFER 章节。
配对缓冲区事件
有时,您有一个需要自言自语的网络程序。 例如,您可以编写一个程序来隧道用户连接 通过某些协议,有时还希望通过隧道连接 它自己在该协议上。您可以通过打开一个 连接到您自己的收听端口并让您的程序使用 当然,它本身,但这会因为拥有你的程序而浪费资源 通过网络堆栈自言自语。
相反,您可以创建一对成对的缓冲区事件,以便所有字节 一个上写的在另一个上收到(反之亦然),但没有实际的 使用平台套接字。
接口
int bufferevent_pair_new(struct event_base *base, int options,
struct bufferevent *pair[2]);
调用 bufferevent_pair_new() 将 pair[0] 和 pair[1] 设置为一对 Buffer事件,每个事件都连接到另一个。所有常用的选项都是 支持,但 BEV_OPT_CLOSE_ON_FREE 除外,它没有效果,并且 BEV_OPT_DEFER_CALLBACKS,它总是在线的。
为什么 bufferevent 对需要在延迟回调的情况下运行?很漂亮 对对中的一个元素执行的操作通常用于调用回调 更改 BufferEvent,从而调用其他 BufferEvent 的回调,以及 以此类推,通过许多步骤。当回调没有被延迟时,这个链 的调用会经常溢出堆栈,饿死其他 连接,并要求所有回调都是可重入的。
配对的缓冲区事件支持刷新;将 mode 参数设置为任一 BEV_NORMAL或BEV_FLUSH强制所有相关数据获得 从对中的一个 BufferEvent 转移到另一个 BufferEvent,忽略 否则会限制它的水印。将模式设置为BEV_FINISHED 此外,在相反的缓冲区事件上生成 EOF 事件。
释放对中的任何一个成员不会自动释放另一个成员或 生成 EOF 事件;它只是让这对的另一名成员成为 未链接。一旦 bufferevent 被取消链接,它将不再成功 读取或写入数据或生成任何事件。
接口
struct bufferevent *bufferevent_pair_get_partner(struct bufferevent *bev)
有时,您可能需要给定 bufferevent 对的其他成员 只有一个成员。为此,您可以调用 bufferevent_pair_get_partner() 函数。它将返回 如果 BEV 是一对的成员,则该对仍然存在。 否则,它将返回 NULL。
Bufferevent 对是 Libevent 2.0.1-alpha 中的新增功能;这 bufferevent_pair_get_partner() 函数是在 Libevent 2.0.6 中引入的。
筛选缓冲区事件
有时,您希望转换通过缓冲区事件传递的所有数据 对象。您可以这样做来添加压缩层,或将协议包装在 另一种运输协议。
接口
enum bufferevent_filter_result {
BEV_OK = 0,
BEV_NEED_MORE = 1,
BEV_ERROR = 2
};
typedef enum bufferevent_filter_result (*bufferevent_filter_cb)(
struct evbuffer *source, struct evbuffer *destination, ev_ssize_t dst_limit,
enum bufferevent_flush_mode mode, void *ctx);
struct bufferevent *bufferevent_filter_new(struct bufferevent *underlying,
bufferevent_filter_cb input_filter,
bufferevent_filter_cb output_filter,
int options,
void (*free_context)(void *),
void *ctx);
bufferevent_filter_new() 函数创建一个新的过滤缓冲区事件, 围绕现有的“基础”缓冲事件进行包装。所有数据均通过以下方式接收 基础 BufferEvent 之前使用“input”筛选器进行转换 到达 Filtering BufferEvent,以及通过 Filtering 发送的所有数据 buffer事件在发送到 基础 BufferEvent。
将筛选器添加到基础 bufferevent 会替换 基础 bufferevent。您仍然可以向基础添加回调 bufferEvent 的 evbuffers,但无法在 bufferevent 上设置回调 本身,如果您希望过滤器仍然有效。
下面介绍了 input_filter 和 output_filter 函数。 选项中支持所有常用选项。如果BEV_OPT_CLOSE_ON_FREE ,则释放筛选缓冲区事件也会释放基础 buffer事件。ctx 字段是传递给过滤器的任意指针 功能;如果提供了free_context函数,则仅在 CTX 上调用它 在关闭筛选 BufferEvent 之前。
每当有新的可读数据时,都会调用输入过滤器函数 在基础输入缓冲区上。输出过滤器函数被调用 每当筛选器的输出缓冲区上有新的可写数据时。每一个 接收一对 EVBUFFER:一个用于从中读取数据的源 evbuffer,以及一个用于将数据写入的目标 evbuffer。dst_limit参数描述了 要添加到目标的字节的上限。过滤功能为 允许忽略此值,但这样做可能会违反高水位线 或速率限制。如果 dst_limit 为 -1,则没有限制。mode 参数告诉筛选器在写入时要有多激进。如果是 BEV_NORMAL,那么它应该尽可能多地写入可以方便地转换。 BEV_FLUSH值意味着尽可能多地写入,并BEV_FINISHED 表示过滤功能还应执行任何清理 在流的末尾是必需的。最后,filter 函数的 ctx 参数是提供给 bufferevent_filter_new() 的 void 指针 构造 函数。
筛选器函数必须返回BEV_OK是否有任何数据已成功写入 目标缓冲区,BEV_NEED_MORE如果没有更多数据可以写入 目标缓冲区,无需获取更多输入或使用不同的刷新 模式,并BEV_ERROR筛选器上是否存在不可恢复的错误。
创建筛选器可以在基础上进行读取和写入 buffer事件。您不需要自行管理读取/写入:过滤器 每当它 不想看。对于 2.0.8-rc 及更高版本,允许 启用/禁用对基础 Buffer 事件的读取和写入 独立于过滤器。但是,如果您这样做,则可以保留 筛选成功获取所需数据。
您无需同时指定输入筛选器和输出筛选器:any 省略的过滤器将替换为传递数据而不进行转换的过滤器 它。
限制最大单次读/写大小
默认情况下,bufferevents 不会读取或写入可能的最大数量 每次调用事件循环的字节数;这样做会导致 奇怪的不公平行为和资源匮乏。另一方面, 默认值可能并非在所有情况下都合理。
接口
int bufferevent_set_max_single_read(struct bufferevent *bev, size_t size);
int bufferevent_set_max_single_write(struct bufferevent *bev, size_t size);
ev_ssize_t bufferevent_get_max_single_read(struct bufferevent *bev);
ev_ssize_t bufferevent_get_max_single_write(struct bufferevent *bev);
两个“set”函数取代了当前的读写最大值 分别。如果大小值为 0 或高于 EV_SSIZE_MAX,则 而是将最大值设置为默认值。这些函数返回 0 成功时为 -1,失败时为 -1。
两个“get”函数返回当前每循环的读取和写入 最大值。
这些函数是在 2.1.1-alpha 中添加的。
缓冲事件和速率限制
某些程序希望限制用于任何单个的带宽量 bufferevent,或一组 bufferEvents。Libevent 2.0.4-alpha 和 Libevent 2.0.5-alpha 添加了一个基本功能,可以对个人进行上限 BufferEvents,或将 BufferEvents 分配给速率限制组。
速率限制模型
Libevent 的速率限制使用令牌桶算法来决定数量 一次读取或写入的字节数。每个受速率限制的对象,在任何给定 time,有一个“读桶”和一个“写桶”,其大小决定了 允许对象立即读取或写入的字节数。每 桶具有重新填充率、最大突发大小和 定时单位或“滴答声”。每当计时单元经过时,铲斗就会重新装满 与再填充率成正比,但如果会变得比其爆发率更满 大小,则任何多余的字节都会丢失。
因此,再填充速率决定了物体的最大平均速率 将发送或接收字节,突发大小决定最大数量 将在单个突发中发送或接收的字节数。计时单元 决定了交通的平滑度。
设置缓冲事件的速率限制
接口
#define EV_RATE_LIMIT_MAX EV_SSIZE_MAX
struct ev_token_bucket_cfg;
struct ev_token_bucket_cfg *ev_token_bucket_cfg_new(
size_t read_rate, size_t read_burst,
size_t write_rate, size_t write_burst,
const struct timeval *tick_len);
void ev_token_bucket_cfg_free(struct ev_token_bucket_cfg *cfg);
int bufferevent_set_rate_limit(struct bufferevent *bev,
struct ev_token_bucket_cfg *cfg);
ev_token_bucket_cfg结构表示 一对令牌存储桶,用于限制单个上的读取和写入 BufferEvent 或一组 BufferEvents。要创建一个,请调用 ev_token_bucket_cfg_new函数并提供最大平均读取速率, 最大读取突发、最大写入速率、最大写入突发和 刻度的长度。如果 tick_len 参数为 NULL,则刻度的长度 默认值为一秒。该函数可能会在出错时返回 NULL。
请注意,read_rate 和 write_rate 参数以 每个刻度的字节数。也就是说,如果刻度是十分之一秒,read_rate是 300,则最大平均读取速率为每 3000 字节 第二。不支持超过 EV_RATE_LIMIT_MAX 的速率和突发值。
要限制缓冲事件的传输速率,请调用 bufferevent_set_rate_limit() 它有一个ev_token_bucket_cfg。该函数在成功时返回 0,在成功时返回 -1 失败。您可以为任意数量的缓冲区事件提供相同的内容 ev_token_bucket_cfg。要删除缓冲事件的速率限制,请调用 bufferevent_set_rate_limit(),为 cfg 参数传递 NULL。
要释放ev_token_bucket_cfg,请调用 ev_token_bucket_cfg_free()。请注意, 目前,在没有缓冲事件使用 ev_token_bucket_cfg。
为一组缓冲区事件设置速率限制
如果要限制,可以将缓冲区事件分配给速率限制组 他们的总带宽使用量。
接口
struct bufferevent_rate_limit_group;
struct bufferevent_rate_limit_group *bufferevent_rate_limit_group_new(
struct event_base *base,
const struct ev_token_bucket_cfg *cfg);
int bufferevent_rate_limit_group_set_cfg(
struct bufferevent_rate_limit_group *group,
const struct ev_token_bucket_cfg *cfg);
void bufferevent_rate_limit_group_free(struct bufferevent_rate_limit_group *);
int bufferevent_add_to_rate_limit_group(struct bufferevent *bev,
struct bufferevent_rate_limit_group *g);
int bufferevent_remove_from_rate_limit_group(struct bufferevent *bev);
要构造速率限制组,bufferevent_rate_limit_group请使用 event_base和最初的ev_token_bucket_cfg。您可以将 bufferevents 添加到 具有 bufferevent_add_to_rate_limit_group() 和 bufferevent_remove_from_rate_limit_group();这些函数返回 0 成功和 -1 错误。
单个缓冲区事件可以是不超过一个速率限制组的成员 一次。缓冲事件可以同时具有单独的速率限制(如 bufferevent_set_rate_limit()) 和组速率限制。当两个限制都是 设置时,每个 BufferEvent 的下限适用。
您可以通过调用来更改现有组的速率限制 bufferevent_rate_limit_group_set_cfg()。成功时返回 0,成功时返回 -1 失败。bufferevent_rate_limit_group_free() 函数释放速率限制 组并删除其所有成员。
从 2.0 版开始,Libevent 的组速率限制试图公平 聚合,但在非常小的时间尺度上实施可能是不公平的。如果 您非常关心日程安排的公平性,请帮忙 带有未来版本的补丁。
检查当前速率限制值
有时,您的代码可能想要检查应用的当前速率限制 对于给定的 BufferEvent 或组。Libevent 提供了一些函数来做到这一点。
接口
ev_ssize_t bufferevent_get_read_limit(struct bufferevent *bev);
ev_ssize_t bufferevent_get_write_limit(struct bufferevent *bev);
ev_ssize_t bufferevent_rate_limit_group_get_read_limit(
struct bufferevent_rate_limit_group *);
ev_ssize_t bufferevent_rate_limit_group_get_write_limit(
struct bufferevent_rate_limit_group *);
上述函数返回 bufferevent 的当前大小(以字节为单位)或 组的读取或写入令牌存储桶。请注意,这些值可以是 如果缓冲事件已强制超过其分配,则为负数。 (刷新 bufferevent 可以执行此操作。
接口
ev_ssize_t bufferevent_get_max_to_read(struct bufferevent *bev);
ev_ssize_t bufferevent_get_max_to_write(struct bufferevent *bev);
这些函数返回 bufferevent 的字节数 愿意立即读取或写入,同时考虑到任何速率限制 适用于 BufferEvent、其速率限制组(如果有)和任何 Libevent 作为一个整体施加的最大读/写值。
接口
void bufferevent_rate_limit_group_get_totals(
struct bufferevent_rate_limit_group *grp,
ev_uint64_t *total_read_out, ev_uint64_t *total_written_out);
void bufferevent_rate_limit_group_reset_totals(
struct bufferevent_rate_limit_group *grp);
每个bufferevent_rate_limit_group跟踪发送的总字节数 它,总共。您可以使用它来跟踪总使用量 Buffer事件。叫 组上的 bufferevent_rate_limit_group_get_totals() 将 *total_read_out 和 *total_written_out 设置为在 BufferEvent 组。当组为 创建,并在 bufferevent_rate_limit_group_reset_totals() 时重置为 0 在组上被调用。
手动调整速率限制
对于具有真正复杂需求的程序,您可能需要调整当前 令牌存储桶的值。您可能希望这样做,例如,如果您的 程序以某种方式生成流量,而不是通过 BufferEvent。
接口
int bufferevent_decrement_read_limit(struct bufferevent *bev, ev_ssize_t decr);
int bufferevent_decrement_write_limit(struct bufferevent *bev, ev_ssize_t decr);
int bufferevent_rate_limit_group_decrement_read(
struct bufferevent_rate_limit_group *grp, ev_ssize_t decr);
int bufferevent_rate_limit_group_decrement_write(
struct bufferevent_rate_limit_group *grp, ev_ssize_t decr);
这些函数递减缓冲区事件中的当前读取或写入存储桶,或者 速率限制组。请注意,递减是有符号的:如果要 递增一个存储桶,传递一个负值。
在速率受限的组中设置尽可能小的份额
通常,您不希望在速率限制中划分可用的字节 在每个刻度的所有缓冲事件中均匀分组。例如,如果您 在具有 10,000 字节的速率限制组中有 10,000 个活动缓冲区事件 可用于写入每个刻度,让每个刻度都不会有效 由于系统调用的开销,BufferEvent 每次调用仅写入 1 个字节 和 TCP 标头。
为了解决这个问题,每个限速组都有一个“最小份额”的概念。 在上面的情况下,而不是允许每个缓冲事件写入 1 字节/刻度,允许 10,000/SHARE 缓冲事件写入 SHARE 字节 每个刻度,其余的将被允许不写任何东西。哪 Buffer允许先写入的事件是每个刻度随机选择的。
选择默认最小份额以提供不错的性能,并且是 当前(截至 2.0.6-RC)设置为 64。您可以使用 以下功能:
接口
int bufferevent_rate_limit_group_set_min_share(
struct bufferevent_rate_limit_group *group, size_t min_share);
将min_share设置为 0 将完全禁用最小共享代码。
Libevent的速率限制自首次以来就具有最低份额 介绍。更改它们的函数首先在 Libevent 中公开 2.0.6-rc。
速率限制实现的局限性
从 Libevent 2.0 开始,速率限制存在一些限制 你应该知道的实现。
- 并非每种 bufferevent 类型都支持速率限制,或者根本不支持速率限制。
- Bufferevent 速率限制组不能嵌套,而 bufferevent 只能是 一次在单个速率限制组中。
- 速率限制实现仅计算在 TCP 中传输的字节数 数据包作为数据,不包括 TCP 标头。
- 读取限制实现依赖于 TCP 堆栈,注意到 应用程序仅以一定的速度消耗数据,然后推回 当 TCP 连接的缓冲区已满时,TCP 连接的另一端。
- 缓冲区事件的一些实现(特别是 Windows IOCP implementation)可能会过度承诺。
- 存储桶开始时有一整块流量。这意味着 buffer事件可以立即开始读取或写入,而不是等到 完整的勾号已经过去了。不过,这也意味着具有 N.1 个报价的速率限制可能会转移 N+1 个报价价值 交通。
- 刻度不能小于 1 毫秒,并且 毫秒被忽略。
TODO:写一个限速的例子
Bufferevents 和 SSL
Bufferevents 可以使用 OpenSSL 库来实现 SSL/TLS 安全 传输层。因为许多应用程序不需要或不想链接 OpenSSL,此功能在单独的库中实现,安装为 “libevent_openssl”。Libevent 的未来版本可以添加对其他 SSL/TLS 库,例如 NSS 或 GnuTLS,但现在 OpenSSL 就是全部 那里。
OpenSSL 功能是在 Libevent 2.0.3-alpha 中引入的,尽管它没有 在 Libevent 2.0.5-beta 或 Libevent 2.0.6-rc 之前运行良好。
本节不是关于 OpenSSL、SSL/TLS 或一般加密的教程。
这些函数都在标头“event2/bufferevent_ssl.h”中声明。
设置和使用基于 OpenSSL 的缓冲区事件
接口
enum bufferevent_ssl_state {
BUFFEREVENT_SSL_OPEN = 0,
BUFFEREVENT_SSL_CONNECTING = 1,
BUFFEREVENT_SSL_ACCEPTING = 2
};
struct bufferevent *
bufferevent_openssl_filter_new(struct event_base *base,
struct bufferevent *underlying,
SSL *ssl,
enum bufferevent_ssl_state state,
int options);
struct bufferevent *
bufferevent_openssl_socket_new(struct event_base *base,
evutil_socket_t fd,
SSL *ssl,
enum bufferevent_ssl_state state,
int options);
您可以创建两种类型的 SSL 缓冲区事件:一种是基于筛选器的缓冲区事件,它 通过另一个基础缓冲区事件或基于套接字的 Buffer Event 进行通信 buffer事件,告诉 OpenSSL 直接通过网络进行通信。在 无论哪种情况,都必须提供 SSL 对象和 SSL 的描述 对象的状态。如果 SSL 是BUFFEREVENT_SSL_CONNECTING 目前作为客户进行谈判,BUFFEREVENT_SSL_ACCEPTING 如果 SSL 当前正在作为服务器执行协商,或者 BUFFEREVENT_SSL_OPEN SSL 握手是否完成。
接受通常的选项;BEV_OPT_CLOSE_ON_FREE使 SSL 对象 当 openSSL BufferEvent 关闭时,基础 FD 或 BufferEvent 将关闭 本身是封闭的。
握手完成后,将调用新 bufferevent 的事件回调 旗帜中BEV_EVENT_CONNECTED。
如果已创建基于套接字的缓冲区事件和 SSL 对象 有套接字,不需要自己提供套接字:通过就行了 -1.您也可以稍后使用 bufferevent_setfd() 设置 fd。
TODO:bufferevent_shutdown() API 完成后将其删除。
请注意,当在 SSL bufferevent 上设置BEV_OPT_CLOSE_ON_FREE时, 不会对 SSL 连接执行干净关机。这有两个 问题:首先,连接似乎已经被对方“破坏” 一方,而不是被关闭干净:另一方不会 能够判断是否关闭了连接,或者是否已断开连接 由攻击者或第三方提供。其次,OpenSSL将处理 会话为“坏”,并从会话缓存中删除。这 可能会导致负载下的 SSL 应用程序性能显著下降。
目前,唯一的解决方法是手动执行延迟SSL关闭。虽然这 中断 TLS RFC,它将确保会话将保留在 缓存一旦关闭。下面的代码实现此变通办法。
例
SSL *ctx = bufferevent_openssl_get_ssl(bev);
/*
* SSL_RECEIVED_SHUTDOWN tells SSL_shutdown to act as if we had already
* received a close notify from the other end. SSL_shutdown will then
* send the final close notify in reply. The other end will receive the
* close notify and send theirs. By this time, we will have already
* closed the socket and the other end's real close notify will never be
* received. In effect, both sides will think that they have completed a
* clean shutdown and keep their sessions valid. This strategy will fail
* if the socket is not ready for writing, in which case this hack will
* lead to an unclean shutdown and lost session on the other end.
*/
SSL_set_shutdown(ctx, SSL_RECEIVED_SHUTDOWN);
SSL_shutdown(ctx);
bufferevent_free(bev);
接口
SSL *bufferevent_openssl_get_ssl(struct bufferevent *bev);
此函数返回 OpenSSL 缓冲区事件或 NULL 使用的 SSL 对象 如果 bev 不是基于 OpenSSL 的缓冲事件。
接口
unsigned long bufferevent_get_openssl_error(struct bufferevent *bev);
此函数返回给定的第一个挂起的 OpenSSL 错误 bufferEvent 的操作,如果没有挂起的错误,则为 0。错误 format 由 openssl 库中的 ERR_get_error() 返回。
接口
int bufferevent_ssl_renegotiate(struct bufferevent *bev);
调用此函数会告诉 SSL 重新协商,并将 bufferevent 告知 调用适当的回调。这是一个高级主题;你应该 除非你真的知道自己在做什么,否则通常避免它,尤其是因为 许多 SSL 版本都存在与以下相关的已知安全问题 重新谈判。
接口
int bufferevent_openssl_get_allow_dirty_shutdown(struct bufferevent *bev);
void bufferevent_openssl_set_allow_dirty_shutdown(struct bufferevent *bev,
int allow_dirty_shutdown);
SSL 协议的所有良好版本(即 SSLv3 和所有 TLS) versions) 支持经过身份验证的关机操作,该操作启用 当事方区分故意关闭与意外关闭或 在底层缓冲区中恶意诱导终止。默认情况下,我们 将正确关机以外的任何内容视为连接错误。 但是,如果 allow_dirty_shutdown 标志设置为 1,则我们处理收盘价 在连接中作为BEV_EVENT_EOF。
allow_dirty_shutdown函数是在 Libevent 2.1.1-alpha 中添加的。
示例:基于 SSL 的简单回显服务器
/* Simple echo server using OpenSSL bufferevents */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/rand.h>
#include <event.h>
#include <event2/listener.h>
#include <event2/bufferevent_ssl.h>
static void
ssl_readcb(struct bufferevent * bev, void * arg)
{
struct evbuffer *in = bufferevent_get_input(bev);
printf("Received %zu bytes\n", evbuffer_get_length(in));
printf("----- data ----\n");
printf("%.*s\n", (int)evbuffer_get_length(in), evbuffer_pullup(in, -1));
bufferevent_write_buffer(bev, in);
}
static void
ssl_acceptcb(struct evconnlistener *serv, int sock, struct sockaddr *sa,
int sa_len, void *arg)
{
struct event_base *evbase;
struct bufferevent *bev;
SSL_CTX *server_ctx;
SSL *client_ctx;
server_ctx = (SSL_CTX *)arg;
client_ctx = SSL_new(server_ctx);
evbase = evconnlistener_get_base(serv);
bev = bufferevent_openssl_socket_new(evbase, sock, client_ctx,
BUFFEREVENT_SSL_ACCEPTING,
BEV_OPT_CLOSE_ON_FREE);
bufferevent_enable(bev, EV_READ);
bufferevent_setcb(bev, ssl_readcb, NULL, NULL, NULL);
}
static SSL_CTX *
evssl_init(void)
{
SSL_CTX *server_ctx;
/* Initialize the OpenSSL library */
SSL_load_error_strings();
SSL_library_init();
/* We MUST have entropy, or else there's no point to crypto. */
if (!RAND_poll())
return NULL;
server_ctx = SSL_CTX_new(SSLv23_server_method());
if (! SSL_CTX_use_certificate_chain_file(server_ctx, "cert") ||
! SSL_CTX_use_PrivateKey_file(server_ctx, "pkey", SSL_FILETYPE_PEM)) {
puts("Couldn't read 'pkey' or 'cert' file. To generate a key\n"
"and self-signed certificate, run:\n"
" openssl genrsa -out pkey 2048\n"
" openssl req -new -key pkey -out cert.req\n"
" openssl x509 -req -days 365 -in cert.req -signkey pkey -out cert");
return NULL;
}
SSL_CTX_set_options(server_ctx, SSL_OP_NO_SSLv2);
return server_ctx;
}
int
main(int argc, char **argv)
{
SSL_CTX *ctx;
struct evconnlistener *listener;
struct event_base *evbase;
struct sockaddr_in sin;
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(9999);
sin.sin_addr.s_addr = htonl(0x7f000001); /* 127.0.0.1 */
ctx = evssl_init();
if (ctx == NULL)
return 1;
evbase = event_base_new();
listener = evconnlistener_new_bind(
evbase, ssl_acceptcb, (void *)ctx,
LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, 1024,
(struct sockaddr *)&sin, sizeof(sin));
event_base_loop(evbase, 0);
evconnlistener_free(listener);
SSL_CTX_free(ctx);
return 0;
}
关于线程和 OpenSSL 的一些说明
Libevent 的内置线程机制不包括 OpenSSL 锁定。 由于 OpenSSL 使用无数的全局变量,因此您仍必须进行配置 OpenSSL 是线程安全的。虽然此过程超出了 Libevent 的范围, 这个话题足以引起讨论。
示例:如何启用线程安全 OpenSSL 的非常简单的示例
/*
* Please refer to OpenSSL documentation to verify you are doing this correctly,
* Libevent does not guarantee this code is the complete picture, but to be used
* only as an example.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <openssl/ssl.h>
#include <openssl/crypto.h>
pthread_mutex_t * ssl_locks;
int ssl_num_locks;
#ifndef WIN32
#define _SSLtid (unsigned long)pthread_self()
#else
#define _SSLtid pthread_self().p
#endif
/* Implements a thread-ID function as requied by openssl */
#if OPENSSL_VERSION_NUMBER < 0x10000000L
static unsigned long
get_thread_id_cb(void)
{
return _SSLtid;
}
#else
static void
get_thread_id_cb(CRYPTO_THREADID *id)
{
CRYPTO_THREADID_set_numeric(id, _SSLtid);
}
#endif
static void
thread_lock_cb(int mode, int which, const char * f, int l)
{
if (which < ssl_num_locks) {
if (mode & CRYPTO_LOCK) {
pthread_mutex_lock(&(ssl_locks[which]));
} else {
pthread_mutex_unlock(&(ssl_locks[which]));
}
}
}
int
init_ssl_locking(void)
{
int i;
ssl_num_locks = CRYPTO_num_locks();
ssl_locks = malloc(ssl_num_locks * sizeof(pthread_mutex_t));
if (ssl_locks == NULL)
return -1;
for (i = 0; i < ssl_num_locks; i++) {
pthread_mutex_init(&(ssl_locks[i]), NULL);
}
#if OPENSSL_VERSION_NUMBER < 0x10000000L
CRYPTO_set_id_callback(get_thread_id_cb);
#else
CRYPTO_THREADID_set_callback(get_thread_id_cb);
#endif
CRYPTO_set_locking_callback(thread_lock_cb);
return 0;
}
原文档地址:https://libevent.org/libevent-book/Ref6_bufferevent.html
上一篇: 【异常】JDK21报错NoSuchFieldError: Class com.sun.tools.javac.tree.JCTree$JCImport does not have member fie
下一篇: C++必修:探索C++的内存管理
本文标签
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。