Libevent


简介

对于C++非阻塞、事件循环相关知识,学习LibEvent是绕不开的一项基本技能。

下面的部分内容为机翻。


关于本文档

本文档将教您如何使用Libevent 2。0(及更高版本)快速编写 C中的便携式非阻塞网络IO程序 我们假设:

关于示例的说明

本文档中的示例应在Linux,FreeBSD上正常工作, OpenBSD,NetBSD,Mac OS X,Solaris和Android。一些例子 可能无法在Windows上编译。


同步非阻塞IO的小介绍

大多数入门程序员从阻塞IO调用开始。 IO调用为 同步的 如果,当您调用它时,它不会返回 直到操作完成或足够的时间 已经通过,您的网络堆栈放弃了。当您在TCP上调用“ connect()”时 连接,例如,您的操作系统将SYN数据包排队到 TCP连接另一侧的主机。它不返回 控制回到您的应用程序,直到它收到SYN ACK 来自相反主机的数据包,或者直到经过足够的时间为止 决定放弃。

这是一个使用阻塞网络的非常简单的客户端的示例 通话。它打开了到www。google。com的连接,并发送了一个简单的连接 HTTP请求,并打印对stdout的响应。

// 一个简单的阻塞HTTP客户端
/* For sockaddr_in */
#include <netinet/in.h>
/* For socket functions */
#include <sys/socket.h>
/* For gethostbyname */
#include <netdb.h>

#include <unistd.h>
#include <string.h>
#include <stdio.h>

int main(int c, char **v)
{
    const char query[] =
        "GET / HTTP/1.0\r\n"
        "Host: www.google.com\r\n"
        "\r\n";
    const char hostname[] = "www.google.com";
    struct sockaddr_in sin;
    struct hostent *h;
    const char *cp;
    int fd;
    ssize_t n_written, remaining;
    char buf[1024];

    /* Look up the IP address for the hostname.   Watch out; this isn't
       threadsafe on most platforms. */
    h = gethostbyname(hostname);
    if (!h) {
        fprintf(stderr, "Couldn't lookup %s: %s", hostname, hstrerror(h_errno));
        return 1;
    }
    if (h->h_addrtype != AF_INET) {
        fprintf(stderr, "No ipv6 support, sorry.");
        return 1;
    }

    /* Allocate a new socket */
    fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0) {
        perror("socket");
        return 1;
    }

    /* Connect to the remote host. */
    sin.sin_family = AF_INET;
    sin.sin_port = htons(80);
    sin.sin_addr = *(struct in_addr*)h->h_addr;
    if (connect(fd, (struct sockaddr*) &sin, sizeof(sin))) {
        perror("connect");
        close(fd);
        return 1;
    }

    /* Write the query. */
    /* XXX Can send succeed partially? */
    cp = query;
    remaining = strlen(query);
    while (remaining) {
      n_written = send(fd, cp, remaining, 0);
      if (n_written <= 0) {
        perror("send");
        return 1;
      }
      remaining -= n_written;
      cp += n_written;
    }

    /* Get an answer back. */
    while (1) {
        ssize_t result = recv(fd, buf, sizeof(buf), 0);
        if (result == 0) {
            break;
        } else if (result < 0) {
            perror("recv");
            close(fd);
            return 1;
        }
        fwrite(buf, 1, result, stdout);
    }

    close(fd);
    return 0;
}

上面代码中的所有网络调用都是 阻阻塞: gethostbyname在成功或失败之前不会返回 解决www。google。com;连接直到返回 已连接;在他们收到数据之前,recv调用不会返回 或关闭;并且发送呼叫至少要等到 将其输出刷新到内核的写缓冲区。

现在,阻塞IO不一定是邪恶的。如果没有别的了 希望您的程序在此期间完成,阻止IO可以正常工作 为了你。但是假设您需要编写一个程序来处理 一次多个连接。为了使我们的例子具体化:假设 您想阅读来自两个连接的输入,但您不知道 哪个连接将首先获得输入。你不能说这样的坏例子

// 坏例子
/* This won't work. */
char buf[1024];
int i, n;
while (i_still_want_to_read()) {
    for (i=0; i<n_sockets; ++i) {
        n = recv(fd[i], buf, sizeof(buf), 0);
        if (n==0)
            handle_close(fd[i]);
        else if (n<0)
            handle_error(fd[i], errno);
        else
            handle_input(fd[i], buf, n);
    }
}

因为如果数据首先到达fd [2],则您的程序甚至都不会尝试 从fd [2]读取,直到从fd [0]fd [1]读取 数据并完成。

有时人们通过多线程或 多进程服务器。多线程最简单的方法之一 具有用于处理每个连接的单独过程(或线程)。 由于每个连接都有其自己的进程,因此阻塞IO调用 等待一个连接不会建立任何其他连接 流程块。

这是另一个示例程序。这是一台琐碎的服务器 对于端口40713上的TCP连接,从其输入一行读取数据 一次,并写出每条线的ROT13混淆 到达。它使用Unix fork()调用为其创建新进程 每个传入连接。

// 伪造ROT13服务器
/* For sockaddr_in */
#include <netinet/in.h>
/* For socket functions */
#include <sys/socket.h>

#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

#define MAX_LINE 16384

char
rot13_char(char c)
{
    /* We don't want to use isalpha here; setting the locale would change
     * which characters are considered alphabetical. */
    if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))
        return c + 13;
    else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))
        return c - 13;
    else
        return c;
}

void
child(int fd)
{
    char outbuf[MAX_LINE+1];
    size_t outbuf_used = 0;
    ssize_t result;

    while (1) {
        char ch;
        result = recv(fd, &ch, 1, 0);
        if (result == 0) {
            break;
        } else if (result == -1) {
            perror("read");
            break;
        }

        /* We do this test to keep the user from overflowing the buffer. */
        if (outbuf_used < sizeof(outbuf)) {
            outbuf[outbuf_used++] = rot13_char(ch);
        }

        if (ch == '\n') {
            send(fd, outbuf, outbuf_used, 0);
            outbuf_used = 0;
            continue;
        }
    }
}

void
run(void)
{
    int listener;
    struct sockaddr_in sin;

    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = 0;
    sin.sin_port = htons(40713);

    listener = socket(AF_INET, SOCK_STREAM, 0);

#ifndef WIN32
    {
        int one = 1;
        setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
    }
#endif

    if (bind(listener, (struct sockaddr*)&sin, sizeof(sin)) < 0) {
        perror("bind");
        return;
    }

    if (listen(listener, 16)<0) {
        perror("listen");
        return;
    }



    while (1) {
        struct sockaddr_storage ss;
        socklen_t slen = sizeof(ss);
        int fd = accept(listener, (struct sockaddr*)&ss, &slen);
        if (fd < 0) {
            perror("accept");
        } else {
            if (fork() == 0) {
                child(fd);
                exit(0);
            }
        }
    }
}

int
main(int c, char **v)
{
    run();
    return 0;
}

那么,我们是否拥有处理多个连接的完美解决方案 一次? 我可以停止写这本书并继续做其他事情吗 现在? 不完全是。首先,过程创建(甚至线程 在某些平台上,创建)可能非常昂贵。在现实生活中 您需要使用线程池而不是创建新进程。 但从根本上讲,线程的规模不会达到您想要的程度。如果 您的程序需要处理成千上万 一次连接,处理成千上万的线程 效率不如尝试每个CPU只有几个线程。

但是,如果线程不是拥有多个连接的答案,那是什么? 在Unix范式中,您可以制作套接字 非阻塞。Unix 调用执行以下操作:

fcntl(fd, F_SETFL, O_NONBLOCK);

其中fd是套接字的文件描述符。 一旦 从那时起,每当您进行fd(套接字)非阻塞 网络调用fd调用将完成操作 立即返回或返回特殊错误代码以指示“ I 现在无法取得任何进展,请重试。” 所以我们的两个插座的例子 可能天真地写为:

// 不好的例子 忙着轮询所有插座
/* This will work, but the performance will be unforgivably bad. */
int i, n;
char buf[1024];
for (i=0; i < n_sockets; ++i)
    fcntl(fd[i], F_SETFL, O_NONBLOCK);

while (i_still_want_to_read()) {
    for (i=0; i < n_sockets; ++i) {
        n = recv(fd[i], buf, sizeof(buf), 0);
        if (n == 0) {
            handle_close(fd[i]);
        } else if (n < 0) {
            if (errno == EAGAIN)
                 ; /* The kernel didn't have any data for us to read. */
            else
                 handle_error(fd[i], errno);
         } else {
            handle_input(fd[i], buf, n);
         }
    }
}

现在我们正在使用非阻塞套接字,上面的代码将 工作…但几乎没有。表演太糟糕了,两个人 原因。首先,当两个连接都没有数据要读取时 循环将无限旋转,耗尽所有CPU周期。 其次,如果您尝试处理多个或两个以上的连接 这种方法,您将对每个内核进行内核调用, 适合您的任何数据。因此,我们需要一种告诉内核的方法 “等到这些套接字之一准备好给我一些数据,然后 告诉我哪些准备好了。”

人们仍然用于此问题的最古老的解决方案是 select()。select()调用获取三组fds(实现为 位数组):一个用于阅读,一个用于写作,一个用于 “例外”。它等待直到其中一个集合的套接字准备就绪 并更改集合以仅包含准备使用的套接字。

再次使用select作为示例:

示例:使用select

// 示例:使用select
/* If you only have a couple dozen fds, this version won't be awful */
fd_set readset;
int i, n;
char buf[1024];

while (i_still_want_to_read()) {
    int maxfd = -1;
    FD_ZERO(&readset);

    /* Add all of the interesting fds to readset */
    for (i=0; i < n_sockets; ++i) {
         if (fd[i]>maxfd) maxfd = fd[i];
         FD_SET(fd[i], &readset);
    }

    /* Wait until one or more fds are ready to read */
    select(maxfd+1, &readset, NULL, NULL, NULL);

    /* Process all of the fds that are still set in readset */
    for (i=0; i < n_sockets; ++i) {
        if (FD_ISSET(fd[i], &readset)) {
            n = recv(fd[i], buf, sizeof(buf), 0);
            if (n == 0) {
                handle_close(fd[i]);
            } else if (n < 0) {
                if (errno == EAGAIN)
                     ; /* The kernel didn't have any data for us to read. */
                else
                     handle_error(fd[i], errno);
             } else {
                handle_input(fd[i], buf, n);
             }
        }
    }
}

这是我们的ROT13服务器的重新实现,使用select()。

// 示例:基于select()的ROT13服务器
/* For sockaddr_in */
#include <netinet/in.h>
/* For socket functions */
#include <sys/socket.h>
/* For fcntl */
#include <fcntl.h>
/* for select */
#include <sys/select.h>

#include <assert.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>

#define MAX_LINE 16384

char
rot13_char(char c)
{
    /* We don't want to use isalpha here; setting the locale would change
     * which characters are considered alphabetical. */
    if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))
        return c + 13;
    else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))
        return c - 13;
    else
        return c;
}

struct fd_state {
    char buffer[MAX_LINE];
    size_t buffer_used;

    int writing;
    size_t n_written;
    size_t write_upto;
};

struct fd_state *
alloc_fd_state(void)
{
    struct fd_state *state = malloc(sizeof(struct fd_state));
    if (!state)
        return NULL;
    state->buffer_used = state->n_written = state->writing =
        state->write_upto = 0;
    return state;
}

void
free_fd_state(struct fd_state *state)
{
    free(state);
}

void
make_nonblocking(int fd)
{
    fcntl(fd, F_SETFL, O_NONBLOCK);
}

int
do_read(int fd, struct fd_state *state)
{
    char buf[1024];
    int i;
    ssize_t result;
    while (1) {
        result = recv(fd, buf, sizeof(buf), 0);
        if (result <= 0)
            break;

        for (i=0; i < result; ++i)  {
            if (state->buffer_used < sizeof(state->buffer))
                state->buffer[state->buffer_used++] = rot13_char(buf[i]);
            if (buf[i] == '\n') {
                state->writing = 1;
                state->write_upto = state->buffer_used;
            }
        }
    }

    if (result == 0) {
        return 1;
    } else if (result < 0) {
        if (errno == EAGAIN)
            return 0;
        return -1;
    }

    return 0;
}

int
do_write(int fd, struct fd_state *state)
{
    while (state->n_written < state->write_upto) {
        ssize_t result = send(fd, state->buffer + state->n_written,
                              state->write_upto - state->n_written, 0);
        if (result < 0) {
            if (errno == EAGAIN)
                return 0;
            return -1;
        }
        assert(result != 0);

        state->n_written += result;
    }

    if (state->n_written == state->buffer_used)
        state->n_written = state->write_upto = state->buffer_used = 0;

    state->writing = 0;

    return 0;
}

void
run(void)
{
    int listener;
    struct fd_state *state[FD_SETSIZE];
    struct sockaddr_in sin;
    int i, maxfd;
    fd_set readset, writeset, exset;

    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = 0;
    sin.sin_port = htons(40713);

    for (i = 0; i < FD_SETSIZE; ++i)
        state[i] = NULL;

    listener = socket(AF_INET, SOCK_STREAM, 0);
    make_nonblocking(listener);

#ifndef WIN32
    {
        int one = 1;
        setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
    }
#endif

    if (bind(listener, (struct sockaddr*)&sin, sizeof(sin)) < 0) {
        perror("bind");
        return;
    }

    if (listen(listener, 16)<0) {
        perror("listen");
        return;
    }

    FD_ZERO(&readset);
    FD_ZERO(&writeset);
    FD_ZERO(&exset);

    while (1) {
        maxfd = listener;

        FD_ZERO(&readset);
        FD_ZERO(&writeset);
        FD_ZERO(&exset);

        FD_SET(listener, &readset);

        for (i=0; i < FD_SETSIZE; ++i) {
            if (state[i]) {
                if (i > maxfd)
                    maxfd = i;
                FD_SET(i, &readset);
                if (state[i]->writing) {
                    FD_SET(i, &writeset);
                }
            }
        }

        if (select(maxfd+1, &readset, &writeset, &exset, NULL) < 0) {
            perror("select");
            return;
        }

        if (FD_ISSET(listener, &readset)) {
            struct sockaddr_storage ss;
            socklen_t slen = sizeof(ss);
            int fd = accept(listener, (struct sockaddr*)&ss, &slen);
            if (fd < 0) {
                perror("accept");
            } else if (fd > FD_SETSIZE) {
                close(fd);
            } else {
                make_nonblocking(fd);
                state[fd] = alloc_fd_state();
                assert(state[fd]);/*XXX*/
            }
        }

        for (i=0; i < maxfd+1; ++i) {
            int r = 0;
            if (i == listener)
                continue;

            if (FD_ISSET(i, &readset)) {
                r = do_read(i, state[i]);
            }
            if (r == 0 && FD_ISSET(i, &writeset)) {
                r = do_write(i, state[i]);
            }
            if (r) {
                free_fd_state(state[i]);
                state[i] = NULL;
                close(i);
            }
        }
    }
}

int
main(int c, char **v)
{
    setvbuf(stdout, NULL, _IONBF, 0);

    run();
    return 0;
}

但是我们还没有完成。因为生成和读取select() 位数组所需的时间与您提供的最大fd成比例 对于select(),select()调用在 插座很高。

不同的操作系统提供了不同的替换 选择功能。这些包括poll(),epoll(),kqueue(), evports和/ dev / poll。所有这些都比 select(),除poll()之外的所有内容都提供O(1)性能以添加套接字, 取下插座,并注意 插座已准备好进行IO。

不幸的是,没有一个有效的接口无处不在 标准。Linux具有epoll(),BSD(包括Darwin)具有 kqueue(),Solaris具有evports和/ dev / poll … 这些都没有 操作系统具有其他任何功能。所以如果你想写一个 便携式高性能异步应用程序,您需要一个 包含所有这些接口并提供任何一种的抽象 其中之一是最有效的。

这就是Libevent API的最低级别为您提供的服务。它 为各种select()替换提供一致的接口, 使用计算机上可用的最高效版本 跑。

这是我们的非阻塞ROT13服务器的另一个版本。这个 时间,它使用Libevent 2而不是select()。请注意,fd_sets 现在不见了:相反,我们将事件与 struct event_base,可以根据select()实现, poll(),epoll(),kqueue()等

// 示例:带有Libevent的低级ROT13服务器
/* For sockaddr_in */
#include <netinet/in.h>
/* For socket functions */
#include <sys/socket.h>
/* For fcntl */
#include <fcntl.h>

#include <event2/event.h>

#include <assert.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>

#define MAX_LINE 16384

void do_read(evutil_socket_t fd, short events, void *arg);
void do_write(evutil_socket_t fd, short events, void *arg);

char
rot13_char(char c)
{
    /* ROT13字符转换 */
    if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))
        return c + 13;
    else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))
        return c - 13;
    else
        return c;
}

struct fd_state {
    char buffer[MAX_LINE]; // 缓冲区
    size_t buffer_used;    // 缓冲区已使用大小

    size_t n_written;      // 已写入数据大小
    size_t write_upto;     // 写入目标大小

    struct event *read_event;  // 读事件
    struct event *write_event; // 写事件
};

struct fd_state *
alloc_fd_state(struct event_base *base, evutil_socket_t fd)
{
    struct fd_state *state = malloc(sizeof(struct fd_state));
    if (!state)
        return NULL;

    // 创建读事件,绑定到fd,回调函数为do_read
    state->read_event = event_new(base, fd, EV_READ|EV_PERSIST, do_read, state);
    if (!state->read_event) {
        free(state);
        return NULL;
    }

    // 创建写事件,绑定到fd,回调函数为do_write
    state->write_event =
        event_new(base, fd, EV_WRITE|EV_PERSIST, do_write, state);

    if (!state->write_event) {
        event_free(state->read_event);
        free(state);
        return NULL;
    }

    state->buffer_used = state->n_written = state->write_upto = 0;

    return state;
}

void
free_fd_state(struct fd_state *state)
{
    // 释放事件和状态
    event_free(state->read_event);
    event_free(state->write_event);
    free(state);
}

void
do_read(evutil_socket_t fd, short events, void *arg)
{
    struct fd_state *state = arg;
    char buf[1024];
    int i;
    ssize_t result;

    while (1) {
        result = recv(fd, buf, sizeof(buf), 0);
        if (result <= 0)
            break;

        for (i = 0; i < result; ++i) {
            if (state->buffer_used < sizeof(state->buffer))
                state->buffer[state->buffer_used++] = rot13_char(buf[i]);
            if (buf[i] == '\n') {
                // 数据行结束,添加写事件
                event_add(state->write_event, NULL);
                state->write_upto = state->buffer_used;
            }
        }
    }

    if (result == 0) {
        free_fd_state(state); // 连接关闭
    } else if (result < 0) {
        if (errno == EAGAIN)
            return;
        perror("recv");
        free_fd_state(state);
    }
}

void
do_write(evutil_socket_t fd, short events, void *arg)
{
    struct fd_state *state = arg;

    while (state->n_written < state->write_upto) {
        ssize_t result = send(fd, state->buffer + state->n_written,
                              state->write_upto - state->n_written, 0);
        if (result < 0) {
            if (errno == EAGAIN)
                return;
            free_fd_state(state);
            return;
        }

        state->n_written += result;
    }

    if (state->n_written == state->buffer_used)
        state->n_written = state->write_upto = state->buffer_used = 0;

    // 写完成后移除写事件
    event_del(state->write_event);
}

void
do_accept(evutil_socket_t listener, short event, void *arg)
{
    struct event_base *base = arg;
    struct sockaddr_storage ss;
    socklen_t slen = sizeof(ss);
    int fd = accept(listener, (struct sockaddr*)&ss, &slen);
    if (fd < 0) {
        perror("accept");
    } else if (fd > FD_SETSIZE) {
        close(fd);
    } else {
        struct fd_state *state;
        evutil_make_socket_nonblocking(fd); // 设置非阻塞
        state = alloc_fd_state(base, fd);
        event_add(state->read_event, NULL); // 添加读事件
    }
}

void
run(void)
{
    evutil_socket_t listener;
    struct sockaddr_in sin;
    struct event_base *base;
    struct event *listener_event;

    base = event_base_new(); // 创建事件循环
    if (!base)
        return;

    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = 0;
    sin.sin_port = htons(40713);

    listener = socket(AF_INET, SOCK_STREAM, 0);
    evutil_make_socket_nonblocking(listener); // 设置监听套接字为非阻塞

#ifndef WIN32
    {
        int one = 1;
        setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
    }
#endif

    if (bind(listener, (struct sockaddr*)&sin, sizeof(sin)) < 0) {
        perror("bind");
        return;
    }

    if (listen(listener, 16) < 0) {
        perror("listen");
        return;
    }

    // 创建监听事件,绑定到listener,回调函数为do_accept
    listener_event = event_new(base, listener, EV_READ|EV_PERSIST, do_accept, (void*)base);
    event_add(listener_event, NULL); // 添加监听事件

    event_base_dispatch(base); // 开始事件循环
}

int
main(int c, char **v)
{
    setvbuf(stdout, NULL, _IONBF, 0);

    run();
    return 0;
}

代码中需要注意的其他事项:而不是将套接字键入为 “ int”,我们使用的是evutil_socket_t类型。而不是打电话 fcntl(O_NONBLOCK)使套接字不阻塞,我们打电话 evutil_make_socket_nonblocking。这些更改使我们的代码兼容 与Win32网络API的不同部分一起使用。

那方便呢?那Windows呢?

您可能已经注意到,随着我们的代码效率的提高, 它也变得更加复杂。回到我们分叉的时候,我们没有 必须为每个连接管理一个缓冲区:我们只有一个单独的 为每个进程分配堆栈缓冲区。我们不需要明确 跟踪每个套接字是读还是写: 我们在代码中的位置。而且我们不需要结构来跟踪如何 每个操作的大部分已完成:我们只是使用循环和堆栈 变量。

此外,如果您对Windows上的网络有丰富的经验, 您会意识到Libevent可能并不理想 如上例所示使用时的性能。在Windows上 快速无阻塞IO的方法不是使用类似select()的界面: 这是通过使用IOCP(IO完成端口)API。不像所有 快速联网API,当套接字时,IOCP不会警告您的程序 是 准备 然后您的程序必须执行的操作。 相反,该程序告诉Windows网络堆栈 开始 一个 网络操作,IOCP告诉程序操作何时 完成。

幸运的是,Libevent 2“ bufferevents”界面解决了这两个问题 这些问题:它使程序更易于编写和提供 Libevent可以在Windows上有效实现的界面 和 在Unix上。

这是最后一次使用bufferevents API的ROT13服务器。

// 示例:带有Libevent的更简单的ROT13服务器
/* For sockaddr_in */
#include <netinet/in.h>
/* For socket functions */
#include <sys/socket.h>
/* For fcntl */
#include <fcntl.h>

#include <event2/event.h>
#include <event2/buffer.h>
#include <event2/bufferevent.h>

#include <assert.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>

#define MAX_LINE 16384

void do_read(evutil_socket_t fd, short events, void *arg);
void do_write(evutil_socket_t fd, short events, void *arg);

char
rot13_char(char c)
{
    /* ROT13字符转换 */
    if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))
        return c + 13;
    else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))
        return c - 13;
    else
        return c;
}

void
readcb(struct bufferevent *bev, void *ctx)
{
    struct evbuffer *input, *output;
    char *line;
    size_t n;
    int i;

    // 获取输入缓冲区和输出缓冲区
    input = bufferevent_get_input(bev);
    output = bufferevent_get_output(bev);

    // 从输入缓冲区读取一行数据并进行ROT13转换
    while ((line = evbuffer_readln(input, &n, EVBUFFER_EOL_LF))) {
        for (i = 0; i < n; ++i)
            line[i] = rot13_char(line[i]);
        evbuffer_add(output, line, n); // 将转换后的数据添加到输出缓冲区
        evbuffer_add(output, "\n", 1); // 添加换行符
        free(line); // 释放读取的行
    }

    // 如果输入缓冲区长度超过最大值,处理剩余数据
    if (evbuffer_get_length(input) >= MAX_LINE) {
        char buf[1024];
        while (evbuffer_get_length(input)) {
            int n = evbuffer_remove(input, buf, sizeof(buf));
            for (i = 0; i < n; ++i)
                buf[i] = rot13_char(buf[i]);
            evbuffer_add(output, buf, n);
        }
        evbuffer_add(output, "\n", 1);
    }
}

void
errorcb(struct bufferevent *bev, short error, void *ctx)
{
    if (error & BEV_EVENT_EOF) {
        /* 连接已关闭,进行清理操作 */
    } else if (error & BEV_EVENT_ERROR) {
        /* 检查errno以确定发生的错误 */
    } else if (error & BEV_EVENT_TIMEOUT) {
        /* 处理超时事件 */
    }
    bufferevent_free(bev); // 释放bufferevent
}

void
do_accept(evutil_socket_t listener, short event, void *arg)
{
    struct event_base *base = arg;
    struct sockaddr_storage ss;
    socklen_t slen = sizeof(ss);
    int fd = accept(listener, (struct sockaddr*)&ss, &slen);
    if (fd < 0) {
        perror("accept");
    } else if (fd > FD_SETSIZE) {
        close(fd);
    } else {
        struct bufferevent *bev;
        evutil_make_socket_nonblocking(fd); // 设置套接字为非阻塞
        bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE); // 创建bufferevent
        bufferevent_setcb(bev, readcb, NULL, errorcb, NULL); // 设置回调函数
        bufferevent_setwatermark(bev, EV_READ, 0, MAX_LINE); // 设置读取水位线
        bufferevent_enable(bev, EV_READ|EV_WRITE); // 启用读写事件
    }
}

void
run(void)
{
    evutil_socket_t listener;
    struct sockaddr_in sin;
    struct event_base *base;
    struct event *listener_event;

    base = event_base_new(); // 创建事件循环
    if (!base)
        return;

    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = 0;
    sin.sin_port = htons(40713);

    listener = socket(AF_INET, SOCK_STREAM, 0);
    evutil_make_socket_nonblocking(listener); // 设置监听套接字为非阻塞

#ifndef WIN32
    {
        int one = 1;
        setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); // 设置SO_REUSEADDR选项
    }
#endif

    if (bind(listener, (struct sockaddr*)&sin, sizeof(sin)) < 0) {
        perror("bind");
        return;
    }

    if (listen(listener, 16)<0) {
        perror("listen");
        return;
    }

    // 创建监听事件,绑定到listener,回调函数为do_accept
    listener_event = event_new(base, listener, EV_READ|EV_PERSIST, do_accept, (void*)base);
    event_add(listener_event, NULL); // 添加监听事件

    event_base_dispatch(base); // 开始事件循环
}

int
main(int c, char **v)
{
    setvbuf(stdout, NULL, _IONBF, 0);

    run();
    return 0;
}

所有这些真的有多有效?

XXXX在此处写一个效率部分。自由主义基准 页面真的过时了。


Preliminaries 预备事项

Libevent from 10,000 feet

Libevent是用于编写快速便携式非阻塞IO的库。它的 设计目标是:

使用Libevent编写的程序应在所有 Libevent 支持的平台。即使没有 好 执行非阻塞IO的方法,Libevent应该支持 方法,以便您的程序可以在受限的环境中运行。

Libevent尝试使用最快的可用非阻塞IO 每个平台上的实现,而不是引入太多 这样做的开销。

Libevent旨在与需要 有成千上万个活动插座。

尽可能以最自然的方式编写程序 Libevent应该是稳定,可移植的方式。

Libevent分为以下几部分:

通用功能可抽象出两者之间的差异 不同平台的网络实现。

这是Libevent的心脏。它提供了抽象的API 各种特定于平台的,基于事件的非阻塞IO后端。 它可以让您知道何时准备好读或写套接字, 基本的超时功能,并检测OS信号。

这些功能提供了更方便的包装 Libevent基于事件的核心。他们让您的申请请求 缓冲读写,而不是在何时通知您 插座已准备就绪,它们会在IO实际拥有时通知您 发生了。

bufferevent 接口支持多种后端实现(backends),以便能够利用不同操作系统提供的更高效的非阻塞 I/O(nonblocking IO)机制。例如,在 Windows 系统上,它可以利用 IOCP API(Input/Output Completion Ports,输入/输出完成端口)来实现更快的非阻塞 I/O 操作。

一个简单的HTTP客户端/服务器实现。

一个简单的DNS客户端/服务器实现。

一个简单的RPC实现

The Libraries

默认libevent会安装以下库的部分

所有核心事件和缓冲区功能。这个库包含所有event_base,evbuffer,bufferevent, 和实用程序功能。

该库定义了特定于协议的功能, 您可能想要也可能不需要您的应用程序,包括HTTP,DNS, 和RPC。

该库的存在是出于历史原因;它包含 libevent_core和libevent_extra的内容。你不应该 用它;它可能会在将来的Libevent版本中消失。

以下库仅安装在某些平台上:

该库基于以下内容添加了线程和锁定实现 pthreads便携式线程库。它与 libevent_core,因此您无需链接到线程 除非您是,否则请使用Libevent 实际上 在 多线程方式。

该库为使用 bufferevents和OpenSSL库。它与 libevent_core,因此您无需链接到OpenSSL 除非您是,否则请使用Libevent 实际上 使用加密 连接。

The Headers

所有Libevent头文件会,装在 event2 目录,主要有三个类别:

API 头文件是定义 Libevent 当前公共接口的头文件。这些头文件没有特殊的后缀。

兼容性头文件包含已弃用函数的定义。除非你正在将程序从旧版本的 Libevent 移植过来,否则不应该包含这些头文件。

这些头文件定义了布局相对不稳定的结构体。其中一些是为了让你能够快速访问结构体的组件而暴露的;另一些则是出于历史原因而暴露的。直接依赖这些头文件中的结构体可能会破坏你的程序与其他版本 Libevent 的二进制兼容性,有时会导致难以调试的问题。这些头文件的后缀为 “_struct.h”。

(此外,还有一些没有 event2 目录的旧版本 Libevent 头文件。请参阅下面的“如果你必须使用旧版本的 Libevent”部分。)

如果您必须使用旧版本的Libevent

Libevent 2.0 对其 API 进行了修订,使其总体上更加合理且不易出错。如果可能的话,你应该编写使用 Libevent 2.0 API 的新程序。但有时你可能需要使用旧的 API,例如为了更新现有的应用程序,或者在某些情况下支持无法安装 Libevent 2.0 或更高版本的环境。

旧版本的 Libevent 头文件较少,并且不会将它们安装在 “event2” 目录下:

OLD HEADER… …REPLACED BY CURRENT HEADERS
event.h event2/event.h, event2/buffer.h event2/bufferevent.h event2/tag.h
evdns.h event2/dns*.h
evhttp.h event2/http*.h
evrpc.h event2/rpc*.h
evutil.h event2/util*.h

在 Libevent 2.0 及更高版本中,旧的头文件仍然作为新头文件的包装器存在。

关于使用旧版本的一些其他注意事项:

下面的各个部分将讨论代码库中特定领域可能遇到的已废弃 API。


配置Libevent库

Setting up the Libevent library

Libevent中的日志消息

Log messages in Libevent.

Libevent 可以记录内部错误和警告。如果在编译时启用了日志支持,它还会记录调试消息。默认情况下,这些日志消息会输出到标准错误(stderr)。你可以通过提供自定义的日志记录函数来覆盖(改变)这种默认行为。

接口:

#define EVENT_LOG_DEBUG 0
#define EVENT_LOG_MSG   1
#define EVENT_LOG_WARN  2
#define EVENT_LOG_ERR   3

/* Deprecated; see note at the end of this section */
#define _EVENT_LOG_DEBUG EVENT_LOG_DEBUG
#define _EVENT_LOG_MSG   EVENT_LOG_MSG
#define _EVENT_LOG_WARN  EVENT_LOG_WARN
#define _EVENT_LOG_ERR   EVENT_LOG_ERR

typedef void (*event_log_cb)(int severity, const char *msg);

void event_set_log_callback(event_log_cb cb);

要覆盖 Libevent 的日志记录行为,可以编写一个符合 event_log_cb 签名的自定义函数,并将其作为参数传递给 event_set_log_callback()。每当 Libevent 需要记录日志消息时,它会将消息传递给你提供的函数。如果想让 Libevent 恢复默认行为,可以再次调用 event_set_log_callback(),并将参数设置为 NULL

样例:

#include <event2/event.h>
#include <stdio.h>

static void discard_cb(int severity, const char *msg)
{
    /* This callback does nothing. */
}

static FILE *logfile = NULL;
static void write_to_file_cb(int severity, const char *msg)
{
    const char *s;
    if (!logfile)
        return;
    switch (severity) {
        case _EVENT_LOG_DEBUG: s = "debug"; break;
        case _EVENT_LOG_MSG:   s = "msg";   break;
        case _EVENT_LOG_WARN:  s = "warn";  break;
        case _EVENT_LOG_ERR:   s = "error"; break;
        default:               s = "?";     break; /* never reached */
    }
    fprintf(logfile, "[%s] %s\n", s, msg);
}

/* Turn off all logging from Libevent. */
void suppress_logging(void)
{
    event_set_log_callback(discard_cb);
}

/* Redirect all Libevent log messages to the C stdio file 'f'. */
void set_logfile(FILE *f)
{
    logfile = f;
    event_set_log_callback(write_to_file_cb);
}

注意:

在用户提供的 event_log_cb 回调函数中调用 Libevent 函数是不安全的!例如,如果你尝试编写一个日志回调函数,使用 bufferevent 将警告消息发送到网络套接字,你可能会遇到奇怪且难以诊断的错误。这种限制在 Libevent 的未来版本中可能会对某些函数移除。

通常情况下,调试日志是未启用的,也不会发送到日志回调中。如果 Libevent 被构建为支持调试日志,你可以手动启用它们。


接口

#define EVENT_DBG_NONE 0
#define EVENT_DBG_ALL 0xffffffffu

void event_enable_debug_logging(ev_uint32_t which);

调试日志非常详细,在大多数情况下不一定有用。调用event_enable_debug_logging() 并传入 EVENT_DBG_NONE 会恢复默认行为;传入 EVENT_DBG_ALL 会启用所有支持的调试日志。未来版本可能会支持更细粒度的选项。

这些函数声明在 <event2/event.h> 中。它们首次出现在 Libevent 1.0c 中,除了 event_enable_debug_logging(),它首次出现在 Libevent 2.1.1-alpha 中。

兼容性注意:

Libevent 2.0.19-stable 之前,EVENT_LOG_* 宏的名称以下划线开头:_EVENT_LOG_DEBUG_EVENT_LOG_MSG_EVENT_LOG_WARN_EVENT_LOG_ERR。这些旧名称已被弃用,仅应用于与 Libevent 2.0.18-stable 及更早版本的向后兼容。它们可能会在 Libevent 的未来版本中被移除。

处理致命错误

“Handling fatal errors”的意思是“处理致命错误”。这通常指在程序运行过程中遇到无法恢复的严重错误时,采取适当的措施(如记录日志、清理资源、终止程序等)来处理这些错误的过程。

当 Libevent 检测到不可恢复的内部错误(例如数据结构损坏)时,其默认行为是调用 exit()abort() 以终止当前运行的进程。这类错误几乎总是意味着某处存在一个 bug:要么在你的代码中,要么在 Libevent 本身。

如果你希望你的应用程序以更优雅的方式处理致命错误,可以通过提供一个函数来覆盖 Libevent 的默认行为,让 Libevent 调用该函数而不是直接退出。

typedef void (*event_fatal_cb)(int err);
void event_set_fatal_callback(event_fatal_cb cb);

要使用这些函数,首先需要定义一个新函数,当 Libevent 遇到致命错误时会调用该函数,然后将其传递给 event_set_fatal_callback()。之后,如果 Libevent 遇到致命错误,它将调用你提供的函数。

你的函数不应将控制权返回给 Libevent;这样做可能会导致未定义行为,并且 Libevent 可能仍会退出以避免崩溃。一旦你的函数被调用,你不应再调用任何其他 Libevent 函数。

这些函数声明在 <event2/event.h> 中。它们首次出现在 Libevent 2.0.3-alpha 中。

内存管理 Memory management

默认情况下,Libevent使用C的内存管理函数分配管理堆内存。如果你拥有自己的内存管理方式可以替换malloc、realloc、free。你也许是拥有更高效的内存分配器或者拥有可视化监控的内存分配器用来观察内存泄漏。

void event_set_mem_functions(void *(*malloc_fn)(size_t sz),
                             void *(*realloc_fn)(void *ptr, size_t sz),
                             void (*free_fn)(void *ptr));

下面是个简单的例子,有必要时是需要加锁的如果需要在多线程程序下。

#include <event2/event.h>
#include <sys/types.h>
#include <stdlib.h>

/* 该联合的目的是确保其大小与包含的所有类型中最大的类型一致。 */
union alignment {
    size_t sz;   // 用于存储大小的类型
    void *ptr;   // 用于存储指针的类型
    double dbl;  // 用于存储双精度浮点数的类型
};
/* 我们需要确保返回的所有内存都具有正确的对齐方式,
   以便能够存储任何类型的数据,包括双精度浮点数。 */
#define ALIGNMENT sizeof(union alignment)

/* 我们需要对指针进行强制转换为 char* 类型,以便调整它们;
   对 void* 类型进行算术运算在标准中是不被支持的。 */
#define OUTPTR(ptr) (((char*)ptr)+ALIGNMENT) // 调整指针以跳过对齐信息
#define INPTR(ptr) (((char*)ptr)-ALIGNMENT) // 调整指针以返回对齐信息的位置

/* 记录总分配的内存大小 */
static size_t total_allocated = 0;

/* 自定义 malloc 函数,用于分配内存并记录分配的大小 */
static void *replacement_malloc(size_t sz)
{
    // 分配内存,额外分配 ALIGNMENT 字节用于存储对齐信息
    void *chunk = malloc(sz + ALIGNMENT);
    if (!chunk) return chunk; // 如果分配失败,返回 NULL
    total_allocated += sz;    // 更新总分配的内存大小
    *(size_t*)chunk = sz;     // 在内存的起始位置存储分配的大小
    return OUTPTR(chunk);     // 返回调整后的指针
}

/* 自定义 realloc 函数,用于重新分配内存并更新记录 */
static void *replacement_realloc(void *ptr, size_t sz)
{
    size_t old_size = 0;
    if (ptr) {
        ptr = INPTR(ptr);          // 获取原始指针
        old_size = *(size_t*)ptr; // 获取原始分配的大小
    }
    ptr = realloc(ptr, sz + ALIGNMENT); // 重新分配内存
    if (!ptr)
        return NULL; // 如果重新分配失败,返回 NULL
    *(size_t*)ptr = sz; // 更新新的分配大小
    total_allocated = total_allocated - old_size + sz; // 更新总分配的内存大小
    return OUTPTR(ptr); // 返回调整后的指针
}

/* 自定义 free 函数,用于释放内存并更新记录 */
static void replacement_free(void *ptr)
{
    ptr = INPTR(ptr);               // 获取原始指针
    total_allocated -= *(size_t*)ptr; // 减去释放的内存大小
    free(ptr);                      // 释放内存
}

/* 启动自定义内存管理函数,用于统计分配的内存大小 */
void start_counting_bytes(void)
{
    event_set_mem_functions(replacement_malloc,  // 设置自定义 malloc 函数
                            replacement_realloc, // 设置自定义 realloc 函数
                            replacement_free);   // 设置自定义 free 函数
}

注意事项:

event_set_mem_functions() 函数声明在 <event2/event.h>. 它首次出现在 Libevent 2.0.1-alpha.

event_set_mem_functions 函数可以在构建libevent项目时就禁用支持,如果库里禁用了你的程序调用了,那么将会无法编译通过或链接失败。在Libevent2.0.2-alpha及其以后的版本你可以使用 宏 EVENT_SET_MEM_FUNCTIONS_IMPLEMENTED 检测event_set_mem_functions是否可用。

锁与线程

如果你正在编写多线程程序,你可能已经知道,同时从多个线程访问相同的数据并不总是安全的。

Libevent 的结构在多线程环境下通常有三种工作方式:

  1. 某些结构本质上是单线程的:永远不要在多个线程中同时使用它们。
  2. 某些结构是可选加锁的:你可以告诉 Libevent 是否需要对每个对象进行加锁,以便在多个线程中同时使用它们。
  3. 某些结构始终是加锁的:如果 Libevent 启用了锁支持,那么这些结构始终可以安全地在多个线程中同时使用。

要在 Libevent 中启用锁支持,你必须告诉 Libevent 使用哪些锁定函数。你需要在调用任何分配需要在线程之间共享的结构的 Libevent 函数之前完成此操作。

如果你使用的是 pthreads 库或原生的 Windows 线程代码,那么你很幸运。Libevent 提供了预定义的函数,可以为你设置正确的 pthreads 或 Windows 锁定函数。

#ifdef WIN32
int evthread_use_windows_threads(void);
#define EVTHREAD_USE_WINDOWS_THREADS_IMPLEMENTED
#endif
#ifdef _EVENT_HAVE_PTHREADS
int evthread_use_pthreads(void);
#define EVTHREAD_USE_PTHREADS_IMPLEMENTED
#endif

这两个函数在成功时返回 0,失败时返回 -1。

如果你需要使用其他线程库,那么你需要做更多的工作。你需要使用你的线程库实现以下功能:

锁相关

条件变量相关

线程相关

然后,你需要通过 evthread_set_lock_callbacksevthread_set_id_callback 接口将这些函数告知 Libevent。

// 线程锁操作模式
#define EVTHREAD_WRITE  0x04   // 写操作
#define EVTHREAD_READ   0x08   // 读操作
#define EVTHREAD_TRY    0x10   // 尝试获取锁(非阻塞)

// 锁类型
#define EVTHREAD_LOCKTYPE_RECURSIVE 1   // 递归锁
#define EVTHREAD_LOCKTYPE_READWRITE 2   // 读写锁

// 锁API版本号
#define EVTHREAD_LOCK_API_VERSION 1

// 线程锁回调结构体
struct evthread_lock_callbacks {
    int lock_api_version;           // API版本号
    unsigned supported_locktypes;   // 支持的锁类型
    void *(*alloc)(unsigned locktype); // 分配锁
    void (*free)(void *lock, unsigned locktype); // 释放锁
    int (*lock)(unsigned mode, void *lock);      // 加锁
    int (*unlock)(unsigned mode, void *lock);    // 解锁
};

// 设置线程锁回调
int evthread_set_lock_callbacks(const struct evthread_lock_callbacks *);

// 设置线程ID回调
void evthread_set_id_callback(unsigned long (*id_fn)(void));

// 条件变量回调结构体
struct evthread_condition_callbacks {
     int condition_api_version; // API版本号
     void *(*alloc_condition)(unsigned condtype); // 分配条件变量
     void (*free_condition)(void *cond);          // 释放条件变量
     int (*signal_condition)(void *cond, int broadcast); // 发送信号(broadcast为1时广播)
     int (*wait_condition)(void *cond, void *lock,
         const struct timeval *timeout); // 等待条件变量,带超时
};

// 设置条件变量回调
int evthread_set_condition_callbacks(
     const struct evthread_condition_callbacks *);

evthread_lock_callbacks 结构体用于描述你的锁回调及其能力。对于上述版本,lock_api_version 字段必须设置为 EVTHREAD_LOCK_API_VERSIONsupported_locktypes 字段必须设置为 EVTHREAD_LOCKTYPE_* 常量的位掩码,用于描述你支持哪些锁类型。(截至 2.0.4-alpha,EVTHREAD_LOCK_RECURSIVE 是必须的,EVTHREAD_LOCK_READWRITE 未被使用。)alloc 函数必须返回指定类型的新锁。free 函数必须释放指定类型锁所占用的所有资源。lock 函数必须尝试以指定模式获取锁,成功时返回 0,失败时返回非 0。unlock 函数必须尝试解锁,成功时返回 0,失败时返回非 0。

已识别的锁类型有:

已识别的锁模式有:

id_fn 参数必须是一个返回无符号长整型的函数,用于标识调用该函数的线程。对于同一个线程,它必须始终返回相同的数值;对于同时运行的不同线程,不能返回相同的数值。

evthread_condition_callbacks 结构体用于描述与条件变量相关的回调。对于上述版本,condition_api_version 字段必须设置为 EVTHREAD_CONDITION_API_VERSIONalloc_condition 函数必须返回一个指向新条件变量的指针,参数为 0。free_condition 函数必须释放条件变量占用的存储和资源。wait_condition 函数有三个参数:由 alloc_condition 分配的条件变量、由你提供的 evthread_lock_callbacks.alloc 分配的锁,以及一个可选的超时时间。调用该函数时锁已被持有;该函数必须释放锁,并等待条件被信号唤醒或超时。wait_condition 出错时返回 -1,条件被信号唤醒时返回 0,超时时返回 1。在返回前,必须确保重新持有锁。最后,signal_condition 函数应唤醒一个正在等待该条件的线程(如果 broadcast 参数为 false),或唤醒所有正在等待的线程(如果 broadcast 参数为 true)。该操作只会在持有与条件变量关联的锁时进行。

关于条件变量的更多信息,请参考 pthreads 的 pthread_cond_* 函数文档,或 Windows 的 CONDITION_VARIABLE 函数文档。

For an example of how to use these functions, see evthread_pthread.c and
evthread_win32.c in the Libevent source distribution.

本节中的函数声明在 <event2/thread.h> 中。它们大多数首次出现在 Libevent 2.0.4-alpha 版本。从 2.0.1-alpha 到 2.0.3-alpha 的 Libevent 版本使用的是较旧的接口来设置锁定函数。event_use_pthreads() 函数要求你的程序链接 event_pthreads 库。

条件变量相关的函数是在 Libevent 2.0.7-rc 版本中新引入的;它们的添加是为了解决一些原本难以解决的死锁问题。

Libevent 可以在构建时禁用锁支持。如果禁用了锁支持,那么使用上述线程相关函数编译的程序将无法运行。

调试锁的使用

为了帮助调试锁的使用,Libevent 提供了一个可选的“锁调试”功能,它会对锁操作进行包装,以捕捉常见的锁错误,包括:

如果发生这些锁错误,Libevent 会通过断言失败(assertion failure)退出。


接口

void evthread_enable_lock_debugging(void);
#define evthread_enable_lock_debuging() evthread_enable_lock_debugging()

注意

此函数必须在创建或使用任何锁之前调用。为安全起见,建议在设置线程相关函数后立即调用它。

该函数首次出现在 Libevent 2.0.4-alpha 版本,名称拼写为 “evthread_enable_lock_debuging()”(有拼写错误)。在 2.1.2-alpha 版本中修正为 “evthread_enable_lock_debugging()”,目前两个名称都被支持

调试事件的使用

Libevent 可以检测并报告一些常见的事件使用错误,包括:

为了跟踪哪些事件已被初始化,Libevent 需要额外使用内存和 CPU,因此只有在调试程序时才应启用调试模式。


接口

void event_enable_debug_mode(void);

此函数必须在创建任何 event_base 之前调用。

在调试模式下,如果你的程序使用了大量通过 event_assign() 创建的事件(而不是 event_new()),可能会耗尽内存。这是因为 Libevent 无法判断通过 event_assign() 创建的事件何时不再被使用。(对于 event_new() 创建的事件,在调用 event_free() 时 Libevent 能检测到事件已失效。)如果你想在调试时避免内存耗尽,可以显式告诉 Libevent 某些事件不再被视为已分配:

void event_debug_unassign(struct event *ev);

当未启用调试时,调用 event_debug_unassign() 没有效果。

#include <event2/event.h>
#include <event2/event_struct.h>
#include <stdlib.h>

// 回调函数,当事件触发时被调用
void cb(evutil_socket_t fd, short what, void *ptr)
{
    // 对于堆上分配的事件,回调参数为 NULL
    // 对于栈上分配的事件,回调参数为事件本身
    struct event *ev = ptr;

    // 如果 ev 不为 NULL,则调用 event_debug_unassign
    // 通知 Libevent 该事件不再被视为已分配(仅在调试模式下有效)
    if (ev)
        event_debug_unassign(ev);
}

/*
 * 一个简单的主循环,等待 fd1 和 fd2 都准备好读取
 * fd1 和 fd2 是两个文件描述符
 * debug_mode 为 1 时启用事件调试模式
 */
void mainloop(evutil_socket_t fd1, evutil_socket_t fd2, int debug_mode)
{
    struct event_base *base;              // 事件处理主循环对象
    struct event event_on_stack, *event_on_heap; // 分别用于栈上和堆上分配的事件

    // 如果启用调试模式,则调用 event_enable_debug_mode
    // 必须在创建 event_base 之前调用
    if (debug_mode)
       event_enable_debug_mode();

    // 创建事件主循环对象
    base = event_base_new();

    // 在堆上创建一个新的事件,监听 fd1 的可读事件,回调函数为 cb,参数为 NULL
    event_on_heap = event_new(base, fd1, EV_READ, cb, NULL);

    // 在栈上分配一个事件,监听 fd2 的可读事件,回调函数为 cb,参数为事件本身
    event_assign(&event_on_stack, base, fd2, EV_READ, cb, &event_on_stack);

    // 将两个事件添加到事件主循环中
    event_add(event_on_heap, NULL);
    event_add(&event_on_stack, NULL);

    // 启动事件主循环,等待事件发生
    event_base_dispatch(base);

    // 释放堆上分配的事件
    event_free(event_on_heap);
    // 释放事件主循环对象
    event_base_free(base);
}

详细事件调试功能只能在编译时通过设置 CFLAGS 环境变量 -DUSE_DEBUG 启用。启用该标志后,任何基于 Libevent 编译的程序都会输出非常详细的日志,记录后端的底层活动。这些日志包括但不限于:

此功能无法通过 API 调用启用或禁用,因此只能用于开发版本。

这些调试函数是在 Libevent 2.0.4-alpha 版本中添加的。

检测 Libevent 的版本

接口

#define LIBEVENT_VERSION_NUMBER 0x02000300
#define LIBEVENT_VERSION "2.0.3-alpha"
const char *event_get_version(void);
ev_uint32_t event_get_version_number(void);

这些宏提供了 Libevent 库的编译时版本信息;相关函数则返回运行时的版本信息。注意,如果你的程序是动态链接到 Libevent 的,这两个版本信息可能会不同。

你可以通过两种格式获取 Libevent 的版本:一种是适合向用户展示的字符串格式,另一种是适合数值比较的 4 字节整数格式。整数格式的高字节表示主版本号,第二字节表示次版本号,第三字节表示补丁版本号,最低字节用于表示发布状态(0 表示正式发布,非 0 表示某个正式版本之后的开发版本)。

因此,已发布的 Libevent 2.0.1-alpha 的版本号为 [02 00 01 00],即 0x02000100。介于 2.0.1-alpha 和 2.0.2-alpha 之间的开发版本可能有版本号 [02 00 01 08],即 0x02000108。

编译时检查

#include <event2/event.h>

#if !defined(LIBEVENT_VERSION_NUMBER) || LIBEVENT_VERSION_NUMBER < 0x02000100
#error "This version of Libevent is not supported; Get 2.0.1-alpha or later."
#endif

int
make_sandwich(void)
{
        /* Let's suppose that Libevent 6.0.5 introduces a make-me-a
           sandwich function. */
#if LIBEVENT_VERSION_NUMBER >= 0x06000500
        evutil_make_me_a_sandwich();
        return 0;
#else
        return -1;
#endif
}

运行时检查

#include <event2/event.h>
#include <string.h>

int
check_for_old_version(void)
{
    const char *v = event_get_version();
    /* This is a dumb way to do it, but it is the only thing that works
       before Libevent 2.0. */
    if (!strncmp(v, "0.", 2) ||
        !strncmp(v, "1.1", 3) ||
        !strncmp(v, "1.2", 3) ||
        !strncmp(v, "1.3", 3)) {

        printf("Your version of Libevent is very old.  If you run into bugs,"
               " consider upgrading.\n");
        return -1;
    } else {
        printf("Running with Libevent version %s\n", v);
        return 0;
    }
}

int
check_version_match(void)
{
    ev_uint32_t v_compile, v_run;
    v_compile = LIBEVENT_VERSION_NUMBER;
    v_run = event_get_version_number();
    if ((v_compile & 0xffff0000) != (v_run & 0xffff0000)) {
        printf("Running with a Libevent version (%s) very different from the "
               "one we were built with (%s).\n", event_get_version(),
               LIBEVENT_VERSION);
        return -1;
    }
    return 0;
}

The macros and functions in this section are defined in <event2/event.h>. The event_get_version() function first appeared in Libevent 1.0c; the others first appeared in Libevent 2.0.1-alpha.

释放全局 Libevent 结构体

“Freeing global Libevent structures”的意思是“释放全局 Libevent 结构体”。
指的是在程序结束或不再需要时,释放 Libevent 全局分配的资源或结构体,以避免内存泄漏。

即使你已经释放了所有通过 Libevent 分配的对象,仍然会有一些全局分配的结构体残留。通常这不是问题:进程退出时,这些结构体都会被自动清理。但这些结构体可能会让某些调试工具误以为 Libevent 存在资源泄漏。如果你需要确保 Libevent 已经释放了所有内部的全局数据结构,可以调用:


接口

void libevent_global_shutdown(void);

此函数不会释放任何由 Libevent 函数返回给你的结构体。如果你想在退出前释放所有资源,需要自己手动释放所有事件、event_base、bufferevent 等对象。

调用 libevent_global_shutdown() 后,其他 Libevent 函数的行为将变得不可预测;因此,除了作为程序中最后一个调用的 Libevent 函数外,不要调用它。唯一的例外是 libevent_global_shutdown() 是幂等的:即使已经调用过多次,也不会有问题。

该函数声明在 <event2/event.h> 中,并在 Libevent 2.1.1-alpha 版本中引入。


创建 event_base

在你使用任何有用的 Libevent 函数之前,需要先分配一个或多个 event_base 结构体。每个 event_base 结构体都保存了一组事件,并可以通过轮询来判断哪些事件处于激活状态。

如果 event_base 被设置为使用锁机制,那么它可以在多个线程之间安全访问。但它的事件循环只能在单个线程中运行。如果你希望多个线程同时进行 IO 轮询,就需要为每个线程分配一个独立的 event_base

未来版本的Libevent可能会支持在多个线程中运行事件的 event_base

每个 event_base 都有一个“方法”(method),即用于判断哪些事件已就绪的后端实现。已识别的方法包括:

用户可以通过环境变量禁用特定的后端。例如,如果你想关闭kqueue后端,可以设置环境变量 EVENT_NOKQUEUE,其他后端以此类推。如果你想在程序内部关闭某些后端,请参考下面关于 event_config_avoid_method() 的说明。

设置默认的 event_base

Setting up a default event_base

event_base_new() 函数会分配并返回一个带有默认设置的新 event_base。它会检查环境变量,并返回一个指向新 event_base 的指针。如果发生错误,则返回 NULL。

在多种方法中选择时,它会选用操作系统支持的最快的方法。


接口

struct event_base *event_base_new(void);

对于大多数程序来说,这就是你所需要的全部。

event_base_new() 函数声明在 event2/event.h 中,它首次出现在 Libevent 1.4.3 版本。

设置复杂的 event_base

Setting up a complicated event_base

如果你想获取的 event_base 类型有更多控制,需要使用 event_config, event_config 是一个不透明结构体,用于保存你对 event_base的偏好信息,当你需要创建 event_base 时,将 event_config 作为参数传递给 event_base_new_with_config()


struct event_config *event_config_new(void);
struct event_base *event_base_new_with_config(const struct event_config *cfg);
void event_config_free(struct event_config *cfg);

要使用这些函数分配 event_base,首先调用 event_config_new() 分配一个新的 event_config。然后,调用其他函数设置 event_config 的相关需求。最后,调用 event_base_new_with_config() 获取新的 event_base。完成后,可以使用 event_config_free() 释放 event_config

// 禁用指定的后端方法(如 "select"、"epoll" 等)
// cfg: event_config 配置对象
// method: 要禁用的方法名称
// 返回值:成功返回0,失败返回-1
int event_config_avoid_method(struct event_config *cfg, const char *method);

// 事件方法特性枚举
enum event_method_feature {
    EV_FEATURE_ET = 0x01,    // 支持边缘触发(Edge Triggered)
    EV_FEATURE_O1 = 0x02,    // 要求后端方法在添加、删除单个事件或单个事件变为激活时,操作复杂度为 O(1)。
    EV_FEATURE_FDS = 0x04,   // 要求后端方法支持任意类型的文件描述符,而不仅仅是套接字。
};

// 要求 event_base 必须支持指定的特性
// cfg: event_config 配置对象
// feature: 所需的特性(可以按位或组合)
// 返回值:成功返回0,失败返回-1
int event_config_require_features(struct event_config *cfg,
                                  enum event_method_feature feature);

// event_base 配置标志枚举
enum event_base_config_flag {
    // 不为 event_base 分配锁。设置此选项可以节省一些加锁和解锁的时间,但会导致 event_base 在多线程环境下不安全且不可用。
    EVENT_BASE_FLAG_NOLOCK = 0x01,
    // 在选择后端方法时不检查 EVENT_* 环境变量。使用此标志前请谨慎考虑:它可能会让用户更难调试你的程序与 Libevent 之间的交互。
    EVENT_BASE_FLAG_IGNORE_ENV = 0x02,
    // 仅适用于 Windows。该标志让 Libevent 在启动时启用所有必要的 IOCP 分发逻辑,而不是按需启用。
    EVENT_BASE_FLAG_STARTUP_IOCP = 0x04,
    // 每次事件循环准备运行超时回调时都检查当前时间,而不是每次超时回调后再检查。这可能会比你预期的消耗更多 CPU,请注意!
    EVENT_BASE_FLAG_NO_CACHE_TIME = 0x08,
    // 告诉 Libevent,如果决定使用 epoll 后端,可以安全地使用更快的 "changelist" 优化。epoll-changelist 后端可以避免在同一个 fd 状态在两次分发函数调用之间被多次修改时产生不必要的系统调用,但如果你给 Libevent 传递了通过 dup() 或其变体克隆的 fd,可能会触发内核 bug 并导致错误结果。如果你使用的不是 epoll 后端,此标志无效。你也可以通过设置环境变量 `EVENT_EPOLL_USE_CHANGELIST` 启用该选项。
    EVENT_BASE_FLAG_EPOLL_USE_CHANGELIST = 0x10,
    // 默认情况下,Libevent 会尝试使用操作系统提供的最快的定时机制。如果有一种更慢但更精确的定时机制,此标志会让 Libevent 使用该机制。如果操作系统没有这种机制,则此标志无效。启用高精度定时器
    EVENT_BASE_FLAG_PRECISE_TIMER = 0x20
};

// 设置 event_base 的配置标志
// cfg: event_config 配置对象
// flag: 配置标志(可以按位或组合)
// 返回值:成功返回0,失败返回-1
int event_config_set_flag(struct event_config *cfg,
    enum event_base_config_flag flag);

上述用于操作 event_config 的函数,成功时返回 0,失败时返回 -1。

注意:很容易设置一个 event_config,要求你的操作系统并不支持的后端。例如,截至 Libevent 2.0.1-alpha,Windows 没有 O(1) 后端,Linux 上也没有同时支持 EV_FEATURE_FDS 和 EV_FEATURE_O1 的后端。如果你的配置无法被 Libevent 满足,event_base_new_with_config() 会返回 NULL。

int event_config_set_num_cpus_hint(struct event_config *cfg, int cpus)

该函数目前仅在 Windows 使用 IOCP 时有用,但将来可能对其他平台也有用。调用此函数会告诉 event_config,它生成的 event_base 在多线程时应尽量合理利用指定数量的 CPU。注意,这只是一个提示:最终 event_base 实际使用的 CPU 数量可能比你选择的多或少。


int event_config_set_max_dispatch_interval(struct event_config *cfg,
    const struct timeval *max_interval, int max_callbacks,
    int min_priority);

此函数通过限制在检查更高优先级事件之前可调用的低优先级事件回调数量,来防止优先级反转。如果 max_interval 非空,事件循环会在每次回调后检查时间,如果已超过 max_interval,则重新扫描高优先级事件。如果 max_callbacks 为非负数,事件循环在调用了 max_callbacks 个回调后也会检查是否有更多事件。这些规则适用于 min_priority 或更高优先级的所有事件。

样例:优先选择边缘触发的后端

struct event_config *cfg;
struct event_base *base;
int i;

/* My program wants to use edge-triggered events if at all possible.  So
   I'll try to get a base twice: Once insisting on edge-triggered IO, and
   once not. */
for (i=0; i<2; ++i) {
    cfg = event_config_new();

    /* I don't like select. */
    event_config_avoid_method(cfg, "select");

    if (i == 0)
        event_config_require_features(cfg, EV_FEATURE_ET);

    base = event_base_new_with_config(cfg);
    event_config_free(cfg);
    if (base)
        break;

    /* If we get here, event_base_new_with_config() returned NULL.  If
       this is the first time around the loop, we'll try again without
       setting EV_FEATURE_ET.  If this is the second time around the
       loop, we'll give up. */
}

样例:避免优先级反转

struct event_config *cfg;
struct event_base *base;

cfg = event_config_new();
if (!cfg)
   /* 处理错误 */;

/* 我要运行两个优先级的事件。我预计某些优先级为1的事件回调会比较慢,
   所以我不希望在检查优先级为0的事件之前,超过100毫秒或执行超过5个回调。 */
struct timeval msec_100 = { 0, 100*1000 };
event_config_set_max_dispatch_interval(cfg, &msec_100, 5, 1);

base = event_base_new_with_config(cfg);
if (!base)
   /* 处理错误 */;

// 设置这个事件循环支持 2个优先级:0 和 1。
event_base_priority_init(base, 2);  // 初始化两个优先级的事件

这些函数和类型都在 <event2/event.h> 中声明。

这个章节中的其他所有内容,最早都出现在 Libevent 2.0.1-alpha 版本中。

检查 event_base 的底层method

Examining an event_base’s backend method

有时你想要看event_base实际可以获得的特定,或者它可以使用的method。

const char **event_get_supported_methods(void);

返回一个支持的method的名字数组,最后一个元素为NULL。

int i;
const char **methods = event_get_supported_methods();
printf("Starting Libevent %s.  Available methods are:\n",
    event_get_version());
for (i=0; methods[i] != NULL; ++i) {
    printf("    %s\n", methods[i]);
}

注意:这个函数会返回一个Libevent 编译时支持的后端方法列表。不过,需要注意的是:你的操作系统在实际运行时,可能并不支持这些方法中的全部。例如:你可能在某个版本的 macOS 上运行,而这个版本中 kqueue 存在严重的 bug,导致无法使用。

const char *event_base_get_method(const struct event_base *base);
enum event_method_feature event_base_get_features(const struct event_base *base);

event_base_get_method调用返回event_base实际使用使用的method。

struct event_base *base;
enum event_method_feature f;

base = event_base_new();
if (!base) {
    puts("Couldn't get an event_base!");
} else {
    printf("Using Libevent with backend method %s.",
        event_base_get_method(base));
    f = event_base_get_features(base);
    if ((f & EV_FEATURE_ET))
        printf("  Edge-triggered events are supported.");
    if ((f & EV_FEATURE_O1))
        printf("  O(1) event notification is supported.");
    if ((f & EV_FEATURE_FDS))
        printf("  All FD types are supported.");
    puts("");
}

这些函数定义在 <event2/event.h> . event_base_get_method 首次出现在 Libevent1.4.3 . 其他的首次出现在 Libevent2.0.1-alpha .

释放一个 event_base

Deallocating an event_base

当你使用完一个 event_base 后,可以使用 event_base_free() 来释放它。

void event_base_free(struct event_base *base);

这个函数不会执行以下操作:

也就是说,你需要在调用 event_base_free() 之前,手动清理所有事件和资源,否则可能会造成内存泄漏或资源未释放。

event_base_free() 定义在头文件 <event2/event.h>中,它最早是在 Libevent1.2 版本中实现的。

在event_base上设置事件优先级

Setting priorities on an event_base

Libevent支持为事件设置多个优先级。不过默认情况下,event_base只支持一个优先级。你可以通过调用 event_base_priority_init 来设置优先级的数量。

int event_base_priority_init(struct event_base *base, int n_priorities);

优先级的编号从0到 n_priorities-1,数值越小表示优先级越高。

例如:设置 n_priorities = 3,那么你可以设置事件的优先级为 0(最高)、1、2(最低)。

注意事项:

查询当前优先级数量

你可以使用下面的函数查询当前 event_base 支持的优先级数:

int event_base_get_npriorities(struct event_base *base);
// 返回值等于当前配置的优先级数量,例如返回3,就说明当前可用的优先级是0、1、2

默认行为说明:

如果你不手动设置事件的优先级,默认会将事件的优先级设置为:

n_priorities / 2

也就是说,如果你设置了 4 个优先级,新建事件默认的优先级就是 2(中等偏下)。

历史版本信息

在fork()之后重新初始化 event_base

Reinitializing an event_base after fork()

并不是所有的事件后端(backend)在调用 fork() 后都能保持正常工作。因此,如果你的程序使用了 fork() 或相关系统调用来创建新进程,而且希望在子进程中继续使用原来的 event_base,你可能需要调用 event_reinit() 来重新初始化它。

int event_reinit(struct event_base *base);
struct event_base *base = event_base_new();

/* ... 向 event_base 添加一些事件 ... */

if (fork()) {
    // 父进程中
    continue_running_parent(base);  // 父进程继续运行
} else {
    // 子进程中
    event_reinit(base);             // 重新初始化 event_base
    continue_running_child(base);   // 子进程继续运行
}

注意事项:

版本信息:

event_reinit() 定义在 <event2/event.h> 中,首次出现在 Libevent 1.4.3-alpha。

过时的event_base函数

Obsolete event_base functions

在早期版本的Libevent中,event_base 强烈依赖于“当前的 event_base”的概念。

这个“当前的 event_base”是一个全局设置,在所有线程间共享。如果你忘记指定哪个 event_base,程序会使用当前的 event_base。然而,由于 event_base 并不是线程安全的,这会导致很多错误。

event_base_new() 的替代函数

在较早版本中,event_base_new()被以下函数取代

struct event_base *event_init(void);

在 Libevent 的早期版本中,一些函数会依赖于“当前的 event_base”,也就是不需要显式传递 event_base 参数。

例如:

当前函数 过时的当前event_base版本
event_base_priority_init() event_priority_init()
event_base_get_method() event_get_method()

关键点总结:

  1. event_init() 用于创建一个新的 event_base,并设置为当前的 event_base,而不需要显式传递。
  2. 早期版本的 Libevent 存在“全局 event_base”,这会导致线程安全问题。
  3. 在这些版本中,某些操作会使用当前的 event_base,即使没有显式指定。

运行事件循环

Running an event loop

运行循环

Running the loop

一旦你创建好一个event_base并注册了一些事件(如何注册后面会讲),你就可以使用 Libevent 的事件循环机制来等待并响应这些事件了。

int event_base_loop(struct event_base *base, int flags);

支持的标志位 flags

宏常量名 含义
EVLOOP_ONCE 循环只运行一次:等待事件触发,执行完激活的事件后就返回。
EVLOOP_NONBLOCK 非阻塞:不等待,只检查是否有事件立即就绪,执行回调后立即返回。
EVLOOP_NO_EXIT_ON_EMPTY 即使没有事件也不退出(适用于事件是从其他线程添加进来的场景)。

默认行为(flags=0):

当你不设置任何flag(即flags=0)时,事件循环会持续运行,知道:

循环过程伪代码

while (有事件注册,或者设置了 EVLOOP_NO_EXIT_ON_EMPTY) {
    如果设置了 EVLOOP_NONBLOCK 或已经有事件 active:
        检查是否有事件触发,如果有则标记为 active;
    否则:
        阻塞等待至少一个事件触发,并标记为 active。

    for (按优先级从高到低遍历) {
        如果当前优先级的事件被激活:
            执行这些事件的回调函数;
            break; // 不执行更低优先级的事件
    }

    如果设置了 EVLOOP_ONCE 或 EVLOOP_NONBLOCK:
        跳出循环;
}

简化调用:event_base_dispatch()

int event_base_dispatch(struct event_base *base);

这是对 event_base_loop(base, 0) 的简化调用,也就是说,它会一直运行事件循环,直到:

使用建议

停止事件循环

Stopping the loop

在事件循环运行中,你可以使用以下两个函数来提前中止循环:

int event_base_loopexit(struct event_base *base, const struct timeval *tv);
int event_base_loopbreak(struct event_base *base);
// 返回值
// 0 成功 -1 调用失败

event_base_loopexit 延迟退出循环

event_base_loopbreak 立即中断循环

注意事项:

示例1:立即退出事件循环

void cb(int sock, short what, void *arg) {
    struct event_base *base = arg;
    event_base_loopbreak(base);
}

void main_loop(struct event_base *base, evutil_socket_t watchdog_fd) {
    struct event *watchdog_event;

    watchdog_event = event_new(base, watchdog_fd, EV_READ, cb, base);
    event_add(watchdog_event, NULL);

    event_base_dispatch(base);
}

说明:当 watchdog socket 有数据可读时,调用 cb(),然后触发 loopbreak() 立即退出循环。

示例2:循环运行每10秒自动退出

void run_base_with_ticks(struct event_base *base) {
    struct timeval ten_sec = {10, 0};

    while (1) {
        event_base_loopexit(base, &ten_sec);
        event_base_dispatch(base);
        puts("Tick");
    }
}

说明:每次循环运行 10 秒后退出,然后打印一次 “Tick”,再进入下一轮。

检查循环是否是被手动停止的:

Libevent 提供了两个函数来判断循环是否被loopexit或loopbreak停止

int event_base_got_exit(struct event_base *base);
int event_base_got_break(struct event_base *base);

These functions are declared in <event2/event.h>. The event_break_loopexit() function was first implemented in Libevent 1.0c; event_break_loopbreak() was first implemented in Libevent 1.4.3.

重新检查事件

Re-checking for events

通常,Libevent 的事件主循环(event_base_loop())按以下流程运行:

  1. 检查是否有事件就绪;
  2. 执行当前优先级最高的活跃事件的回调;
  3. 再次检查事件;
  4. 重复以上流程。

event_base_loopcontinue()的用途

有时候,你可能希望在回调执行完毕后立即重新检查事件,而不是继续执行当前优先级下的其他事件。这时候你可以使用:

int event_base_loopcontinue(struct event_base *base);
// 返回值
// 0 成功 -1 失败

🧠 它是做什么的?

它的作用就像 event_base_loopbreak() 是用来中断循环那样,event_base_loopcontinue() 是用来告诉 Libevent:

“当前这个事件回调执行完后,不要继续执行其它活跃事件,而是立即重新检查事件。”

📌 示例场景

你可能在一个事件回调里动态添加了新事件,想要它马上被检查并触发,就可以使用 event_base_loopcontinue()。

⚠️ 注意事项

📅 可用版本

这个函数是从 Libevent 2.1.2-alpha 开始提供的。

#include <stdio.h>
#include <stdlib.h>
#include <event2/event.h>

// 用于演示的第二个事件
void second_event_cb(evutil_socket_t fd, short what, void *arg) {
    printf("Second event triggered!\n");
}

// 第一个事件,在它的回调中调用 event_base_loopcontinue()
void first_event_cb(evutil_socket_t fd, short what, void *arg) {
    struct event_base *base = (struct event_base *)arg;

    printf("First event triggered!\n");

    // 创建并添加第二个事件(短延迟)
    struct timeval delay = {0, 10 * 1000}; // 10ms
    struct event *second_event = evtimer_new(base, second_event_cb, NULL);
    evtimer_add(second_event, &delay);

    // 通知事件循环:当前回调执行完后,立即重新检查事件
    if (event_base_loopcontinue(base) == 0) {
        printf("Requested immediate re-check for events.\n");
    } else {
        fprintf(stderr, "Failed to request loopcontinue.\n");
    }
}

int main() {
    struct event_base *base = event_base_new();
    if (!base) {
        fprintf(stderr, "Failed to create event_base\n");
        return 1;
    }

    // 添加第一个定时器事件
    struct timeval start_delay = {0, 100 * 1000}; // 100ms
    struct event *first_event = evtimer_new(base, first_event_cb, base);
    evtimer_add(first_event, &start_delay);

    // 启动事件循环
    event_base_dispatch(base);

    event_free(first_event);
    event_base_free(base);
    return 0;
}
// First event triggered!
// Requested immediate re-check for events.
// Second event triggered!

内部时间缓存

Checking the internal time cache

🧩 为什么需要缓存时间?

在事件回调函数中,有时你想要获取当前时间。但直接调用 gettimeofday() 可能会产生系统调用的开销(在某些操作系统上比较重)。为了解决这个问题,Libevent 提供了缓存时间的方式。

📌 获取缓存时间

int event_base_gettimeofday_cached(struct event_base *base,
                                   struct timeval *tv_out);

功能说明:

📌 注意:

缓存时间是在开始执行当前这轮回调时生成的,因此随着回调执行的时间变长,时间的误差也会增大。

🔄 手动更新时间缓存

int event_base_update_cache_time(struct event_base *base);

功能说明:

🗓️ 可用版本

#include <stdio.h>
#include <stdlib.h>
#include <event2/event.h>
#include <sys/time.h>

void timer_cb(evutil_socket_t fd, short what, void *arg) {
    struct event_base *base = (struct event_base *)arg;
    struct timeval cached_time;

    // 获取 Libevent 缓存的时间
    if (event_base_gettimeofday_cached(base, &cached_time) == 0) {
        printf("Cached time (before update): %ld.%06ld\n",
               (long)cached_time.tv_sec, (long)cached_time.tv_usec);
    } else {
        fprintf(stderr, "Failed to get cached time.\n");
    }

    // 模拟一些耗时操作
    struct timeval delay = {1, 0}; // 睡 1 秒
    select(0, NULL, NULL, NULL, &delay);

    // 手动更新缓存时间
    event_base_update_cache_time(base);

    // 再次获取缓存时间
    if (event_base_gettimeofday_cached(base, &cached_time) == 0) {
        printf("Cached time (after update): %ld.%06ld\n",
               (long)cached_time.tv_sec, (long)cached_time.tv_usec);
    }
}

int main() {
    struct event_base *base;
    struct event *timer_event;
    struct timeval delay = {0, 100 * 1000}; // 100 毫秒

    base = event_base_new();
    if (!base) {
        fprintf(stderr, "Could not initialize libevent!\n");
        return 1;
    }

    // 创建一个一次性的定时器事件
    timer_event = evtimer_new(base, timer_cb, base);
    evtimer_add(timer_event, &delay);

    // 启动事件循环
    event_base_dispatch(base);

    // 清理资源
    event_free(timer_event);
    event_base_free(base);
    return 0;
}

输出event_base状态信息

Dumping the event_base status

void event_base_dump_events(struct event_base *base, FILE *f);

📌 功能说明

event_base_dump_events() 用于 调试 程序或 Libevent 的状态,它会将当前 event_base 中 所有已添加事件的状态 信息打印出来,格式可读性强,适合人工分析。

这个信息包括但不限于:

注意:这个输出格式并非稳定接口,未来版本中可能会变动。

🧠 使用场景

📅 版本可用性

#include <stdio.h>
#include <stdlib.h>
#include <event2/event.h>

// 简单的事件回调函数
void demo_cb(evutil_socket_t fd, short what, void *arg) {
    printf("Demo event triggered.\n");
}

int main() {
    struct event_base *base = event_base_new();
    if (!base) {
        fprintf(stderr, "Failed to create event base\n");
        return 1;
    }

    // 创建一个定时器事件,5秒后触发
    struct timeval delay = {5, 0};
    struct event *demo_event = evtimer_new(base, demo_cb, NULL);
    evtimer_add(demo_event, &delay);

    // 🧾 打印当前事件状态到 stdout
    printf("=== 当前 event_base 中的事件状态 ===\n");
    event_base_dump_events(base, stdout);

    // 启动事件循环
    event_base_dispatch(base);

    // 释放资源
    event_free(demo_event);
    event_base_free(base);
    return 0;
}

遍历 event_base 中的所有事件

Running a function over every event in an event_base

typedef int (*event_base_foreach_event_cb)(const struct event_base *,
                                           const struct event *, void *);

int event_base_foreach_event(struct event_base *base,
                             event_base_foreach_event_cb fn,
                             void *arg);

📌 功能说明

event_base_foreach_event() 用于 遍历一个 event_base 中所有当前处于挂起(pending)或活跃(active)状态的事件。
Libevent 会对每个事件调用一次用户提供的回调函数。

🧠 回调函数规则

回调函数的签名为:

int callback(const struct event_base *base, const struct event *ev, void *arg);
// 返回 0:继续遍历下一个事件;
// 返回 非0 值:中断遍历,立即返回该值。

⚠️ 使用限制

📅 版本可用性

该函数自 Libevent 2.1.2-alpha 起提供。

已废弃的事件循环函数

Obsolete event loop functions

📜 背景

在较早版本的 Libevent 中,事件系统使用一个全局的“当前事件主循环(current event_base)”。这意味着你不需要(也不能)手动指定 event_base 对象,所有事件都默认关联到全局的 base 上。

为了支持这种机制,一些事件循环函数提供了“无 base 参数”的版本。但从 Libevent 2.0 开始,引入了线程安全的 event_base 机制,推荐使用新的接口。

🔄 当前函数与废弃函数对照表

✅ 当前函数(推荐使用) ❌ 废弃版本(基于全局 base)
event_base_dispatch(base) event_dispatch()
event_base_loop(base, flags) event_loop(flags)
event_base_loopexit(base, tv) event_loopexit(tv)
event_base_loopbreak(base) event_loopbreak()

🧠 注意事项

✅ 推荐做法

旧代码中常见:

event_dispatch();  // 使用全局base

请替换为:

struct event_base *base = event_base_new();
event_base_dispatch(base);  // 明确指定base

Working with events

Libevent的基本工作单元:事件(event)。在libevent中最基本的操作单元就是“事件(event)”。每个事件代表一组可以触发它的条件,包括:

事件的生命周期:

事件的生命周期基本一致,主要流程如下:

  1. 初始化

    你通过某个Libevent函数设置事件并关联到一个event_base,此时事件已经“初始化”

  2. 添加(pending)

    调用 event_add(), 事件就被添加到event_base,变成“等待中(pending)”

  3. 变为活跃(active)

    当事件触发条件满足(比如文件描述符状态变化,或超时时间到了),事件就会变成“活跃状态(active)”。

  4. 调用回调函数(callback)

    一旦事件活跃,Libevent就会调用你提供的回调函数。

  5. 是否持久(persistent)

  6. 删除事件(取消等待)

    你可以通过调用 event_del() 来把一个“等待中的事件”变成“非等待”。

  7. 再次添加

    你也可以再次调用 evnet_add() 让事件重新进入pending状态。

创建事件对象

Constructing event objects

在Libevent中,要创建一个新的事件对象,可以使用:

struct event *event_new(
    struct event_base *base,
    evutil_socket_t fd,
    short what,
    event_callback_fn cb,
    void *arg
);

📘 参数说明

参数 说明
base 事件主循环对象(event_base)
fd 文件描述符,如果是 EV_TIMEOUT,可设为 -1
what 事件类型标志(下方详解)
cb 事件触发后的回调函数
arg 传入回调的用户数据指针

🔖 常见事件标志(what)

标志 含义
EV_READ 文件描述符可读时触发
EV_WRITE 文件描述符可写时触发
EV_TIMEOUT 超时后触发
EV_SIGNAL 信号事件
EV_PERSIST 事件在触发后仍然保留
EV_ET 使用边缘触发模式(需要后端支持)

🧠 重要提示

🌰 示例代码:监听两个 socket 的读写事件

#include <stdio.h>
#include <event2/event.h>

void cb_func(evutil_socket_t fd, short what, void *arg) {
    const char *info = arg;

    printf("触发事件 fd=%d:%s%s%s%s [%s]\n",
        (int)fd,
        (what & EV_TIMEOUT) ? " timeout" : "",
        (what & EV_READ)    ? " read"    : "",
        (what & EV_WRITE)   ? " write"   : "",
        (what & EV_SIGNAL)  ? " signal"  : "",
        info
    );
}

void main_loop(evutil_socket_t fd1, evutil_socket_t fd2) {
    struct event_base *base = event_base_new();

    // 创建事件对象
    struct event *ev_read = event_new(base, fd1, EV_READ | EV_PERSIST, cb_func, "读取事件");
    struct event *ev_write = event_new(base, fd2, EV_WRITE | EV_PERSIST, cb_func, "写入事件");

    // 添加事件到主循环中
    struct timeval timeout = {5, 0}; // 5 秒后触发一次(用于演示 EV_TIMEOUT)
    event_add(ev_read, &timeout); // 带超时
    event_add(ev_write, NULL);    // 没有超时

    // 启动事件循环
    event_base_dispatch(base);

    // 清理资源
    event_free(ev_read);
    event_free(ev_write);
    event_base_free(base);
}

上面函数被定义在<event2/event.h>,首次出现在Libevent 2.0.1-alpha,event_callback_fn_type 首次作为typedef出现在 Libevent 2.0.4-alpha。

Libevent 事件标志说明

The event flags

Libevent 的事件类型是通过组合一组标志(flag)来描述的。你在创建事件或回调中会遇到以下几种:

🔖 常用事件标志一览表

标志常量 含义
EV_TIMEOUT 超时事件:当设置的时间到达后触发。⚠️ 注意:在 event_new() 中指定这个标志是无效的,是否为超时事件是由你在 event_add() 时是否传入超时时间决定的。它只会在回调的 what 参数中被设置,用于表示当前事件是因超时触发的。
EV_READ 可读事件:当文件描述符可读时触发。
EV_WRITE 可写事件:当文件描述符可写时触发。
EV_SIGNAL 信号事件:用于监听系统信号(如 SIGINT、SIGTERM 等)。需配合 signal 相关 API 使用,具体见后续的信号构造方法。
EV_PERSIST 持久事件:事件触发后不会被自动移除,直到你主动调用 event_del()。默认情况下,事件在被触发一次后就会被删除。
EV_ET 边缘触发(Edge-Triggered)事件:如果底层后端支持,可以开启此标志进行边缘触发模式。⚠️ 对 EV_READ 和 EV_WRITE 的行为有影响,使用此模式时需自行确保读写操作足够彻底。

💡 小提示

🧠 示例(标志组合用法)

event_new(base, fd, EV_READ | EV_PERSIST, cb, NULL);

这个表示创建一个文件描述符可读时触发的持久事件。

关于事件持久化 EV_PERSIST

About Event Persistence

在 Libevent 中,一个事件默认是一次性的(非持久)。这意味着:

🌟 使用 EV_PERSIST 的作用

如果你在创建事件时添加了 EV_PERSIST 标志:

event_new(base, fd, EV_READ | EV_PERSIST, callback, arg);

那么该事件将变成持久事件,即:

⏱ 超时的特别行为

对于带有超时的持久事件,比如你设置了 EV_READ | EV_PERSIST 并添加了一个 5 秒的超时:

struct timeval five_sec = {5, 0};
event_add(ev, &five_sec);

那么超时将按以下规则重复发生:

🎯 示例图解

时间轴(EV_PERSIST + 5 秒 timeout)

[0s]  添加事件
[3s]  fd 可读 → 回调触发 → 5 秒计时重新开始
[7s]  fd 未可读 → 超时触发 → 回调触发 → 5 秒计时重新开始
[12s] fd 可读 → 回调触发 → 5 秒计时重新开始

✅ 回调中处理持久事件

你可以在事件回调中手动结束持久事件,用:

event_del(ev);

使用 event_self_cbarg() 实现事件自引用

Creating an event as its own callback argument

🧠 背景介绍

有时候你希望事件的回调函数能访问它自身的指针,比如在回调中调用 event_del() 或 event_free()。但问题是:event_new() 还没返回 struct event * 的时候,怎么把这个指针传进回调参数 arg 呢?

这时候就可以使用 event_self_cbarg() 这个“魔法”指针。

🧰 接口说明

void *event_self_cbarg(void);

这个函数返回一个特殊指针,告诉 event_new():“请把事件对象本身作为 arg 传给回调函数。”

⚠️ 只能用于构造事件相关的函数,比如:

✅ 示例代码

#include <stdio.h>
#include <stdlib.h>
#include <event2/event.h>

static int n_calls = 0;

void cb_func(evutil_socket_t fd, short what, void *arg)
{
    struct event *me = arg;

    printf("cb_func 被调用了 %d 次。\n", ++n_calls);

    if (n_calls >= 5) {
        printf("达到最大调用次数,移除事件。\n");
        event_del(me);
        event_free(me);
    }
}

void run(struct event_base *base)
{
    struct timeval one_sec = { 1, 0 };
    struct event *ev;

    // 创建一个每秒触发一次的事件,并传入自己作为 arg 参数
    ev = event_new(base, -1, EV_PERSIST, cb_func, event_self_cbarg());
    event_add(ev, &one_sec);

    // 启动事件主循环
    event_base_dispatch(base);
}

int main()
{
    struct event_base *base = event_base_new();
    run(base);
    event_base_free(base);
    return 0;
}

📝 输出示例(每秒打印一次):

cb_func 被调用了 1 次。
cb_func 被调用了 2 次。
cb_func 被调用了 3 次。
cb_func 被调用了 4 次。
cb_func 被调用了 5 次。
达到最大调用次数,移除事件。

evtimer_ 宏:纯定时事件简化工具

Timeout-only events

Libevent 提供了一组简化宏来处理只基于超时的事件,它们本质上只是对 event_* 函数的封装,更易读易写。

🧰 接口说明

宏名 功能
evtimer_new(base, cb, arg) 创建一个只带超时的事件,相当于 event_new(base, -1, 0, cb, arg)
evtimer_add(ev, tv) 添加定时事件,相当于 event_add(ev, tv)
evtimer_del(ev) 删除定时事件,相当于 event_del(ev)
evtimer_pending(ev, tv_out) 检查事件是否仍在等待中,相当于 event_pending(ev, EV_TIMEOUT, tv_out)

⚠️ 注意:这些事件不是持续触发的,默认触发一次就会自动取消。若需持续定时触发,请使用 EV_PERSIST。

✅ 示例代码:每隔 3 秒执行一次任务(纯定时)

#include <stdio.h>
#include <stdlib.h>
#include <event2/event.h>

void timeout_cb(evutil_socket_t fd, short what, void *arg)
{
    printf("定时器触发!消息:%s\n", (char *)arg);
}

int main()
{
    struct event_base *base = event_base_new();

    // 使用 evtimer_new 创建一个定时事件(仅触发一次)
    struct event *ev = evtimer_new(base, timeout_cb, "这是一个 3 秒定时器");

    struct timeval three_seconds = {3, 0};
    evtimer_add(ev, &three_seconds);

    // 启动事件循环
    event_base_dispatch(base);

    // 清理资源
    event_free(ev);
    event_base_free(base);
    return 0;
}

📝 输出示例(运行后 3 秒输出):

定时器触发!消息:这是一个 3 秒定时器

📌 使用场景

构建信号事件(Signal Events)

🚦 Constructing signal events

Libevent 支持对 POSIX 信号(如 SIGHUP、SIGINT 等) 的监听与响应,使用 evsignal_new() 可创建一个与信号绑定的事件对象。

🔧 接口说明(与 event_new 类似)

#define evsignal_new(base, signum, cb, arg) \
    event_new(base, signum, EV_SIGNAL|EV_PERSIST, cb, arg)

🔁 由于信号通常是持久性事件(即一直监听),所以默认加了 EV_PERSIST。

🧰 常用宏简写(对应底层函数)

宏名 等价函数
evsignal_add(ev, tv) event_add(ev, tv)
evsignal_del(ev) event_del(ev)
evsignal_pending(ev, what, tv_out) event_pending(ev, what, tv_out)

✅ 示例:监听 SIGHUP(挂起信号)

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <event2/event.h>

void sighup_function(evutil_socket_t sig, short events, void *arg)
{
    printf("接收到 SIGHUP 信号,进行优雅重载...\n");
    // 可在这里实现重载配置、重新打开日志等逻辑
}

int main()
{
    struct event_base *base = event_base_new();

    // 创建 SIGHUP 信号事件
    struct event *hup_event = evsignal_new(base, SIGHUP, sighup_function, NULL);

    // 注册事件
    evsignal_add(hup_event, NULL);

    printf("运行中,发送 SIGHUP 信号测试...\n");
    event_base_dispatch(base);

    // 清理资源
    event_free(hup_event);
    event_base_free(base);
    return 0;
}

📌 输出示例(运行时手动发送信号)

kill -HUP <你的程序PID>
# 终端会打印:
# 接收到 SIGHUP 信号,进行优雅重载...

💡 使用场景建议

使用信号事件时的注意事项

⚠️ Caveats when working with signals

在当前版本的 Libevent 中,大多数后端(如 select、epoll 等)在同一进程中只能有一个 event_base 正在监听信号事件。

✅ 建议:一个进程只使用一个 event_base 来监听信号。

🛠 创建用户触发事件(User-triggered Events)

有时你需要一个事件可以在合适时机 手动激活 —— 比如:

✅ 创建方式

struct event *user = event_new(base, -1, 0, user_cb, myarg);

🚫 不需要调用 event_add()

✅ 触发方式

event_active(user, 0, 0);

🧪 示例:用户触发事件

#include <event2/event.h>
#include <stdio.h>

void user_cb(evutil_socket_t fd, short what, void *arg) {
    printf("用户触发事件被调用啦!arg = %s\n", (char *)arg);
}

int main() {
    struct event_base *base = event_base_new();

    // 创建用户触发事件
    struct event *user_ev = event_new(base, -1, 0, user_cb, "hello");

    // 模拟延迟触发
    struct timeval delay = {1, 0};
    struct event *timer_ev = evtimer_new(base, [](evutil_socket_t, short, void *arg){
        event_active((struct event *)arg, 0, 0);
    }, user_ev);
    evtimer_add(timer_ev, &delay);

    printf("启动事件循环...\n");
    event_base_dispatch(base);

    // 清理资源
    event_free(user_ev);
    // 程序某处直接释放,不需要先 event_del()
    event_free(timer_ev); // ✅ 安全
    event_base_free(base);
    return 0;
}

运行后 1 秒钟内打印:

启动事件循环...
用户触发事件被调用啦!arg = hello

使用非堆分配方式设置事件

✨ Setting up events without heap-allocation

出于性能等原因,有些人喜欢将事件作为更大结构体的一部分来分配。这样做可以在每次使用事件时节省以下开销:

⚠️ 但这种做法可能会破坏与其他版本 Libevent 的二进制兼容性,因为事件结构的大小可能不同。

这些性能差异通常非常小,对大多数应用来说并不重要。除非你清楚地知道由于堆分配事件带来了显著的性能问题,否则应该坚持使用 event_new()。未来 Libevent 如果使用了更大的 struct event,而你使用了 event_assign(),则可能会引发难以诊断的错误。

🔧 接口

int event_assign(struct event *event, struct event_base *base,
    evutil_socket_t fd, short what,
    void (*callback)(evutil_socket_t, short, void *), void *arg);

所有参数与 event_new() 相同,唯一的区别是 event 参数必须指向一个未初始化的事件结构体。成功时返回 0,发生内部错误或参数无效时返回 -1。

📌 示例

#include <event2/event.h>
#include <event2/event_struct.h> // ⚠️ 包含此头文件会破坏二进制兼容性
#include <stdlib.h>

struct event_pair {
    evutil_socket_t fd;
    struct event read_event;
    struct event write_event;
};

void readcb(evutil_socket_t, short, void *);
void writecb(evutil_socket_t, short, void *);

struct event_pair *event_pair_new(struct event_base *base, evutil_socket_t fd)
{
    struct event_pair *p = malloc(sizeof(struct event_pair));
    if (!p) return NULL;
    p->fd = fd;
    event_assign(&p->read_event, base, fd, EV_READ|EV_PERSIST, readcb, p);
    event_assign(&p->write_event, base, fd, EV_WRITE|EV_PERSIST, writecb, p);
    return p;
}

你也可以使用 event_assign() 来初始化栈上或静态分配的事件。

⚠️ 警告

绝不要对一个已经挂起(pending)的事件再次调用 event_assign(),否则可能引起极其难以调试的问题。如果事件已初始化并处于事件队列中,必须先调用 event_del()。

🧩 便捷宏定义

#define evtimer_assign(event, base, callback, arg) \
    event_assign(event, base, -1, 0, callback, arg)

#define evsignal_assign(event, base, signum, callback, arg) \
    event_assign(event, base, signum, EV_SIGNAL|EV_PERSIST, callback, arg)

🔄 保持兼容性的做法

如果你需要使用 event_assign() 并希望保持与未来版本 Libevent 的二进制兼容性,可以调用以下函数获取事件结构的大小:

size_t event_get_struct_event_size(void);

此函数返回为一个事件结构体预留的字节数。

💡 注意:未来该函数的返回值可能小于 sizeof(struct event),表示结构体末尾的额外字节只是用于将来版本的填充空间。

💡 更安全的示例(基于运行时获取大小)

#include <event2/event.h>
#include <stdlib.h>

struct event_pair {
    evutil_socket_t fd;
};

#define EVENT_AT_OFFSET(p, offset) ((struct event*)(((char*)(p)) + (offset)))
#define READEV_PTR(pair) EVENT_AT_OFFSET((pair), sizeof(struct event_pair))
#define WRITEEV_PTR(pair) EVENT_AT_OFFSET((pair), sizeof(struct event_pair) + event_get_struct_event_size())
#define EVENT_PAIR_SIZE() (sizeof(struct event_pair) + 2 * event_get_struct_event_size())

void readcb(evutil_socket_t, short, void *);
void writecb(evutil_socket_t, short, void *);

struct event_pair *event_pair_new(struct event_base *base, evutil_socket_t fd)
{
    struct event_pair *p = malloc(EVENT_PAIR_SIZE());
    if (!p) return NULL;
    p->fd = fd;
    event_assign(READEV_PTR(p), base, fd, EV_READ|EV_PERSIST, readcb, p);
    event_assign(WRITEEV_PTR(p), base, fd, EV_WRITE|EV_PERSIST, writecb, p);
    return p;
}

📜 历史版本信息

使事件处于等待状态与非等待状态

Making events pending and non-pending 🕒

创建事件之后,只有将其添加后,事件才会实际生效,可以通过 event_add() 实现。

int event_add(struct event *ev, const struct timeval *tv);

📌 功能:将一个尚未等待的事件添加到其所在的事件循环中,使其变为“等待中”。

✅ 成功时返回 0,失败时返回 -1。

📎 注意事项:

⚠️ 提示: 不要把 tv 设置为超时触发的具体时间点,而应该设置为“从现在开始的等待时长”。

❌ 错误示例:

tv->tv_sec = time(NULL) + 10;  // 错!会等几十年
int event_del(struct event *ev);

📌 功能:将一个已初始化的事件设为非等待状态并取消激活。

📎 注意事项:

int event_remove_timer(struct event *ev);

📌 功能:从一个等待中的事件中移除超时设置,但保留其 I/O 或信号部分。

定义位置与历史 📚

具有优先级的事件

Events with priorities ⚡️

当多个事件同时触发时,Libevent 默认不保证回调执行的顺序。不过,你可以通过设置事件优先级,让某些事件被优先处理。

💡 如何设置优先级?

每个 event_base 可以拥有多个优先级队列。在将事件添加到 event_base 之前(但初始化之后),你可以设置它的优先级。

int event_priority_set(struct event *event, int priority);

📌 参数说明:

📎 如果有多个不同优先级的事件同时激活,Libevent 会优先执行高优先级事件的回调函数,然后才检查是否有低优先级事件要处理。

🌟 示例代码

#include <event2/event.h>

void read_cb(evutil_socket_t, short, void *);
void write_cb(evutil_socket_t, short, void *);

void main_loop(evutil_socket_t fd)
{
    struct event *important, *unimportant;
    struct event_base *base;

    base = event_base_new();
    event_base_priority_init(base, 2); // 初始化两个优先级:0(高)和 1(低)

    important = event_new(base, fd, EV_WRITE | EV_PERSIST, write_cb, NULL);
    unimportant = event_new(base, fd, EV_READ | EV_PERSIST, read_cb, NULL);

    event_priority_set(important, 0);    // 高优先级
    event_priority_set(unimportant, 1);  // 低优先级

    /* 只要 fd 可写,就会优先触发 write_cb。
       read_cb 只有在 write_cb 没有处于激活状态时才会被执行。*/
}

⚠️ 默认优先级

如果你不设置事件的优先级,Libevent 会默认使用
👉 event_base 中队列数量的一半作为该事件的默认优先级。

📜 历史版本支持

检查事件状态

🔍 Inspecting event status

有时你可能想判断一个事件是否已经添加,或者想看看它当前绑定了哪些内容(比如 FD、回调函数等)。

✅ 判断事件是否处于挂起状态:event_pending()

int event_pending(const struct event *ev, short what, struct timeval *tv_out);

📌 作用:

📌 返回值:

🛠️ 获取事件信息的函数

函数名 功能
event_get_fd(ev) 获取事件绑定的文件描述符(fd)
event_get_signal(ev) 获取绑定的信号编号(如果是信号事件)
event_get_base(ev) 获取事件所属的 event_base
event_get_events(ev) 返回事件的类型(如 EV_READ, EV_WRITE 等)
event_get_callback(ev) 获取事件绑定的回调函数
event_get_callback_arg(ev) 获取传递给回调的参数指针
event_get_priority(ev) 获取事件当前设置的优先级

🧰 获取事件的完整配置:event_get_assignment()

void event_get_assignment(const struct event *event,
    struct event_base **base_out,
    evutil_socket_t *fd_out,
    short *events_out,
    event_callback_fn *callback_out,
    void **arg_out);

💡 这个函数会将事件的所有配置信息一次性写入对应输出指针中。

🧪 示例:更换事件的回调函数(前提:事件不能挂起)

#include <event2/event.h>
#include <stdio.h>

/* 替换 ev 的回调函数,ev 必须是未挂起状态 */
int replace_callback(struct event *ev, event_callback_fn new_callback,
    void *new_callback_arg)
{
    struct event_base *base;
    evutil_socket_t fd;
    short events;

    int pending;

    // 判断是否正在挂起或激活
    pending = event_pending(ev, EV_READ|EV_WRITE|EV_SIGNAL|EV_TIMEOUT, NULL);
    if (pending) {
        fprintf(stderr, "错误!尝试替换一个已挂起的事件的回调函数!\n");
        return -1;
    }

    // 获取当前配置
    event_get_assignment(ev, &base, &fd, &events, NULL, NULL);

    // 使用新回调重新配置
    event_assign(ev, base, fd, events, new_callback, new_callback_arg);
    return 0;
}

📜 支持版本记录:

函数 起始版本
event_pending() Libevent 0.1
event_get_fd() / event_get_signal() Libevent 2.0.1-alpha
event_get_base() Libevent 2.0.2-alpha
event_get_priority() Libevent 2.1.2-alpha
其他 event_get_*() 和 event_get_assignment() Libevent 2.0.4-alpha

获取当前正在运行的事件

🔄 Finding the currently running event

有时为了调试或其他目的,你可能想知道当前正在执行的哪个事件,Libevent提供了相关的接口。

🧩 接口定义

struct event *event_base_get_running_event(struct event_base *base);

📌 说明:

🧠 使用场景:

🛠️ 示例:

void generic_cb(evutil_socket_t fd, short what, void *arg) {
    struct event_base *base = (struct event_base *)arg;
    struct event *ev = event_base_get_running_event(base);
    
    printf("当前正在执行的事件地址是: %p\n", (void *)ev);
}

🗓️ 版本信息:

一次性事件配置 event_base_once()

☄️ Configuring one-off events

如果你只需要执行一次某个事件(例如一次性定时器、一次性网络响应),而不需要重复添加或删除事件,也不需要它持续存在(EV_PERSIST),那你可以使用 event_base_once()。

🧩 接口定义

int event_base_once(struct event_base *base,
                    evutil_socket_t fd,
                    short events,
                    void (*cb)(evutil_socket_t, short, void *),
                    void *cb_arg,
                    const struct timeval *timeout);

📝 参数说明

⚠️ 不能使用 EV_SIGNAL 或 EV_PERSIST!

✅ 特点

🌟 示例:一次性超时事件

#include <event2/event.h>
#include <stdio.h>

void timeout_cb(evutil_socket_t fd, short what, void *arg) {
    puts("定时器触发啦!⏰");
}

int main() {
    struct event_base *base = event_base_new();
    struct timeval tv = { 5, 0 }; // 5 秒

    // 设置一个5秒后触发的定时事件,只触发一次
    event_base_once(base, -1, EV_TIMEOUT, timeout_cb, NULL, &tv);

    // 开始事件循环
    event_base_dispatch(base);
    event_base_free(base);
    return 0;
}

📎 版本信息

手动激活事件 event_active()

Manually activating an event

虽然很少用,但有时候你可能需要在事件本身的条件(如 IO 就绪、超时等)尚未触发的情况下,强制让事件执行一次 —— 这时候就用 event_active()。

🧩 接口定义

// 直接将事件所在优先队列中标记为能触发
void event_active(struct event *ev, int what, short ncalls);

☝️ 调用后并不会让事件变成 “pending”,只是强制执行对应的回调。

⚠️ 注意事项

❗千万别递归调用 event_active()!否则会陷入“事件永远执行,其他事件永远等不到”的困境,导致资源耗尽、程序卡死。

🚫 错误示例:无限递归

struct event *ev;

// 如果高优先级这样 那么低优先级会造成永远饥饿
static void cb(int sock, short which, void *arg) {
    // ❌ 每次回调都重新激活自己,会导致无限循环
    std::cout << "1" << std::endl;
    event_active(ev, EV_WRITE, 0);
    std::cout << "2" << std::endl;
}

int main() {
    struct event_base *base = event_base_new();
    ev = event_new(base, -1, EV_PERSIST | EV_READ, cb, NULL);
    event_add(ev, NULL);
    event_active(ev, EV_WRITE, 0);
    event_base_loop(base, 0);
    return 0;
}

⚠️ 这个代码会让 cb() 永远执行,其他任何事件都不会被调度。

✅ 方法一:使用定时器避免死循环

struct event *ev;
struct timeval tv = {0, 0};  // 立即触发

static void cb(int sock, short which, void *arg) {
    if (!evtimer_pending(ev, NULL)) {
        event_del(ev);              // 先删除
        evtimer_add(ev, &tv);       // 再添加:形成一个周期性调用
    }
}

int main() {
    struct event_base *base = event_base_new();
    ev = evtimer_new(base, cb, NULL);
    evtimer_add(ev, &tv);
    event_base_loop(base, 0);
    return 0;
}

✅ 方法二:使用 event_config_set_max_dispatch_interval() 限制连续执行数量

struct event *ev;

static void cb(int sock, short which, void *arg) {
    event_active(ev, EV_WRITE, 0);
}

int main() {
    struct event_config *cfg = event_config_new();
    event_config_set_max_dispatch_interval(cfg, NULL, 16, 0); // 每最多处理 16 次回调,就检查其他事件
    struct event_base *base = event_base_new_with_config(cfg);

    ev = event_new(base, -1, EV_PERSIST | EV_READ, cb, NULL);
    event_add(ev, NULL);
    event_active(ev, EV_WRITE, 0);

    event_base_loop(base, 0);
    return 0;
}

实际样例,下面的程序会输出

🔥 Infinite callback running
🔥 Infinite callback end
🔥 Infinite callback running
🔥 Infinite callback end
....重复下去
#include <event2/event.h>
#include <iostream>

struct event *ev_infinite = nullptr;
struct event *ev_timer = nullptr;

// 不断激活自己的事件回调
static void cb_infinite(evutil_socket_t, short, void *)
{
    std::cout << "🔥 Infinite callback running" << std::endl;

    // 立即再次激活自己(产生无限循环)
    event_active(ev_infinite, EV_WRITE, 0);

    std::cout << "🔥 Infinite callback end" << std::endl;
}

// 定时器事件回调(如果能执行,说明没有被饿死)
static void cb_timer(evutil_socket_t, short, void *)
{
    std::cout << "💡 Timer fired! (Should not see this if starvation happens)" << std::endl;
}

int main()
{
    // 初始化事件循环
    struct event_base *base = event_base_new();

    // 创建一个无限激活的事件(不会等待 IO,仅逻辑上激活)
    ev_infinite = event_new(base, -1, EV_PERSIST | EV_READ, cb_infinite, nullptr);
    event_add(ev_infinite, nullptr);
    event_active(ev_infinite, EV_WRITE, 0); // 首次激活

    // 创建一个 1 秒触发的定时器事件
    struct timeval tv = {1, 0}; // 2 秒
    ev_timer = evtimer_new(base, cb_timer, nullptr);
    evtimer_add(ev_timer, &tv);

    // 启动事件循环
    std::cout << "🔁 Starting event loop...\n";
    event_base_loop(base, 0);

    // 清理资源(永远不会执行到这里,因为事件循环卡住)
    event_free(ev_infinite);
    event_free(ev_timer);
    event_base_free(base);
    return 0;
}

公共超时优化

Optimizing common timeouts

介绍的是 Libevent 对大量相同超时事件的优化机制,称为 common timeout optimization(公共超时优化)。👇

📌 背景问题

默认情况下,Libevent 使用 二叉堆(binary heap) 管理所有带超时的事件。这样每次插入/删除事件的时间复杂度是 O(log n),这是最优的通用算法。

但如果你有很多(比如上万个)事件,它们都用 相同的超时时间(例如 10 秒),那用堆就浪费了 —— 因为堆对这种场景并不高效。

✅ 优化方案:公共超时队列(common timeout)

Libevent 提供了一个机制:

把所有使用同一个超时时间的事件,放入一个 双向链表队列,它的插入和删除复杂度是 O(1)。

const struct timeval *event_base_init_common_timeout(
    struct event_base *base, const struct timeval *duration);

你传入一个 event_base 和一个超时时间,比如{10, 0},Libevent 返回一个特殊的 timeval 指针(注意:不是你传入的那个对象)。你用这个返回的 timeval 去注册事件,Libevent 就知道用 O(1) 队列 而不是堆了。

#include <event2/event.h>
#include <string.h>

struct timeval ten_seconds = {10, 0};

// 初始化公共超时机制(只做一次)
void initialize_timeout(struct event_base *base) {
    struct timeval input = {10, 0};
    const struct timeval *optimized_tv;

    optimized_tv = event_base_init_common_timeout(base, &input);

    // 把优化后的 timeout 替换掉原始的 timeval(注意不能直接使用 input)
    memcpy(&ten_seconds, optimized_tv, sizeof(struct timeval));
}

// 封装事件添加函数,判断是否使用优化
int my_event_add(struct event *ev, const struct timeval *tv) {
    if (tv && tv->tv_sec == 10 && tv->tv_usec == 0)
        return event_add(ev, &ten_seconds);  // 使用优化后的 timeval
    else
        return event_add(ev, tv);
}

🧠 注意事项

特征 二叉堆 公共超时优化
超时分布 随机超时 相同超时(如全部 10 秒)
性能 O(log n) O(1)
用法 默认 需调用 event_base_init_common_timeout()

区分已初始化的事件与已清除的内存

Telling a good event apart from cleared memory 🧠

Libevent 提供了函数,用于区分已初始化的事件与通过将内存设置为 0(例如通过 calloc() 分配或通过 memset() 或 bzero() 清除)来清除的内存。

int event_initialized(const struct event *ev);
#define evsignal_initialized(ev) event_initialized(ev)
#define evtimer_initialized(ev) event_initialized(ev)

警告 ⚠️

这些函数不能可靠地区分已初始化的事件和未初始化的内存块。除非你确定内存已经被清除或初始化为事件,否则不应使用这些函数。

通常,除非你有非常特定的应用需求,否则不需要使用这些函数。通过 event_new() 返回的事件始终是初始化过的。

示例 💡

#include <event2/event.h>
#include <stdlib.h>

// 定义一个结构体,表示一个“读者”
struct reader {
    evutil_socket_t fd;  // 读者的文件描述符
};

// 计算 reader 结构体的实际大小,包括事件结构体的大小
#define READER_ACTUAL_SIZE() \
    (sizeof(struct reader) + \
     event_get_struct_event_size())

// 获取 reader 结构体中 event 的指针
#define READER_EVENT_PTR(r) \
    ((struct event *) (((char*)(r)) + sizeof(struct reader)))

// 分配一个 reader 结构体并初始化
struct reader *allocate_reader(evutil_socket_t fd)
{
    struct reader *r = calloc(1, READER_ACTUAL_SIZE());  // 使用 calloc 分配内存并初始化为 0
    if (r)
        r->fd = fd;  // 设置文件描述符
    return r;
}

// 读回调函数的声明
void readcb(evutil_socket_t, short, void *);

// 向事件基中添加一个读事件
int add_reader(struct reader *r, struct event_base *b)
{
    struct event *ev = READER_EVENT_PTR(r);  // 获取 reader 结构体中的 event 指针

    // 检查事件是否已经初始化,如果没有初始化则进行初始化
    if (!event_initialized(ev))
        event_assign(ev, b, r->fd, EV_READ, readcb, r);  // 将事件与回调函数关联

    return event_add(ev, NULL);  // 添加事件到事件队列中
}

过时的事件操作函数

Obsolete event manipulation functions

在 Libevent 2.0 之前的版本中,并没有 event_assign() 或 event_new() 这样的函数。取而代之的是:

void event_set(struct event *event, evutil_socket_t fd, short what,
        void(*callback)(evutil_socket_t, short, void *), void *arg);
int event_base_set(struct event_base *base, struct event *event);

其中:

⏱ 其他变体函数

过去还有一些专门处理 定时器 和 信号 的变体:

在更早的版本中:

使用signal_前缀代替 evsignal_ 例如,signal_set()、signal_add()、signal_del()、signal_pending()、signal_initialized()

在0.6之前的老版本,定时器使用timeout_前缀,例如: timeout_set()、timeout_add()、timeout_del()、timeout_pending()、timeout_initialized()

🧩 宏与兼容性

在 2.0 之前,取 fd 或信号不是用函数,而是宏:

🔒 线程安全问题

在 2.0 之前,Libevent 不支持锁,所以:

所有修改事件状态的函数(如 event_add()、event_del()、event_active()、event_base_once())只能在运行事件循环的线程中调用。

🔁 EV_PERSIST 的行为

在 2.0 之前:

❌ 同 fd 同事件类型的限制

在 2.0 之前:


工具可移植性函数

Utility and portability functions

<event2/util.h> 头文件定义了许多在使用 Libevent 实现可移植应用程序时可能会很有用的函数。Libevent 在其内部实现中也使用了这些类型和函数。

基本类型 evutil_socket_t

大多数非Windows系统中,socket通常就是一个int类型,操作系统会以数字顺序分配 socket 描述符。而在 Windows 上,socket 是 SOCKET 类型,本质上是一个类似指针的 句柄(handle),其分配顺序是不可预测的。

为了保证跨平台时的类型一致性,Libevent 引入了一个统一的 socket 类型:

#ifdef WIN32
#define evutil_socket_t intptr_t
#else
#define evutil_socket_t int
#endif

📎 evutil_socket_t 是一个 可以安全保存 socket() 或 accept() 返回值的整数类型,避免了在 Windows 上的指针截断风险。

🧪 此类型自 Libevent 2.0.1-alpha 版本引入。

标准整数类型

Standard integer types 🔢

有时你可能会在一个不支持C98的老旧系统上开发,比如缺少 stdint.h。为了解决这种情况,Libevent定义了自己的一套与stdint.h类似、指定位宽的整数类型,方便你跨平台使用。

📋 类型对应表(位宽 + 有符号/无符号 + 范围):

类型名 位宽(bit) 有符号? 最大值常量 最小值常量
ev_uint64_t 64 ❌ 无符号 EV_UINT64_MAX 0
ev_int64_t 64 ✅ 有符号 EV_INT64_MAX EV_INT64_MIN
ev_uint32_t 32 ❌ 无符号 EV_UINT32_MAX 0
ev_int32_t 32 ✅ 有符号 EV_INT32_MAX EV_INT32_MIN
ev_uint16_t 16 ❌ 无符号 EV_UINT16_MAX 0
ev_int16_t 16 ✅ 有符号 EV_INT16_MAX EV_INT16_MIN
ev_uint8_t 8 ❌ 无符号 EV_UINT8_MAX 0
ev_int8_t 8 ✅ 有符号 EV_INT8_MAX EV_INT8_MIN

🎯 这些类型的位宽是精确固定的(就像 stdint.h 的 uint32_t 等),用于保证跨平台数据一致性。

🆕 类型定义自 Libevent 1.4.0-beta 起提供;而对应的 MAX/MIN 常量是在 Libevent 2.0.4-alpha 中引入的。

其他兼容性类型

Miscellaneous compatibility types

Libevent定义了几个与平台兼容的类型,确保代码在不容系统上能正确运行。

📋 类型说明:

📅 引入版本:

定时器移植性函数

Timer portability functions

在某些平台上,可能没有定义标准的timeval操作函数,因此Libevent提供了我们自己的实现。

📋 接口说明:

evutil_timeradd(tvp, uvp, vvp)
// 将第一个参数和第二个参数相加,并将结果存储在第三个参数中。
evutil_timersub(tvp, uvp, vvp):
// 从第一个参数中减去第二个参数,并将结果存储在第三个参数中。
evutil_timerclear(tvp):
// 清除一个 timeval,将其值设置为零。
evutil_timerisset(tvp):
// 检查 timeval 是否已设置。如果非零,则返回 true,否则返回 false。
evutil_timercmp(tvp, uvp, cmp):
// 比较两个 timeval,并根据提供的关系操作符 cmp 返回 true 或 false。
// 例如,evutil_timercmp(t1, t2, <=) 意思是:“t1 是否小于等于 t2?”。
// 注意:Libevent 的 timercmp 支持所有的 C 关系操作符(即 <、>、==、!=、<= 和 >=)。
evutil_gettimeofday(struct timeval *tv, struct timezone *tz):
// 设置 tv 为当前时间。tz 参数未使用。

🧑‍💻 示例代码:

struct timeval tv1, tv2, tv3;

/* 设置 tv1 = 5.5 秒 */
tv1.tv_sec = 5; tv1.tv_usec = 500*1000;

/* 设置 tv2 = 当前时间 */
evutil_gettimeofday(&tv2, NULL);

/* 设置 tv3 = 5.5 秒后的时间 */
evutil_timeradd(&tv1, &tv2, &tv3);

/* 所有 3 个条件都应该打印为 true */
if (evutil_timercmp(&tv1, &tv1, ==))  /* == "如果 tv1 == tv1" */
   puts("5.5 sec == 5.5 sec");
if (evutil_timercmp(&tv3, &tv2, >=))  /* == "如果 tv3 >= tv2" */
   puts("The future is after the present.");
if (evutil_timercmp(&tv1, &tv2, <))   /* == "如果 tv1 < tv2" */
   puts("It is no longer the past.");

📅 引入版本:

这些函数最早在 Libevent 1.4.0-beta 中引入,evutil_gettimeofday() 函数则是在 Libevent 2.0 中引入的。

⚠️ 注意:

在 Libevent 1.4.4 之前,使用 <= 或 >= 与 timercmp 时是不安全的。

Socket API兼容性

Socket API compatibility

由于历史原因,Windows并没有很好地兼容 Berkeley Socket API,因此Libevent提供了一些函数和宏来模拟兼容的行为。

🧯 关闭 socket

int evutil_closesocket(evutil_socket_t s);
#define EVUTIL_CLOSESOCKET(s) evutil_closesocket(s)

🧪 Socket 错误码处理

#define EVUTIL_SOCKET_ERROR()
#define EVUTIL_SET_SOCKET_ERROR(errcode)
#define evutil_socket_geterror(sock)
#define evutil_socket_error_to_string(errcode)

📌 注意:

🏃 设置非阻塞 socket

int evutil_make_socket_nonblocking(evutil_socket_t sock);

🔁 设置 socket 可重用

int evutil_make_listen_socket_reuseable(evutil_socket_t sock);

❌ 设置 socket 为 close-on-exec

int evutil_make_socket_closeonexec(evutil_socket_t sock);

🔗 创建 socketpair

int evutil_socketpair(int family, int type, int protocol, evutil_socket_t sv[2]);

⚠️ 注意:某些 Windows 系统上,如果防火墙禁止了 127.0.0.1 的回环连接,该函数可能失败。

跨平台字符串处理函数

Portable string manipulation functions

📌 evutil_strtoll — 安全的 64 位字符串转整数函数

ev_int64_t evutil_strtoll(const char *s, char **endptr, int base);

🔍 功能说明:

📎 注意事项:

✨ evutil_snprintf 和 evutil_vsnprintf — 安全格式化输出函数

int evutil_snprintf(char *buf, size_t buflen, const char *format, ...);
int evutil_vsnprintf(char *buf, size_t buflen, const char *format, va_list ap);

🔍 功能说明:

📎 优势:

📌 这两个函数自 Libevent 1.4.5 起引入。

与区域设置无关的字符串处理函数

Locale-independent string manipulation functions

📌 evutil_ascii_strcasecmp 与 evutil_ascii_strncasecmp

// 引入版本:Libevent 2.0.3-alpha。
int evutil_ascii_strcasecmp(const char *str1, const char *str2);
int evutil_ascii_strncasecmp(const char *str1, const char *str2, size_t n);

🔍 功能说明:

evutil_ascii_strcasecmp("Hello", "hello");  // 返回 0,表示相等
evutil_ascii_strncasecmp("HelloWorld", "hello", 5);  // 只比较前 5 个字符,也返回 0

与IPv6相关的兼容性辅助函数

IPv6 helper and portability functions

🌐 evutil_inet_ntop 与 evutil_inet_pton

const char *evutil_inet_ntop(int af, const void *src, char *dst, size_t len);
int evutil_inet_pton(int af, const char *src, void *dst);

✅ 功能说明:

参数解释:

返回值:

函数 成功 失败
evutil_inet_ntop 返回 dst 指针 返回 NULL
evutil_inet_pton 返回 1 0 表示格式错误,-1 表示地址族不支持

📦 evutil_parse_sockaddr_port

int evutil_parse_sockaddr_port(const char *str, struct sockaddr *out, int *outlen);

✅ 功能说明:

支持的格式:

[ipv6]:port(例如:[::1]:80)

ipv6(例如:::1)

[ipv6]

ipv4:port(例如:127.0.0.1:8080)

ipv4(例如:127.0.0.1)

❗若未指定端口,默认设为 0。

参数说明:

返回值:

⚖️ evutil_sockaddr_cmp

int evutil_sockaddr_cmp(const struct sockaddr *sa1, const struct sockaddr *sa2, int include_port);

✅ 功能说明:

参数:

返回值:

< 0:sa1 < sa2
0:地址相等
> 0:sa1 > sa2

💡 用于地址排序或地址是否相同判断时非常有用!

引入版本

函数名 引入版本
evutil_inet_ntop / evutil_inet_pton / evutil_parse_sockaddr_port Libevent 2.0.1-alpha
evutil_sockaddr_cmp Libevent 2.0.3-alpha

结构体宏兼容性函数

🏗️ Structure macro portability functions

#define evutil_offsetof(type, field) /* ... */

✅ 功能说明:

参数:

struct Example {
    int a;
    char b;
};
size_t offset = evutil_offsetof(struct Example, b);  // 获取成员 b 的偏移量

注意事项:

安全随机数生成器

Secure random number generator

许多应用程序(包括evdns)需要来源于不可预测的随机数,用于确保其安全性.

evutil_secure_rng_get_bytes:

void evutil_secure_rng_get_bytes(void *buf, size_t n);

✅ 功能说明:

示例:

unsigned char buffer[32]; // 用于存放随机数据的缓冲区
evutil_secure_rng_get_bytes(buffer, sizeof(buffer));  // 获取 32 字节的随机数据

evutil_secure_rng_init:

int evutil_secure_rng_init(void);

✅ 功能说明:

evutil_secure_rng_add_bytes:

void evutil_secure_rng_add_bytes(const char *dat, size_t datlen);

✅ 功能说明:

该函数允许你手动向熵池中添加更多的随机字节。这在常规使用中通常不需要,但在某些特定的安全环境中,可能需要通过该方法增强熵源的质量。

使用场景:


Bufferevent: 概念与基础

Bufferevents: concepts and basics

在大多数情况下,应用程序除了响应事件之外,还需要进行一定的数据缓冲。举个常见的写数据的场景:

📝 写入数据的常规步骤:

  1. 决定向连接写入一些数据,把数据放进缓冲区。
  2. 等待连接变得可写。
  3. 写入尽可能多的数据。
  4. 记录已经写入的数据量;如果还有剩余数据,继续等待连接可写。

这种带缓冲的I/O模式非常普遍,因此 Libevent 提供了 bufferevent(缓冲事件)机制来封装这个过程。

🧩 什么是 Bufferevent?

一个 bufferevent 由以下几个部分组成:

区别于常规事件机制(只在 socket 可读/可写时调用回调),bufferevent 提供了更高级别的接口,它会在满足读取或写入条件时自动调用你提供的回调函数。

⚙️ Bufferevent 的类型

Libevent 提供了几种不同类型的 bufferevent,它们遵循统一接口:

类型 描述
socket-based bufferevent 使用标准 socket 进行收发数据,基于 event_* 系列接口
asynchronous-IO bufferevent 使用 Windows 的 IOCP 实现异步 I/O(仅限 Windows,实验性质)
filtering bufferevent 可对数据进行处理(如压缩/翻译)再传递给底层 bufferevent
paired bufferevents 两个 bufferevent 互相传递数据(可用于测试、虚拟通道等)

Bufferevent 和 Evbuffer

Bufferevents and evbuffers

每个 bufferevent 都有一个输入缓冲区和一个输出缓冲区。这些缓冲区的类型是 struct evbuffer。当你有数据要通过 bufferevent 写出时,你将数据添加到输出缓冲区;当 bufferevent 有数据可供你读取时,你从输入缓冲区中提取数据。

回调函数与水位线

Callbacks and watermarks

每个 bufferevent 都有两个与数据相关的回调函数:一个读取回调和一个写入回调。默认情况下:

你可以通过调整 bufferevent 的读取和写入“水位线”来重定义这些函数的行为。

每个 bufferevent 有四个水位线:

⚠️ 事件回调(Error/Event Callback)

一个 bufferevent 还具有一个“错误”或“事件”回调,用于通知应用程序有关非数据相关事件,例如连接关闭或发生错误。

定义了一下时间标志:

标志 含义
BEV_EVENT_READING 在 bufferevent 上读取操作期间发生了事件。请查看其他标志以了解具体事件。
BEV_EVENT_WRITING 在 bufferevent 上写入操作期间发生了事件。请查看其他标志以了解具体事件。
BEV_EVENT_ERROR 在 bufferevent 操作期间发生了错误。有关错误的详细信息,请调用 EVUTIL_SOCKET_ERROR()。
BEV_EVENT_TIMEOUT bufferevent 上的超时已到期。
BEV_EVENT_EOF 在 bufferevent 上收到文件结束(EOF)指示。
BEV_EVENT_CONNECTED bufferevent 上完成了一个请求的连接操作。

延迟回调

Deferred callbacks⏳

默认情况下,bufferevent的回调函数会在对应条件发生时立即执行,(evbuffer 的回调也是如此;我们稍后会讲到它们。)

这种立即调用可能会在依赖关系变复杂时引发问题。例如,假设有一个回调会在 evbuffer A 变空时将数据移入,而另一个回调会在 evbuffer A 变满时处理数据。由于这些调用都是在栈上进行的,如果依赖关系变得足够复杂,就可能会导致栈溢出。

为了解决这个问题,你可以告诉 bufferevent(或 evbuffer),它的回调应该是延迟的。当满足延迟回调的条件时,它不会立即执行,而是被排入 event_loop() 调用中的队列,在常规事件的回调之后再执行。

(延迟回调是从 Libevent 2.0.1-alpha 引入的。)

bufferevent的选项标志

Option flags for bufferevents

在创建bufferevent时,你可以使用一个或多个标志来改变其行为。支持的标志包括:

(BEV_OPT_UNLOCK_CALLBACKS 是在 Libevent 2.0.5-beta 中引入的。上述其他选项是在 Libevent 2.0.1-alpha 中新增的。)

使用基于 socket 的 bufferevent

Working with socket-based bufferevents

最简单使用的 bufferevent 类型是基于 socket 的类型。这种 bufferevent 使用 Libevent 底层的事件机制来检测底层网络 socket 何时准备好读取和/或写入操作,并使用底层网络调用(如 readv、writev、WSASend 或 WSARecv)来发送和接收数据。

创建一个基于 socket 的 bufferevent

Creating a socket-based bufferevent

你可以使用 bufferevent_socket_new() 来创建一个基于 socket 的 bufferevent:

// 这个函数成功时返回一个 bufferevent,失败时返回 NULL。
// bufferevent_socket_new() 函数是在 Libevent 2.0.1-alpha 中引入的。
struct bufferevent *bufferevent_socket_new(
    struct event_base *base,
    evutil_socket_t fd,
    enum bufferevent_options options);

💡 提示

请确保传递给 bufferevent_socket_new 的 socket 是非阻塞模式。Libevent 提供了一个方便的函数 evutil_make_socket_nonblocking 来完成这个操作。

在基于socket的bufferevent上发起连接

Launching connections on socket-based bufferevents

如果 bufferevent 的 socket 还未连接,你可以通过该函数发起一个新连接。

int bufferevent_socket_connect(struct bufferevent *bev,
    struct sockaddr *address, int addrlen);
// 返回
// 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) {
         /* 我们已经连接上 127.0.0.1:8080。
            通常此处应启动读写等操作。 */
    } else if (events & BEV_EVENT_ERROR) {
         /* 连接过程中发生错误。 */
    }
}

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); /* 端口 8080 */

    bev = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE);

    bufferevent_setcb(bev, NULL, NULL, eventcb, NULL);

    // bufferevent_socket_connect() 函数是在 Libevent 2.0.2-alpha 中引入的。
    if (bufferevent_socket_connect(bev,
        (struct sockaddr *)&sin, sizeof(sin)) < 0) {
        /* 连接启动失败 */
        bufferevent_free(bev);
        return -1;
    }

    event_base_dispatch(base);
    return 0;
}

你只有在使用 bufferevent_socket_connect() 发起连接时,才会收到 BEV_EVENT_CONNECTED 事件。如果你自己调用了 connect(),连接完成只会触发写事件(不是 CONNECTED)。

如果你希望手动调用 connect(),但仍希望在连接成功时收到 BEV_EVENT_CONNECTED 事件,可以在 connect() 返回 -1 且 errno 为 EAGAIN 或 EINPROGRESS 时,调用:

bufferevent_socket_connect(bev, NULL, 0);

通过主机名发起连接

Launching connections by hostname

我们经常希望将解析主机名和发起连接合并为一个操作。Libevent提供了这样的一个接口:

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

该函数解析主机名 hostname,查找类型为 family的地址。(允许的 family 类型为 AF_INET、AF_INET6 和 AF_UNSPEC。)

如果名称解析失败,会使用错误事件调用 event 回调。如果解析成功,它就像 bufferevent_connect 一样发起连接操作。

和 bufferevent_socket_connect() 一样,此函数会通知 Libevent,bufferevent 上的任何现有 socket 均未连接,只有在解析完成并连接成功后,才能对该 socket 进行读写操作。

如果发生错误,可能是 DNS 主机名解析错误。你可以通过调用 bufferevent_socket_get_dns_error() 来获取最近一次的错误。如果返回的错误码是 0,则表示没有检测到 DNS 错误。

通用 bufferevent 操作

Generic bufferevent operations

本节中的函数适用于多种类型的 bufferevent 实现。

释放 bufferevent

Freeing a bufferevent

// 📌 该函数是在 Libevent 0.8 中引入的。
void bufferevent_free(struct bufferevent *bev);

该函数用于释放一个bufferevent。bufferevent在内部是使用引用计数机制的。所以如果你在它还有待执行的延迟回调(deferred callback)时调用 bufferevent_free(), 它不会立即销毁,而是会等回调执行完毕后再真正释放。不过,bufferevent_free() 会尽量尽快释放 bufferevent。

⚠️ 注意:

如果 bufferevent 中还有待写入的数据,这些数据大概率不会被刷新(flush)就直接释放了。

如果设置了 BEV_OPT_CLOSE_ON_FREE 标志,并且该 bufferevent 有一个 socket 或底层 bufferevent 作为其传输方式(transport),那么在释放 bufferevent 的时候,该传输对象也会被关闭。

操作回调函数、水位标记和启用状态

Manipulating callbacks, watermarks, and enabled operations

这部分介绍了如何设置 bufferevent 的回调函数、启用或禁用读写事件、以及配置读写的“水位”限制。

// 用于处理数据读写的回调函数
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);

✅ 如果某个回调不需要,可以传入 NULL 来禁用。

📤 获取当前设置的回调函数

void bufferevent_getcb(struct bufferevent *bufev,
    bufferevent_data_cb *readcb_ptr,
    bufferevent_data_cb *writecb_ptr,
    bufferevent_event_cb *eventcb_ptr,
    void **cbarg_ptr);

🔛 启用 / 禁用读写操作

void bufferevent_enable(struct bufferevent *bufev, short events);
void bufferevent_disable(struct bufferevent *bufev, short events);
short bufferevent_get_enabled(struct bufferevent *bufev);

🌊 设置读写水位(Watermarks)

void bufferevent_setwatermark(struct bufferevent *bufev,
    short events,
    size_t lowmark,
    size_t highmark);

读回调:

// 从输入缓冲中读取数据,并统计读取的总字节数。
struct info {
    const char *name;
    size_t total_drained;
};
void read_callback(struct bufferevent *bev, void *ctx)
{
    struct info *inf = ctx;
    size_t len = evbuffer_get_length(bufferevent_get_input(bev));
    if (len) {
        inf->total_drained += len;
        evbuffer_drain(bufferevent_get_input(bev), len);
        printf("Drained %lu bytes from %s\n", len, inf->name);
    }
}

事件回调

// 连接关闭(EOF)或发生错误时处理清理逻辑。
void event_callback(struct bufferevent *bev, short events, void *ctx)
{
    if (events & BEV_EVENT_EOF) { ... }
    if (events & BEV_EVENT_ERROR) { ... }
}

设置bufferevent

bufferevent_setwatermark(b1, EV_READ, 128, 0);
bufferevent_setcb(b1, read_callback, NULL, event_callback, info1);
bufferevent_enable(b1, EV_READ);
// 设置读水位:只在输入缓冲区 >= 128 字节时触发读回调。
// 启用读操作

操作 bufferevent 中的数据

Manipulating data in a bufferevent

Libevent 的 bufferevent 是一种高级封装,使用起来比裸 evbuffer 更方便。下面介绍如何读写其中的数据。

📥 获取输入输出缓冲区

struct evbuffer *bufferevent_get_input(struct bufferevent *bufev);
struct evbuffer *bufferevent_get_output(struct bufferevent *bufev);

⚡一旦你移除输入数据 / 添加输出数据,bufferevent 会自动重新启动读取或写入操作。

✏️ 向输出缓冲区写入数据

int bufferevent_write(struct bufferevent *bufev,
    const void *data, size_t size);

int bufferevent_write_buffer(struct bufferevent *bufev,
    struct evbuffer *buf);

✅ 成功返回 0,失败返回 -1。

📤 从输入缓冲区读取数据

size_t bufferevent_read(struct bufferevent *bufev, void *data, size_t size);

int bufferevent_read_buffer(struct bufferevent *bufev,
    struct evbuffer *buf);

🧪 示例代码详解

1️⃣ 将数据转为大写再发送回去

void read_callback_uppercase(struct bufferevent *bev, void *ctx)
{
    char tmp[128];
    size_t n;
    while (1) {
        n = bufferevent_read(bev, tmp, sizeof(tmp));
        if (n <= 0)
            break;
        for (int i = 0; i < n; ++i)
            tmp[i] = toupper(tmp[i]);
        bufferevent_write(bev, tmp, n);
    }
}

2️⃣ 简单代理的读回调

struct proxy_info {
    struct bufferevent *other_bev;
};

void read_callback_proxy(struct bufferevent *bev, void *ctx)
{
    struct proxy_info *inf = ctx;
    bufferevent_read_buffer(bev, bufferevent_get_output(inf->other_bev));
}

3️⃣ 写入斐波那契数列数据

struct count {
    unsigned long last_fib[2];
};

void write_callback_fibonacci(struct bufferevent *bev, void *ctx)
{
    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);
    }

    bufferevent_write_buffer(bev, tmp);
    evbuffer_free(tmp);
}

bufferevent 的读写超时机制

Read- and write timeouts

bufferevent 支持设置读写超时,当在一段时间内没有成功读取或写入数据时,可以触发超时事件。

void bufferevent_set_timeouts(struct bufferevent *bufev,
    const struct timeval *timeout_read,
    const struct timeval *timeout_write);

🔍 超时行为说明

超时生效的前提是:

🚨 超时触发时的事件回调

✅ 示例代码(设置 5 秒读超时,10 秒写超时)

#include <event2/bufferevent.h>
#include <event2/util.h>

struct timeval r_timeout = {5, 0};  // 5 秒读超时
struct timeval w_timeout = {10, 0}; // 10 秒写超时

struct bufferevent *bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);

bufferevent_set_timeouts(bev, &r_timeout, &w_timeout);

📌 实战建议

  1. 心跳机制:搭配超时使用心跳包,避免连接长时间无数据读写。
  2. 重连策略:在 eventcb 中处理 BEV_EVENT_TIMEOUT 时,可自动尝试重新连接。
  3. 避免误判:当你知道连接会闲置时,可以考虑关闭 timeout,或适当延长时间。

buferrevent_flush()发起手动冲刷操作

Initiating a flush on a bufferevent

在 Libevent 中,bufferevent_flush() 用于强制立即读取或写入数据,即使原本存在的一些机制(如水位、水流控制)会阻止这些操作。

int bufferevent_flush(struct bufferevent *bufev,
    short iotype,
    enum bufferevent_flush_mode state);
// -1 发生错误
// 0 没有冲刷任何数据
// 1 成功冲刷了一些数据
参数 说明
bufev 要操作的 bufferevent
iotype 操作类型:EV_READ(强制读)、EV_WRITE(强制写)、`EV_READ
tate 冲刷模式(见下表)

🚦 state 模式解释

模式 含义
BEV_NORMAL 正常操作(不强制)
BEV_FLUSH 强制冲刷尽可能多的数据
BEV_FINISHED 表示不再发送数据,相当于告诉对方“写完了”

💡 BEV_FINISHED 类似于 shutdown(fd, SHUT_WR) 的语义。

⚠️ 注意事项

✅ 示例:强制将写缓冲区内容写出(如果类型支持)

if (bufferevent_flush(bev, EV_WRITE, BEV_FLUSH) < 0) {
    fprintf(stderr, "Flush failed!\n");
}

🧠 实用场景(理论上)

虽然 bufferevent_flush() 支持有限,但如果你使用的是自定义传输机制(比如基于 bufferevent_pair 或 bufferevent_filter),它可以:

类型相关的 bufferevent 接口函数

Type-specific bufferevent functions

这些函数仅对特定类型的 bufferevent 有效,特别是基于 socket 的 bufferevent。

🎚️ 设置/获取优先级

int bufferevent_priority_set(struct bufferevent *bufev, int pri);
int bufferevent_get_priority(struct bufferevent *bufev);

🧵 设置/获取文件描述符(fd)

int bufferevent_setfd(struct bufferevent *bufev, evutil_socket_t fd);
evutil_socket_t bufferevent_getfd(struct bufferevent *bufev);

🏗️ 获取事件主循环 base

struct event_base *bufferevent_get_base(struct bufferevent *bev);

🔄 获取底层传输 bufferevent(用于过滤器)

struct bufferevent *bufferevent_get_underlying(struct bufferevent *bufev);

手动锁定和解锁 bufferevent

Manually locking and unlocking a bufferevent

在多线程环境中,有时候你可能希望对一个 bufferevent 执行一系列原子操作。Libevent 提供了相关的接口来手动加锁和解锁 bufferevent,以保证这些操作的线程安全。

// 这些函数从 Libevent 2.0.6-rc 开始支持。
void bufferevent_lock(struct bufferevent *bufev);
void bufferevent_unlock(struct bufferevent *bufev);

使用要点

典型用途场景

当你需要对一个 bufferevent 执行多个步骤,并希望中间不被其他线程打断时,就可以手动加锁,例如:

bufferevent_lock(bev);

// 一系列对 bev 的读写或状态修改
bufferevent_write(bev, "data", 4);
bufferevent_flush(bev, EV_WRITE, BEV_FLUSH);

bufferevent_unlock(bev);

注意事项

过时的 bufferevent 功能

Obsolete bufferevent functionality

在 Libevent 1.4 与 Libevent 2.0 之间,bufferevent 后端代码经历了大量重构。在旧的接口中,访问 struct bufferevent 的内部结构有时是正常的,并且会使用依赖于这种访问的宏。

使情况更复杂的是,旧代码有时使用以 “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(b) EVBUFFER_INPUT(b)
bufferevent_get_output(b) EVBUFFER_OUTPUT(b)

旧的函数定义在 event.h 中,而不是在 event2/bufferevent.h 中。

如果你仍然需要访问 bufferevent 结构体的通用部分的内部,可以包含 event2/bufferevent_struct.h。但我们不推荐这样做:struct bufferevent 的内容在 Libevent 的版本之间是会变动的。如果包含 event2/bufferevent_compat.h,则可以使用本节中提到的宏和名称。

设置 bufferevent 的接口在旧版本中有所不同:

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() 函数只创建基于 socket 的 bufferevent,且使用的是已废弃的“默认” event_base。调用 bufferevent_base_set 仅适用于 socket bufferevent,用于设置其 event_base。

在旧版本中,设置超时使用的是秒数,而不是 struct timeval:

void bufferevent_settimeout(struct bufferevent *bufev,
    int timeout_read, int timeout_write);

最后请注意,Libevent 2.0 之前版本中底层的 evbuffer 实现效率非常低,以至于在高性能应用中使用 bufferevent 会显得不太合适。

bufferevent tcp echo server

// g++ main.cpp -o main.exe --std=c++11 -levent
#include <event2/event.h>
#include <event2/bufferevent.h>
#include <event2/listener.h>
#include <string.h>
#include <arpa/inet.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>

#define PORT 9999

static void signal_cb(evutil_socket_t sig, short events, void *user_data) {
    struct event_base *base = (struct event_base *)user_data;
    printf("捕获到信号 %d,准备退出...\n", sig);
    event_base_loopexit(base, NULL);
}

static void echo_read_cb(struct bufferevent *bev, void *ctx) {
    char buffer[1024];
    int n;

    while ((n = bufferevent_read(bev, buffer, sizeof(buffer))) > 0) {
        // 把收到的数据原样写回客户端
        bufferevent_write(bev, buffer, n);
    }
}

static void echo_event_cb(struct bufferevent *bev, short events, void *ctx) {
    if (events & BEV_EVENT_EOF) {
        printf("客户端关闭连接\n");
    } else if (events & BEV_EVENT_ERROR) {
        printf("发生错误\n");
    } else if (events & BEV_EVENT_TIMEOUT) {
        printf("超时事件\n");
    }
    bufferevent_free(bev);
}

static void accept_conn_cb(struct evconnlistener *listener,
                           evutil_socket_t fd,
                           struct sockaddr *address,
                           int socklen,
                           void *ctx) {
    struct event_base *base = (struct event_base *)ctx;
    struct bufferevent *bev;

    bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
    bufferevent_setcb(bev, echo_read_cb, NULL, echo_event_cb, NULL);
    bufferevent_enable(bev, EV_READ | EV_WRITE);
}

int main() {
    struct event_base *base;
    struct evconnlistener *listener;
    struct sockaddr_in sin;

    // 创建 event_base
    base = event_base_new();
    if (!base) {
        fprintf(stderr, "无法创建 event_base\n");
        return 1;
    }

    // 注册 SIGINT (Ctrl+C) 信号处理
    struct event *signal_event = evsignal_new(base, SIGINT, signal_cb, base);
    event_add(signal_event, NULL);

    // 监听地址和端口
    memset(&sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = htonl(INADDR_ANY);
    sin.sin_port = htons(PORT);

    // 创建监听器
    listener = evconnlistener_new_bind(base, accept_conn_cb, base,
                                       LEV_OPT_REUSEABLE | LEV_OPT_CLOSE_ON_FREE, -1,
                                       (struct sockaddr *)&sin, sizeof(sin));

    if (!listener) {
        fprintf(stderr, "无法创建监听器\n");
        return 1;
    }

    printf("Echo 服务器启动,监听端口 %d...\n", PORT);
    event_base_dispatch(base);  // 事件循环

    // 清理资源
    evconnlistener_free(listener);
    event_free(signal_event);
    event_base_free(base);
    return 0;
}

Bufferevent: 高级主题

Bufferevents: advanced topics

本章介绍了Libevent中bufferevent实现的一些高级特性,这些特性在一般使用中并不是必需的。如果你只是刚开始学习如何使用bufferevent,那么你现在可以跳过 本章,继续阅读有关evbuffer的章节。

配对的bufferevent

Paired bufferevents

有时候,你可能会需要让一个网络程序“与自己通信”。例如,你可能编写了一个程序,用某种协议来隧道化(tunnel)用户连接,但有时它也希望将自己的连接通过该协议进行隧道化。当然,你可以简单地连接到自己的监听端口,并让程序通过网络栈和自身通信,但这会浪费资源。

为了解决这个问题,你可以创建一对配对的 bufferevent,使得在一个 bufferevent 上写入的所有字节都会被另一个 bufferevent 接收(反之亦然),而且这个过程不涉及实际的底层平台 socket。

// 自 Libevent 2.0.1-alpha 起提供;
int bufferevent_pair_new(struct event_base *base, int options,
    struct bufferevent *pair[2]);

调用bufferevent_pair_new()会将pair[0] 和 pair[1] 设置为一对已经相互连接的bufferevent。除了 BEV_OPT_CLOSE_ON_FREE(无效)BEV_OPT_DEFFER_CALLBACKS(始终启用)之外,所有常规选项都支持。

🤔 为什么需要启用延迟回调?

因为在配对 bufferevent 的一端进行的某个操作,可能会触发一个回调,而这个回调又会修改 bufferevent,从而引发另一端的bufferevent回调,以此类推, 形成多个步骤的调用链。如果没有启用延迟回调(defer callbacks),这种调用链非常容易导致栈溢出、阻塞其他连接,并且要求所有回调函数都是可重入的。

🔁 刷新机制支持

配对bufferevent支持刷新操作:

🗑️ 内存释放行为

释放配对中的某一个 bufferevent 不会自动释放另一个,也不会触发 EOF 事件。此操作只是让配对关系解除。解除配对后的 bufferevent 将无法再读写数据,也不会触发事件。

🔍 如何获取配对另一端?

// 自 Libevent 2.0.6 起提供。
struct bufferevent *bufferevent_pair_get_partner(struct bufferevent *bev);

🧪 示例:使用配对 bufferevent 进行进程内通信

#include <event2/event.h>
#include <event2/bufferevent.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

// 读取回调
void read_cb(struct bufferevent *bev, void *ctx) {
    char buffer[256];
    int n = bufferevent_read(bev, buffer, sizeof(buffer) - 1);
    buffer[n] = '\0'; // 添加字符串结束符
    printf("🔹 接收到数据: %s\n", buffer);
}

// 错误和事件回调
void event_cb(struct bufferevent *bev, short events, void *ctx) {
    if (events & BEV_EVENT_EOF) {
        printf("📴 EOF 事件\n");
    } else if (events & BEV_EVENT_ERROR) {
        printf("❌ 错误事件\n");
    }
}

int main() {
    // 初始化 event base
    struct event_base *base = event_base_new();
    if (!base) {
        perror("无法创建 event_base");
        return 1;
    }

    struct bufferevent *pair[2];

    // 创建配对 bufferevent
    if (bufferevent_pair_new(base, 0, pair) < 0) {
        fprintf(stderr, "创建 bufferevent 对失败\n");
        return 1;
    }

    // 为 pair[1] 设置读取和事件回调(pair[0] 作为写入方)
    bufferevent_setcb(pair[1], read_cb, NULL, event_cb, NULL);
    bufferevent_enable(pair[1], EV_READ | EV_WRITE);

    // 写入数据到 pair[0],会被 pair[1] 接收
    const char *msg = "Hello from pair[0]!";
    bufferevent_write(pair[0], msg, strlen(msg));

    // 启动事件循环
    event_base_dispatch(base);

    // 清理资源
    bufferevent_free(pair[0]);
    bufferevent_free(pair[1]);
    event_base_free(base);

    return 0;
}

数据过滤魔法

Filtering bufferevents

有时你希望对 bufferevent 中传输的所有数据进行变换处理,比如添加压缩层,或者用一个协议包装另一个协议进行传输。Libevent 提供了 bufferevent_filter 接口,允许你像“数据管道”一样,对进出的数据加上“过滤器”来处理。有点像中间件。

🧩 接口定义

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,               // 写入目标的字节上限(可为 -1 表示无限制)
    enum bufferevent_flush_mode mode,   // 刷新模式(BEV_NORMAL、BEV_FLUSH、BEV_FINISHED)
    void *ctx                           // 自定义上下文参数
);

🏗️ 创建过滤 bufferevent

struct bufferevent *bufferevent_filter_new(
    struct bufferevent *underlying,          // 底层 bufferevent
    bufferevent_filter_cb input_filter,      // 输入过滤函数
    bufferevent_filter_cb output_filter,     // 输出过滤函数
    int options,                             // 创建选项
    void (*free_context)(void *),            // 上下文释放函数
    void *ctx                                // 自定义上下文参数
);

⚙️ 工作机制

⚠️ 你不能再给底层 bufferevent 设置回调函数,否则过滤器可能无法正常工作。不过你仍然可以对其 evbuffer 添加监听器。

🧠 回调函数返回值说明

返回值 意义说明
BEV_OK 成功写入目标缓冲区,继续传输
BEV_NEED_MORE 需要更多输入或刷新条件,暂时无法继续传输
BEV_ERROR 出现不可恢复错误,终止过滤

🧪 示例:大小写转换过滤器 Demo

将所有输入字母转换为大写,再原样输出👇

#include <event2/event.h>
#include <event2/bufferevent.h>
#include <event2/buffer.h>
#include <ctype.h>
#include <string.h>
#include <stdio.h>

// 输入过滤器:小写转大写
enum bufferevent_filter_result input_filter(
    struct evbuffer *src, struct evbuffer *dst,
    ev_ssize_t limit, enum bufferevent_flush_mode mode, void *ctx)
{
    size_t len = evbuffer_get_length(src);
    unsigned char *data = evbuffer_pullup(src, len);

    for (size_t i = 0; i < len; ++i)
    {
        char upper = toupper(data[i]);
        evbuffer_add(dst, &upper, 1);
    }

    evbuffer_drain(src, len);
    return BEV_OK;
}

// 输出过滤器:不处理
enum bufferevent_filter_result output_filter(
    struct evbuffer *src, struct evbuffer *dst,
    ev_ssize_t limit, enum bufferevent_flush_mode mode, void *ctx)
{
    evbuffer_add_buffer(dst, src);
    return BEV_OK;
}

// 读取回调
void read_cb(struct bufferevent *bev, void *ctx)
{
    char buffer[256];
    int n = bufferevent_read(bev, buffer, sizeof(buffer) - 1);
    buffer[n] = '\0';
    printf("🔍 收到过滤后内容:%s\n", buffer);

    struct event_base *base = (struct event_base *)ctx;
    event_base_loopexit(base, NULL); // 退出事件循环
}

int main()
{
    struct event_base *base = event_base_new();

    // 创建一对互联 bufferevent
    struct bufferevent *pair[2];
    bufferevent_pair_new(base, BEV_OPT_CLOSE_ON_FREE, pair);

    // 将其中一个设置为带过滤器的
    struct bufferevent *filtered = bufferevent_filter_new(
        pair[1], input_filter, output_filter,
        BEV_OPT_CLOSE_ON_FREE, NULL, base);

    // 设置回调
    bufferevent_setcb(filtered, read_cb, NULL, NULL, base);
    bufferevent_enable(filtered, EV_READ | EV_WRITE);
    bufferevent_enable(pair[0], EV_READ | EV_WRITE);

    // 向 pair[0] 写数据,相当于发给 filtered
    const char *msg = "hello, filter!";
    bufferevent_write(pair[0], msg, strlen(msg));

    // 开始事件循环
    event_base_dispatch(base);

    // 清理
    bufferevent_free(pair[0]); // filtered 会自动释放它自己的底层 pair[1]
    bufferevent_free(filtered);
    event_base_free(base);

    return 0;
}
// 🔍 收到过滤后内容:HELLO, FILTER!

限制每次读/写的最大数据量

Limiting maximum single read/write size

在默认情况下,bufferevent并不会在每次事件循环中读/写尽可能多的数据,这是为了避免资源抢占或其他连接饿死(starvation)的现象。但有些场景下,你可能希望自定义这个限制。

🧩 接口说明

这组接口是 Libevent 2.1.1-alpha 新增的,使用前请确认版本支持。

// 设置:限制每次最多读/写多少字节
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);

📌 用法说明

✅ 示例:限制为每次最多读 64 字节、写 128 字节

bufferevent_set_max_single_read(bev, 64);
bufferevent_set_max_single_write(bev, 128);

这表示这个bufferevent:

🧠 为什么要设置?

这种限制可以用于:

Bufferevents与速率限制

Bufferevents and Rate-limiting

有些程序希望限制某个连接(bufferevent)或一组连接的带宽使用量,比如防止单个客户端占满全部带宽,或者控制整体流量。Libevent从 2.0.4-alpha2.0.5-alpha 版本起,开始支持这一功能。

🧱 基本思想

Libevent的速率限制机制主要包括:

  1. 为单个bufferevent设置速率限制
  2. 将多个bufferevent分配到一个共享速率限制组

这套机制是通过令牌桶(Token Bucket)实现的。

Libevent 的速率限制模型

The rate-limiting model

Libevent 通过 令牌桶算法(Token Bucket Algorithm) 来控制每次可以读写多少字节。这种机制广泛用于网络限速、带宽控制等场景。

🪣 速率限制的核心概念

每个被限速的对象,如 bufferevent 都拥有两个“桶”;

桶类型 用途
🧺 读桶(read bucket) 限制能立即读取的最大字节数
🪣 写桶(write bucket) 限制能立即写入的最大字节数

每个桶具备以下属性:

属性 含义
✅ 填充速率(rate) 每个时间单位(tick)补充的字节数
💥 最大容量(burst) 桶最大可以存放的字节数,即允许的突发传输量
⏱️ 时间单位(tick) 控制多久更新一次桶的容量,单位为 struct timeval

📈 工作原理详解

📊 举例说明(图解感知):

假设配置如下:

read_rate  = 500   // 每tick_len时间补充 500 字节
read_burst = 1000  // 最多瞬间读 1000 字节
tick_len   = 1s

🎯 三个参数如何影响限速行为?

参数 决定什么? 比喻
rate 平均传输速率 每秒从水龙头流入桶里的水量 🚰
burst 最大瞬间传输量 桶最大能装多少水 🪣
tick 精度和频率 多久检查/补水一次 ⏱️

✅ 小结

设置bufferevent的速率限制

Setting a rate limit on a bufferevent

Libevent 使用 ev_token_bucket_cfg 配置结构体,对单个或一组 bufferevent 进行带宽控制(如限制读写速率和突发传输量)。

🛠️ 接口说明

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);
参数 说明
read_rate / write_rate 每个 tick 允许的最大字节数(平均速率)
read_burst / write_burst 瞬间允许的最大字节数(突发速率)
tick_len 每次桶刷新间隔,默认 1 秒(NULL)

✅ 示例:限制每秒最多读取 1024 字节,瞬间不超过 2048 字节

#include <event2/event.h>
#include <event2/bufferevent.h>
#include <event2/buffer.h>
#include <event2/rate_limit.h>
#include <stdio.h>
#include <string.h>

// 简单的回调函数:读取数据
void read_cb(struct bufferevent *bev, void *ctx) {
    char buf[128];
    int n = bufferevent_read(bev, buf, sizeof(buf) - 1);
    buf[n] = '\0';
    printf("📥 收到数据: %s\n", buf);
}

int main() {
    struct event_base *base = event_base_new();

    // 创建 socket bufferevent(-1 表示不关联 socket,用作模拟)
    struct bufferevent *bev = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE);

    // 创建 token bucket 限速配置
    struct timeval tick = {1, 0}; // 1 秒
    struct ev_token_bucket_cfg *cfg = ev_token_bucket_cfg_new(
        1024, 2048,   // 读速率:每秒1024字节,最多突发2048
        1024, 2048,   // 写速率:同上
        &tick
    );

    // 设置限速
    if (bufferevent_set_rate_limit(bev, cfg) < 0) {
        fprintf(stderr, "❌ 设置限速失败\n");
        return 1;
    }

    // 设置读回调
    bufferevent_setcb(bev, read_cb, NULL, NULL, NULL);
    bufferevent_enable(bev, EV_READ | EV_WRITE);

    // 模拟输入数据
    const char *msg = "Hello, this is a test message to check rate-limiting in bufferevent.\n";
    struct evbuffer *inbuf = bufferevent_get_input(bev);
    evbuffer_add(inbuf, msg, strlen(msg)); // 添加到“底层”输入缓冲区,触发 read_cb

    // 启动事件循环
    event_base_dispatch(base);

    // 清理
    bufferevent_free(bev);
    ev_token_bucket_cfg_free(cfg); // 确保 bev 已经不再使用 cfg 后释放
    event_base_free(base);
    return 0;
}

💡注意事项

给多个bufferevent设置共享限速

Setting a rate limit on a group of bufferevents

当你有多个连接(例如客户端连接池),希望它们总共不能超过某个带宽上限,就可以使用 rate limit group(速率限制组)。

📦 接口说明

struct bufferevent_rate_limit_group *bufferevent_rate_limit_group_new(
    struct event_base *base,
    const struct ev_token_bucket_cfg *cfg);
函数 功能
bufferevent_rate_limit_group_new 创建一个新的限速组
bufferevent_add_to_rate_limit_group 将 bufferevent 加入限速组
bufferevent_remove_from_rate_limit_group 移除
bufferevent_rate_limit_group_set_cfg 动态调整限速参数
bufferevent_rate_limit_group_free 释放整个组(会清空成员)

🧠 一个 bufferevent:

🧪 示例:两个 bufferevent 共享 1KB/s 带宽上限

#include <event2/event.h>
#include <event2/bufferevent.h>
#include <event2/rate_limit.h>
#include <stdio.h>
#include <string.h>

void read_cb(struct bufferevent *bev, void *ctx) {
    char buf[128];
    int n = bufferevent_read(bev, buf, sizeof(buf)-1);
    buf[n] = '\0';
    printf("📨 [%s] 收到数据: %s\n", (char *)ctx, buf);
}

int main() {
    struct event_base *base = event_base_new();

    // 创建两个 bufferevent
    struct bufferevent *bev1 = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE);
    struct bufferevent *bev2 = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE);

    // 创建 token bucket 配置:总共每秒只允许 1024 字节,最多突发 2048 字节
    struct timeval tick = {1, 0};
    struct ev_token_bucket_cfg *cfg = ev_token_bucket_cfg_new(
        1024, 2048,   // read
        1024, 2048,   // write
        &tick
    );

    // 创建 group 并绑定配置
    struct bufferevent_rate_limit_group *group =
        bufferevent_rate_limit_group_new(base, cfg);

    // 添加两个 bufferevent 到这个组
    bufferevent_add_to_rate_limit_group(bev1, group);
    bufferevent_add_to_rate_limit_group(bev2, group);

    // 设置读回调
    bufferevent_setcb(bev1, read_cb, NULL, NULL, "连接1");
    bufferevent_setcb(bev2, read_cb, NULL, NULL, "连接2");

    bufferevent_enable(bev1, EV_READ | EV_WRITE);
    bufferevent_enable(bev2, EV_READ | EV_WRITE);

    // 模拟往 input 中写数据
    const char *msg = "Test message for rate-limited group.\n";
    evbuffer_add(bufferevent_get_input(bev1), msg, strlen(msg));
    evbuffer_add(bufferevent_get_input(bev2), msg, strlen(msg));

    // 启动事件循环
    event_base_dispatch(base);

    // 清理
    bufferevent_free(bev1);
    bufferevent_free(bev2);
    bufferevent_rate_limit_group_free(group);
    ev_token_bucket_cfg_free(cfg);
    event_base_free(base);
    return 0;
}

🧠 小结

方式 限制作用域 适用场景
bufferevent_set_rate_limit 每个连接单独限制 控制单连接流量
bufferevent_rate_limit_group 共享总流量限制 控制“总资源占用”

查询当前速率限制

Inspecting current rate-limit values

🧪 1. 查询当前 token bucket 剩余容量(可能是负值)

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

🔍 2. 查询当前最多能读/写多少字节(实际能进行的数据量)

ev_ssize_t bufferevent_get_max_to_read(struct bufferevent *bev);
ev_ssize_t bufferevent_get_max_to_write(struct bufferevent *bev);

这些考虑了:

🧠:这是判断「我能不能写/读」的实际接口。

📈 3. 查看限速组累计传输字节总量

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

✅ 示例:打印当前可用额度和总用量

ev_ssize_t r = bufferevent_get_read_limit(bev);
ev_ssize_t w = bufferevent_get_write_limit(bev);
printf("📊 连接当前可读:%zd 字节,可写:%zd 字节\n", r, w);

ev_ssize_t max_r = bufferevent_get_max_to_read(bev);
ev_ssize_t max_w = bufferevent_get_max_to_write(bev);
printf("🚦 实际最多能读:%zd,最多能写:%zd\n", max_r, max_w);

ev_uint64_t total_r, total_w;
bufferevent_rate_limit_group_get_totals(group, &total_r, &total_w);
printf("📈 总共读取:%llu 字节,总共写入:%llu 字节\n",
       (unsigned long long)total_r, (unsigned long long)total_w);

手动调整 token bucket 的当前值

Manually adjusting rate limits

🔧 手动调整 bufferevent 或 group 的限额

// 对单个 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);

🧠 说明:

📦 使用场景举例:

  1. 🔌 你从别的通道(非 bufferevent,比如 raw socket)发送了一些数据,但仍然想让它遵守限速规则 ➜ 手动调用 decrement_write_limit 来扣除写限额。
  2. ⏱️ 某个限速组中,某个 bufferevent 暂时不用了,你可以把这部分限额“借”给其它连接 ➜ 手动减组里某个的额度,增加另一个。

✅ 示例:手动减掉 500 字节写限额

if (bufferevent_decrement_write_limit(bev, 500) == 0) {
    printf("✅ 成功手动扣除了写额度 500 字节\n");
} else {
    fprintf(stderr, "❌ 减少写限额失败!\n");
}

限速组中的最小分配份额机制

Setting the smallest share possible in a rate-limited group

🧩 什么是最小分配份额(min_share)?

假设你创建了一个限速组,设定写速率为 10,000 字节每 tick,但是这个组内有 10,000 个 bufferevent,如果平均分配,那每个连接每 tick 只能写 1 字节,这样太低效(系统调用开销、TCP 包头太大)。

所以 Libevent 引入了 “最小份额机制(minimum share)”:

在每个 tick 内,只允许部分 bufferevent 写数据,每个被选中的 bufferevent 可以写 min_share 字节,未被选中的本 tick 不能写。

默认 min_share = 64,即:

这个机制保证吞吐效率比“所有连接平均分配 1 字节”要高得多。

🛠️ 接口:设置最小份额

int bufferevent_rate_limit_group_set_min_share(
    struct bufferevent_rate_limit_group *group, size_t min_share);

🌟 示例代码:

// 假设你已有 rate-limit group 对象 group
bufferevent_rate_limit_group_set_min_share(group, 128); // 每 tick 至少分给部分连接 128 字节额度

📌 注意事项:

Libevent 限速机制的一些已知限制

Limitations of the rate-limiting implementation

1️⃣ 并非所有 bufferevent 类型都支持限速

2️⃣ 限速组不能嵌套

3️⃣ 限速仅统计应用层数据字节

4️⃣ 读限速依赖 TCP 栈反馈机制

5️⃣ 某些平台下可能存在超额传输

6️⃣ token 桶在创建时默认已有 1 tick 额度

7️⃣ tick 的最小精度是 1 毫秒

Bufferevent 与 SSL/TLS

Bufferevents and SSL

Libevent 支持通过 OpenSSL 库 为 bufferevent 添加 SSL/TLS 安全传输功能。

📦 实现方式

📝 提示:将来版本的 Libevent 可能支持其他 SSL/TLS 库(如 NSS、GnuTLS),但 目前仅支持 OpenSSL。

🕐 支持历史

📚 头文件

使用 SSL 相关的 bufferevent 函数,需包含:

#include <event2/bufferevent_ssl.h>

设置和使用基于 OpenSSL 的 bufferevent

Setting up and using an OpenSSL-based bufferevent

enum bufferevent_ssl_state {
    BUFFEREVENT_SSL_OPEN = 0, // SSL 握手已完成,连接已建立。
    BUFFEREVENT_SSL_CONNECTING = 1,// SSL 正在作为客户端进行协商。
    BUFFEREVENT_SSL_ACCEPTING = 2// SSL 正在作为服务器进行协商。
};
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 关闭,虽然这违反了 TLS RFC,但可以确保会话在关闭后仍保留在缓存中。

SSL *ctx = bufferevent_openssl_get_ssl(bev);

/*
 * SSL_RECEIVED_SHUTDOWN 告诉 SSL_shutdown 假装我们已经
 * 从另一端收到了关闭通知。SSL_shutdown 会发送最终的关闭通知作为回应。
 * 另一端会收到关闭通知并发送他们的关闭通知。在这时,我们会已经关闭了套接字,
 * 另一端的真实关闭通知将永远不会收到。实际上,双方都会认为已经完成了
 * 干净的关闭,并保持会话有效。这个策略会失败,如果套接字没有准备好写入,
 * 这时这个 hack 会导致不干净的关闭,并且另一端的会话丢失。
 */
SSL_set_shutdown(ctx, SSL_RECEIVED_SHUTDOWN);
SSL_shutdown(ctx);
bufferevent_free(bev);

接口

SSL *bufferevent_openssl_get_ssl(struct bufferevent *bev);

该函数返回OpenSSL bufferevent 使用的SSL对象,如果bev不是OpenSSL 基础的 bufferevent,则返回 NULL。

unsigned long bufferevent_get_openssl_error(struct bufferevent *bev);

该函数返回给定 bufferevent 操作的第一个待处理的 OpenSSL 错误,如果没有待处理的错误,则返回 0。

int bufferevent_ssl_renegotiate(struct bufferevent *bev);

调用此函数会要求 SSL 重新协商,并触发相应的 bufferevent 回调。一般情况下,除非你非常清楚自己在做什么,否则应该避免使用它,特别是由于许多 SSL 版本存在与重新协商相关的已知安全问题。

// allow_dirty_shutdown 函数在 Libevent 2.1.1-alpha 版本中添加。
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 版本)都支持经过认证的关闭操作,这使得双方能够区分意图关闭和由于意外或恶意原因导致的连接中断。默认情况下,除了正常的关闭外,其他任何情况都被视为连接错误。如果 allow_dirty_shutdown 标志设置为 1,则我们会将连接关闭视为 BEV_EVENT_EOF。

简单的基于 SSL 的回显服务器示例

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

    /* 初始化 OpenSSL 库 */
    SSL_load_error_strings();
    SSL_library_init();
    /* 必须有熵,否则加密无意义 */
    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 的一些笔记

Some notes on threading and OpenSSL

Libevent 的内建线程机制并不涵盖 OpenSSL 锁定机制。由于 OpenSSL 使用了大量的全局变量,因此你必须仍然配置 OpenSSL 以保证线程安全。尽管这个过程超出了 Libevent 的范围,但由于该主题经常出现,因此值得讨论。

启用线程安全 OpenSSL 的简单示例

/*
 * 请参考 OpenSSL 文档以验证你是否正确地执行了这些操作,
 * Libevent 并不保证此代码是完整的,仅供作为示例使用。
 */
#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

/* 实现 OpenSSL 所要求的线程 ID 函数 */
#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;
}

代码说明:

  1. 线程 ID 函数 (get_thread_id_cb):
  1. 线程锁回调 (thread_lock_cb):
  1. 初始化 SSL 锁定机制 (init_ssl_locking):

Evbuffers: 用于缓冲IO的实用功能

Evbuffers: utility functionality for buffered IO

Libevent的evbuffer功能实现了一个字节队列,它针对向尾部添加数据和从头部移除数据做了优化。

Evbuffer 的设计目的是为了在实现「缓冲式网络 IO」中的“缓冲区”部分时提供通用且实用的支持。它不负责调度 IO 或在 IO 就绪时触发相关操作——这些任务是由 bufferevent 来完成的。

本章节中的函数,除非特别说明,均声明于头文件 event2/buffer.h 中。

创建或释放 evbuffer

Creating or freeing an evbuffer

// 这些函数自 Libevent 0.8 版本 起就已经存在。
struct evbuffer *evbuffer_new(void);
void evbuffer_free(struct evbuffer *buf);

这两个函数的作用非常直观:

Evbuffer与线程安全

Evbuffers and Thread-safety

// Libevent 2.0.1-alpha 版本 起引入
int evbuffer_enable_locking(struct evbuffer *buf, void *lock);
void evbuffer_lock(struct evbuffer *buf);
void evbuffer_unlock(struct evbuffer *buf);

默认情况下,evbuffer 不是线程安全的,也就是说不能同时在多个线程中访问同一个 evbuffer。

如果你需要在多线程环境中安全地访问evbuffer,可以调用 evbuffer_enable_locking() 来启用锁机制:

evbuffer_lock()evbuffer_unlock() 函数分别用于加锁与解锁 evbuffer,通常用来将一组操作变为原子操作。如果该 evbuffer 没有启用锁机制,这两个函数将不执行任何操作。

📌 注意: 你通常不需要在每一个操作前后都手动调用 evbuffer_lock() 和 evbuffer_unlock()。一旦你启用了锁,每个单独的 evbuffer 操作本身就是原子的。

检查 evbuffer

Inspecting an evbuffer

获取evbuffer存储的字节数

size_t evbuffer_get_length(const struct evbuffer *buf);

获取evbuffer开头连续存储的空间大小

size_t evbuffer_get_contiguous_space(const struct evbuffer *buf);

向evbuffer添加数据:基础用法

Adding data to an evbuffer: basics

追加原始数据

int evbuffer_add(struct evbuffer *buf, const void *data, size_t datlen);

以格式化方法追加数据

int evbuffer_add_printf(struct evbuffer *buf, const char *fmt, ...);
int evbuffer_add_vprintf(struct evbuffer *buf, const char *fmt, va_list ap);

预扩展缓冲区容量

int evbuffer_expand(struct evbuffer *buf, size_t datlen);

💡 示例

/* 以下是两种将 "Hello world 2.0.1" 添加到缓冲区的方法: */

/* 方法一:直接添加字符串 */
evbuffer_add(buf, "Hello world 2.0.1", 17);

/* 方法二:使用格式化输出 */
evbuffer_add_printf(buf, "Hello %s %d.%d.%d", "world", 2, 0, 1);

📜 版本信息

在两个 evbuffer 之间移动数据

Moving data from one evbuffer to another

为了提高效率,Libevent 提供了专门优化的函数,用于在两个 evbuffer 之间移动数据。

移动全部或部分数据

// evbuffer_add_buffer():自 Libevent 0.8 引入
int evbuffer_add_buffer(struct evbuffer *dst, struct evbuffer *src);
// evbuffer_remove_buffer():自 Libevent 2.0.1-alpha 引入
int evbuffer_remove_buffer(struct evbuffer *src, struct evbuffer *dst, size_t datlen);

向evbuffer头部添加数据

Adding data to the front of an evbuffer

在缓冲区前端插入数据

// 这两个函数自 Libevent 2.0.1-alpha 起被引入。
int evbuffer_prepend(struct evbuffer *buf, const void *data, size_t size);
int evbuffer_prepend_buffer(struct evbuffer *dst, struct evbuffer* src);

⚠️ 注意事项

这些函数应当谨慎使用,绝不能在与 bufferevent 共享的 evbuffer 上调用。

重新整理evbuffer的内部布局

Rearranging the internal layout of an evbuffer

有时候你希望查看evbuffer前部的前N个字节,并以连续字节数组的形式读取。为了实现这一点,你必须先确保缓冲区的前部数据是连续的。

将前部数据线性化

unsigned char *evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size);

说明:

⚠️ 注意: 如果 size 很大,该操作可能非常慢,因为可能需要复制整个缓冲区内容。

💡 示例

#include <event2/buffer.h>
#include <event2/util.h>
#include <string.h>

int parse_socks4(struct evbuffer *buf, ev_uint16_t *port, ev_uint32_t *addr)
{
    // 解析 SOCKS4 请求的前 8 个字节(协议格式固定)
    unsigned char *mem;

    mem = evbuffer_pullup(buf, 8);

    if (mem == NULL) {
        // 缓冲区内数据不足
        return 0;
    } else if (mem[0] != 4 || mem[1] != 1) {
        // 不是 SOCKS4 或命令错误
        return -1;
    } else {
        // 提取端口和 IP 地址
        memcpy(port, mem + 2, 2);
        memcpy(addr, mem + 4, 4);
        *port = ntohs(*port);
        *addr = ntohl(*addr);

        // 数据验证通过,正式移除缓冲区前 8 字节
        evbuffer_drain(buf, 8);
        return 1;
    }
}

📝 补充说明

调用 evbuffer_pullup() 时,如果传入的size正好等于 evbuffer_get_contiguous_space() 的返回值,不会触发任何数据复制或移动。

📜 版本信息

evbuffer_pullup() 函数自 Libevent 2.0.1-alpha 引入。在此之前的 Libevent 版本总是强制将 evbuffer 数据保持为连续的,即便因此带来较高开销。

从evbuffer中移除数据

Removing data from an evbuffer

移除或提取缓冲区前部数据

// 自 Libevent 0.8 引入
int evbuffer_drain(struct evbuffer *buf, size_t len);
// 自 Libevent 0.9 引入
int evbuffer_remove(struct evbuffer *buf, void *data, size_t datlen);

从 evbuffer 中复制数据(不移除)

Copying data out from an evbuffer

有时候你希望复制缓冲区前端的数据,但又不想从中移除这些数据。例如:你可能想判断一条完整的记录是否已到达,但不想像 evbuffer_remove() 那样清除数据,也不想像 evbuffer_pullup() 那样内部重排缓冲区。

复制数据但不抽取

// 首次出现在 Libevent 2.0.5-alpha
ev_ssize_t evbuffer_copyout(struct evbuffer *buf, void *data, size_t datlen);
// 首次出现在 Libevent 2.1.1-alpha
ev_ssize_t evbuffer_copyout_from(struct evbuffer *buf,
     const struct evbuffer_ptr *pos,
     void *data_out, size_t datlen);

⚠️ 如果你觉得复制操作过慢,可以考虑使用 evbuffer_peek() 来零拷贝查看数据。

💡 示例:获取一条完整记录

#include <event2/buffer.h>
#include <event2/util.h>
#include <stdlib.h>

int get_record(struct evbuffer *buf, size_t *size_out, char **record_out)
{
    // 假设协议格式为:4 字节记录长度(网络字节序) + 若干字节内容
    size_t buffer_len = evbuffer_get_length(buf);
    ev_uint32_t record_len;
    char *record;

    if (buffer_len < 4)
       return 0; // 长度字段还未收到

    // 使用 copyout 查看长度字段,但不移除数据
    evbuffer_copyout(buf, &record_len, 4);
    record_len = ntohl(record_len); // 转换为主机字节序

    if (buffer_len < record_len + 4)
        return 0; // 数据还没完全到达

    // 数据够了,可以取出了!
    record = malloc(record_len);
    if (record == NULL)
        return -1;

    evbuffer_drain(buf, 4);                 // 移除长度字段
    evbuffer_remove(buf, record, record_len); // 移除实际记录数据

    *record_out = record;
    *size_out = record_len;
    return 1;
}

基于行的输入读取

Line-oriented input

enum evbuffer_eol_style {
    EVBUFFER_EOL_ANY,
    EVBUFFER_EOL_CRLF,
    EVBUFFER_EOL_CRLF_STRICT,
    EVBUFFER_EOL_LF,
    EVBUFFER_EOL_NUL
};

char *evbuffer_readln(struct evbuffer *buffer, size_t *n_read_out,
    enum evbuffer_eol_style eol_style);

🔚 支持的换行格式

枚举值 描述
EVBUFFER_EOL_LF 行尾是一个换行符 ,ASCII 为 0x0A。
EVBUFFER_EOL_CRLF_STRICT 行尾是一个回车 一个换行 ,ASCII 为 0x0D 0x0A。
EVBUFFER_EOL_CRLF 行尾是可选的回车加换行,即 或 ,用于兼容宽松格式的协议。
EVBUFFER_EOL_ANY 任意数量的 组合,主要为了兼容旧版使用。
EVBUFFER_EOL_NUL 行尾是一个 ASCII NUL 字节(值为 0)。
char *request_line;
size_t len;

request_line = evbuffer_readln(buf, &len, EVBUFFER_EOL_CRLF);
if (!request_line) {
    /* 第一行尚未接收完整 */
} else {
    if (!strncmp(request_line, "HTTP/1.0 ", 9)) {
        /* 识别出 HTTP 1.0 请求 */
    }
    free(request_line);
}

🕒 版本信息

在evbuffer中搜索数据

Searching within an evbuffer

📌 evbuffer_ptr 结构体

evbuffer_ptr结构体用于表示evbuffer中的一个位置,并可用于遍历该缓冲区。

struct evbuffer_ptr {
    ev_ssize_t pos;
    struct {
        /* 内部字段 */
    } _internal;
};

🧰 接口函数

struct evbuffer_ptr evbuffer_search(struct evbuffer *buffer,
    const char *what, size_t len, const struct evbuffer_ptr *start);

struct evbuffer_ptr evbuffer_search_range(struct evbuffer *buffer,
    const char *what, size_t len, const struct evbuffer_ptr *start,
    const struct evbuffer_ptr *end);

struct evbuffer_ptr evbuffer_search_eol(struct evbuffer *buffer,
    struct evbuffer_ptr *start, size_t *eol_len_out,
    enum evbuffer_eol_style eol_style);

🔧 设置 evbuffer_ptr 位置

enum evbuffer_ptr_how {
    EVBUFFER_PTR_SET,
    EVBUFFER_PTR_ADD
};

int evbuffer_ptr_set(struct evbuffer *buffer, struct evbuffer_ptr *pos,
    size_t position, enum evbuffer_ptr_how how);

💡 示例:统计子串出现次数

#include <event2/buffer.h>
#include <string.h>

/* 统计 buf 中 str 出现的次数 */
int count_instances(struct evbuffer *buf, const char *str)
{
    size_t len = strlen(str);
    int total = 0;
    struct evbuffer_ptr p;

    if (!len)
        return -1;  // 不要统计长度为 0 的字符串

    evbuffer_ptr_set(buf, &p, 0, EVBUFFER_PTR_SET);

    while (1) {
         p = evbuffer_search(buf, str, len, &p);
         if (p.pos < 0)
             break;
         total++;
         evbuffer_ptr_set(buf, &p, 1, EVBUFFER_PTR_ADD);
    }

    return total;
}

⚠️ 注意事项

任何修改 evbuffer 或其内部结构的操作(例如添加、移除数据)都会使现有的 evbuffer_ptr 失效且不可再使用。

📜 版本信息

这些接口首次出现在 Libevent 2.0.1-alpha。

无需复制即可查看数据

Inspecting data without copying it

有时你想读取 evbuffer 中的数据,但不希望像 evbuffer_copyout() 那样将其复制出来,也不想像 evbuffer_pullup() 那样重新排列内部内存。

有时你甚至可能想查看 evbuffer 中间部分的数据。

struct evbuffer_iovec {
    void *iov_base;
    size_t iov_len;
};

int evbuffer_peek(struct evbuffer *buffer, ev_ssize_t len,
    struct evbuffer_ptr *start_at,
    struct evbuffer_iovec *vec_out, int n_vec);

如果 start_at == NULL,则从缓冲区起始位置开始查看,否则从指定位置 start_at 开始。

✨ 示例一:查看前两个数据块并写入 stderr

{
    int n, i;
    struct evbuffer_iovec v[2];
    n = evbuffer_peek(buf, -1, NULL, v, 2);
    for (i = 0; i < n; ++i) {
        fwrite(v[i].iov_base, 1, v[i].iov_len, stderr);
    }
}

📤 示例二:将前 4096 字节写入 stdout

{
    int n, i, r;
    struct evbuffer_iovec *v;
    size_t written = 0;

    n = evbuffer_peek(buf, 4096, NULL, NULL, 0); // 先计算需要多少块
    v = malloc(sizeof(struct evbuffer_iovec) * n); // 分配数组
    n = evbuffer_peek(buf, 4096, NULL, v, n); // 填充数组

    for (i = 0; i < n; ++i) {
        size_t len = v[i].iov_len;
        if (written + len > 4096)
            len = 4096 - written;
        r = write(1, v[i].iov_base, len);
        if (r <= 0)
            break;
        written += len;
    }
    free(v);
}

🧩 示例三:找到 start\n 后的 16KB 数据并交给 consume() 函数处理

{
    struct evbuffer_ptr ptr;
    struct evbuffer_iovec v[1];
    const char s[] = "start\n";
    int n_written = 0;

    ptr = evbuffer_search(buf, s, strlen(s), NULL);
    if (ptr.pos == -1)
        return; // 没有找到 start 字符串

    if (evbuffer_ptr_set(buf, &ptr, strlen(s), EVBUFFER_PTR_ADD) < 0)
        return; // 越界

    while (n_written < 16 * 1024) {
        if (evbuffer_peek(buf, -1, &ptr, v, 1) < 1)
            break;
        consume(v[0].iov_base, v[0].iov_len);
        n_written += v[0].iov_len;

        if (evbuffer_ptr_set(buf, &ptr, v[0].iov_len, EVBUFFER_PTR_ADD) < 0)
            break;
    }
}

📝 注意事项

📜 版本信息

此函数首次出现在 Libevent 2.0.2-alpha 中。

直接向 evbuffer 添加数据

Adding data to an evbuffer directly

有时候你希望直接将数据写入 evbuffer,而不是先写入字符数组再通过 evbuffer_add() 拷贝进去。 此时可以使用两个高级函数:evbuffer_reserve_space() 和 evbuffer_commit_space()。 这些函数和 evbuffer_peek() 一样,使用 evbuffer_iovec 结构体来直接访问 evbuffer 的内部内存。

🧩 接口定义

int evbuffer_reserve_space(struct evbuffer *buf, ev_ssize_t size,
    struct evbuffer_iovec *vec, int n_vecs);

int evbuffer_commit_space(struct evbuffer *buf,
    struct evbuffer_iovec *vec, int n_vecs);

注意:写入到这些内存块中的数据在调用 evbuffer_commit_space() 前不会生效。

你可以:

📌 evbuffer_commit_space()

⚠️ 注意事项与陷阱

💡 示例:无拷贝生成 2048 字节数据并写入缓冲区

struct evbuffer_iovec v[2];
int n, i;
size_t n_to_add = 2048;

/* 预留 2048 字节空间 */
n = evbuffer_reserve_space(buf, n_to_add, v, 2);
if (n <= 0)
    return;

for (i = 0; i < n && n_to_add > 0; ++i) {
    size_t len = v[i].iov_len;
    if (len > n_to_add)
        len = n_to_add;
    if (generate_data(v[i].iov_base, len) < 0)
        return; // 生成失败,直接中断
    v[i].iov_len = len; // 设置实际写入的长度
    n_to_add -= len;
}

/* 提交数据 */
if (evbuffer_commit_space(buf, v, i) < 0)
    return; // 提交失败

❌ 错误示例(不要模仿)

struct evbuffer_iovec v[2];

{
  evbuffer_reserve_space(buf, 1024, v, 2);
  evbuffer_add(buf, "X", 1);
  // ❌ 错误:调用 evbuffer_add 后指针可能失效
  memset(v[0].iov_base, 'Y', v[0].iov_len-1);
  evbuffer_commit_space(buf, v, 1);
}

{
  const char *data = "Here is some data";
  evbuffer_reserve_space(buf, strlen(data), v, 1);
  // ❌ 错误:不要直接修改 iov_base 指针
  v[0].iov_base = (char*) data;
  v[0].iov_len = strlen(data);
  evbuffer_commit_space(buf, v, 1);
}

🧾 版本信息

这些函数自 Libevent 2.0.2-alpha 起以现在的接口形式存在。

使用 evbuffer 进行网络 IO

Network IO with evbuffers

在Libevent中,最常见的evbuffer用法就是网络IO。Libevent提供了一组接口,方便你直接将evbuffer与网络socket进行读写操作。

📡 接口定义

int evbuffer_write(struct evbuffer *buffer, evutil_socket_t fd);
int evbuffer_write_atmost(struct evbuffer *buffer, evutil_socket_t fd, ev_ssize_t howmuch);
int evbuffer_read(struct evbuffer *buffer, evutil_socket_t fd, int howmuch);

📥 evbuffer_read()

从文件描述符(如 socket)fd 中读取最多 howmuch 字节,追加到 buffer 尾部。

📤 evbuffer_write_atmost()

将buffer前部的最多howmuch字节写入文件描述符

🔄 evbuffer_write()

这是 evbuffer_write_atmost() 的简化版本:

evbuffer_write(buffer, fd)

evbuffer_write_atmost(buffer, fd, -1)

也就是说,它会尝试写出 buffer 中尽可能多的数据。

🧠 使用注意事项

🧾 版本信息

evbuffer 与回调机制

Evbuffers and callbacks

使用 evbuffer 的用户常常希望能在数据被添加或移除时收到通知,为此 Libevent 提供了一个通用的回调机制。

struct evbuffer_cb_info {
    size_t orig_size;   // 修改前的字节数
    size_t n_added;     // 添加的字节数
    size_t n_deleted;   // 移除的字节数
};

typedef void (*evbuffer_cb_func)(struct evbuffer *buffer,
    const struct evbuffer_cb_info *info, void *arg);

➕ 注册回调

struct evbuffer_cb_entry *evbuffer_add_cb(struct evbuffer *buffer,
    evbuffer_cb_func cb, void *cbarg);

✅ 示例代码

struct total_processed {
    size_t n;
};

void count_megabytes_cb(struct evbuffer *buffer,
    const struct evbuffer_cb_info *info, void *arg)
{
    struct total_processed *tp = arg;
    size_t old_n = tp->n;
    tp->n += info->n_deleted;
    int megabytes = (tp->n >> 20) - (old_n >> 20);
    for (int i = 0; i < megabytes; ++i)
        putc('.', stdout);  // 每处理1MB,输出一个点
}

void operation_with_counted_bytes(void)
{
    struct total_processed *tp = malloc(sizeof(*tp));
    tp->n = 0;
    struct evbuffer *buf = evbuffer_new();
    evbuffer_add_cb(buf, count_megabytes_cb, tp);

    // 使用 evbuffer ...
    evbuffer_free(buf);  // 释放 buffer 本身
    free(tp);            // 别忘释放你自己分配的结构体
}

⛔ 注意:释放非空的 evbuffer 不会触发 “数据删除” 回调,也不会释放你传给回调的 arg 指针。

❌ 移除或临时禁用回调

🔄 移除回调

int evbuffer_remove_cb_entry(struct evbuffer *buffer,
    struct evbuffer_cb_entry *ent);
int evbuffer_remove_cb(struct evbuffer *buffer,
    evbuffer_cb_func cb, void *cbarg);

🚫 启用 / 禁用 回调

#define EVBUFFER_CB_ENABLED 1

int evbuffer_cb_set_flags(struct evbuffer *buffer,
                          struct evbuffer_cb_entry *cb,
                          ev_uint32_t flags);
int evbuffer_cb_clear_flags(struct evbuffer *buffer,
                            struct evbuffer_cb_entry *cb,
                            ev_uint32_t flags);

⏳ 延迟触发回调(defer callbacks)

int evbuffer_defer_callbacks(struct evbuffer *buffer,
                             struct event_base *base);

📌 版本信息

避免数据拷贝的evbuffer网络IO

Avoiding data copies with evbuffer-based IO

在进行快速网络编程时,减少数据拷贝是非常重要的。为了提高性能,Libevent 提供了通过引用直接将数据添加到 evbuffer 的机制,避免不必要的内存复制。

typedef void (*evbuffer_ref_cleanup_cb)(const void *data,
    size_t datalen, void *extra);

int evbuffer_add_reference(struct evbuffer *outbuf,
    const void *data, size_t datlen,
    evbuffer_ref_cleanup_cb cleanupfn, void *extra);

✅ 示例代码

#include <event2/buffer.h>
#include <stdlib.h>
#include <string.h>

#define HUGE_RESOURCE_SIZE (1024*1024)  // 1MB 资源大小

struct huge_resource {
    int reference_count;  // 资源引用计数
    char data[HUGE_RESOURCE_SIZE];  // 资源数据
};

// 创建新资源
struct huge_resource *new_resource(void) {
    struct huge_resource *hr = malloc(sizeof(struct huge_resource));
    hr->reference_count = 1;
    memset(hr->data, 0xEE, sizeof(hr->data));  // 填充数据
    return hr;
}

// 释放资源
void free_resource(struct huge_resource *hr) {
    --hr->reference_count;
    if (hr->reference_count == 0)
        free(hr);
}

// 清理函数
static void cleanup(const void *data, size_t len, void *arg) {
    free_resource(arg);  // 释放资源
}

// 将资源添加到 evbuffer
void spool_resource_to_evbuffer(struct evbuffer *buf,
    struct huge_resource *hr)
{
    ++hr->reference_count;  // 引用计数增加
    evbuffer_add_reference(buf, hr->data, HUGE_RESOURCE_SIZE, cleanup, hr);
}

解释

  1. 创建资源:new_resource 函数创建一个巨大的资源,并填充数据 1MB
  2. 引用计数:reference_count 用来管理资源的生命周期。每次引用资源时,计数增加,释放时计数减少。
  3. 添加到 evbuffer:spool_resource_to_evbuffer 函数通过 evbuffer_add_reference 将资源的内存直接添加到 evbuffer,而不是进行数据拷贝。
  4. 清理函数:cleanup 在资源不再需要时被调用,减少引用计数并在计数为零时释放资源。

🎯 注意

📅 版本信息

将文件添加到evbuffer

Adding a file to an evbuffer

某些操作系统提供了将文件数据直接写入网络的机制,而无需将数据复制到用户空间。这些操作系统中,Libevent提供了一个简单的接口来实现这一功能。

📋 接口说明

int evbuffer_add_file(struct evbuffer *output, int fd, ev_off_t offset,
    size_t length);

该函数将文件中的 length 字节,从 offset 开始,添加到 evbuffer 的末尾。成功返回 0,失败返回 -1。

⚠️ 警告

在Libevent 2.0.x 中,通过 evbuffer_add_file() 添加的数据只能通过以下方式可靠地处理。

不能 通过 evbuffer_remove() 提取这些数据,或通过 evbuffer_pullup() 等函数将其线性化。Libevent 2.1.x 试图解决这个限制。

🚀 操作系统支持

如果操作系统支持 splice() 或 sendfile(),Libevent 会使用这些机制直接将文件数据从文件描述符发送到网络,而无需将数据复制到用户空间。如果 splice 或 sendfile 不可用,但操作系统支持 mmap(),Libevent 会尝试使用 mmap 映射文件,以避免将数据复制到用户空间。否则,Libevent 会从磁盘读取数据到内存中。

💡 文件描述符关闭

文件描述符会在数据被从 evbuffer 刷新或 evbuffer 被释放时关闭。如果你希望对文件有更细粒度的控制,或者不希望在这些时机关闭文件,请参考下面的 file_segment 功能。

🎯 示例代码

假设你想将一个文件的内容添加到 evbuffer 并通过网络发送:

#include <event2/buffer.h>
#include <fcntl.h>
#include <unistd.h>

void send_file(struct evbuffer *output, const char *filename) {
    int fd = open(filename, O_RDONLY);  // 打开文件
    if (fd == -1) {
        perror("Unable to open file");
        return;
    }

    // 将文件添加到 evbuffer
    if (evbuffer_add_file(output, fd, 0, 1024) == -1) {
        perror("Failed to add file to evbuffer");
        close(fd);
        return;
    }

    // 关闭文件描述符
    close(fd);
}

说明

  1. 打开文件:使用open打开文件,获取文件描述符
  2. 将文件添加到evbuffer:调用evbuffer_add_file 将文件地前1024字节添加到 evbuffer 中。
  3. 关闭文件描述:操作完成后关闭文件描述符

📅 版本信息

evbuffer_add_file 函数自 Libevent 2.0.1-alpha 开始提供,支持将文件直接添加到 evbuffer 中。

精细控制与文件片段

Fine-grained control with file segments

evbuffer_add_file() 接口在多次添加相同文件时效率较低,因为它会接管文件地所有权。

struct evbuffer_file_segment;

struct evbuffer_file_segment *evbuffer_file_segment_new(
        int fd, ev_off_t offset, ev_off_t length, unsigned flags);
void evbuffer_file_segment_free(struct evbuffer_file_segment *seg);
int evbuffer_add_file_segment(struct evbuffer *buf,
    struct evbuffer_file_segment *seg, ev_off_t offset, ev_off_t length);

evbuffer_file_segment_new() 函数创建并返回一个新的 evbuffer_file_segment 对象,表示存储在文件描述符 fd 中的文件片段,该片段从 offset 开始,包含 length 字节。出错时返回 NULL。

文件片段通过 sendfile、splice、mmap、CreateFileMapping 或 malloc() 读取实现,具体使用最轻量的支持机制,并在需要时切换到更重的机制。(例如,如果操作系统支持 sendfile 和 mmap,那么文件片段最初可以只通过 sendfile 实现,直到尝试检查其内容时才会使用 mmap()。)你可以通过以下标志来精细控制文件片段的行为:

一旦你拥有了 evbuffer_file_segment,你可以使用 evbuffer_add_file_segment() 将其部分或全部添加到 evbuffer中,这里的 offset 参数是指文件片段中的偏移量,而不是文件本身中的偏移量。

当你不再需要使用文件片段时,可以使用 evbuffer_file_segment_free() 来释放它。实际的存储不会被释放,直到没有任何 evbuffer 再持有该文件片段的引用。

typedef void (*evbuffer_file_segment_cleanup_cb)(
    struct evbuffer_file_segment const *seg, int flags, void *arg);

void evbuffer_file_segment_add_cleanup_cb(struct evbuffer_file_segment *seg,
        evbuffer_file_segment_cleanup_cb cb, void *arg);

你可以为文件片段添加一个回调函数,当文件片段的最后一个引用被释放并且文件片段即将被释放时,该回调将被调用。此回调函数不得尝试恢复文件片段,或将其添加到任何缓冲区等操作。

这些文件片段功能首次出现在 Libevent 2.1.1-alpha 版本中;evbuffer_file_segment_add_cleanup_cb() 函数是在 2.1.2-alpha 版本中添加的。

通过引用将一个evbuffer添加到另一个evbuffer

Adding an evbuffer to another by reference

你也可以通过引用将一个 evbuffer 添加到另一个 evbuffer:与其从一个缓冲区移除内容并将其添加到另一个,不如直接给一个 evbuffer 一个对另一个 evbuffer 的引用,它会像复制所有字节一样表现。

int evbuffer_add_buffer_reference(struct evbuffer *outbuf,
    struct evbuffer *inbuf);

evbuffer_add_buffer_reference() 函数的行为就像你将所有数据从 inbuf 复制到 outbuf,但不会执行不必要的复制。成功时返回 0,失败时返回 -1。

请注意,随后对 inbuf 内容的更改不会反映在 outbuf 中:该函数是通过引用添加 evbuffer 当前的内容,而不是添加整个 evbuffer。

还需要注意的是,不能嵌套缓冲区引用:一个已经作为 outbuf 被传递给 evbuffer_add_buffer_reference 的缓冲区,不能再次作为 inbuf 参与另一次调用。

此功能是在 Libevent 2.1.1-alpha 版本中引入的。

使evbuffer仅支持添加或移除操作

Making an evbuffer add- or remove-only

你可以使用以下函数暂时禁用对 evbuffer 前端或后端的修改。bufferevent 代码在内部使用这些函数来防止意外修改输出缓冲区的前端或输入缓冲区的后端。

int evbuffer_freeze(struct evbuffer *buf, int at_front);
int evbuffer_unfreeze(struct evbuffer *buf, int at_front);

废弃的evbuffer函数

Obsolete evbuffer functions

在 Libevent 2.0 之前,所有的 evbuffer 都是作为一个连续的内存块实现的,这导致访问效率非常低。随着 Libevent 2.0 的发布,evbuffer 接口发生了很多变化,导致之前依赖这些接口的代码无法继续正常工作。

event.h 头文件曾经暴露了 struct evbuffer 的内部结构,但这些接口已不再可用,因为在 1.4 和 2.0 之间发生了太多变化。

为了访问 evbuffer 中的字节数,曾经有 EVBUFFER_LENGTH() 宏。实际的数据可以通过 EVBUFFER_DATA() 获取。现在,这些接口都可以在 event2/buffer_compat.h 中找到。需要注意的是,EVBUFFER_DATA(b) 实际上是 evbuffer_pullup(b, -1) 的别名,这可能非常耗时。

一些其他的废弃接口包括:

char *evbuffer_readline(struct evbuffer *buffer);
unsigned char *evbuffer_find(struct evbuffer *buffer,
    const unsigned char *what, size_t len);

回调接口也发生了变化:

废弃的接口

typedef void (*evbuffer_cb)(struct evbuffer *buffer,
    size_t old_len, size_t new_len, void *arg);
void evbuffer_setcb(struct evbuffer *buffer, evbuffer_cb cb, void *cbarg);

这些废弃的函数仍然可以在 event2/buffer_compat.h 中找到。


连接监听器:接受 TCP 连接

Connection listeners: accepting TCP connections

evconnlistener 机制为你提供了一种方式,用于监听和接收传入的TCP连接。

在本节中的所有函数和类型都在 event2/listener.h 中声明,除非另有说明,它们首次出现在 Libevent 2.0.2-alpha 版本中。

创建或释放 evconnlistener

Creating or freeing an evconnlistener

接口

struct evconnlistener *evconnlistener_new(
                    struct event_base *base,
                    evconnlistener_cb cb, void *ptr, unsigned flags, int backlog,
                    evutil_socket_t fd);

struct evconnlistener *evconnlistener_new_bind(
                    struct event_base *base,
                    evconnlistener_cb cb, void *ptr, unsigned flags, int backlog,
                    const struct sockaddr *sa, int socklen);

void evconnlistener_free(struct evconnlistener *lev);

这两个 evconnlistener_new*() 函数都会分配并返回一个新的连接监听器对象。连接监听器使用一个 event_base 来记录 当指定监听套接字上有新的TCP连接到来时的情况,并新的连接到来时,它会调用你提供的回调函数。

在这两个函数中,base 参数是监听器用来监听连接的 event_base。cb函数是在接收到新连接时调用的回调;如果 cb 为 NULL,则监听器被视为禁用状态,直到设置了回调函数。ptr 指针会作为参数传递给回调函数。

flags 参数控制监听器的行为 —— 后面会详细介绍。

backlog 参数控制网络栈允许在任何时刻处于未接受(not-yet-accepted)状态下等待的最大连接数;详细信息请参阅你所使用系统的 listen() 函数文档。如果 backlog 为负数,Libevent 会尝试为 backlog 选择一个合适的值;如果为零,Libevent 假定你已经在提供的套接字上调用了 listen()

这两个函数在设置监听套接字的方法上有所不同。evconnlistener_new() 函数假设你已经将套接字绑定到你希望监听的端口,并且你是通过 fd 参数传递这个套接字的。如果你希望由 Libevent 自己分配并绑定一个套接字,请调用 evconnlistener_new_bind(),并传入你希望绑定的 sockaddr 以及其长度。

小提示 📝:使用 evconnlistener_new 时,请确保你的监听套接字处于非阻塞模式,可以使用 evutil_make_socket_nonblocking 或手动设置正确的套接字选项。如果监听套接字处于阻塞模式,可能会发生未定义行为。

要释放一个连接监听器,请将其传递给 evconnlistener_free()

可识别的flags

Recognized flags

以下是可以传递给 evconnlistener_new() 函数 flags 参数的标志。你可以将任意数量的这些标志通过按位或(OR)组合在一起使用。

默认情况下,当连接监听器接受一个新的传入套接字时,它会将其设置为非阻塞模式,以便你可以将其与 Libevent 的其他部分一起使用。如果你不希望这种行为,设置此标志。

如果设置了此选项,当你释放连接监听器时,它会关闭其底层的套接字。

如果设置了此选项,连接监听器会在其底层监听套接字上设置 close-on-exec 标志,更多信息请参阅你所使用平台的 fcntlFD_CLOEXEC 文档。

在某些平台上,默认情况下,一旦监听套接字被关闭,其他套接字需要等待一段时间后才能绑定到同一个端口。设置此选项后,Libevent 会将套接字标记为可重用,这样一旦关闭,另一个套接字就可以马上绑定并监听同一端口。

为监听器分配锁,从而可以安全地在多个线程中使用监听器。此功能首次出现在 Libevent 2.0.8-rc 版本。

将监听器初始化为禁用状态,而不是启用状态。你可以通过 evconnlistener_enable() 手动启用它。此功能首次出现在 Libevent 2.1.1-alpha 版本。

如果可能的话,通知内核在某个套接字接收到数据并准备好读取之前,不要将其标记为已接受。如果你的协议在连接建立后并不会立即由客户端发送数据,请不要使用此选项,因为在这种情况下,此选项可能会导致内核永远不会通知你有新连接。并非所有操作系统都支持此选项:在不支持的系统上,此选项不会产生任何效果。此功能首次出现在 Libevent 2.1.1-alpha 版本。

连接监听器回调函数

The connection listener callback

接口

typedef void (*evconnlistener_cb)(struct evconnlistener *listener,
    evutil_socket_t sock, struct sockaddr *addr, int len, void *ptr);

当接收到一个新的连接时,会调用你提供的回调函数。

启用和禁用 evconnlistener

Enabling and disabling an evconnlistener 🔔

接口

int evconnlistener_disable(struct evconnlistener *lev);
int evconnlistener_enable(struct evconnlistener *lev);

这些函数可以临时禁用或重新启用监听新的连接。

调整evconnlistener的回调函数

Adjusting an evconnlistener’s callback

接口

void evconnlistener_set_cb(struct evconnlistener *lev,
    evconnlistener_cb cb, void *arg);

此函数用于调整已有 evconnlistener 的回调函数以及回调参数。

该函数首次出现在 Libevent 2.0.9-rc版本中。

检查evconnlistener

Inspecting an evconnlistener

接口

evutil_socket_t evconnlistener_get_fd(struct evconnlistener *lev);
struct event_base *evconnlistener_get_base(struct evconnlistener *lev);

这些函数分别返回监听器关联的套接字和 event_base。

evconnlistener_get_fd() 函数首次出现在 Libevent 2.0.3-alpha版本。

检测错误

Detecting errors

你可以设置一个错误回调函数,每当监听器上的 accept() 调用失败时,该回调函数会被通知。如果遇到某些错误情况, 而不及时处理可能会导致进程锁死,那么设置错误回调是很重要的。

接口

typedef void (*evconnlistener_errorcb)(struct evconnlistener *lis, void *ptr);

void evconnlistener_set_error_cb(struct evconnlistener *lev,
    evconnlistener_errorcb errorcb);

如果你使用 evconnlistener_set_error_cb() 在监听器上设置了错误回调,当监听器上发生错误时,回调函数会被调用。

它会接收监听器作为第一个参数,以及在调用 evconnlistener_new() 时传入的ptr参数作为第二个参数。此函数首次出现在 Libevent 2.0.8-rc版本。

示例代码:一个Echo服务器

Example code: an echo server

示例

#include <event2/listener.h>
#include <event2/bufferevent.h>
#include <event2/buffer.h>

#include <arpa/inet.h>

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>

static void
echo_read_cb(struct bufferevent *bev, void *ctx)
{
    /* 当 bev 上有数据可读时调用此回调函数。*/
    struct evbuffer *input = bufferevent_get_input(bev);
    struct evbuffer *output = bufferevent_get_output(bev);

    /* 将输入缓冲区中的所有数据复制到输出缓冲区。*/
    evbuffer_add_buffer(output, input);
}

static void
echo_event_cb(struct bufferevent *bev, short events, void *ctx)
{
    if (events & BEV_EVENT_ERROR)
        perror("从 bufferevent 出现错误");
    if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
        bufferevent_free(bev);
    }
}

static void
accept_conn_cb(struct evconnlistener *listener,
    evutil_socket_t fd, struct sockaddr *address, int socklen,
    void *ctx)
{
    /* 收到一个新连接!为其创建一个 bufferevent。*/
    struct event_base *base = evconnlistener_get_base(listener);
    struct bufferevent *bev = bufferevent_socket_new(
        base, fd, BEV_OPT_CLOSE_ON_FREE);

    bufferevent_setcb(bev, echo_read_cb, NULL, echo_event_cb, NULL);

    bufferevent_enable(bev, EV_READ | EV_WRITE);
}

static void
accept_error_cb(struct evconnlistener *listener, void *ctx)
{
    struct event_base *base = evconnlistener_get_base(listener);
    int err = EVUTIL_SOCKET_ERROR();
    fprintf(stderr, "监听器出现错误 %d (%s),正在关闭。\n",
        err, evutil_socket_error_to_string(err));

    event_base_loopexit(base, NULL);
}

int
main(int argc, char **argv)
{
    struct event_base *base;
    struct evconnlistener *listener;
    struct sockaddr_in sin;

    int port = 9876;

    if (argc > 1) {
        port = atoi(argv[1]);
    }
    if (port <= 0 || port > 65535) {
        puts("无效的端口号");
        return 1;
    }

    base = event_base_new();
    if (!base) {
        puts("无法创建事件基础对象");
        return 1;
    }

    /* 使用 sockaddr 前先清零,以防止平台特定字段带来的干扰。*/
    memset(&sin, 0, sizeof(sin));
    /* 设置为 IPv4 地址 */
    sin.sin_family = AF_INET;
    /* 监听 0.0.0.0(所有地址) */
    sin.sin_addr.s_addr = htonl(0);
    /* 监听指定端口 */
    sin.sin_port = htons(port);

    listener = evconnlistener_new_bind(base, accept_conn_cb, NULL,
        LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, -1,
        (struct sockaddr*)&sin, sizeof(sin));
    if (!listener) {
        perror("无法创建监听器");
        return 1;
    }
    evconnlistener_set_error_cb(listener, accept_error_cb);

    event_base_dispatch(base);
    return 0;
}

Libevent的DNS功能

使用Libevent进行DNS: 高级与低级功能

Using DNS with Libevent: high and low-level functionality

Libevent提供了一些用于解析DNS名称API,同时也提供了一个用于实现简单DNS服务器的功能。

我们将先介绍用于名称查询的高级功能,然后再介绍低级功能和服务器端的功能。

💡 注意:Libevent 当前的 DNS 客户端实现存在一些已知的限制:它不支持 TCP 查询、不支持 DNSSec,也不支持任意类型的记录。我们希望在未来的某个版本中修复这些问题,但目前这些功能尚未实现。

前置知识:可移植的阻塞式名称解析

🧩 Preliminaries: Portable blocking name resolution

为了帮助移植那些已经使用阻塞式名称解析的程序,Libevent提供了一个可移植版本的标准 getaddrinfo() 接口。

当你的程序需要在某些平台上运行,而这些平台要么没有 getaddrinfo() 函数,要么 getaddrinfo() 的实现不符合标准时(这种情况其实非常多😱),这个功能就非常有用了。

标准 getaddrinfo() 接口在 RFC 3493 第 6.1 节中有定义。下面的 “兼容性说明” 部分会总结 Libevent 版的实现有哪些不符合标准的地方。

✨ 接口说明

struct evutil_addrinfo {
    int ai_flags;
    int ai_family;
    int ai_socktype;
    int ai_protocol;
    size_t ai_addrlen;
    char *ai_canonname;
    struct sockaddr *ai_addr;
    struct evutil_addrinfo *ai_next;
};

宏定义(可与 ai_flags 配和使用)

#define EVUTIL_AI_PASSIVE     /* ... */
#define EVUTIL_AI_CANONNAME   /* ... */
#define EVUTIL_AI_NUMERICHOST /* ... */
#define EVUTIL_AI_NUMERICSERV /* ... */
#define EVUTIL_AI_V4MAPPED    /* ... */
#define EVUTIL_AI_ALL         /* ... */
#define EVUTIL_AI_ADDRCONFIG  /* ... */

配和接口:

int evutil_getaddrinfo(
        const char *nodename, 
        const char *servname,
        const struct evutil_addrinfo *hints, 
        struct evutil_addrinfo **res);

void evutil_freeaddrinfo(struct evutil_addrinfo *ai);

const char *evutil_gai_strerror(int err);

🛠 使用说明

🎯 hints 字段说明

标志 说明
EVUTIL_AI_PASSIVE 用于监听时(nodename NULL ➔ 0.0.0.0)
EVUTIL_AI_CANONNAME 返回主机的规范名
EVUTIL_AI_NUMERICHOST 只允许字面 IP 地址,禁止 DNS 解析
EVUTIL_AI_NUMERICSERV 只允许十进制端口号
EVUTIL_AI_V4MAPPED 支持将 IPv4 地址映射为 IPv6
EVUTIL_AI_ALL 和 V4MAPPED 配合,返回所有 IPv4 映射地址
EVUTIL_AI_ADDRCONFIG 只返回当前设备支持的地址族

🧹 内存管理

⚠️ 错误码列表

如果查询失败,会返回以下错误码之一。

错误码 说明
EVUTIL_EAI_ADDRFAMILY 地址族与 nodename 不兼容
EVUTIL_EAI_AGAIN 暂时性错误,稍后重试
EVUTIL_EAI_FAIL 非可恢复错误,DNS 解析器可能坏了
EVUTIL_EAI_BADFLAGS hints 中 ai_flags 无效
EVUTIL_EAI_FAMILY hints 中 ai_family 不支持
EVUTIL_EAI_MEMORY 内存不足
EVUTIL_EAI_NODATA 主机存在,但没有地址信息
EVUTIL_EAI_NONAME 主机不存在
EVUTIL_EAI_SERVICE 服务不存在
EVUTIL_EAI_SOCKTYPE socket 类型不支持
EVUTIL_EAI_SYSTEM 其他系统错误(需要检查 errno)
EVUTIL_EAI_CANCEL 查询被取消(只在异步查询中出现)

可以用 evutil_gai_strerror() 将错误码转为可读的字符串。

📄 注意事项

如果你的操作系统本身定义了 struct addrinfo,那么 evutil_addrinfo 其实就是它的别名。

同理,EVUTIL_AI_*EVUTIL_EAI_* 宏如果系统原生有,也直接用系统的定义。

📚 示例:解析主机名并进行阻塞连接

#include <event2/util.h>

#include <sys/socket.h>
#include <sys/types.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>

evutil_socket_t
get_tcp_socket_for_host(const char *hostname, ev_uint16_t port)
{
    char port_buf[6];
    struct evutil_addrinfo hints;
    struct evutil_addrinfo *answer = NULL;
    int err;
    evutil_socket_t sock;

    /* 将端口号转换成字符串 */
    evutil_snprintf(port_buf, sizeof(port_buf), "%d", (int)port);

    /* 设置 hints */
    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_UNSPEC;  /* IPv4 或 IPv6 都可以 */
    hints.ai_socktype = SOCK_STREAM; /* TCP 流 */
    hints.ai_protocol = IPPROTO_TCP;
    hints.ai_flags = EVUTIL_AI_ADDRCONFIG;

    /* 查询主机名 */
    err = evutil_getaddrinfo(hostname, port_buf, &hints, &answer);
    if (err != 0) {
        fprintf(stderr, "解析 '%s' 出错: %s\n",
                hostname, evutil_gai_strerror(err));
        return -1;
    }

    assert(answer); /* 成功的话至少有一个结果 */

    /* 创建 socket 并连接 */
    sock = socket(answer->ai_family,
                  answer->ai_socktype,
                  answer->ai_protocol);
    if (sock < 0)
        return -1;
    if (connect(sock, answer->ai_addr, answer->ai_addrlen)) {
        /* 注意这里是阻塞式 connect */
        EVUTIL_CLOSESOCKET(sock);
        return -1;
    }

    return sock;
}

📢 注意:

使用 evdns_getaddrinfo() 实现非阻塞的主机名解析

Non-blocking hostname resolution with evdns_getaddrinfo()

常规的 getaddrinfo() 接口,以及上面提到的 evutil_getaddrinfo(),主要问题在于它们是阻塞的:调用它们时,当前线程必须等待查询 DNS 服务器并接收响应。在使用 Libevent 时,这种行为通常是不希望出现的。

因此,为了实现非阻塞解析,Libevent 提供了一组函数,可以发起 DNS 请求,并通过 Libevent 来等待服务器响应。

接口定义

typedef void (*evdns_getaddrinfo_cb)(
    int result, struct evutil_addrinfo *res, void *arg);

struct evdns_getaddrinfo_request;

struct evdns_getaddrinfo_request *evdns_getaddrinfo(
    struct evdns_base *dns_base,
    const char *nodename, const char *servname,
    const struct evutil_addrinfo *hints_in,
    evdns_getaddrinfo_cb cb, void *arg);

void evdns_getaddrinfo_cancel(struct evdns_getaddrinfo_request *req);

⚡ 注意:

示例:使用 evdns_getaddrinfo() 实现非阻塞并发域名解析

#include <event2/dns.h>
#include <event2/util.h>
#include <event2/event.h>

#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>

int n_pending_requests = 0;
struct event_base *base = NULL;

struct user_data {
    char *name; // 要解析的域名
    int idx;    // 在命令行参数中的位置
};

void callback(int errcode, struct evutil_addrinfo *addr, void *ptr)
{
    struct user_data *data = ptr;
    const char *name = data->name;
    if (errcode) {
        printf("%d. %s -> %s\n", data->idx, name, evutil_gai_strerror(errcode));
    } else {
        struct evutil_addrinfo *ai;
        printf("%d. %s", data->idx, name);
        if (addr->ai_canonname)
            printf(" [%s]", addr->ai_canonname);
        puts("");
        for (ai = addr; ai; ai = ai->ai_next) {
            char buf[128];
            const char *s = NULL;
            if (ai->ai_family == AF_INET) {
                struct sockaddr_in *sin = (struct sockaddr_in *)ai->ai_addr;
                s = evutil_inet_ntop(AF_INET, &sin->sin_addr, buf, 128);
            } else if (ai->ai_family == AF_INET6) {
                struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)ai->ai_addr;
                s = evutil_inet_ntop(AF_INET6, &sin6->sin6_addr, buf, 128);
            }
            if (s)
                printf("    -> %s\n", s);
        }
        evutil_freeaddrinfo(addr);
    }
    free(data->name);
    free(data);
    if (--n_pending_requests == 0)
        event_base_loopexit(base, NULL);
}

int main(int argc, char **argv)
{
    int i;
    struct evdns_base *dnsbase;

    if (argc == 1) {
        puts("No addresses given.");
        return 0;
    }
    base = event_base_new();
    if (!base)
        return 1;
    dnsbase = evdns_base_new(base, 1);
    if (!dnsbase)
        return 2;

    for (i = 1; i < argc; ++i) {
        struct evutil_addrinfo hints;
        struct evdns_getaddrinfo_request *req;
        struct user_data *user_data;
        memset(&hints, 0, sizeof(hints));
        hints.ai_family = AF_UNSPEC;
        hints.ai_flags = EVUTIL_AI_CANONNAME;
        hints.ai_socktype = SOCK_STREAM;
        hints.ai_protocol = IPPROTO_TCP;

        if (!(user_data = malloc(sizeof(struct user_data)))) {
            perror("malloc");
            exit(1);
        }
        if (!(user_data->name = strdup(argv[i]))) {
            perror("strdup");
            exit(1);
        }
        user_data->idx = i;

        ++n_pending_requests;
        req = evdns_getaddrinfo(
            dnsbase, argv[i], NULL,
            &hints, callback, user_data);
        if (req == NULL) {
          printf("    [request for %s returned immediately]\n", argv[i]);
          /* 不需要自己释放 user_data,回调里已经处理了 */
        }
    }

    if (n_pending_requests)
      event_base_dispatch(base);

    evdns_base_free(dnsbase, 0);
    event_base_free(base);

    return 0;
}

小结✨

创建与配置 evdns_base

Creating and configuring an evdns_base

在你能使用 evdns 进行非阻塞DNS查询之前,需要先创建并配置一个 evdns_base 对象。

每个 evdns_base 负责:

接口定义

struct evdns_base *evdns_base_new(struct event_base *event_base, int initialize);
void evdns_base_free(struct evdns_base *base, int fail_requests);

说明

小结 ✨

从系统配置初始化 evdns

Initializing evdns from the system configuration

如果你希望对 evdns_base 的初始化过程有更多控制,可以在调用 evdns_base_new()时,把 initialize 参数设为0,然后自己手动调用下面的函数来配置。

接口定义

#define DNS_OPTION_SEARCH     1
#define DNS_OPTION_NAMESERVERS 2
#define DNS_OPTION_MISC       4
#define DNS_OPTION_HOSTSFILE  8
#define DNS_OPTIONS_ALL       15

int evdns_base_resolv_conf_parse(struct evdns_base *base, int flags, const char *filename);

#ifdef WIN32
int evdns_base_config_windows_nameservers(struct evdns_base *);
#define EVDNS_BASE_CONFIG_WINDOWS_NAMESERVERS_IMPLEMENTED
#endif

函数说明

evdns_base_resolv_conf_parse()

支持的flags选项

选项 说明
DNS_OPTION_SEARCH 读取 domain、search 字段及 ndots 配置,用来处理不完整主机名的解析。
DNS_OPTION_NAMESERVERS 读取 nameserver 字段,添加 DNS 服务器。
DNS_OPTION_MISC 读取其他一些杂项配置,比如超时、最大重试次数等。
DNS_OPTION_HOSTSFILE 解析 /etc/hosts 文件,把里面的条目也纳入解析。
DNS_OPTIONS_ALL 读取所有能识别的配置。

Windows 特别说明

在 Windows 系统上,因为没有标准的 resolv.conf 文件,所以 Libevent 提供了:

resolv.conf 文件格式

libevent识别的关键字包括

关键字 说明
nameserver 后跟一个 DNS 服务器的 IP 地址(可以是 IP:Port 或 [IPv6]:port 格式)。
domain 本地默认域名。
search 本地域名搜索列表,用来补全不完整的主机名。
options 其他可选配置项,以空格分隔,例如 timeout:5 ndots:2。

options支持的子选项

选项 说明 默认值
ndots:INTEGER 本地搜索时,域名前至少需要有几个.(点)才认为是完整的。 1
timeout:FLOAT 单次 DNS 请求的超时时间(秒)。 5
max-timeouts:INT 连续多少次超时就认为 nameserver 挂了。 3
max-inflight:INT 同时最多允许多少个未完成的 DNS 请求。 64
attempts:INT 单个请求最多重试几次。 3
randomize-case:INT 是否在请求中随机大小写(增加安全性,防范 DNS 污染攻击)。 1
bind-to:ADDRESS 发送 DNS 请求时绑定的本地地址。
initial-probe-timeout:FLOAT nameserver 挂掉后,第一次探测它恢复的超时时间(秒)。 10
getaddrinfo-allow-skew:FLOAT evdns_getaddrinfo() 请求 IPv4 和 IPv6 后,等待另一种类型回复的额外等待时间(秒)。 3

⚠️ 不认识的关键字或选项会被自动忽略,不会报错。

小结 ✨

手动配置 evdns

Configuring evdns manually

如果你想要对 evdns的行为有更细粒度的控制,可以使用下面这些函数接口。

接口定义

int evdns_base_nameserver_sockaddr_add(struct evdns_base *base,
                                       const struct sockaddr *sa,
                                       ev_socklen_t len,
                                       unsigned flags);

int evdns_base_nameserver_ip_add(struct evdns_base *base,
                                 const char *ip_as_string);

int evdns_base_load_hosts(struct evdns_base *base,
                          const char *hosts_fname);

void evdns_base_search_clear(struct evdns_base *base);

void evdns_base_search_add(struct evdns_base *base,
                           const char *domain);

void evdns_base_search_ndots_set(struct evdns_base *base,
                                 int ndots);

int evdns_base_set_option(struct evdns_base *base,
                          const char *option,
                          const char *val);

int evdns_base_count_nameservers(struct evdns_base *base);

各函数解释

  1. evdns_base_nameserver_sockaddr_add()

这个接口是在 Libevent 2.0.7-rc 版本新增的。

  1. evdns_base_nameserver_ip_add()
  1. evdns_base_load_hosts()
  1. evdns_base_search_clear()
  1. evdns_base_search_add()
  1. evdns_base_search_ndots_set()
  1. evdns_base_set_option()
  1. evdns_base_count_nameservers()

小结 🎯

功能 推荐接口
手动添加 DNS 服务器(地址对象) evdns_base_nameserver_sockaddr_add()
手动添加 DNS 服务器(IP字符串) evdns_base_nameserver_ip_add()
加载自定义 hosts 文件 evdns_base_load_hosts()
修改搜索后缀 evdns_base_search_clear() / evdns_base_search_add()
设置单独选项 evdns_base_set_option()
检查 DNS 服务器数量 evdns_base_count_nameservers()

evdns模块的库级配置

Library-side configuration

在 evdns 模块中,有几个函数可以用于设置整个库级别(不是单独的 evdns_base)的一些配置。

接口定义

typedef void (*evdns_debug_log_fn_type)(int is_warning, const char *msg);

void evdns_set_log_fn(evdns_debug_log_fn_type fn);

void evdns_set_transaction_id_fn(ev_uint16_t (*fn)(void));

各接口解释

  1. evdns_set_log_fn()
void fn(int is_warning, const char *msg)

✅ 举例:可以让日志输出到终端、文件,或者集成到你自己的日志系统中。

  1. evdns_set_transaction_id_fn()

小结 🎯

配置 作用 备注
evdns_set_log_fn() 设置日志回调 可输出警告或普通日志
evdns_set_transaction_id_fn() 设置自定义随机事务ID生成函数 2.0.4-alpha之后通常不需要设置

🚀 额外补充:日志回调小示例

如果你想快速实现一个把 evdns 日志打印到终端的函数,可以这样写:

#include <stdio.h>
#include <event2/dns.h>

void my_evdns_log_fn(int is_warning, const char *msg) {
    if (is_warning) {
        fprintf(stderr, "[WARN] %s\n", msg);
    } else {
        fprintf(stdout, "[INFO] %s\n", msg);
    }
}

int main() {
    // 初始化你的 event_base...
    
    // 设置 evdns 的日志回调
    evdns_set_log_fn(my_evdns_log_fn);
    
    // 继续你的程序逻辑
}

这样一来,evdns 的内部信息就可以跟你的程序日志集成起来了,方便调试!🔍

低层级DNS接口

Low-level DNS interfaces

有时候你可能需要比 evdns_getaddrinfo() 更细粒度地控制DNS请求。Libevent提供了一些底层接口可以让你手动发起、控制特定的DNS查询。

当前缺少的功能:

目前,Libevent 的 DNS 支持还不完整,缺少一些低层 DNS 库通常具备的功能,比如:

⚡ 如果你需要这些高级特性:

接口定义

#define DNS_QUERY_NO_SEARCH /* 不使用搜索域 */

#define DNS_IPv4_A         /* 查询 A 记录 (IPv4 地址) */
#define DNS_PTR            /* 查询 PTR 记录 (反向解析) */
#define DNS_IPv6_AAAA      /* 查询 AAAA 记录 (IPv6 地址) */

typedef void (*evdns_callback_type)(int result, char type, int count,
    int ttl, void *addresses, void *arg);

主要的查询函数

函数 作用
evdns_base_resolve_ipv4 查询 IPv4 地址 (A记录)
evdns_base_resolve_ipv6 查询 IPv6 地址 (AAAA记录)
evdns_base_resolve_reverse 反向查询 IPv4 地址对应的主机名 (PTR记录)
evdns_base_resolve_reverse_ipv6 反向查询 IPv6 地址对应的主机名 (PTR记录)

各参数详细说明

❗注意:反向查询(reverse lookup)不会使用搜索域,所以 DNS_QUERY_NO_SEARCH 对它们无效。

回调函数(callback)的行为

DNS 错误码表(DNS Errors)

错误码 含义
DNS_ERR_NONE 没有错误
DNS_ERR_FORMAT 服务器无法理解查询
DNS_ERR_SERVERFAILED 服务器内部错误
DNS_ERR_NOTEXIST 没有对应记录
DNS_ERR_NOTIMPL 服务器不支持这种查询类型
DNS_ERR_REFUSED 服务器因策略拒绝查询
DNS_ERR_TRUNCATED 响应过大,UDP包截断
DNS_ERR_UNKNOWN 未知内部错误
DNS_ERR_TIMEOUT 等待超时
DNS_ERR_SHUTDOWN evdns 系统被用户要求关闭
DNS_ERR_CANCEL 用户取消了请求
DNS_ERR_NODATA 返回了响应,但没有答案(2.0.15引入)

辅助函数

const char *evdns_err_to_string(int err);
void evdns_cancel_request(struct evdns_base *base, struct evdns_request *req);

调用取消后,对应回调会被立即触发,result 设为 DNS_ERR_CANCEL。

🌟 小总结

Libevent 提供了:

如果你想要做更复杂的 DNS 查询,比如 CNAME、MX、SRV记录,或者用TCP传输——那么还是建议考虑其他更专业的DNS库(比如 c-ares)。

暂停 DNS 客户端操作并更换 Nameserver

Suspending DNS client operations and changing nameservers

有时候,你可能希望在不中断正在进行的 DNS 请求的情况下,重新配置或者关闭 DNS 子系统。Libevent 提供了相应的接口来实现这一需求。

接口定义

int evdns_base_clear_nameservers_and_suspend(struct evdns_base *base);
int evdns_base_resume(struct evdns_base *base);

函数说明

🧹 evdns_base_clear_nameservers_and_suspend(base)

▶️ evdns_base_resume(base)

返回值:

(这两个函数是从 Libevent 2.0.1-alpha 版本引入的。)

🌟 小总结

在 Libevent 中,如果你需要:

就可以这样做

// 1. 暂停并清空现有 nameserver
evdns_base_clear_nameservers_and_suspend(base);

// 2. 添加新的 nameserver
evdns_base_nameserver_ip_add(base, "8.8.8.8");

// 3. 恢复
evdns_base_resume(base);

DNS服务器接口

DNS server interfaces

Libevent 除了可以作为DNS 客户端发起请求外,还提供了一些简单的功能,可以让你构建一个非常基础的 DNS 服务器,用于处理和响应 UDP 上收到的 DNS 查询请求。

📖 前置要求

这一部分的内容假设你已经对 DNS 协议有一定了解,例如:

如果你对这些还不是很熟悉,建议先稍微补一下 DNS 基础知识哦!🧠

创建和关闭一个DNS服务器

Creating and closing a DNS server

接口

struct evdns_server_port *evdns_add_server_port_with_base(
    struct event_base *base,
    evutil_socket_t socket,
    int flags,
    evdns_request_callback_fn_type callback,
    void *user_data);

typedef void (*evdns_request_callback_fn_type)(
    struct evdns_server_request *request,
    void *user_data);

void evdns_close_server_port(struct evdns_server_port *port);

🛠️ 使用说明

  1. 开始监听DNS请求

要开始监听 DNS 查询请求,你需要调用 evdns_add_server_port_with_base()。 这个函数的参数解释如下:

👉 成功调用后,会返回一个新的 evdns_server_port * 句柄。

  1. 关闭DNS服务器

当你不再需要处理DNS请求时,可以调用 evdns_close_server_port() 来关闭服务器并清理资源:

void evdns_close_server_port(struct evdns_server_port *port);

只需要传入之前返回的 evdns_server_port * 对象就可以了。

📅 版本信息

检查DNS请求

Examining a DNS request

📜 当前情况

遗憾的是,Libevent目前还没有提供特别优雅的编程接口来查看 DNS 请求。

因此,如果你想检查请求内容,需要手动包含头文件:

#include <event2/dns_struct.h>

然后直接查看 evdns_server_request 结构体。

官方也提到,希望未来的版本能改善这一点。😅

🛠️ 接口(Interface)

  1. evdns_server_request 结构体
struct evdns_server_request {
    int flags;                         // 请求中设置的 DNS 标志位
    int nquestions;                    // 请求中包含的问题数量
    struct evdns_server_question **questions;  // 指向问题数组的指针
};
  1. evdns_server_question 结构体
struct evdns_server_question {
    int type;                 // 请求的记录类型,例如 A/AAAA/PTR 等
    int dns_question_class;   // 请求的类,通常是 EVDNS_CLASS_INET(即 IN 类)
    char name[1];              // 查询的域名(NUL 结尾的字符串)
};

⚠️ 注意:在 Libevent 1.4 之前,这里的 dns_question_class 字段名字叫 class,由于在 C++ 中 class 是关键字,所以后来改了。如果你用的是老代码,需要注意这一点。

  1. 类型常量定义
#define EVDNS_QTYPE_AXFR 252  // 区域传送请求(AXFR)
#define EVDNS_QTYPE_ALL  255  // 请求所有类型记录(ANY)

还有其他常见类型,比如 A、AAAA、PTR 等(常规 DNS 类型)。

📬 获取发起请求的客户端地址

如果你想知道是谁发起了某个 DNS 查询,可以用这个接口:

int evdns_server_request_get_requesting_addr(struct evdns_server_request *req,
    struct sockaddr *sa, int addr_len);

👉 成功返回 0,失败返回 -1。

这个函数是从 Libevent 1.3c 开始引入的。

响应 DNS 请求

Responding to DNS requests

🛠️ 基本流程

每当你的 DNS 服务器收到一个请求时,Libevent 会调用你提供的回调函数,并附带一个 user_data 指针。

在回调函数中,你必须选择:

🧩 添加应答记录(Resource Records)

在响应请求前,你可以往响应中添加一条或多条答案记录。

添加 A 记录(IPv4 地址)

int evdns_server_request_add_a_reply(struct evdns_server_request *req,
    const char *name, int n, const void *addrs, int ttl);

添加 AAAA 记录(IPv6 地址)

int evdns_server_request_add_aaaa_reply(struct evdns_server_request *req,
    const char *name, int n, const void *addrs, int ttl);

和 add_a_reply 类似,不过 addrs 是 n × 16 字节的 IPv6 地址。

添加 CNAME 记录(别名)

int evdns_server_request_add_cname_reply(struct evdns_server_request *req,
    const char *name, const char *cname, int ttl);

添加 PTR 记录(反向解析)

int evdns_server_request_add_ptr_reply(struct evdns_server_request *req,
    struct in_addr *in, const char *inaddr_name, const char *hostname,
    int ttl);

⚡ 注意 in 和 inaddr_name 必须至少指定一个。

🧰 添加任意类型的记录

如果以上接口不满足你的需求,可以使用更通用的方法添加任意类型的 RR:

int evdns_server_request_add_reply(struct evdns_server_request *req,
    int section, const char *name, int type, int dns_class, int ttl,
    int datalen, int is_name, const char *data);

参数说明:

常见宏定义

// Section 部分
#define EVDNS_ANSWER_SECTION     0
#define EVDNS_AUTHORITY_SECTION  1
#define EVDNS_ADDITIONAL_SECTION 2

// Type 类型
#define EVDNS_TYPE_A     1
#define EVDNS_TYPE_NS    2
#define EVDNS_TYPE_CNAME 5
#define EVDNS_TYPE_SOA   6
#define EVDNS_TYPE_PTR  12
#define EVDNS_TYPE_MX   15
#define EVDNS_TYPE_TXT  16
#define EVDNS_TYPE_AAAA 28

// Class 类别
#define EVDNS_CLASS_INET 1

📨 发送或丢弃响应

发送响应

int evdns_server_request_respond(struct evdns_server_request *req, int err);

丢弃请求

int evdns_server_request_drop(struct evdns_server_request *req);

🏴 设置响应的 Flags

如果想在响应消息中设置特定标志位(如 AA 或 RD),可以调用:

void evdns_server_request_set_flags(struct evdns_server_request *req, int flags);

常用标志位:

#define EVDNS_FLAGS_AA  0x400  // Authoritative Answer
#define EVDNS_FLAGS_RD  0x080  // Recursion Desired

📢 所有这些接口都是从 Libevent 1.3 版本引入的, 只有 evdns_server_request_set_flags() 是 2.0.1-alpha 新增的。

DNS服务器样例

DNS Server example

这个示例程序实现了一个非常基础的 DNS Server,监听本地端口 5353,只处理和 localhost 相关的查询(A、AAAA、PTR 记录)。

#include <event2/dns.h>          // libevent 的 DNS 支持
#include <event2/dns_struct.h>   // evdns_server_request 结构体定义
#include <event2/util.h>         // 工具函数,如 evutil_make_socket_nonblocking
#include <event2/event.h>        // 事件循环相关

#include <sys/socket.h>          // 套接字接口
#include <stdio.h>               // 标准输入输出
#include <string.h>              // 字符串处理
#include <assert.h>              // 断言(这里没用上)

// 尝试绑定到 5353 端口。53 端口是标准 DNS 端口,但通常需要 root 权限才能绑定。
#define LISTEN_PORT 5353

// 127.0.0.1 的反向解析域名(PTR 查询使用)
#define LOCALHOST_IPV4_ARPA "1.0.0.127.in-addr.arpa"
// ::1 的反向解析域名(IPv6 PTR 查询使用)
#define LOCALHOST_IPV6_ARPA ("1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0."         \
                             "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa")

// localhost 的 IPv4 地址
const ev_uint8_t LOCALHOST_IPV4[] = { 127, 0, 0, 1 };
// localhost 的 IPv6 地址
const ev_uint8_t LOCALHOST_IPV6[] = { 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1 };

// 返回 DNS 记录的 TTL(生存时间),单位秒
#define TTL 4242

/*
 * 这个玩具版 DNS 服务器回调函数,处理:
 * - 名字是 "localhost" 的 A(IPv4)和 AAAA(IPv6)查询
 * - 地址是 127.0.0.1 或 ::1 的反向查询(PTR记录)
 */
void server_callback(struct evdns_server_request *request, void *data)
{
    int i;
    int error = DNS_ERR_NONE; // 初始化没有错误

    // 遍历请求中的所有问题(通常一个请求中只有一个问题,但也可能多个)
    for (i = 0; i < request->nquestions; ++i) {
        const struct evdns_server_question *q = request->questions[i];
        int ok = -1;

        // 使用大小写不敏感、locale独立的字符串比较
        if (0 == evutil_ascii_strcasecmp(q->name, "localhost")) {
            // 查询名字是 "localhost"
            if (q->type == EVDNS_TYPE_A)
                // 返回 A 记录(IPv4 127.0.0.1)
                ok = evdns_server_request_add_a_reply(
                       request, q->name, 1, LOCALHOST_IPV4, TTL);
            else if (q->type == EVDNS_TYPE_AAAA)
                // 返回 AAAA 记录(IPv6 ::1)
                ok = evdns_server_request_add_aaaa_reply(
                       request, q->name, 1, LOCALHOST_IPV6, TTL);
        } 
        else if (0 == evutil_ascii_strcasecmp(q->name, LOCALHOST_IPV4_ARPA)) {
            // 查询名字是 127.0.0.1 的反向解析
            if (q->type == EVDNS_TYPE_PTR)
                ok = evdns_server_request_add_ptr_reply(
                       request, NULL, q->name, "LOCALHOST", TTL);
        } 
        else if (0 == evutil_ascii_strcasecmp(q->name, LOCALHOST_IPV6_ARPA)) {
            // 查询名字是 ::1 的反向解析
            if (q->type == EVDNS_TYPE_PTR)
                ok = evdns_server_request_add_ptr_reply(
                       request, NULL, q->name, "LOCALHOST", TTL);
        } 
        else {
            // 如果问题不是我们能处理的,设置错误码为 "名字不存在"
            error = DNS_ERR_NOTEXIST;
        }

        // 如果处理失败,且之前没错误,设置为服务器失败
        if (ok < 0 && error == DNS_ERR_NONE)
            error = DNS_ERR_SERVERFAILED;
    }

    // 最后发送响应
    evdns_server_request_respond(request, error);
}

int main(int argc, char **argv)
{
    struct event_base *base;               // 事件循环对象
    struct evdns_server_port *server;       // DNS服务器端口对象
    evutil_socket_t server_fd;              // 套接字
    struct sockaddr_in listenaddr;          // 本地绑定地址结构体

    // 创建事件循环
    base = event_base_new();
    if (!base)
        return 1;

    // 创建一个 UDP socket
    server_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (server_fd < 0)
        return 2;

    // 清零 listenaddr,并设置 IPv4 地址、端口
    memset(&listenaddr, 0, sizeof(listenaddr));
    listenaddr.sin_family = AF_INET;
    listenaddr.sin_port = htons(LISTEN_PORT);  // 端口号(网络字节序)
    listenaddr.sin_addr.s_addr = INADDR_ANY;    // 监听所有网卡

    // 绑定 socket 到本地地址
    if (bind(server_fd, (struct sockaddr*)&listenaddr, sizeof(listenaddr)) < 0)
        return 3;

    // 设置 socket 为非阻塞
    // (如果是阻塞的,第一次收到请求后 event loop 会被"卡死")
    if (evutil_make_socket_nonblocking(server_fd) < 0)
        return 4;

    // 把 socket 加入 event loop,并设置请求处理回调
    server = evdns_add_server_port_with_base(base, server_fd, 0,
                                             server_callback, NULL);

    // 启动事件循环,开始监听请求
    event_base_dispatch(base);

    // 清理资源
    evdns_close_server_port(server);
    event_base_free(base);

    return 0;
}

✨ 总结

这个服务器能做的事情非常简单:

过时的 DNS 接口

Obsolete DNS interfaces

过时接口列表

void evdns_base_search_ndots_set(struct evdns_base *base, const int ndots);
int evdns_base_nameserver_add(struct evdns_base *base, unsigned long int address);
void evdns_set_random_bytes_fn(void (*fn)(char *, size_t));

struct evdns_server_port *evdns_add_server_port(evutil_socket_t socket,
    int flags, evdns_request_callback_fn_type callback, void *user_data);

📜 关于全局 evdns_base 的说明

🔥 新旧函数对照表

当前函数 过时的全局版本
event_base_new() evdns_init()
evdns_base_free() evdns_shutdown()
evdns_base_nameserver_add() evdns_nameserver_add()
evdns_base_count_nameservers() evdns_count_nameservers()
evdns_base_clear_nameservers_and_suspend() evdns_clear_nameservers_and_suspend()
evdns_base_resume() evdns_resume()
evdns_base_nameserver_ip_add() evdns_nameserver_ip_add()
evdns_base_resolve_ipv4() evdns_resolve_ipv4()
evdns_base_resolve_ipv6() evdns_resolve_ipv6()
evdns_base_resolve_reverse() evdns_resolve_reverse()
evdns_base_resolve_reverse_ipv6() evdns_resolve_reverse_ipv6()
evdns_base_set_option() evdns_set_option()
evdns_base_resolv_conf_parse() evdns_resolv_conf_parse()
evdns_base_search_clear() evdns_search_clear()
evdns_base_search_add() evdns_search_add()
evdns_base_search_ndots_set() evdns_search_ndots_set()
evdns_base_config_windows_nameservers() evdns_config_windows_nameservers()

🧩 其他补充


HTTP 服务器

使用内置 HTTP 服务器

Using the built-in HTTP server

如果你想要构建原生应用程序,Libevent 提供的纯网络接口非常有用;

但如今越来越常见的开发方式是,围绕 HTTP 协议和网页来构建应用,网页可以加载数据,或者更常见的是动态刷新数据。

要使用 Libevent 的 HTTP 服务,基本结构和前面描述的主要网络事件模型是一样的,区别在于:你不需要自己处理网络接口的细节,HTTP 封装模块已经帮你做好了。

整个流程变得非常简单,主要包含以下四个步骤:

  1. 初始化(initialize)
  2. 启动 HTTP 服务器(start HTTP server)
  3. 设置 HTTP 回调函数(set HTTP callback function)
  4. 进入事件循环(enter event loop)

除此之外,你只需要在回调函数中编写代码来发送响应数据即可。

Example: A basic HTTP server

#include <string.h>
#include <signal.h>
#include <event2/buffer.h>
#include <event2/event.h>
#include <event2/http.h>

static void
generic_request_handler(struct evhttp_request *req, void *ctx)
{
        struct evbuffer *reply = evbuffer_new();

        evbuffer_add_printf(reply, "It works!");
        evhttp_send_reply(req, HTTP_OK, NULL, reply);
        evbuffer_free(reply);
}

static void
signal_cb(evutil_socket_t fd, short event, void *arg)
{
        printf("%s signal received\n", strsignal(fd));
        event_base_loopbreak(arg);
}

int
main()
{
        ev_uint16_t http_port = 8080;
        char *http_addr = "0.0.0.0";
        struct event_base *base;
        struct evhttp *http_server;
        struct event *sig_int;

        base = event_base_new();

        http_server = evhttp_new(base);
        evhttp_bind_socket(http_server, http_addr, http_port);
        evhttp_set_gencb(http_server, generic_request_handler, NULL);

        sig_int = evsignal_new(base, SIGINT, signal_cb, base);
        event_add(sig_int, NULL);

        printf("Listening requests on http://%s:%d\n", http_addr, http_port);

        event_base_dispatch(base);

        evhttp_free(http_server);
        event_free(sig_int);
        event_base_free(base);
}

🧩 理解 Libevent HTTP 示例基础

在前面的示例基础上,这里的代码基本结构应该已经比较容易理解了。

主要的核心要素是:

🚀 Libevent HTTP 封装提供了丰富的功能,例如:

📦 让我们进一步扩展示例,让 Libevent 充当一个类似 Nginx 的静态内容服务器!

// 引入标准库头文件
#include <dirent.h>
#include <fcntl.h>
#include <limits.h>
#include <signal.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

// 引入 Libevent 相关头文件
#include <event2/buffer.h>
#include <event2/event.h>
#include <event2/http.h>

// 引入 Bootstrap CDN 地址,用于生成目录列表页面
#define BOOTSTRAP_CDN "https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist"
#define BOOTSTRAP_JS BOOTSTRAP_CDN "/js"
#define BOOTSTRAP_CSS BOOTSTRAP_CDN "/css"

// 扩展名与 Content-Type 映射表
static const struct table_entry {
        const char *extension;
        const char *content_type;
} content_type_table[] = {
        {"txt", "text/plain"},
        {"c", "text/plain"},
        {"h", "text/plain"},
        {"html", "text/html"},
        {"htm", "text/htm"},
        {"css", "text/css"},
        {"gif", "image/gif"},
        {"jpg", "image/jpeg"},
        {"jpeg", "image/jpeg"},
        {"png", "image/png"},
        {"pdf", "application/pdf"},
        {"ps", "application/postscript"},
        {NULL, NULL},
};

// 添加 Content-Length 头部
static void
add_content_length(struct evhttp_request *req, unsigned len)
{
        char buf[128];
        snprintf(buf, sizeof(buf), "%u", len);
        evhttp_add_header(
                evhttp_request_get_output_headers(req), "Content-Length", buf);
}

// 定义目录分隔符
#if defined(WIN32)
#define DIR_SEPARATOR '\\'
#else
#define DIR_SEPARATOR '/'
#endif

// 拼接两个路径
static void
path_join(char *destination, const char *path1, const char *path2)
{
        if (path1 && *path1) {
                ssize_t len = strlen(path1);
                strcpy(destination, path1);

                if (destination[len - 1] == DIR_SEPARATOR) {
                        if (path2 && *path2) {
                                strcpy(destination + len,
                                        (*path2 == DIR_SEPARATOR) ? (path2 + 1) : path2);
                        }
                } else {
                        if (path2 && *path2) {
                                if (*path2 == DIR_SEPARATOR)
                                        strcpy(destination + len, path2);
                                else {
                                        destination[len] = DIR_SEPARATOR;
                                        strcpy(destination + len + 1, path2);
                                }
                        }
                }
        } else if (path2 && *path2)
                strcpy(destination, path2);
        else
                destination[0] = '\0';
}

// 根据文件扩展名猜测 Content-Type
static const char *
guess_content_type(const char *path)
{
        const char *last_period, *extension;
        const struct table_entry *ent;
        last_period = strrchr(path, '.');
        if (!last_period || strchr(last_period, '/'))
                goto not_found;
        extension = last_period + 1;
        for (ent = &content_type_table[0]; ent->extension; ++ent) {
                if (!evutil_ascii_strcasecmp(ent->extension, extension))
                        return ent->content_type;
        }

not_found:
        return "application/stream";
}

// 处理请求并发送静态文件或目录列表
static void
send_file_to_user(struct evhttp_request *req, void *arg)
{
        struct evbuffer *evb = NULL;
        struct evhttp_uri *decoded = NULL;
        struct stat st;
        int fd = -1;
        const char *static_dir = "."; // 静态资源根目录

        enum evhttp_cmd_type cmd = evhttp_request_get_command(req);
        if (cmd != EVHTTP_REQ_GET && cmd != EVHTTP_REQ_HEAD) {
                return;
        }

        // 解析请求 URI
        decoded = evhttp_uri_parse(evhttp_request_get_uri(req));
        if (!decoded) {
                evhttp_send_error(req, HTTP_BADREQUEST, 0);
                return;
        }

        // 获取请求路径
        const char *path = evhttp_uri_get_path(decoded);
        if (!path)
                path = "/";

        // 解码 URL
        char *decoded_path = evhttp_uridecode(path, 0, NULL);
        if (decoded_path == NULL)
                goto err;

        // 防止目录穿越攻击
        if (strstr(decoded_path, ".."))
                goto err;

        char whole_path[PATH_MAX] = {0};
        const char *type = NULL;
        path_join(whole_path, static_dir, decoded_path);

        // 解析真实路径
        char *real_file = realpath(whole_path, NULL);
        if (real_file) {
                strncpy(whole_path, real_file, sizeof(whole_path));
                free(real_file);
        } else {
                // 查找是否有 .gz 压缩版
                type = guess_content_type(whole_path);

                char gz_path[PATH_MAX + 3] = {0};
                snprintf(gz_path, sizeof(gz_path), "%s.gz", whole_path);
                char *real_file = realpath(gz_path, NULL);
                if (real_file) {
                        evhttp_add_header(evhttp_request_get_output_headers(req),
                                "Content-Encoding", "gzip");
                        strncpy(whole_path, real_file, sizeof(whole_path));
                        free(real_file);
                } else {
                        fprintf(stderr, "File '%s' not found\n", whole_path);
                        evhttp_send_error(req, HTTP_NOTFOUND, NULL);
                        goto done;
                }
        }

        if (stat(whole_path, &st) < 0) {
                goto err;
        }

        if ((evb = evbuffer_new()) == NULL) {
                evhttp_send_error(req, HTTP_INTERNAL, 0);
                goto cleanup;
        }

        bool dir_mode = false;

        // 处理目录模式
        if (S_ISDIR(st.st_mode)) {
                char index_file[PATH_MAX + 11];
                snprintf(index_file, sizeof(index_file), "%s/index.html", whole_path);

                if (stat(index_file, &st) < 0)
                        dir_mode = true;
                else
                        strcpy(whole_path, index_file);
        }

        if (dir_mode) {
                // 生成目录浏览页面
                DIR *d;
                struct dirent *ent;

                const char *trailing_slash = "";

                if (!strlen(path) || path[strlen(path) - 1] != '/')
                        trailing_slash = "/";
                if (!(d = opendir(whole_path))) {
                        goto err;
                }

                evbuffer_add_printf(evb,
                        "<!DOCTYPE html>\n"
                        "<html lang=\"en\">"
                        "<head>\n"
                        "<meta name=\"viewport\" "
                        "content=\"width=device-width,initial-scale=1\">\n"
                        "<title>%s</title>\n"
                        "<link rel=\"shortcut icon\" href=\"/favicon.png\">\n"
                        "<link rel=\"stylesheet\" href=\"" BOOTSTRAP_CSS
                        "/bootstrap.min.css\">\n"
                        "<script src=\"" BOOTSTRAP_JS
                        "/bootstrap.bundle.min.js\"></script>\n"
                        "<base href='%s%s'>\n"
                        "</head>\n"
                        "<body id=\"top\">\n"
                        "<nav class=\"navbar navbar-expand-lg navbar-dark sticky-top\">\n"
                        "<div class=\"container\">\n"
                        "</div>\n"
                        "</nav>\n"
                        "<main>\n"
                        "<div class=\"container p-3\">\n"
                        "<h2>%s</h2>\n"
                        "<ul class=\"list-unstyled my-3\">\n",
                        decoded_path,
                        path,
                        trailing_slash, decoded_path);
                while ((ent = readdir(d))) {
                        const char *name = ent->d_name;
                        if (strcmp(name, ".") == 0)
                                continue;
                        if (strcmp(path, "/") == 0 && strcmp(name, "..") == 0)
                                continue;
                        evbuffer_add_printf(evb, "<li><a href=\"%s\">%s</a>\n", name,
                                name);
                }
                evbuffer_add_printf(evb, "</ul></div>\n</main>\n<footer class=\"p-3\">\n<div class=\"container\"></div>\n</footer>\n</body></html>\n");
                closedir(d);

                add_content_length(req, evbuffer_get_length(evb));
                if (cmd == EVHTTP_REQ_HEAD)
                        evbuffer_drain(evb, evbuffer_get_length(evb));
                evhttp_add_header(evhttp_request_get_output_headers(req),
                        "Content-Type", "text/html");
        } else {
                // 处理普通文件
                if (type == NULL)
                        type = guess_content_type(whole_path);
                evhttp_add_header(
                        evhttp_request_get_output_headers(req), "Content-Type", type);

                if (st.st_size != 0) {
                        if ((fd = open(whole_path, O_RDONLY)) == -1) {
                                if (errno == ENOENT) {
                                        fprintf(stderr, "File '%s' not found\n", whole_path);
                                        evhttp_send_error(req, HTTP_NOTFOUND, NULL);
                                } else {
                                        evhttp_send_error(req, HTTP_INTERNAL, NULL);
                                }
                                goto done;
                        }
                        if (cmd != EVHTTP_REQ_HEAD) {
                                if (evbuffer_add_file(evb, fd, 0, st.st_size) != 0) {
                                        evhttp_send_error(req, HTTP_INTERNAL, NULL);
                                        goto cleanup;
                                }
                        }
                }
                add_content_length(req, st.st_size);
        }
        evhttp_send_reply(req, HTTP_OK, "OK", evb);

        goto done;

err:
        evhttp_send_error(req, HTTP_NOTFOUND, NULL);
cleanup:
        if (fd >= 0)
                close(fd);

done:
        if (decoded)
                evhttp_uri_free(decoded);
        if (decoded_path)
                free(decoded_path);
        if (evb)
                evbuffer_free(evb);
}

// 信号处理回调函数
static void
signal_cb(evutil_socket_t fd, short event, void *arg)
{
        printf("%s signal received\n", strsignal(fd));
        event_base_loopbreak(arg);
}

// 主程序入口
int
main()
{
        ev_uint16_t http_port = 8080;
        char *http_addr = "0.0.0.0"; // 监听所有网卡
        struct event_base *base;
        struct evhttp *http_server;
        struct event *sig_int;

        base = event_base_new();

        http_server = evhttp_new(base);
        evhttp_bind_socket(http_server, http_addr, http_port);
        evhttp_set_gencb(http_server, send_file_to_user, NULL);

        // 注册 Ctrl+C(SIGINT) 信号处理器
        sig_int = evsignal_new(base, SIGINT, signal_cb, base);
        event_add(sig_int, NULL);

        printf("Listening requests on http://%s:%d\n", http_addr, http_port);

        // 启动事件循环
        event_base_dispatch(base);

        // 释放资源
        evhttp_free(http_server);
        event_free(sig_int);
        event_base_free(base);
}

如你所见,我们将通用的 generic_request_handler() 替换为了专门的 send_file_to_user() 处理函数,它负责收到的请求: