美文网首页c++库
libevent primer

libevent primer

作者: 老瓦在霸都 | 来源:发表于2020-08-31 18:03 被阅读0次

    看到一篇 libevent 的介绍文件,写得由浅入深,颇有趣味,随手翻译在此,并增加了一点 libevent 自身的介绍。
    原文来自http://www.wangafu.net/~nickm/libevent-book/01_intro.html
    These documents are Copyright (c) 2009-2012 by Nick Mathewson, and are made available under the Creative Commons Attribution-Noncommercial-Share Alike license, version 3.0. Future versions may be made available under a less restrictive license.

    多数初级程序员是从阻塞式IO调用开始编程的。一个 IO 调用称为同步的,它直到操作完成或者操作超时了才会返回。

    当你在 TCP 连接时调用了 “connect”,例如你的操作系统发送了一个 “SYN” 消息给TCP 连接另一端的主机。 它不会马上返回,会一直等到你的应用程序从对端收到了一个 “SYN ACK” 消息或者等待超时了。

    • 示例 http_client.c
      为方便运行,代码中的 google.com 改成了 baidu.com
    /* 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.baidu.com\r\n"
            "\r\n";
        const char hostname[] = "www.baidu.com";
        struct sockaddr_in sin;
        struct hostent *h;
        const char *cp;
        int fd;
        ssize_t n_written, remaining;
        char buf[1024];
    
        /* 根据 hostname 查找 IP 地址,注意这个函数在多数平台上都不是线程安全的 */
        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;
        }
    
        /* 分配一个新的 socket */
        fd = socket(AF_INET, SOCK_STREAM, 0);
        if (fd < 0) {
            perror("socket");
            return 1;
        }
    
        /*  连接远程主机 */
        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;
        }
    
        /* 将这个http 查询写入socket  */
        /* 为避免TCP 没能完整发送数据名,用一个循环来检查并发送余下字节 */
        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;
        }
    
        /* 接收发回的响应 */
        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;
    }
    
    • Makefile
    http_client:
            gcc -g -o http_client http_client.c
    clean:
            rm -f http_client
    

    编译并测试一下

    make http_client
    ./http_client
    # 打印了接收的的 HTTP 消息
    
    HTTP/1.0 200 OK
    Accept-Ranges: bytes
    Cache-Control: no-cache
    ...
    

    以上所有的网络调用都是阻塞的

    • gethostbyname 一直等到解析 www.baidu.com 成功或者失败才会返回
    • connect 走到连接上了才会返回
    • recv 调用一直等到接收到数据或者连接关闭才会返回
    • send 调用会一直等到所输出到内核的写缓存中才会返回

    阻塞式 IO不一定就不好。 如果你在此期间不想让程序执行其他任何操作,那么阻塞式 IO 工作得很好。 但是假设你需要编写一个程序来一次处理多个连接,那就两说了。 为了使我们的示例更具体:假设您想从两个连接中读取输入,并且您不知道哪个连接将首先获得输入。

    你不能这样写代码:

    /* 这块代码是无效的 */
    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上的因为数据先到达了 fd[2], 你的程序将不会尝试先从 TCP连接,一次从其输入的一行中读取数据,并在到达时写出每行的ROT13模糊处理。 它使用Unix fork() 调用为每个传入连接创建一个新进程。

    (注:ROT 13 也称旋转13,是一种简单的字母替换加密算法,将此字母替换为它在字母表之后的第13个字母,由于只有26个字母,两次编码等于一次解码)

    • 示例 rot13_server.c
    /* 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 仅处理几个线程。

    那么,线程化不是处理多个连接的好方法,什么方法才是呢?答案是让你的 socket 不要阻塞。

    设置为非阻塞的调用如下

    fcntl(fd, F_SETFL, O_NONBLOCK);
    

    这里的 fd 是 socket 的文件描述符
    (一个文件描述符是内核在打开socket 分配给它的编号,你可以用它进行指向 socket 的调用)

    一旦你将 fd ( 对应的socket ) 设置为非阻塞的,从此以垢 ,每当对 fd 进行网络操作时,相应的调用立即返回或者返回特殊的错误代码以指示“我现在无法取得任何进展,请再试一次”。故此,我们的两路连接示例可以简略地写成

    • 反例:
    /* 这个例子虽然能工作,但是性能差到无法忍受 */
    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);
             }
        }
    }
    

    现在我们使用了非阻塞 socket 调用, 上面的代码也能工作,但是几乎不能用。其性能非常糟糕,有两个原因,其一,当在任何一个连接上都没有要读取的数据时,将陷入无限循环,从而耗尽所有CPU周期。 其二,如果你尝试使用这种方法处理一个或两个以上的连接,则将对每个连接进行内核调用,无论该连接是否具有任何数据。

    因此,我们需要一种告诉内核 “等到这些 socket 中有一个准备好了给我一些数据,再告诉我哪些已经准备好”的方法。

    人们仍然在使用的最古老的解决方案是select()。 select() 调用采用三组fds(以位数组的形式实现)

    • 一组用于读取,
    • 一组用于写入,
    • 以及一组用于异常。

    它等待直到其中一组中的一个socket 准备就绪,然后将这几组更改为仅包含可供使用的 socket。

    这是我们再次使用select的示例

    • 示例:使用 select
    /* 如果你仅仅有一些文件句柄,这个版本的性能不会太差 */
    fd_set readset;
    int i, n;
    char buf[1024];
    
    while (i_still_want_to_read()) {
        int maxfd = -1;
        FD_ZERO(&readset);
    
       /* 将所有感兴趣的文件句柄放入 readset */
        for (i=0; i < n_sockets; ++i) {
             if (fd[i]>maxfd) maxfd = fd[i];
             FD_SET(fd[i], &readset);
        }
    
        /* 等待一个或多个文件句柄可读 */
        select(maxfd+1, &readset, NULL, NULL, NULL);
    
        /* 处理仍然在 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_server.c 用 select() 重新实现一遍

    • 示例 rot13_server_select.c
    /* 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() 的 比特数据 (bit array ) 所花费的时间与最大的 fd 成正比,当 socket 的 fd 文件句柄编号很大时, select() 调用的时间会急剧增加。

    (在用户空间方面,可以使生成和读取位数组的时间与你为select() 提供的fds的数量成比例。但是在内核空间方面,读取位数组所花费的时间与位数组中的最大fd成正比,不管在程序往select() 中添加了多少文件句柄,这个最大fd 都接近于在整个程序中使用的文件句柄的总数)

    不同的操作系统提供了不同的 select() 替换函数以供选择,包括poll(),epoll(),kqueue(),evports 和 /dev/ poll。所有这些函数调用都提供比 select() 更好的性能,除了 poll() 之外,所有这些函数都为 O(1) 的性能来添加 socket ,移除 socket 以及通知 socket 已准备好进行读写。

    不幸的是,没有一个有效的接口是普遍存在的标准。 Linux有epoll(),BSD(包括Darwin)有kqueue(),Solaris有evports 和 /dev/poll…,而这些操作系统都没有其他操作系统所拥有的接口。因此,如果要编写可移植的高性能异步应用程序,则需要一个包装所有这些接口的抽象,并能选取其中任何一个最有效的。

    这就是 libevent API 所提供的最低级别的功能。它使用运行它的计算机上可用的最高效的版本,为各种select() 替换提供一致的界面。

    • 示例, 使用 libevent 来实现 rot13_server: rot13_server_event.c
    /* 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;
    }
    

    ( 代码中需要注意的其他事项:我们不是使用 int 来作为socket 的类型,而是使用 evutil_socket_t 为作为 socket 的类型。不是调用 fcntl(O_NONBLOCK) 来使 socket 成为非阻塞状态,而是调用evutil_make_socket_nonblocking 。这些更改使我们代码与 Win32 网络API的不同部分兼容。)

    便利性呢? (Windows呢?)

    您可能已经注意到,随着我们的代码变得越来越高效,它也变得越来越复杂。回到 forking 那个版本时,我们不必为每个连接管理缓冲区:我们为每个进程只有一个单独的堆栈分配缓冲区。我们不需要明确地跟踪每个套接字是否正在读取或写入:这在代码中是隐式的。而且,我们不需要一种结构来跟踪每个操作完成了多少:我们只使用了循环和堆栈变量。

    此外,如果您对Windows上的联网具有深厚的经验,您会意识到 libevent在如上例中那样使用时可能无法获得最佳性能。在Windows上,执行快速异步IO的方法不是使用类似select() 的接口:而是使用IOCP(IO完成端口)API。与所有快速联网API不同,当 socket 准备好要执行的程序必须执行的操作时,IOCP 不会通知你的程序。而是由程序告诉Windows网络堆栈开始网络操作,而IOCP告诉程序操作何时完成。

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

    • 示例:使用 Libevent bufferevents API 的更简单的ROT13服务器 rot13_server_event2.c
    /* 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

    libevent是用于开发可伸缩网络服务器的事件通知库。 libevent API提供了一种机制,该机制可在文件描述符上发生特定事件或达到超时后执行回调函数。此外,由于信号或定期超时,libevent还支持回调。

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

    Libevent还提供了用于缓冲网络IO的复杂框架,并支持套接字,过滤器,速率限制,SSL,零拷贝文件传输和IOCP。 Libevent包括对几种有用协议的支持,包括DNS,HTTP和最小的RPC框架。

    • 标准用法

    每个使用libevent的程序都必须包含<event.h>标头,并将-levent标志传递给链接器。在使用库中的任何功能之前,必须调用event_init() 或event_base_new() 来执行libevent库的一次性初始化。

    • 活动通知

    对于要监视的每个文件描述符,必须声明一个事件结构并调用event_set() 来初始化该结构的成员。要启用通知,您可以通过调用event_add() 将结构添加到受监视事件的列表中。只要活动结构处于活动状态,它就必须保持分配状态,因此应在堆上进行分配。最后,调用event_dispatch() 循环和调度事件。

    • I / O缓冲器

    libevent在常规事件回调的基础上提供了一个抽象。这种抽象称为缓冲事件。缓冲事件提供输入和输出缓冲区,这些缓冲区会自动填充和耗尽。缓冲事件的用户不再直接处理I / O,而是从输入读取并写入输出缓冲区。

    通过bufferevent_new() 初始化后,可以将bufferevent结构与bufferevent_enable() 和bufferevent_disable() 重复使用。与其直接读写套接字,不如调用bufferevent_read() 和bufferevent_write() 。

    启用读取功能后,bufferevent将尝试从文件描述符读取并调用read回调。每当输出缓冲区的水位低于写低水位线(默认为0)时,就会执行写回调。

    • 计时器

    libevent也可用于创建计时器,该计时器在一定时间后将调用回调。 evtimer_set()函数准备要用作计时器的事件结构。要激活计时器,请调用evtimer_add()。可以通过调用evtimer_del() 禁用计时器。

    • 超时时间

    除了简单的计时器外,libevent还可将超时事件分配给文件描述符,只要经过一定时间而文件描述符上没有任何活动,就会触发该事件。 timeout_set() 函数初始化事件结构以用作超时。初始化后,必须使用timeout_add() 激活事件。要取消超时,请调用timeout_del() 。

    • 异步DNS解析

    libevent提供了应该使用的异步DNS解析器,而不是标准的DNS解析器功能。可以通过在程序中包含<evdns.h>标头来导入这些功能。在使用任何解析器功能之前,必须调用evdns_init() 来初始化库。要将主机名转换为IP地址,请调用evdns_resolve_ipv4()函数。要执行反向查找,您可以调用evdns_resolve_reverse() 函数。所有这些功能都使用回调来避免在执行查询时阻塞。

    • 事件驱动的HTTP服务器

    libevent提供了一个非常简单的事件驱动的HTTP服务器,可以将其嵌入程序中并用于服务HTTP请求。

    要使用此功能,您需要在程序中包含<evhttp.h>标头。通过调用evhttp_new() 创建服务器。添加地址和端口以使用evhttp_bind_socket() 进行侦听。然后,您注册一个或多个回调以处理传入的请求。可以通过evhttp_set_cb() 函数为每个URI分配一个回调。通用回调函数也可以通过evhttp_set_gencb() 注册。如果尚未为给定URI注册其他回调,则将调用此回调。

    • RPC服务器和客户端的框架

    libevents提供了用于创建RPC服务器和客户端的框架。它负责封送和解封所有数据结构。

    API Reference

    主要接口参见如下的头文件

    event.h The primary libevent header

    evdns.h Asynchronous DNS resolution

    evhttp.h An embedded libevent-based HTTP server

    evrpc.h A framework for creating RPC servers and clients

    编译

    • Install openssl
    • set environment of openssl
      export PKG_CONFIG_PATH="/usr/local/opt/openssl@1.1/lib/pkgconfig”
    • cd libevent
    • ./configure
    • make
    • make verify
    • make install

    以上示例的 make file

    http_client:
            gcc -g -o http_client http_client.c
    rot13:
            gcc -g -o rot13_server rot13_server.c
    rot13_select:
            gcc      -g -o rot13_server_select rot13_server_select.c
    rot13_event:
            gcc      -g -o rot13_server_event rot13_server_event.c -levent
    rot13_event2:
            gcc      -g -o rot13_server_event2 rot13_server_event2.c  -levent
    clean:
            rm -f http_client rot13_server rot13_server_select rot13_server_event rot13_server_event2
    
    ./rot13_server
    $ nc 127.0.0.1 40713
    hello
    uryyb
    uryyb
    hello
    ^C
    

    参考资料

    相关文章

      网友评论

        本文标题:libevent primer

        本文链接:https://www.haomeiwen.com/subject/azausktx.html