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);

baseevent_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_filteroutput_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_ratewrite_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