libevent 学习文档-翻译

Libevent 学习记录

英文文档
Libevent 深入浅出

英文文档翻译

第一节 关于文档

这些文档是 Nick Mathewson 的版权所有 © 2009-2012,并根据知识共享署名-非商业性使用-相同方式共享许可 3.0 版提供。未来的版本可能会在限制较少的许可下提供。

此外,这些文档中的源代码示例也根据所谓的“3-Clause”或“Modified”BSD 许可证获得许可。有关完整条款,请参阅 随这些文档分发的 license_bsd 文件。

有关本文档的最新版本,请参阅 http://www.wangafu.net/~nickm/libevent-book/TOC.html

要获取本文档最新版本的源代码,请安装 git 并运行“git clone git://github.com/nmathewson/libevent-book.git”

第0章:关于本文档

本文档将教您如何使用 Libevent 2.0(及更高版本)用 C 编写快速可移植的异步网络 IO 程序。我们假设:

  • 你已经知道了 C.
  • 您已经知道基本的 C 网络调用(socket()、connect() 等)。

示例说明

本文档中的示例应该可以在 Linux、FreeBSD、OpenBSD、NetBSD、Mac OS X、Solaris 和 Android 上正常运行。某些示例可能无法在 Windows 上编译。

最后更新时间:美国东部时间 2012-11-18 19:34:24

第1章:异步IO的简单介绍。

异步 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 调用不会返回;并且 send 调用在至少将其输出刷新到内核的写缓冲区之前不会返回。

现在,阻塞 IO 不一定是坏事。如果在此期间你不希望你的程序做任何其他事情,阻塞 IO 对你来说会很好。但是假设您需要编写一个程序来一次处理多个连接。为了使我们的示例具体化:假设您想从两个连接读取输入,并且您不知道哪个连接将首先获得输入。你不能说不好的例子

/* 这行不通。*/
字符缓冲区 [1024];
整数i, n;
while (i_still_want_to_read()) {
     for (i=0; i<n_sockets; ++i) { 
        n = recv(fd[i], buf, sizeof (buf), 0);
        如果(n==0)
            句柄关闭(fd[i]);else if (n<0) 
            handle_error(fd[i], errno); 否则
            handle_input(fd[i], buf, n); 
    } 
}

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

有时人们使用多线程或多进程服务器来解决这个问题。进行多线程的最简单方法之一是使用单独的进程(或线程)来处理每个连接。由于每个连接都有自己的进程,等待一个连接的阻塞 IO 调用不会使任何其他连接的进程阻塞。

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

Example: Forking ROT13 server

ROT13解释: ROT13回转13位rotate by 13 places,有时中间加了个连字符称作ROT-13)是一种简易的替换式密码。它是一种在英文网络论坛用作隐藏八卦(spoiler)、妙句、谜题解答以及某些脏话的工具,目的是逃过版主或管理员的匆匆一瞥。ROT13被描述成“杂志字谜上下颠倒解答的Usenet点对点体”。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 是套接字的文件描述符。
[文件描述符是内核在打开套接字时分配给它的编号。你使用这个数字来进行引用套接字的 Unix 调用。]
一旦你使 fd (套接字)非阻塞,从那时起,每当你对 fd 进行网络调用时,调用要么立即完成操作,要么返回一个特殊错误代码表示“我现在无法取得任何进展,请重试。” 因此,我们的双套接字示例可能会天真地写成:

Bad Example: busy-polling all sockets

/* 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() 调用需要三组 fds(实现为位数组):一组用于读取,一组用于写入,一组用于“异常”。它一直等到其中一组中的套接字准备就绪,然后将这些组更改为仅包含准备好使用的套接字。

这是我们的示例,使用 select:

示例:使用选择

Example: Using 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);
             }
        }
    }
}

And here’s a reimplementation of our ROT13 server, using select() this time.

Example: select()-based ROT13 server

/* 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() 位数组所花费的时间与您为 select() 提供的最大 fd 成正比,所以当套接字数量很高时,select() 调用的规模会非常大。
[在用户空间方面,可以使生成和读取位数组所花费的时间与您为 select() 提供的 fd 数量成正比。但是在内核方面,读取位数组所花费的时间与位数组中最大的 fd 成正比,这往往是在整个程序中使用的 fd 总数左右,无论有多少 fd 被添加到集合中选择()。]

不同的操作系统为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)
{
    /* 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;

    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;
    state->read_event = event_new(base, fd, EV_READ|EV_PERSIST, do_read, state);
    if (!state->read_event) {
        free(state);
        return NULL;
    }
    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;

    assert(state->write_event);
    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) {
        assert(state->write_event);
        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') {
                assert(state->write_event);
                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) // XXXX use evutil macro
            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) // XXX use evutil macro
                return;
            free_fd_state(state);
            return;
        }
        assert(result != 0);

        state->n_written += result;
    }

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

    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) { // XXXX eagain??
        perror("accept");
    } else if (fd > FD_SETSIZE) {
        close(fd); // XXX replace all closes with EVUTIL_CLOSESOCKET */
    } else {
        struct fd_state *state;
        evutil_make_socket_nonblocking(fd);
        state = alloc_fd_state(base, fd);
        assert(state); /*XXX err*/
        assert(state->write_event);
        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; /*XXXerr*/

    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_event = event_new(base, listener, EV_READ|EV_PERSIST, do_accept, (void*)base);
    /*XXX check it */
    event_add(listener_event, NULL);

    event_base_dispatch(base);
}

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

    run();
    return 0;
}

(代码中需要注意的其他事项:我们使用 evutil_socket_t 类型而不是将套接字键入为“int”。我们调用 evutil_make_socket_nonblocking 而不是调用 fcntl(O_NONBLOCK) 来使套接字成为非阻塞。这些更改使我们的代码与 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)
{
    /* 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
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);

    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) {
        /* Too long; just process what there is and go on so that the buffer
         * doesn't grow infinitely long. */
        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) {
        /* connection has been closed, do any clean up here */
        /* ... */
    } else if (error & BEV_EVENT_ERROR) {
        /* check errno to see what error occurred */
        /* ... */
    } else if (error & BEV_EVENT_TIMEOUT) {
        /* must be a timeout event handle, handle it */
        /* ... */
    }
    bufferevent_free(bev);
}

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_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; /*XXXerr*/

    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_event = event_new(base, listener, EV_READ|EV_PERSIST, do_accept, (void*)base);
    /*XXX check it */
    event_add(listener_event, NULL);

    event_base_dispatch(base);
}

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

    run();
    return 0;
}

Libevent 参考手册:预备知识

来自 10,000 英尺的 Libevent

Libevent 是一个用于编写快速可移植非阻塞 IO 的库。其设计目标是:

  • 可移植性

    使用 Libevent 编写的程序应该可以在 Libevent 支持的所有平台上运行。即使没有真正好的 方法来做非阻塞 IO,Libevent 也应该支持马马虎虎的方法,这样你的程序才能在受限的环境中运行。

  • 速度

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

  • 可扩展性

    Libevent 被设计为即使在需要有数万个活动套接字的程序中也能很好地工作。

  • 方便

    只要有可能,使用 Libevent 编写程序的最自然的方式应该是稳定的、可移植的方式。

Libevent分为以下几个组件:

  • evutil

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

  • 事件和 event_base

    这是 Libevent 的核心。它为各种特定于平台的、基于事件的非阻塞 IO 后端提供了一个抽象 API。它可以让您知道套接字何时准备好读取或写入,执行基本的超时功能,并检测操作系统信号。

  • 缓冲事件

    这些函数为 Libevent 的基于事件的核心提供了一个更方便的包装器。它们让您的应用程序请求缓冲读取和写入,而不是在套接字准备好时通知您,它们让您知道 IO 何时实际发生。bufferevent 接口也有多个后端,所以 它可以利用提供更快方法的系统 非阻塞 IO,例如 Windows IOCP API。

  • 缓冲区

    该模块实现了缓冲区事件底层的缓冲区,并提供了高效和/或方便访问的功能。

  • evhttp

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

  • evdns

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

  • evrpc

    一个简单的 RPC 实现。

动态库

构建 Libevent 时,默认情况下会安装以下库:

  • libevent_core

    所有核心事件和缓冲区功能。该库包含所有 event_base、evbuffer、bufferevent 和实用程序函数。

  • libevent_extra

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

  • libevent

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

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

  • libevent_pthreads

    该库基于 pthreads 可移植线程库添加了线程和锁定实现。它与 libevent_core 分离,因此您无需链接 pthread 即可使用 Libevent,除非您实际上以多线程方式使用 Libevent。

  • libevent_openssl

    该库为使用 bufferevents 和 OpenSSL 库的加密通信提供支持。它与 libevent_core 分开,因此您无需链接 OpenSSL 即可使用 Libevent,除非您实际使用的是加密连接。

头文件

当前所有的公共 Libevent 头文件都安装在event2 目录下。标头分为三大类:

  • API 头文件

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

  • 兼容性头文件

    兼容性标头包括不推荐使用的函数的定义。除非您从旧版本的 Libevent 移植程序,否则不应包含它。

  • 结构头文件

    这些标头定义了具有相对易变布局的结构。其中一些是公开的,以防您需要快速访问结构组件;有些是由于历史原因暴露出来的。直接依赖头文件中的任何结构可能会破坏程序与其他版本的 Libevent 的二进制兼容性,有时以难以调试的方式。这些标头具有后缀“_struct.h”

(还有旧版本的 Libevent 标头没有 event2目录。请参阅下面的“如果您必须使用旧版本的 Libevent”。)

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

Libevent 2.0 已将其 API 修改为更合理且不易出错。如果可能,您应该编写新程序来使用 Libevent 2.0 API。但有时您可能需要使用较旧的 API,以更新现有应用程序,或支持由于某种原因无法安装 Libevent 2.0 或更高版本的环境。

旧版本的 Libevent 具有较少的标头,并且没有将它们安装在“event2”下:

老头……由当前标题替换
事件.hevent2/event*.h, event2/buffer*.h event2/bufferevent*.h event2/tag*.h
evdns.h事件2/dns*.h
evhttp.h事件2/http*.h
evrpc.h事件2/rpc*.h
evutil.h事件2/util*.h

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

关于使用旧版本的其他一些说明:

  • 在 1.4 之前,只有一个库“libevent”,其中包含当前分为 libevent_core 和 libevent_extra 的功能。
  • 2.0之前,不支持加锁;Libevent 可能是线程安全的,但前提是您确保永远不会同时使用来自两个线程的相同结构。

下面的各个部分将讨论您可能会在代码库的特定区域遇到过时的 API。

版本状态说明

1.4.7 之前的 Libevent 版本应该被认为是完全过时的。1.3e 之前的 Libevent 版本应该被认为充满了错误。

(另外,请不要向 Libevent 维护者发送任何 1.4.x 或更早版本的新功能——它应该保持稳定版本。如果您在 1.3x 或更早版本中遇到错误,请确保它仍然存在在您报告它之前存在于最新的稳定版本中:后续版本的发生是有原因的。)

设置 Libevent 库

Libevent 有一些在整个过程中共享的全局设置。 这些影响整个库。

在调用Libevent 任何部分之前必须对这些设置进行更改。

在 Libevent 中的LOG 信息

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 想要记录一条消息时,它都会将它传递给您提供的函数。您可以通过使用 NULL 作为参数再次调用 event_set_log_callback() 来让 Libevent 返回其默认行为。

dg:

#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 函数是不安全的!例如,如果您尝试编写使用 bufferevents 将警告消息发送到网络套接字的日志回调,您可能会遇到奇怪且难以诊断的错误。在未来版本的 Libevent 中,某些功能可能会取消此限制。

通常,调试日志不会启用,也不会发送到日志回调。如果 Libevent 是为支持它们而构建的,您可以手动打开它们。

#define EVENT_DBG_NONE 0
#define EVENT_DBG_ALL 0xffffffffu

void event_enable_debug_logging(ev_uint32_t which);

处理致命错误

当 Libevent 检测到不可恢复的内部错误(例如损坏的数据结构)时,其默认行为是调用 exit() 或 abort() 以离开当前运行的进程。这些错误几乎总是意味着某处存在错误:无论是在您的代码中,还是在 Libevent 本身中。

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

例子

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

内存管理

默认情况下,Libevent 使用 C 库的内存管理函数从堆中分配内存。您可以通过提供自己的 malloc、realloc 和 free 替代品让 Libevent 使用另一个内存管理器。如果您有一个希望 Libevent 使用的更高效的分配器,或者如果您有一个希望 Libevent 使用以查找内存泄漏的检测分配器,您可能希望执行此操作。

接口

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

这是一个简单的例子,它用计算分配的字节总数的变量替换了 Libevent 的分配函数。实际上,您可能希望在此处添加锁定以防止 Libevent 在多个线程中运行时出现错误。

例子

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

/* This union's purpose is to be as big as the largest of all the
 * types it contains. */
union alignment {
    size_t sz;
    void *ptr;
    double dbl;
};
/* We need to make sure that everything we return is on the right
   alignment to hold anything, including a double. */
#define ALIGNMENT sizeof(union alignment)

/* We need to do this cast-to-char* trick on our pointers to adjust
   them; doing arithmetic on a void* is not standard. */
#define OUTPTR(ptr) (((char*)ptr)+ALIGNMENT)
#define INPTR(ptr) (((char*)ptr)-ALIGNMENT)

static size_t total_allocated = 0;
static void *replacement_malloc(size_t sz)
{
    void *chunk = malloc(sz + ALIGNMENT);
    if (!chunk) return chunk;
    total_allocated += sz;
    *(size_t*)chunk = sz;
    return OUTPTR(chunk);
}
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;
    *(size_t*)ptr = sz;
    total_allocated = total_allocated - old_size + sz;
    return OUTPTR(ptr);
}
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,
                            replacement_realloc,
                            replacement_free);
}

注意:

  • 替换内存管理函数会影响所有将来从 Libevent 分配、调整大小或释放内存的调用。因此,您需要确保在调用任何其他 Libevent 函数*之前替换这些函数。*否则,Libevent 将使用您的 free 版本来释放从 C 库的 malloc 版本返回的内存。
  • 您的 malloc 和 realloc 函数需要返回与 C 库具有相同对齐方式的内存块。
  • 您的 realloc 函数需要正确处理 realloc(NULL, sz)(即,将其视为 malloc(sz))。
  • 您的 realloc 函数需要正确处理 realloc(ptr, 0) (即,将其视为 free(ptr))。
  • 您的free 函数不需要处理 free(NULL)。
  • 您的 malloc 函数不需要处理 malloc(0)。
  • 如果您从多个线程使用 Libevent,则替换的内存管理函数需要是线程安全的。
  • Libevent 将使用这些函数来分配它返回给您的内存。因此,如果您想释放由 Libevent 函数分配和返回的内存,并且您已经替换了 malloc 和 realloc 函数,那么您可能必须使用替换的 free 函数来释放它。

锁和线程

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

Libevent 结构通常可以三种方式与多线程一起工作。

  • 一些结构本质上是单线程的:从多个线程同时使用它们是绝对不安全的。
  • 某些结构是可选锁定的:您可以告诉每个对象的 Libevent 是否需要同时从多个线程中使用它。
  • 一些结构总是被锁定的:如果 Libevent 在支持锁定的情况下运行,那么它们总是可以安全地同时从多个线程中使用。

要在 Libevent 中获得锁定,您必须告诉 Libevent 要使用哪些锁定函数。在调用分配需要在线程之间共享的结构的任何 Libevent 函数之前,您需要执行此操作。

如果您使用的是 pthreads 库或本机 Windows 线程代码,那么您很幸运。有预定义的函数可以设置 Libevent 为您使用正确的 pthread 或 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。

如果您需要使用不同的线程库,那么您还有一些工作要做。您需要定义使用您的库来实现的函数:

  • Locks
  • locking
  • unlocking
  • lock allocation
  • lock destruction
  • Conditions
  • condition variable creation
  • condition variable destruction
  • waiting on a condition variable
  • signaling/broadcasting to a condition variable
  • Threads
  • thread ID detection

然后你告诉 Libevent 这些函数,使用 evthread_set_lock_callbacks 和 evthread_set_id_callback 接口。

#define EVTHREAD_WRITE  0x04
#define EVTHREAD_READ   0x08
#define EVTHREAD_TRY    0x10

#define EVTHREAD_LOCKTYPE_RECURSIVE 1
#define EVTHREAD_LOCKTYPE_READWRITE 2

#define EVTHREAD_LOCK_API_VERSION 1

struct evthread_lock_callbacks {
       int lock_api_version;
       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 *);

void evthread_set_id_callback(unsigned long (*id_fn)(void));

struct evthread_condition_callbacks {
        int condition_api_version;
        void *(*alloc_condition)(unsigned condtype);
        void (*free_condition)(void *cond);
        int (*signal_condition)(void *cond, int broadcast);
        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_VERSION。supported_locktypes 字段必须设置为 EVTHREAD_LOCKTYPE_* 常量的位掩码,以描述您可以支持的锁类型。(从 2.0.4-alpha 开始,EVTHREAD_LOCK_RECURSIVE 是强制性的,EVTHREAD_LOCK_READWRITE 未使用。)alloc函数必须返回指定类型的新锁。free函数必须释放指定类型的锁持有的所有资源。 lock函数必须尝试在指定模式下获取锁,成功返回 0,失败返回非零。解锁_ 函数必须尝试解锁锁,成功返回 0,失败返回非零。

公认的锁类型有:

  • 0

    一个常规的、不必要的递归锁。

  • EVTHREAD_LOCKTYPE_RECURSIVE

    不会阻止已经持有它的线程再次请求它的锁。一旦持有它的线程解锁它的次数与它最初被锁定的次数一样多,其他线程就可以获得锁。

  • EVTHREAD_LOCKTYPE_READWRITE

    一种锁,它允许多个线程一次持有它以供读取,但一次只允许一个线程持有它以供写入。作家排除所有读者。

公认的锁定模式有:

  • EVTHREAD_READ

    仅适用于 READWRITE 锁:获取或释放锁以进行读取。

  • EVTHREAD_WRITE

    仅适用于 READWRITE 锁:获取或释放锁以进行写入。

  • EVTHREAD_TRY

    仅用于锁定:仅当可以立即获取锁时才获取锁。

id_fn 参数必须是一个函数,返回一个无符号长整数,标识哪个线程正在调用该函数。它必须始终为同一个线程返回相同的数字,并且如果两个不同的线程同时执行,则决不能为它们返回相同的数字。

evthread_condition_callbacks 结构描述了与条件变量相关的回调。对于上述版本,lock_api_version 字段必须设置为 EVTHREAD_CONDITION_API_VERSION。alloc_condition 函数必须返回一个指向新条件变量的指针。它接收 0 作为其参数。free_condition 函数必须释放条件变量持有的存储和资源。wait_condition 函数接受三个参数:由 alloc_condition 分配的条件、由您提供的 evthread_lock_callbacks.alloc 函数分配的锁和可选的超时。每当调用函数时,锁都会被持有;该函数必须释放锁,并等待条件发出信号或直到(可选)超时已过。wait_condition 函数应在错误时返回 -1,如果条件已发出信号,则为 0,超时时为 1。在它返回之前,它应该确保它再次持有锁。最后,signal_condition 函数应该导致一个线程等待唤醒条件(如果其广播参数为假)和所有当前等待条件唤醒的线程(如果其广播参数为真)。只有在持有与条件相关的锁时才会持有它。

有关条件变量的更多信息,请查看 pthread 的 pthread_cond_* 函数或 Windows 的 CONDITION_VARIABLE 函数的文档。

有关如何使用这些函数的示例,请参阅 evthread_pthread.c 和
Libevent 源代码分发中的 evthread_win32.c。

本节中的函数在 <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 会以断言失败退出。

Interface

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

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

调试事件使用

使用 Libevent 可以为您检测和报告的事件有一些常见错误。他们包括:

  • 将未初始化的结构事件视为已初始化。
  • 尝试重新初始化一个挂起的结构事件。

跟踪初始化了哪些事件需要 Libevent 使用额外的内存和 CPU,因此您应该只在实际调试程序时启用调试模式。

Interface

void event_enable_debug_mode(void);

此函数只能在创建任何 event_base 之前调用。

使用调试模式时,如果您的程序使用大量使用 event_assign() [不是 event_new()] 创建的事件,您可能会耗尽内存。发生这种情况是因为 Libevent 无法确定何时不再使用使用 event_assign() 创建的事件。(当你对它调用 event_free() 时,它可以告诉 event_new() 事件已经失效。)如果你想避免在调试时耗尽内存,你可以明确告诉 Libevent 此类事件不再被视为分配:

Interface

void event_debug_unassign(struct event *ev);

未启用调试时调用 event_debug_unassign() 无效。

Example

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

#include <stdlib.h>

void cb(evutil_socket_t fd, short what, void *ptr)
{
    /* We pass 'NULL' as the callback pointer for the heap allocated
     * event, and we pass the event itself as the callback pointer
     * for the stack-allocated event. */
    struct event *ev = ptr;

    if (ev)
        event_debug_unassign(ev);
}

/* Here's a simple mainloop that waits until fd1 and fd2 are both
 * ready to read. */
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;

    if (debug_mode)
       event_enable_debug_mode();

    base = event_base_new();

    event_on_heap = event_new(base, fd1, EV_READ, cb, NULL);
    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 的版本

新版本的 Libevent 可以添加功能并删除错误。有时您会想要检测 Libevent 版本,以便您可以:

  • 检测已安装的 Libevent 版本是否足以构建您的程序。
  • 显示 Libevent 版本以进行调试。
  • 检测 Libevent 的版本,以便您可以警告用户有关错误或解决它们。

Interface

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

Example: Run-time checks

#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;
}

释放全局 Libevent 结构

即使您已经释放了使用 Libevent 分配的所有对象,也会留下一些全局分配的结构。这通常不是问题:一旦进程退出,无论如何它们都会被清理掉。但是拥有这些结构会使一些调试工具误以为 Libevent 正在泄漏资源。如果您需要确保 Libevent 已释放所有内部库全局数据结构,您可以调用:

Interface

void libevent_global_shutdown(void);

此函数不会释放 Libevent 函数返回给您的任何结构。如果要在退出前释放所有内容,则需要自己释放所有事件、event_bases、bufferevents 等。

调用 libevent_global_shutdown() 会使其他 Libevent 函数的行为不可预测;不要调用它,除非你的程序调用的最后一个 Libevent 函数。一个例外是 libevent_global_shutdown() 是幂等的:即使它已经被调用也可以调用它。

创建 event_base

在使用任何有趣的 Libevent 函数之前,您需要分配一个或多个 event_base 结构。每个 event_base 结构都包含一组事件,并且可以轮询以确定哪些事件处于活动状态。

如果将 event_base 设置为使用锁定,则在多个线程之间访问它是安全的。但是,它的循环只能在单个线程中运行。如果要让多个线程轮询 IO,则需要为每个线程设置一个 event_base。

TIP:libevent event_bases 未来可能支持多线程安全,目前只能单线程。

每个 event_base 都有一个“方法”或一个后端,用于确定哪些事件已准备就绪。公认的方法是:

  • select
  • poll
  • epoll
  • kqueue
  • devpoll
  • evport
  • win32

用户可以使用环境变量禁用特定的后端。如果要关闭 kqueue 后端,设置 EVENT_NOKQUEUE 环境变量,依此类推。如果您想从程序中关闭后端,请参阅下面的 event_config_avoid_method() 注释。

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

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

Interface

struct event_base *event_base_new(void);

对于大多数程序,这就是您所需要的。

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

设置复杂的 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。

Interface

int event_config_avoid_method(struct event_config *cfg, const char *method);

enum event_method_feature {
    EV_FEATURE_ET = 0x01,
    EV_FEATURE_O1 = 0x02,
    EV_FEATURE_FDS = 0x04,
};
int event_config_require_features(struct event_config *cfg,
                                  enum event_method_feature feature);

enum event_base_config_flag {
    EVENT_BASE_FLAG_NOLOCK = 0x01,
    EVENT_BASE_FLAG_IGNORE_ENV = 0x02,
    EVENT_BASE_FLAG_STARTUP_IOCP = 0x04,
    EVENT_BASE_FLAG_NO_CACHE_TIME = 0x08,
    EVENT_BASE_FLAG_EPOLL_USE_CHANGELIST = 0x10,
    EVENT_BASE_FLAG_PRECISE_TIMER = 0x20
};
int event_config_set_flag(struct event_config *cfg,
    enum event_base_config_flag flag);
  • 调用 event_config_avoid_method 告诉 Libevent 按名称避免特定的可用后端。
  • 调用 event_config_require_feature() 告诉 Libevent 不要使用任何不能提供所有功能集的后端。
  • 调用 event_config_set_flag() 告诉 Libevent 在构建事件库时设置以下一个或多个运行时标志。

event_config_require_features 的识别特征值为:

  • EV_FEATURE_ET

    需要支持边缘触发 IO 的后端方法。

  • EV_FEATURE_O1

    需要一个后端方法,其中添加或删除单个事件或使单个事件变为活动状态是 O(1) 操作。

  • EV_FEATURE_FDS

    需要一个可以支持任意文件描述符类型的后端方法,而不仅仅是套接字。

event_config_set_flag() 的可识别选项值为:

  • EVENT_BASE_FLAG_NOLOCK

    不要为 event_base 分配锁。设置此选项可能会节省一点时间来锁定和释放 event_base,但会使从多个线程访问它变得不安全且无功能。

  • EVENT_BASE_FLAG_IGNORE_ENV

    在选择要使用的后端方法时不要检查 EVENT_* 环境变量。在使用这个标志之前要三思:它会让用户更难调试你的程序和 Libevent 之间的交互。

  • EVENT_BASE_FLAG_STARTUP_IOCP

    仅在 Windows 上,此标志使 Libevent 在启动时启用任何必要的 IOCP 调度逻辑,而不是按需启用。

  • EVENT_BASE_FLAG_NO_CACHE_TIME

    不要在每次事件循环准备好运行超时回调时检查当前时间,而是在每次超时回调后检查它。这可能会使用比您预期更多的 CPU,所以要小心!

  • EVENT_BASE_FLAG_EPOLL_USE_CHANGELIST

    告诉 Libevent,如果它决定使用 epoll 后端,那么使用更快的基于“changelist”的后端是安全的。epoll-changelist 后端可以避免在调用后端的 dispatch 函数之间同一个 fd 的状态被多次修改的情况下不必要的系统调用,但是如果你给 Libevent 克隆的任何 fd,它也会触发一个内核错误,导致错误结果dup() 或其变体。如果您使用 epoll 以外的后端,则此标志无效。您还可以通过设置 EVENT_EPOLL_USE_CHANGELIST 环境变量来打开 epoll-changelist 选项。

    解释: 使用changelist后,对事件的修改不会立即作用到epoll上,而是把这些修改保存到一个list上,在作用到epoll之前,这些修改可以抵消,合并。比如说,对于同一个事件,先add再del,虽然什么也没做,如果不使用changelist就会多出2个不必要的系统调用。当然如果这种操作并不多的话,使用changelist反而多了额外的开销(相比系统调用,这种开销微不足道),libevent将选择权交给了你。

    http://www.ilovecpp.com/2018/05/10/libevent-using-changlist/

  • EVENT_BASE_FLAG_PRECISE_TIMER

    默认情况下,Libevent 尝试使用操作系统提供的最快的可用计时机制。如果有较慢的计时机制提供更细粒度的计时精度,则此标志告诉 Libevent 改用该计时机制。如果操作系统没有提供这种更慢但更精确的机制,则此标志无效。

以上操作 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。

Interface

int event_config_set_num_cpus_hint(struct event_config *cfg, int cpus)

此功能目前仅在使用 IOCP 时对 Windows 有用,但将来可能对其他平台有用。调用它会告诉 event_config 它生成的 event_base 应该在多线程时尝试充分利用给定数量的 CPU。请注意,这只是一个提示:事件库最终可能使用比您选择的更多或更少的 CPU。

Interface

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

此函数通过限制在检查更多高优先级事件之前可以调用多少个低优先级事件回调来防止优先级反转。如果 max_interval 不为 null,则事件循环会检查每次回调后的时间,如果 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)
   /* Handle error */;

/* I'm going to have events running at two priorities.  I expect that
   some of my priority-1 events are going to have pretty slow callbacks,
   so I don't want more than 100 msec to elapse (or 5 callbacks) before
   checking for priority-0 events. */
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)
   /* Handle error */;

event_base_priority_init(base, 2);

EVENT_BASE_FLAG_IGNORE_ENV 标志首次出现在 Libevent 2.0.2-alpha 中。EVENT_BASE_FLAG_PRECISE_TIMER 标志首次出现在 Libevent 2.1.2-alpha 中。event_config_set_num_cpus_hint() 函数是 Libevent 2.0.7-rc 中的新功能,而 event_config_set_max_dispatch_interval() 是 2.1.1-alpha 中的新功能。本节中的所有其他内容首先出现在 Libevent 2.0.1-alpha 中。

检查 event_base 的后端方法

有时您想查看 event_base 中实际可用的功能,或者它正在使用的方法。

Interface

const char **event_get_supported_methods(void);

event_get_supported_methods() 函数返回一个指向此版本 Libevent 中支持的方法名称数组的指针。数组中的最后一个元素为 NULL。

Example

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 编译支持的方法列表。当 Libevent 尝试运行时,您的操作系统实际上可能并不支持所有这些。例如,您可能在一个 OSX 版本上,其中 kqueue 错误太多而无法使用。

Interface

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 使用的实际方法的名称。event_base_get_features() 调用返回它支持的功能的位掩码。

Example

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() 调用首先在 Libevent 1.4.3 中可用。其他的最早出现在 Libevent 2.0.1-alpha 中。

释放 event_base

Interface

void event_base_free(struct event_base *base);

此函数不会释放当前与 event_base 关联的任何事件,或关闭它们的任何套接字,或释放它们的任何指针。

event_base_free() 函数在 <event2/event.h> 中定义。它首先在 Libevent 1.2 中实现。

event_base 上设置优先级

Libevent 支持在一个事件上设置多个优先级。但是,默认情况下,event_base 仅支持单个优先级。您可以通过调用 event_base_priority_init() 来设置 event_base 的优先级数。

Interface

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

此函数在成功时返回 0,在失败时返回 -1。base参数是要修改的event_base,n_priorities 是要支持的优先级数。它必须至少为 1。新事件的可用优先级将从 0(最重要)到 n_priorities-1(最不重要)编号。

有一个常量 EVENT_MAX_PRIORITIES,它设置 n_priorities 值的上限。使用更高的 n_priorities 值调用此函数是错误的。

必须在任何事件激活之前调用此函数。最好在创建 event_base 后立即调用它。

要查找当前基数支持的优先级数,可以调用 event_base_getnpriorities()。

Interface

int event_base_get_npriorities(struct event_base *base);

返回值等于基中配置的优先级数。因此,如果 event_base_get_npriorities() 返回 3,则允许的优先级值为 0、1 和 2。

Example

For an example, see the documentation for event_priority_set below.

默认情况下,与此基础关联的所有新事件都将以等于 n_priorities / 2 的优先级进行初始化。

event_base_priority_init 函数在 <event2/event.h> 中定义。它从 Libevent 1.0 开始可用。event_base_get_npriorities() 函数是 Libevent 2.1.1-alpha 中的新功能。

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

在调用 fork() 之后,并非所有事件后端都保持干净。因此,如果您的程序使用 fork() 或相关系统调用来启动一个新进程,并且您希望在分叉后继续使用 event_base,则可能需要重新初始化它。

Interface

int event_reinit(struct event_base *base);

The function returns 0 on success, -1 on failure.

Example

struct event_base *base = event_base_new();

/* ... add some events to the event_base ... */

if (fork()) {
    /* In parent */
    continue_running_parent(base); /*...*/
} else {
    /* In child */
    event_reinit(base);
    continue_running_child(base); /*...*/
}

过时的 event_base 函数

旧版本的 Libevent 在很大程度上依赖于“当前”事件库的概念。“当前” event_base 是所有线程共享的全局设置。如果你忘记指定你想要的 event_base,你会得到当前的。由于 event_bases 不是线程安全的,这很容易出错。

而不是 event_base_new(),有:

Interface

struct event_base *event_init(void);

这个函数像 event_base_new() 一样工作,并将当前基数设置为分配的基数。没有其他方法可以改变当前的基础。

本节中的一些 event_base 函数具有在当前基础上运行的变体。这些函数的行为与当前函数一样,只是它们不带基本参数。

当前功能过时的当前基础版本
event_base_priority_init()event_priority_init()
event_base_get_method()event_get_method()

使用事件循环

运行循环

一旦你有一个 event_base 并注册了一些事件(请参阅下一节关于如何创建和注册事件),你会希望 Libevent 等待事件并提醒你它们。

Interface

#define EVLOOP_ONCE             0x01
#define EVLOOP_NONBLOCK         0x02
#define EVLOOP_NO_EXIT_ON_EMPTY 0x04
int event_base_loop(struct event_base *base, int flags);

默认情况下,event_base_loop() 函数会运行event_base,直到其中没有注册更多事件。为了运行循环,它反复检查是否已触发任何已注册的事件(例如,读取事件的文件描述符是否准备好读取,或者超时事件的超时是否准备好到期)。一旦发生这种情况,它会将所有触发的事件标记为“活动”,并开始运行它们。

您可以通过在其flags参数中设置一个或多个标志来更改 event_base_loop() 的行为。如果设置了 EVLOOP_ONCE,则循环将等待直到某些事件变为活动状态,然后运行活动事件,直到没有更多可运行的事件,然后返回。如果设置了 EVLOOP_NONBLOCK,则循环不会等待事件触发:它只会检查是否有任何事件准备好立即触发,如果是,则运行它们的回调。

通常,一旦没有挂起或活动的事件,循环就会退出。您可以通过传递 EVLOOP_NO_EXIT_ON_EMPTY 标志来覆盖此行为——例如,如果您要从其他线程添加事件。如果您确实设置了 EVLOOP_NO_EXIT_ON_EMPTY,则循环将继续运行,直到有人调用 event_base_loopbreak(),或调用 event_base_loopexit(),或发生错误。

完成后,如果 event_base_loop() 正常退出,则返回 0,如果由于后端中的一些未处理的错误而退出,则返回 -1,如果因为没有更多挂起或活动事件而退出,则返回 1。

为了帮助理解,下面是 event_base_loop 算法的大致总结:

Pseudocode

while (any events are registered with the loop,
        or EVLOOP_NO_EXIT_ON_EMPTY was set) {

    if (EVLOOP_NONBLOCK was set, or any events are already active)
        If any registered events have triggered, mark them active.
    else
        Wait until at least one event has triggered, and mark it active.

    for (p = 0; p < n_priorities; ++p) {
       if (any event with priority of p is active) {
          Run all active events with priority of p.
          break; /* Do not run any events of a less important priority */
       }
    }

    if (EVLOOP_ONCE was set or EVLOOP_NONBLOCK was set)
       break;
}

As a convenience, you can also call:

Interface

int event_base_dispatch(struct event_base *base);

event_base_dispatch() 调用与 event_base_loop() 相同,没有设置标志。因此,它会一直运行,直到没有更多注册事件或 event_base_loopbreak() 或 event_base_loopexit() 被调用。

这些函数在 <event2/event.h> 中定义。它们自 Libevent 1.0 以来就存在。

停止循环

如果您希望活动的事件循环在所有事件都从中删除之前停止运行,您可以调用两个略有不同的函数。

Interface

int event_base_loopexit(struct event_base *base,
                        const struct timeval *tv);
int event_base_loopbreak(struct event_base *base);

event_base_loopexit() 函数告诉 event_base 在给定时间过去后停止循环。如果tv参数为 NULL,则 event_base 立即停止循环。如果 event_base 当前正在为任何活动事件运行回调,它将继续运行它们,并且在它们全部运行之前不会退出。

event_base_loopbreak() 函数告诉 event_base 立即退出其循环。它与 event_base_loopexit(base, NULL) 的不同之处在于,如果 event_base 当前正在为任何活动事件运行回调,它将在完成当前处理的事件后立即退出。

还要注意 event_base_loopexit(base,NULL) 和 event_base_loopbreak(base) 在没有事件循环运行时的行为不同:loopexit 安排事件循环的下一个实例在下一轮回调运行后立即停止(就像它已被调用一样与 EVLOOP_ONCE) 而 loopbreak 仅停止当前正在运行的循环,如果事件循环未运行则无效。

这两种方法都在成功时返回 0,在失败时返回 -1。

Example: Shut down immediately

#include <event2/event.h>

/* Here's a callback function that calls loopbreak */
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;

    /* Construct a new event to trigger whenever there are any bytes to
       read from a watchdog socket.  When that happens, we'll call the
       cb function, which will make the loop exit immediately without
       running any other active events at all.
     */
    watchdog_event = event_new(base, watchdog_fd, EV_READ, cb, base);

    event_add(watchdog_event, NULL);

    event_base_dispatch(base);
}

Example: Run an event loop for 10 seconds, then exit.

#include <event2/event.h>

void run_base_with_ticks(struct event_base *base)
{
  struct timeval ten_sec;

  ten_sec.tv_sec = 10;
  ten_sec.tv_usec = 0;

  /* Now we run the event_base for a series of 10-second intervals, printing
     "Tick" after each.  For a much better way to implement a 10-second
     timer, see the section below about persistent timer events. */
  while (1) {
     /* This schedules an exit ten seconds from now. */
     event_base_loopexit(base, &ten_sec);

     event_base_dispatch(base);
     puts("Tick");
  }
}

有时您可能想知道您对 event_base_dispatch() 或 event_base_loop() 的调用是否正常退出,或者是因为对 event_base_loopexit() 或 event_base_break() 的调用。您可以使用这些函数来判断是否调用了 loopexit 或 break:

Interface

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

如果循环分别用 event_base_loopexit() 或 event_base_break() 停止,这两个函数将返回 true,否则返回 false。下次启动事件循环时,它们的值将被重置。

这些函数在 <event2/event.h> 中声明。event_break_loopexit() 函数首先在 Libevent 1.0c 中实现;event_break_loopbreak() 最初是在 Libevent 1.4.3 中实现的。

重新检查事件

通常,Libevent 检查事件,然后运行具有最高优先级的所有活动事件,然后再次检查事件,依此类推。但有时您可能想在当前回调运行后立即停止 Libevent,并告诉它再次扫描。通过类比 event_base_loopbreak(),您可以使用函数 event_base_loopcontinue() 来执行此操作。

Interface

int event_base_loopcontinue(struct event_base *);

如果我们当前没有运行事件回调,则调用 event_base_loopcontinue() 无效。

此功能是在 Libevent 2.1.2-alpha 中引入的。

检查内部时间缓存

有时您想在事件回调中获得当前时间的大致视图,并且您希望在不自己调用 gettimeofday() 的情况下获得它(可能是因为您的操作系统将 gettimeofday() 实现为系统调用,并且您试图避免系统调用高架)。

在回调中,您可以向 Libevent 询问它对开始执行这一轮回调的当前时间的看法:

Interface

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

如果 event_base 当前正在执行回调,则event_base_gettimeofday_cached() 函数将其 *tv_out参数的值设置为缓存时间。*否则,它会调用 evutil_gettimeofday() 来获取当前的实际时间。成功返回 0,失败返回负数。

如果 event_base 当前正在执行回调,则event_base_gettimeofday_cached() 函数将其 *tv_out参数的值设置为缓存时间。*否则,它会调用 evutil_gettimeofday() 来获取当前的实际时间。成功返回 0,失败返回负数。

请注意,由于在 Libevent 开始运行回调时会缓存 timeval,因此它至少会有点不准确。如果您的回调需要很长时间才能运行,则可能非常不准确。要强制立即更新缓存,您可以调用此函数:

Interface

int event_base_update_cache_time(struct event_base *base);

它在成功时返回 0,在失败时返回 -1,并且如果基础没有运行它的事件循环则无效。

event_base_gettimeofday_cached() 函数是 Libevent 2.0.4-alpha 中的新功能。Libevent 2.1.1-alpha 添加了 event_base_update_cache_time()。

转储 event_base 状态

Interface

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

为了帮助调试您的程序(或调试 Libevent!),您有时可能需要在 event_base 中添加的所有事件及其状态的完整列表。调用 event_base_dump_events() 将此列表写入提供的 stdio 文件。

该列表旨在供人类阅读;它的格式在未来版本的 Libevent 中改变。

此功能是在 Libevent 2.0.1-alpha 中引入的。

在 event_base 中的每个事件上运行一个函数

Interface

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() 关联的每个当前活动或未决事件。提供的回调将在每个事件中按未指定的顺序仅调用一次。event_base_foreach_event() 的第三个参数将作为第三个参数传递给回调的每个调用。

回调函数必须返回 0 以继续迭代,或返回某个其他整数以停止迭代。回调函数最终返回的任何值都将由 event_base_foreach_function() 返回。

您的回调函数不得修改它接收到的任何事件,或向事件库添加或删除任何事件,或以其他方式修改与事件库关联的任何事件,否则可能会发生未定义的行为,直至或包括崩溃和堆粉碎。

event_base 锁将在调用 event_base_foreach_event() 期间保持 - 这将阻止其他线程对 event_base 执行任何有用的操作,因此请确保您的回调不会花费很长时间。

此功能是在 Libevent 2.1.2-alpha 中添加的。

过时的事件循环函数

如上所述,旧版本的 Libevent API 具有“当前”事件库的全局概念。

本节中的一些事件循环函数具有在当前基础上运行的变体。这些函数的行为与当前函数一样,只是它们不带基本参数。

Current functionObsolete current-base version
event_base_dispatch()event_dispatch()
event_base_loop()event_loop()
event_base_loopexit()event_loopexit()
event_base_loopbreak()event_loopbreak()

因为在 Libevent 2.0 之前 event_base 不支持锁定,所以这些函数不是完全线程安全的:不允许从执行事件循环的线程之外的线程调用 _loopbreak() 或 _loopexit() 函数。

处理事件

Libevent 的基本操作单元是事件。每个事件代表一组条件,包括:

  • A file descriptor being ready to read from or write to.
  • A file descriptor becoming ready to read from or write to (Edge-triggered IO only).
  • A timeout expiring.
  • A signal occurring.
  • A user-triggered event.

事件具有相似的生命周期。一旦调用 Libevent 函数来设置事件并将其与事件库相关联,它就会变为 initialized。此时,您可以添加,使其 在基础中挂起。当事件未决时,如果触发事件的条件发生(例如,其文件描述符更改状态或超时到期),则事件变为活动状态,并运行其(用户提供的)回调函数。如果事件配置为 persistent,则它保持挂起状态。如果它不是持久的,它会在其回调运行时停止挂起。您可以通过删除待处理的事件使其成为非待处理的,您可以添加一个非挂起的事件,使其再次挂起。

构造事件对象

要创建新事件,请使用 event_new() 接口。

Interface

#define EV_TIMEOUT      0x01
#define EV_READ         0x02
#define EV_WRITE        0x04
#define EV_SIGNAL       0x08
#define EV_PERSIST      0x10
#define EV_ET           0x20

typedef void (*event_callback_fn)(evutil_socket_t, short, void *);

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

void event_free(struct event *event);

event_new() 函数尝试分配和构造一个新事件以用于basewhat参数是上面列出的一组标志。(它们的语义在下面描述。)如果fd是非负数,它是我们将观察读取或写入事件的文件。当事件处于活动状态时,Libevent 将调用提供的 cb函数,将其作为参数传递:文件描述符fd 、触发的**所有事件的位域以及构造函数时为arg传递的值。

如果出现内部错误或无效参数,event_new() 将返回 NULL。

所有新事件都已初始化且非挂起。要使事件挂起,请调用 event_add()(如下所述)。

要解除分配事件,请调用 event_free()。对挂起或活动的事件调用 event_free() 是安全的:这样做会使事件在解除分配之前处于非挂起和非活动状态。

Example

#include <event2/event.h>

void cb_func(evutil_socket_t fd, short what, void *arg)
{
        const char *data = arg;
        printf("Got an event on socket %d:%s%s%s%s [%s]",
            (int) fd,
            (what&EV_TIMEOUT) ? " timeout" : "",
            (what&EV_READ)    ? " read" : "",
            (what&EV_WRITE)   ? " write" : "",
            (what&EV_SIGNAL)  ? " signal" : "",
            data);
}

void main_loop(evutil_socket_t fd1, evutil_socket_t fd2)
{
        struct event *ev1, *ev2;
        struct timeval five_seconds = {5,0};
        struct event_base *base = event_base_new();

        /* The caller has already set up fd1, fd2 somehow, and make them
           nonblocking. */

        ev1 = event_new(base, fd1, EV_TIMEOUT|EV_READ|EV_PERSIST, cb_func,
           (char*)"Reading event");
        ev2 = event_new(base, fd2, EV_WRITE|EV_PERSIST, cb_func,
           (char*)"Writing event");

        event_add(ev1, &five_seconds);
        event_add(ev2, NULL);
        event_base_dispatch(base);
}

上述函数定义在 <event2/event.h> 中,最早出现在 Libevent 2.0.1-alpha 中。event_callback_fn 类型首先作为 typedef 出现在 Libevent 2.0.4-alpha 中。

事件标志

EV_TIMEOUT

此标志指示在超时过去后变为活动状态的事件。

构造事件时忽略 EV_TIMEOUT 标志:您
可以在添加事件时设置超时,也可以不设置。这是
超时时在回调函数的“what”参数中设置
已经发生了。
  • EV_READ 此标志指示当提供的文件描述符准备好读取时变为活动状态的事件。

  • EV_WRITE 此标志指示当提供的文件描述符准备好写入时变为活动状态的事件。

  • EV_SIGNAL 用于实现信号检测。请参阅下面的“构建信号事件”。

  • EV_PERSIST 表示事件是持久的。请参阅下面的“关于事件持久性”。

  • EV_ET 如果底层 event_base 后端支持边缘触发事件,则指示该事件应该是边缘触发的。这会影响 EV_READ 和 EV_WRITE 的语义。

从 Libevent 2.0.1-alpha 开始,任何数量的事件都可以同时在相同条件下挂起。例如,如果给定的 fd 准备好读取,您可能有两个将变为活动的事件。他们的回调运行的顺序是未定义的。

这些标志在 <event2/event.h> 中定义。除了 EV_ET,它在 Libevent 1.0 之前就已经存在,它是在 Libevent 2.0.1-alpha 中引入的。

关于事件持久性

默认情况下,每当一个挂起的事件变为活动状态(因为它的 fd 准备好读取或写入,或者因为它的超时到期),它在其回调被执行之前就变为非挂起的。因此,如果你想让事件再次挂起,你可以从回调函数内部再次调用 event_add() 。

但是,如果在事件上设置了 EV_PERSIST 标志,则该事件是 持久的。 这意味着事件即使在其回调被激活时仍保持挂起。如果你想让它在它的回调中不挂起,你可以在它上面调用 event_del() 。

只要事件的回调运行,持久事件的超时就会重置。因此,如果您有一个带有 EV_READ|EV_PERSIST 标志且超时为 5 秒的事件,则该事件将变为活动状态:

  • 每当套接字准备好读取时。
  • 自从事件上次激活以来已经过去了 5 秒。
创建一个事件作为它自己的回调参数

通常,您可能希望创建一个将自身作为回调参数接收的事件。但是,您不能只将指向事件的指针作为参数传递给 event_new(),因为它还不存在。要解决这个问题,可以使用 event_self_cbarg()。

Interface

void *event_self_cbarg();

event_self_cbarg() 函数返回一个“魔术”指针,当作为事件回调参数传递时,它告诉 event_new() 创建一个接收自身作为其回调参数的事件。

Example

#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 called %d times so far.\n", ++n_calls);

    if (n_calls > 100)
       event_del(me);
}

void run(struct event_base *base)
{
    struct timeval one_sec = { 1, 0 };
    struct event *ev;
    /* We're going to set up a repeating timer to get called called 100
       times. */
    ev = event_new(base, -1, EV_PERSIST, cb_func, event_self_cbarg());
    event_add(ev, &one_sec);
    event_base_dispatch(base);
}

此函数也可以与 event_new()、evtimer_new()、evsignal_new()、event_assign()、evtimer_assign() 和 evsignal_assign() 一起使用。但是,它不能作为非事件的回调参数。

event_self_cbarg() 函数是在 Libevent 2.1.1-alpha 中引入的。

Timeout-only events 仅超时事件

为方便起见,您可以使用一组以 evtimer_ 开头的宏来代替 event_* 调用来分配和操作纯超时事件。除了提高代码的清晰度之外,使用这些宏没有任何好处。

Interface

#define evtimer_new(base, callback, arg) \
    event_new((base), -1, 0, (callback), (arg))
#define evtimer_add(ev, tv) \
    event_add((ev),(tv))
#define evtimer_del(ev) \
    event_del(ev)
#define evtimer_pending(ev, tv_out) \
    event_pending((ev), EV_TIMEOUT, (tv_out))

这些宏从 Libevent 0.6 开始就存在,除了 evtimer_new(),它首先出现在 Libevent 2.0.1-alpha 中。

构造信号事件

Libevent 还可以监视 POSIX 风格的信号。要为信号构造处理程序,请使用:

Interface

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

参数与 event_new 相同,只是我们提供了一个信号编号而不是文件描述符。

Example

struct event *hup_event;
struct event_base *base = event_base_new();

/* call sighup_function on a HUP signal */
hup_event = evsignal_new(base, SIGHUP, sighup_function, NULL);

请注意,信号回调在信号发生后在事件循环中运行,因此它们可以安全地调用您不应该从常规 POSIX 信号处理程序调用的函数。

警告不要在信号事件上设置超时。它可能不受支持。[FIXME:这是真的吗?]

在处理信号事件时,您还可以使用一组方便的宏。

Interface

#define evsignal_add(ev, tv) \
    event_add((ev),(tv))
#define evsignal_del(ev) \
    event_del(ev)
#define evsignal_pending(ev, what, tv_out) \
    event_pending((ev), (what), (tv_out))
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值