C1000K之Libevent源码分析



简介

说到异步IO,高并发之类的名词, 可能很多人第一反应就是 select, poll, epoll, kqueue 之类的底层代码库。 但是其实除非你要写一个 Nginx 性能级别的服务器, 否则直接使用 epoll 之类的还是太过底层, 诸多不便,要榨干整个异步编程的高并发性能还需要开发很多相关组件, 而
Libevent 就是作为更好用的高性能异步编程网络库而生, 他帮你包装了各种 buffer 和 event, 甚至也提供了更加高层的 http 和 rpc 等接口, 可以让你脱离底层细节,更加专注于服务的其他核心功能的实现。 当然,要真正用好它,还是需要懂不少关于他的实现原理。

如果是第一次接触 Libevent 的可以先看一篇非常好的入门文章:
Libevent-book , 文章主要从 C10K 问题的发展循序渐进, 分别讲了在高并发连接的情况下, 多线程解决方案, 多进程解放方案会遇到的问题, 从而引出为什么异步IO是当前解决高并发连接最有效的方案。

ideawu 实现的 C1000K 服务器 icomet 核心就是基于 Libevent 实现的。

本文源码分析基于 Libevent master 分支 commit 6dba1694c89119c44cef03528945e5a5978ab43a 版本的代码。

事件循环

既然是异步IO,事件驱动,自然会有事件循环(event loop) 。 Libevent 的事件循环是通过调用 event_base_dispatch 来实现, 其实 event_base_dispatch 函数也是调用了 event_base_loop, 代码如下:

int
event_base_dispatch(struct event_base *event_base)
{
    return (event_base_loop(event_base, 0));
}

运行事件循环的函数肯定是阻塞函数, 拿 linux 平台来说, libevent 的事件循环其实就是循环调用 epoll_wait 函数,

int epoll_wait(int epfd, struct epoll_event *events,
              int maxevents, int timeout);

在没有任何事件被触发的时候,epoll_wait 是阻塞等待的, 而且,在 Libevent 里,定时器的实现其实就是通过 epoll_wait 的 timeout 参数实现的。

如果只有一个单线程的话,一旦调用了 event_base_dispatch 之后,这个线程就会被事件完完全全的霸占, 无法进行任何其他的操作。

如果我们需要临时手动激活任何其他事件的话, 则需要借助另一个线程来操作(因为主线程仍然在阻塞等待中)。

在 event_active 函数的解释里面就有一句话说的就是这个事情:

One common use in multithreaded programs is to wake the thread running event_base_loop() from another thread.

事件是 Libevent 的最小单位

这里的事件指的就是 struct event 数据结构。

事件可以注册的各种信号如下:

#define EV_TIMEOUT   0x01
   Indicates that a timeout has occurred.
#define EV_READ   0x02
   Wait for a socket or FD to become readable.
#define EV_WRITE   0x04
   Wait for a socket or FD to become writeable.
#define EV_SIGNAL   0x08
   Wait for a POSIX signal to be raised.
#define EV_PERSIST   0x10
   Persistent event: won't get removed automatically when
   activated.
#define EV_ET   0x20
   Select edge-triggered behavior, if supported by the backend.

几乎所有其它更上层的数据结构都是基于 struct event 的包装来完成的。

核心数据结构

就拿 基于 Libevent 写一个 HTTP 服务器举例来说说, 编程时需要理解的几个核心数据结构是:

  • evhttp_request
  • evhttp_connection
  • bufferevent
  • evbuffer

从上到下是从高层到底层的关系, 下文的顺序也是从高层往底层分析。

evhttp_request

struct evhttp_request {
    // 每个 evhttp_request 都内含一个 evhttp_connection 来负责数据传输
    struct evhttp_connection *evcon;

    //输入输出的两个buffer(这两个buffer的数据是从 evhttp_connection 里面的 input_buffer 和 output_buffer 拷贝过来的。)

    struct evbuffer *input_buffer;  /* read data */
    ev_int64_t ntoread;
    unsigned chunked:1,     /* a chunked request */
        userdone:1;         /* the user has sent all data */

    struct evbuffer *output_buffer; /* outgoing post or data */

    //和HTTP协议有关的各种回调函数:

    void (*cb)(struct evhttp_request *, void *);
    void *cb_arg;

    void (*chunk_cb)(struct evhttp_request *, void *);

    int (*header_cb)(struct evhttp_request *, void *);

    void (*error_cb)(enum evhttp_request_error, void *);

    void (*on_complete_cb)(struct evhttp_request *, void *);
    void *on_complete_cb_arg;

    // 其它
    // ......
};

evhttp_connection

evhttp_request 和 evhttp_connection 的关系很简单,拿协议栈来对比的话, 前者代表的是 HTTP 协议,即应用层协议, 后者代表的是 TCP 协议,即传输层协议。 前者需要管理所有和 HTTP 相关的数据内容,比如 HTTP header 数据和 body 数据。

struct evhttp_connection {

    // socket文件描述符
    evutil_socket_t fd;

    // evhttp_connection 含有一个bufferevent,基于它进行数据传输。
    struct bufferevent *bufev;

    // 和数据传输有关的状态
    enum evhttp_connection_state state;

    //和 HTTP 协议无关的数据传输回调函数
    void (*cb)(struct evhttp_connection *, void *);
    void *cb_arg;

    void (*closecb)(struct evhttp_connection *, void *);
    void *closecb_arg;

    // 其它
    // ......
};

evhttp_connection 结构里面含有 enum evhttp_connection_state state 变量, 这个和
Thrift异步IO服务器源码分析 里面的 保持每个连接的状态是一个道理。 维护状态变化是异步IO服务编程的必要条件。

enum evhttp_connection_state {
    EVCON_DISCONNECTED, /**< not currently connected not trying either*/
    EVCON_CONNECTING,   /**< tries to currently connect */
    EVCON_IDLE,     /**< connection is established */
    EVCON_READING_FIRSTLINE,/**< reading Request-Line (incoming conn) or
                 **< Status-Line (outgoing conn) */
    EVCON_READING_HEADERS,  /**< reading request/response headers */
    EVCON_READING_BODY, /**< reading request/response body */
    EVCON_READING_TRAILER,  /**< reading request/response chunked trailer */
    EVCON_WRITING       /**< writing request/response headers/body */
};

bufferevent

bufferevent 就是包装了 EV_READ event 和 EV_WRITE event , 并且带有读写缓冲区(evbuffer)的更高层的单位。

struct bufferevent {
    // 读写事件
    struct event ev_read;
    struct event ev_write;

    // 读写缓冲区
    struct evbuffer *input;
    struct evbuffer *output;

    // 注意三个回调函数是核心
    bufferevent_data_cb readcb;
    bufferevent_data_cb writecb;
    bufferevent_event_cb errorcb;
    void *cbarg;

    // 其他
    // ......
};

我觉得上面代码就非常简洁易懂了, 两个读写时间没什么好说的, 两个读写缓冲区也是必须的(evbuffer的实现在后面会谈到), 三个回调函数就是核心。 读和写的回调函数没什么好说的, 唯一需要注意的是
bufferevent_event_cb errorcb 这个回调函数是必须注册的, 它关系到当该 bufferevent 对应的时间发生读写外的任何行为(比如socket关闭)时, 都会触发。

整理一遍 bufferevent 的事件处理过程就是:

  • 当可读事件发生时,调用 readcb 将 socket 的数据 通过 recv 读出来存入 input 缓冲区;
  • 当可写事件发生时,调用 writecb 将 output 缓冲区里面的数据通过 socket send 发送出去;
  • 当其他事件发生时,比如 socket close 发生,进行相应的数据清理退出工作。

evbuffer

基本上的异步IO服务里的buffer都是一个德行(包括Nginx也是这样), 都是即是数组又是链表(类似C++ STL里面的deque)。 对于 libevent 来说, evbuffer 是一个链表,管理整个缓冲区的头指针和尾指针,

struct evbuffer {
    struct evbuffer_chain *first;
    struct evbuffer_chain *last;
    size_t total_len;
    //......
};

对于 evbuffer 这个链表的每个单元,也就是 evbuffer_chain 来说, 则是数组(连续内存空间),

struct evbuffer_chain {
    struct evbuffer_chain *next;
    size_t buffer_len;
    size_t off;
    unsigned char *buffer;
    //......
};

总结

对于我个人而言,读源码的时候主要是从核心数据结构入手, 如果理解了这几个核心数据结构, 一般就能猜到这些数据结构的相关函数都有哪些。 可以围绕着这些结构去找相关的函数为己所用。

http://yanyiwu.com/work/2014/12/10/asyncronous-io-libevent.html

时间: 2024-10-27 05:59:37

C1000K之Libevent源码分析的相关文章

Thrift异步IO服务器源码分析

 最近在使用 libevent 开发项目,想起之前写 Thrift源码剖析 的时候说到关于 TNonblockingServer 以后会单独写一篇解析, 现在是时候了,就这篇了. 以下内容依然是基于 thrift-0.9.0 . 概述 现在随着 Node.js 的兴起,很多人着迷 eventloop , 经常是不明真相就会各种追捧,其实 eventloop 只是 一种高并发的解决方案.Thrift 的 TNonblockingServer 就是该解决方案的典型实现之一. 而且,Thrift

WebWork2源码分析

web Author: zhuam   昨晚一口气看完了夏昕写的<<Webwork2_Guide>>,虽然文档资料很简洁,但仍不失为一本好的WebWork2书籍,看的出作者的经验和能力都是非常的老道,在此向作者的开源精神致敬,并在此引用夏昕的那句话So many open source projects, Why not Open your Documents?   今天下载了最新的WebWork2版本, 开始了源码分析,这份文档只能算是我的个人笔记,也没时间细细校对,且个人能力有

JUnir源码分析(一)

一.引子 JUnit源码是我仔细阅读过的第一个开源项目源码.阅读高手写的代码能学到一些好的编程风格和实现思路,这是提高自己编程水平行之有效的方法,因此早就想看看这些赫赫有名的框架是怎么回事了.今天就拿最简单的JUnit下手,也算开始自己的源码分析之路.   JUnit作为最著名的单元测试框架,由两位业界有名人士协力完成,已经经历了多次版本升级(了解JUnit基础.JUnit实践).JUnit总体来说短小而精悍,有不少值得我们借鉴的经验在里面:但是也有一些不足存在,当然这对于任何程序来说都是难免的

java io学习(三) 管道的简介,源码分析和示例

管道(PipedOutputStream和PipedInputStream)的简介,源码分析和示例 本章,我们对java 管道进行学习. java 管道介绍 在java中,PipedOutputStream和PipedInputStream分别是管道输出流和管道输入流. 它们的作用是让多线程可以通过管道进行线程间的通讯.在使用管道通信时,必须将PipedOutputStream和PipedInputStream配套使用. 使用管道通信时,大致的流程是:我们在线程A中向PipedOutputStr

java io学习(二)ByteArrayOutputStream的简介,源码分析和示例

ByteArrayOutputStream的简介,源码分析和示例(包括OutputStream) 前面学习ByteArrayInputStream,了解了"输入流".接下来,我们学习与ByteArrayInputStream相对应的输出流,即ByteArrayOutputStream. 本章,我们会先对ByteArrayOutputStream进行介绍,在了解了它的源码之后,再通过示例来掌握如何使用它. ByteArrayOutputStream 介绍 ByteArrayOutputS

java io学习(一)ByteArrayInputStream的简介,源码分析和示例

ByteArrayInputStream的简介,源码分析和示例(包括InputStream) 我们以ByteArrayInputStream,拉开对字节类型的"输入流"的学习序幕. 本章,我们会先对ByteArrayInputStream进行介绍,然后深入了解一下它的源码,最后通过示例来掌握它的用法. ByteArrayInputStream 介绍 ByteArrayInputStream 是字节数组输入流.它继承于InputStream. 它包含一个内部缓冲区,该缓冲区包含从流中读取

mahout源码分析之DistributedLanczosSolver(七) 总结篇

Mahout版本:0.7,hadoop版本:1.0.4,jdk:1.7.0_25 64bit. 看svd算法官网上面使用的是亚马逊的云平台计算的,不过给出了svd算法的调用方式,当算出了eigenVectors后,应该怎么做呢?比如原始数据是600*60(600行,60列)的数据,计算得到的eigenVectors是24*60(其中的24是不大于rank的一个值),那么最后得到的结果应该是original_data乘以eigenVectors的转置这样就会得到一个600*24的矩阵,这样就达到了

mahout源码分析之DistributedLanczosSolver(五)

Mahout版本:0.7,hadoop版本:1.0.4,jdk:1.7.0_25 64bit. 1. Job 篇 接上篇,分析到EigenVerificationJob的run方法: public int run(Path corpusInput, Path eigenInput, Path output, Path tempOut, double maxError, double minEigenValue, boolean inMemory, Configuration conf) thro

Jquery源码分析---概述

jQuery是一个非常优秀的JS库,与Prototype,YUI,Mootools等众多的Js类库 相比,它剑走偏锋,从web开发实用的角度出发,抛除了其它Lib中一些不实用的 东西,为开发者提供了短小精悍的类库.其短小精悍,使用简单方便,性能高效 ,能极大地提高开发效率,是开发web应用的最佳的辅助工具之一.因此大部分 开发者在抛弃Prototype而选择Jquery来进行web开发. 一些开发人员在使用jquery时,由于仅仅只知道Jquery文档中的使用方法, 不明白Jquery的运行原理