网络编程(三):从libevent到事件通知机制

wKiom1ZyPVjQSxPEAAEXxh6uhMs860.jpg


由于POSIX标准的滞后性事件通知API的混乱一直保持到现在 所有就有libevent、libev甚至后面的libuv的出现为跨平台编程扫清障碍。


下面是WikiPedia对于libevent的介绍

   libevent是一个异步事件处理软件函式库以BSD许可证发布。 libevent提供了一组应用程序编程接口API让程序员, 可以设定某些事件发生时所执行的函式也就是说libevent可以用来取代网络服务器所使用的事件循环检查框架。

由于可以省去对网络的处理且拥有不错的效能 有些软件使用libevent作为网络底层的函式库如ChromiumChrome的开源版、 memcached、Tor。

按照libevent的官方网站libevent库提供了以下功能当一个文件描述符的特定事件 如可读可写或出错发生了或一个定时事件发生了 libevent就会自动执行用户指定的回调函数来处理事件。


目前libevent已支持以下接口/dev/poll, kqueue(2), event ports, select(2), poll(2) 和epoll(4)。


libevent的内部事件机制完全是基于所使用的接口的。因此libevent非常容易移植 也使它的扩展性非常容易。目前libevent已在以下操作系统中编译通过 LinuxBSDMac OS XSolaris和Windows。


libevent的高明之处还在于它把fd读写、信号、DNS、定时器甚至idle空闲 都抽象化成了event事件。


我们可以简单看一下一个简单的基于libevent的网络server 这有助于我们理解event-driven programming事件驱动编程 也为我们后续的实操做准备。


我在代码中增加了详细的注释希望大家能大致明白event-driven programming 的一半方法:

/*   这是一个示例性质的libevent的程序监听在TCP的9995端口。
    当连接建立成功后它将会给Client回应一个消息"Hello, World!\n"
    发送完毕后就将连接关闭。
    
    程序也处理了SIGINT (ctrl-c)信号收到这个信号后优雅退出程序。
    
    这个程序也用到了一些libevent比较高级的API“bufferevent”
    这套API将buffer的“水位线”也抽象成了event来处理灵感应该是来自    
    Windows平台的IOCP。
*/

// 引入常用Linux系统头文件 
#include <string.h>
#include <errno.h>
#include <stdio.h>
#include <signal.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>

// 引入libevent 2.x相关的头文件 
#include <event2/bufferevent.h>
#include <event2/buffer.h>
#include <event2/listener.h>
#include <event2/util.h>
#include <event2/event.h>

// 定义字符串常量将会回应给Client用
 static const char MESSAGE[] = "Hello, World!\n";
 
// server监听的端口 
static const int PORT = 9995;

// 定义几个event callback的prototype原型 
static void listener_cb(struct evconnlistener * , evutil_socket_t,
    struct sockaddr * , int socklen, void * );
static void conn_writecb(struct bufferevent * , void * );
static void conn_eventcb(struct bufferevent * , short, void * );
static void signal_cb(evutil_socket_t, short, void * );

// 定义标准的main函数 
int
main(int argc, char ** argv)
{
    // event_base是整个event循环必要的结构体 
    struct event_base * base;
    // libevent的高级API专为监听的FD使用 
    struct evconnlistener * listener;
    // 信号处理event指针 
    struct event * signal_event;
    // 保存监听地址和端口的结构体 
    struct sockaddr_in sin;

    // 分配并初始化event_base 
    base = event_base_new();
    if (!base) {
        // 如果发生任何错误向stderr标准错误输出打一条日志退出 
        // 在C语言里很多返回指针的API都以返回null为出错的返回值 
        // if (!base) 等价于 if (base == null) 
        fprintf(stderr, "Could not initialize libevent!\n");
        return 1;
    }

    // 初始化sockaddr_in结构体监听在0.0.0.0:9995 
    memset(&sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_port = htons(PORT);

    // bind在上面制定的IP和端口同时初始化listen的事件循环和callbacklistener_cb 
    // 并把listener的事件循环注册在event_basebase上 
    listener = evconnlistener_new_bind(base, listener_cb, (void * )base,
        LEV_OPT_REUSEABLE|LEV_OPT_CLOSE_ON_FREE, -1,
        (struct sockaddr*)&sin,
        sizeof(sin));

    if (!listener) {
        // 如果发生任何错误向stderr标准错误输出打一条日志退出 
        fprintf(stderr, "Could not create a listener!\n");
        return 1;
    }

    // 初始化信号处理event 
    signal_event = evsignal_new(base, SIGINT, signal_cb, (void * )base);

    // 把这个callback放入base中 
    if (!signal_event || event_add(signal_event, NULL)<0) {
        fprintf(stderr, "Could not create/add a signal event!\n");
        return 1;
    }

    // 程序将在下面这一行内启动event循环只有在调用event_base_loopexit后 
    // 才会从下面这个函数返回并向下执行各种清理函数导致整个程序退出 
    event_base_dispatch(base);

    // 各种清理free 
    evconnlistener_free(listener);
    event_free(signal_event);
    event_base_free(base);

    printf("done\n");
    return 0;}// 监听端口的event callback static voidlistener_cb(struct evconnlistener * listener, evutil_socket_t fd,
    struct sockaddr * sa, int socklen, void * user_data){
    struct event_base * base = user_data;
    struct bufferevent * bev;

    // 新建一个bufferevent设定BEV_OPT_CLOSE_ON_FREE 
    // 保证bufferevent被free的时候fd也会被关闭 
    bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
    if (!bev) {
        fprintf(stderr, "Error constructing bufferevent!");
        event_base_loopbreak(base);
        return;
    }
    // 设定写buffer的event和其它event 
    bufferevent_setcb(bev, NULL, conn_writecb, conn_eventcb, NULL);
    // 开启向fd中写的event 
    bufferevent_enable(bev, EV_WRITE);
    // 关闭从fd中读写入buffer的event 
    bufferevent_disable(bev, EV_READ);
    // 向buffer中写入"Hello, World!\n" 
    // 上面的操作保证在fd可写时将buffer中的内容写出去 
    bufferevent_write(bev, MESSAGE, strlen(MESSAGE));
}

// 每次fd可写数据非阻塞写入后会雕也难怪conn_writecb
// 这个函数每次检查eventbuffer的剩余大小如果为0 
// 表示数据已经全部写完将eventbuffer free掉 
// 由于在上面设定了BEV_OPT_CLOSE_ON_FREE所以fd也会被关闭 
static void
conn_writecb(struct bufferevent * bev, void * user_data)
{
    struct evbuffer * output = bufferevent_get_output(bev);
    if (evbuffer_get_length(output) == 0) {
        printf("flushed answer\n");
        bufferevent_free(bev);
    }
}

// 处理读、写event之外的event的callback 
static void
conn_eventcb(struct bufferevent * bev, short events, void * user_data)
{
    if (events & BEV_EVENT_EOF) {
        // Client端关闭连接 
        printf("Connection closed.\n");
    } else if (events & BEV_EVENT_ERROR) {
        // 连接出错 
        printf("Got an error on the connection: %s\n",
            strerror(errno));
    }
    // 如果还有其它的event没有处理那就关闭这个bufferevent 
    bufferevent_free(bev);
}

// 信号处理event收到SIGINT (ctrl-c)信号后延迟2s退出event循环 
static void
signal_cb(evutil_socket_t sig, short events, void * user_data)
{
    struct event_base * base = user_data;
    struct timeval delay = { 2, 0 };

    printf("Caught an interrupt signal; exiting cleanly in two seconds.\n");

    event_base_loopexit(base, &delay);
}

可以看出用这种方式写出来的异步非阻塞server的逻辑还是比较容易理解的。


和协程的实现方式相比这种方式完全避免了“手工”的上线文切换 有利于CPU的分支预测的成功率能发挥CPU处理网络连接的的最大潜能。

水平触发LT & 边沿触发ET

在struct epoll_event里有连个FlagEPOLLET和EPOLLLT让初学者很难以理解

以下是一段关于epoll的man文档

epoll is a variant of poll(2) that can be used either as an edge-triggered or a level-triggered interface and scales well to large numbers of watched file descriptors. The following system calls are pro- vided to create and manage an epoll instance:


  • An epoll instance created by epoll_create(2), which returns a file descriptor referring to the epoll instance. (The more recent epoll_create1(2) extends the functionality of epoll_create(2).)


  • Interest in particular file descriptors is then registered via epoll_ctl(2). The set of file descriptors currently registered on an epoll instance is sometimes called an epoll set.


  • Finally, the actual wait is started by epoll_wait(2).


Level-Triggered and Edge-Triggered The epoll event distribution interface is able to behave both as edge-triggered (ET) and as level-triggered (LT). The difference between the two mechanisms can be described as follows. Suppose that this scenario happens:


  1. The file descriptor that represents the read side of a pipe (rfd) is registered on the epoll instance.

  2. A pipe writer writes 2 kB of data on the write side of the pipe.

  3. A call to epoll_wait(2) is done that will return rfd as a ready file descriptor.

  4. The pipe reader reads 1 kB of data from rfd.

  5. A call to epoll_wait(2) is done.


If the rfd file descriptor has been added to the epoll interface using the EPOLLET (edge-triggered) flag, the call to epoll_wait(2) done in step 5 will probably hang despite the available data still present in the file input buffer; meanwhile the remote peer might be expecting a response based on the data it already sent. The reason for this is that edge-triggered mode only delivers events when changes occur on the monitored file descriptor. So, in step 5 the caller might end up waiting for some data that is already present inside the input buffer. In the above example, an event on rfd will be generated because of the write done in 2 and the event is consumed in 3. Since the read operation done in 4 does not consume the whole buffer data, the call to epoll_wait(2) done in step 5 might block indefinitely.


An application that employs the EPOLLET flag should use non-blocking file descriptors to avoid having a blocking read or write starve a task that is handling multiple file descriptors. The suggested way to use epoll as an edge-triggered (EPOLLET) interface is as follows:


i with non-blocking file descriptors; and


ii by waiting for an event only after read(2) or write(2) return EAGAIN.


By contrast, when used as a level-triggered interface (the default, when EPOLLET is not specified), epoll is simply a faster poll(2), and can be used wherever the latter is used since it shares the same semantics.


Since even with edge-triggered epoll, multiple events can be generated upon receipt of multiple chunks of data, the caller has the option to specify the EPOLLONESHOT flag, to tell epoll to disable the associated file descriptor after the receipt of an event with epoll_wait(2). When the EPOLLONESHOT flag is specified, it is the caller’s responsibility to rearm the file descriptor using epoll_ctl(2) with EPOLL_CTL_MOD.

在epoll的man文档里我们会看到一个花费大量篇幅描述的两个概念:

LTLevel Triggered水平触发 ETEdge Triggered边沿触发


作者当年花费了九牛二虎之力也没能领悟这段“经文”。后来一个偶然的机会 一个做电子设计的朋友给我讲明白了其中的道道。


为了弄明白LTLevel Triggered水平触发 和 ETEdge Triggered边沿触发 我们先要了解这个Level和Edge是什么涵义Level翻译成中文这里准确的涵义应该是电平 Edge是边沿。


这两个词曾经是电子信号领域的一个专有名词。如果用时序图来标示一个数字电信号“010” 应该是类似下图所示


wKioL1ZyPUigFytbAAAw1Tf5HEs318.png

  • 低电平表示0。

  • 高电平表示1。

  • 0向1变化的竖线就是上升沿。

  • 1向0变化的竖线就是下降沿。

  • 在0或者1的情况下触发的信号就是LTLevel Triggered水平触发

  • 在0向1、1向0变化的过程中触发的信号就是 和 ETEdge Triggered边沿触发

0或1都是一个状态而0向1、1向0变化则只是一个事件。


我们很直观的就可以得出结论LT是一个持续的状态ET是个事件性的一次性状态。


二者的差异在于Level Triggered模式下只要某个socket处于readable/writable状态 无论什么时候进行epoll_wait都会返回该socket


而Edge Triggered模式下只有某个socket从unreadable变为readable或 从unwritable变为writable时epoll_wait才会返回该socket。


虽然有很多资料表明ET模式的销量会比LT稍高 但ET模式的编程由于事件只通知一次很容易犯错误导致程序假死我们推荐epoll工作于LT模式。 除非你很清楚你选择的是什么。

闲话QQ的通信协议

如果大家研究过早期的腾讯QQ的通信协议可以发现QQ的通信协议是基于UDP的。 这点从今天的角度看来显得十分的怪异因为用UDP这种无连接的协议 实现一套保证消息可靠性的聊天服务的难度是非常之高的。


了解过那段历史的同学可能知道当时UDP的确是QQ的唯一选择。 当年QQ达到百万人同时在线的时候国外的同行还没有认为C10K是个问题。 想要用TCP承担百万人同时在线在当时的技术条件下恐怕要付出上千台服务器的代价 这对于当时的"小企鹅"来说是绝对负担不起的一笔投入。


由于缺乏操作系统对于高性能TCP协议的支持想要在极为有限的服务器条件下 处理QQ的C1000K问题UDP的确是当时的腾讯架构师的唯一选择。


★著作权归作者所有。

商业转载请联系作者获得授权非商业转载请注明出处。
作者大家可以看我的知乎专栏
链接
http://zhuanlan.zhihu.com/auxten/20315482
这是一个系列的文章之四之五已经写完了会陆陆续续搬到Linuxtone着急的同学可以看

  • 网络编程四互联网中TCP Socket服务器的实现过程需要考虑哪些安全问题

  • 网络编程五长连接&连接池的应用

==================================

》》想要了解更多精彩内容欢迎关注》》

 

联系Reboot-有更多技术分享、交流请加群:238757010

你可能感兴趣的:(epoll,poll,unix网络编程)