Libevent教程001: 簡介與配置

張浮生發表於2018-05-18

本文內容大致翻譯自 libevent-book, 但不是照本翻譯. 成文時, libevent最新的穩定版為 2.1.8 stable. 即本文如無特殊說明, 所有描述均以 2.1.8 stable 版本為準.

本文為系列文章的第一篇, 對應libevent-book的 chapter 0 + chapter 1 + R0 + R1

0. 前提條件

這個文件是對libevent的介紹與指導, 閱讀文件需要你具有以下的能力:

  1. 你精通C語言
  2. 你至少了解Unix網路程式設計.
  3. 你會安裝libevent
  4. 你大致知道libevent是幹什麼用的.

1. 基本概念: 阻塞/非阻塞/同步/非同步/回撥機制的討論

這裡首先要解釋四個名詞: 阻塞, 非阻塞, 同步, 非同步. 它們都是修飾"介面"的形容詞, 或者說的土一點, 它們都是修飾"函式"的形容詞.

同步, 還是非同步, 是從"訊息通訊"的視角去描述這個介面的行為. 而所謂的訊息通訊, 你可以簡單的把"函式"想象成一個淘寶客服, 把"呼叫方"想象成你自己. 呼叫函式的過程其實就是三步:

  1. "你"詢問"淘寶客服"一個問題. 比如, "在嗎?". 在這個場景中, 你就是"呼叫方", "淘寶客服"是函式, 而那句"在嗎?", 則是函式引數, 你把函式引數傳遞給函式.
  2. "淘寶客服"進行後臺處理. 這時淘寶客服接收到了你的詢問訊息, 如果他沒有在忙, 那麼他可以立即回覆你. 如果他現在正在忙, 比如正在吃飯, 比如正在和老婆吵架, 比如淘寶客服需要先看一下你之前的行為記錄, 然後再決定如何回覆你.(比如他看到你正在瀏覽一雙襪子,覺得你在潛在的買家, 他決定回覆你. 比如他看到你三天前下單買了一雙襪子, 但襪子還沒有發貨, 他覺得你有退貨的風險, 從而決定不理你, 假裝不在.) 這個客服思考決斷的過程, 就是函式內部進行處理運算的過程. 當然這個例子很簡單, 有些牽強.
  3. 最終, 淘寶客服回覆了你, "在的, 親". 這裡, 回覆這個動作, 就是函式返回, 而"在的, 親"這句話, 就是函式的返回值.

你從這個角度去看, 函式呼叫, 就是訊息通訊的過程, 你傳送訊息給函式, 函式經過一番運算思考, 把結果再回發給你.

所謂的同步, 非同步, 指的是:

  1. 這個淘寶客服很老實, 對於每個顧客發來的問題, 他都需要經過一番思考, 再進行答覆. 這個函式很老實, 對於每個函式呼叫, 都很老實的根據傳入引數進行計算, 再返回結果. 也是是說, 在淘寶客服思考結束之前, 這個客服不會向你傳送答覆, 你也收不到答覆. 也就是說, 在函式運算結束之前, 函式不會返回, 你也得不到返回值. 那麼, 這個客服是同步的, 這個函式呼叫的過程是同步呼叫, 這個函式是同步的.
  2. 假如這個淘寶客服很不老實, 他裝了一個自動答覆小程式. 對於每個詢問的顧客, 都先自動回覆一句"親, 現在很忙喲, 客服MM可能過一會才能給你答覆". 也就是說, 顧客在發出詢問之後, 立即就能得到一個答覆. 也就是說, 呼叫方在呼叫一個函式之後, 這個函式就立即返回了. 而真正的結果, 可能在過五分鐘之後才會給你. 即是五分鐘之後客服對你說"在的呢, 親". 這樣的函式, 就叫非同步函式.

非同步客服需要解決一個問題: 當真正的運算結果得出之後, 被呼叫的客服如何通知作為呼叫方的你, 取走答案. 在淘寶客戶端上, 是通過手機的震動訊息提醒, 是通過聊天框的紅點.

所以, 關於同步, 和非同步, 這裡做一個稍微正式一點的總結:

  1. 同步的過程: 呼叫方傳參->函式運算->函式返回運算結果.
  2. 非同步的過程: 呼叫方傳參->函式說我知道了, 然後過了五分鐘, 函式說我算出來了, 結果在這裡, 你來取.

這裡我們著眼於訊息的傳遞, 通訊方式, 也就是站在函式的角度去看, 結果是如何傳遞給呼叫方的. 同步介面, 運算結果在"函式呼叫"這個場景下就返回給了呼叫方. 非同步介面: 運算結果在"函式呼叫"這個場景之後的某個不定的時刻, 通過某種通知方式, 傳遞給呼叫方.

整個過程中我們忽略了一件事: 就是, 在函式執行運算的過程中, 呼叫方在幹什麼. 也是是, 在淘寶客服內心思考如何回覆你的時候, 你在幹什麼.

這就引出了阻塞與非阻塞:

  1. 阻塞: 在函式執行運算的過程中, 當前執行緒什麼也做不了. 在等待客服回覆的過程中, 你什麼也不做, 就在那乾等著, 直到他回覆了你.
  2. 非阻塞: 在函式執行去處的過程中, 當前執行緒可以去做其它事情. 在等待客服回覆的過程中, 你上了個廁所, 還順便洗了個澡.

換句話說:

  1. 同步與非同步, 描述的是 被呼叫的函式, 如何將結果返回給呼叫者
  2. 阻塞與非阻塞, 描述的是 呼叫方, 在得到結果之前能不能脫身

這是兩個維度上的邏輯概念, 這兩個維度互相有一定的干涉, 並不是完全正交的兩個維度, 這樣, 既然是兩個維度, 那麼就有四種組合.

  1. 同步, 且阻塞: 呼叫方發起呼叫直至得到結果之前, 都不能幹其它事情. 被調函式接收到引數直到運算結束之前, 都不會返回.
  2. 同步, 非阻塞: 呼叫方發起呼叫直至得到結果之前這段時間, 可以做其它事情. 但被調函式接收到引數直到運算結束之前, 都不會返回. 很顯然這個邏輯概念說得通, 但其實是反常理的. 因為: 如果呼叫方在發起呼叫之後, 得到結果(函式返回)之前, 要去做其它事情, 那麼就有一個隱含的前提條件: 呼叫方必須知道本次呼叫的耗時, 且被調方(函式)嚴格遵守這個時間約定. 一毫秒不多, 一毫秒不少. 這在程式碼的世界裡是很難達到的.
  3. 非同步, 且阻塞: 呼叫方發起呼叫直至得到結果之前, 都不能幹其它事情. 被呼叫函式接收到引數之後立即返回, 但在隨後的某個時間點才把運算結果傳遞給呼叫方. 之後呼叫方繼續活動. 這個邏輯概念依然說得通, 但是很彆扭. 這就相當於, 在你問淘寶客服問題的時候, 淘寶客服的自動回覆機器人已經給你說了"客服很忙喲, 可能過一會才能答覆你", 但你就是啥也不幹, 非得等到客服答覆你之後, 才去上廁所. 這種情景在程式碼世界裡可能發生, 但似乎很智障.
  4. 非同步, 非阻塞: 呼叫方發起呼叫直至得到結果之前這段時間, 可以做其它事情. 被調函式接收到引數後立即返回, 但在之後的某一個時間點才把運算結果傳遞給呼叫方. 這說起來很繞口, 舉個栗子, 還是客服:

    1. 你拿出手機, 向客服傳送訊息, "在嗎?". 然後把手機放桌子上, 轉向上廁所去了.
    2. 客服收到你的訊息, 機器人回覆你"不好意思, 客服現在很忙, 但我們會盡快答覆你的, 親!".
    3. 你上廁所回來了, 看手機沒訊息, 又去吃飯了.
    4. 客服開始處理你的訊息, 終於開始給你真正的回覆"親, 2333號客服為您服務, 你有什麼要了解的嗎?".
    5. 你吃飯的過程中, 手機震動, 你點開淘寶, 發現有了回覆. 整個流程結束.

可以看到

  1. 阻塞方式下, 呼叫方總是能第一時間拿到呼叫結果. 因為在阻塞期間, 呼叫方啥也不幹, 就等著函式返回結果. 非阻塞方式下, 呼叫方一般都是在函式返回了結果之後才去檢視運算結果.
  2. 非同步方式下, 被呼叫方可以推遲處理任務. 客服收到你的訊息後可以先把飯吃完, 函式收到你的呼叫後並不一定立即就開始運算.
  3. 同步且阻塞, 雙方都是槓精, 都是老實人. 理解起來比較自然.
  4. 非同步非阻塞, 呼叫方不在乎什麼時候能得到運算結果. 被呼叫方不在乎呼叫方著急不著急, 雙方都是佛系青年. 理解起來也比較自然.

還有一個點要給大家介紹到, 就是回撥函式. 在上面講過, 非同步呼叫, 需要函式以某種機制, 在運算結果得出之後, 將運算結果傳遞給呼叫方. 但回撥函式又繞了一個彎.

假設沒有回撥函式機制, 非同步流程就是:

  1. 顧客詢問客服, "你們家有沒有紅色36D的胸罩啊? 我想給我老婆買一件, 我老婆的胸是36D的". 然後去上廁所去了
  2. 自動機器人向顧客回覆"很忙喲, 請耐心等待"
  3. 客服開始處理顧客的詢問. 去庫房查貨.
  4. 庫房有貨, 客服要想辦法將這個資訊送到顧客手中. 他通過淘寶客戶端發表了答覆, 淘寶客戶端導致手機震動, 這個震動訊號通知了顧客.
  5. 顧客在廁所正拉屎, 看到手機上的訊息提醒, 思考了一分鐘, 顧客下單購買了這個胸罩.

這個流程裡顧客做了兩件事:

  1. 詢問客服"有沒有36D的紅色胸罩". 這是呼叫函式的行為
  2. 在得到肯定的答覆之後, 下單購買了這個胸罩. 這是得到函式返回的運算結果, 並根據運算結果進一步執行程式流程.(呼叫了另外一個函式: 購買)

而淘寶客服只做了一件事:

  1. 查詢庫房裡是否有貨

而有了回撥機制後, 非同步流程就是這樣的:

  1. 顧客詢問客服, "你們家有沒有紅色36D的胸罩?". 然後顧客把手機交給祕書, 叮囑道:"你盯著這個客服, 如果她說有, 你就下單買了, 地址寫我家, 如果沒有, 你就啥也不做". 然後顧客坐上了出差的飛機
  2. 自動機器人向顧客回覆"很忙喲, 請耐心等待"
  3. 客服開始處理顧客的詢問. 去庫房查貨.
  4. 庫房有貨, 客服要想辦法將這個資訊送到顧客手中. 他通過淘寶客戶端發表了答覆, 淘寶客戶端導致手機震動, 這個震動訊號通知了祕書.
  5. 祕書根據老闆的指示, 下單購買了這個胸罩.

這個流程裡, 顧客做了兩件事:

  1. 詢問客服"有沒有36D的胸罩". 這是呼叫函式行為.
  2. 向祕書叮囑. 這是向訊息監控方註冊回撥函式的行為. 訊息監控方負責接收函式的返回結果. 回撥函式則是: "如果有, 就買給老闆夫人, 如果沒有, 就什麼也不做"

淘寶客服只做了一件事:

  1. 查詢庫房裡是否有貨

而訊息監控方, 也就是祕書, 做了一件事:

  1. 根據客服的答覆選擇不同的行為. 即在函式呼叫結果得出之後, 呼叫回撥函式.

這就是回撥函式的一個生動的例子, 回撥函式機制中有了一個呼叫結果監控方, 就是祕書, 這個角色承擔著非常重要的職責: 即是在函式返回結果之後, 呼叫對應的回撥函式. 回撥機制一般都實現在非同步呼叫框架之中, 對於寫程式碼的人來說是透明的, 它簡化了呼叫方的職責與智力負擔, 一定程度上抽象了程式碼邏輯, 簡化了程式設計模型(注意: 是一定程度上!). 有了回撥機制:

  1. 呼叫方不必再去關心函式返回結果以及返回時機. 不必通過輪詢或其它方式去檢查非同步函式是否返回了結果.
  2. 呼叫方在呼叫時就向呼叫結果監控方註冊合適的回撥, 在呼叫函式那一刻, 將後續業務邏輯寫在回撥函式中, 只負責呼叫就行了. 程式碼越寫越像狀態機.

不過正所謂回撥一時爽, 除錯火葬廠. 寫過JavaScript的同學對這一點一定是深有體會. 當程式不能正確執行的時候, 除錯很蛋疼. 非同步框架本身由於函式返回時機不確定, 除錯就比較蛋疼, 再加上回撥機制, 那真是火葬廠了. 特別是回撥巢狀回撥, 裡面套個七八層的時候, 那真是把圖靈從墳裡挖出來也沒用的絕望場景.

2. 非同步IO與多路複用技術

我們先來看一段經典的同步且阻塞的HTTP客戶端程式:

#include <netinet/in.h>     // for socketaddr_in
#include <sys/socket.h>     // for socket functions
#include <netdb.h>          // for gethostbyname
#include <sys/errno.h>      // for errno

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

int main(int argc, char ** argv)
{
    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[4096];

    /*
     * Look up the IP address for the hostname.
     * Watch out; this isn't threadsafe on most platforms.
     */
    h = gethostbyname(hostname);

    if(!h)
    {
        fprintf(stderr, "E: gethostbyname(%s) failed. ErrMsg: %s\n", hostname, hstrerror(h_errno));
        return -__LINE__;
    }

    if(h->h_addrtype != AF_INET)
    {
        fprintf(stderr, "E: gethostbyname(%s) returned an non AF_INET address.\n", hostname);
        return -__LINE__;
    }

    /*
     * Allocate a new socket
     */
    fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd < 0)
    {
        fprintf(stderr, "E: socket failed: %s\n", strerror(errno));
        return -__LINE__;
    }

    /*
     * 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)) != 0)
    {
        fprintf(stderr, "E: connect to %s failed: %s\n", hostname, strerror(errno));
        close(fd);
        return -__LINE__;
    }

    /*
     * 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)
        {
            fprintf(stderr, "E: send failed: %s\n", strerror(errno));
            close(fd);
            return -__LINE__;
        }

        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)
        {
            fprintf(stderr, "recv failed: %s\n", strerror(errno));
            close(fd);
            return -__LINE__;
        }

        fwrite(buf, 1, result, stdout);
    }

    close(fd);

    return 0;
}

在上面的示例程式碼裡, 大部分有關網路與IO的函式呼叫, 都是阻塞式的. 比如gethostbyname, 在DNS解析成功域名之前是不返回的(或者解析失敗了會返回失敗), connect函式, 在與對端主機成功建立TCP連線之前是不返回的(或者連線失敗), 再比如recvsend函式, 在成功操作, 或明確失敗之前, 也是不返回的.

阻塞式IO確實比較土, 上面的程式編譯執行的時候, 如果你網路狀況不好, 可能會卡一兩秒才會讀到百度的首頁, 這卡就是因為阻塞IO的緣故. 當然, 雖然比較土, 但像這樣的場合, 使用阻塞IO是沒什麼問題的. 但假如你想寫一個程式同時讀取兩個網站的首頁的話, 就比較麻煩了: 因為你不知道哪個網站會先響應你的請求.. 你可以寫一些, 比如像下面這樣的, 很土的程式碼:

char buf[4096];
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[]陣列裡有兩個網站的連線, fd[0]接著百度, fd[1]接著hao123, 假如hao123正常響應了, 可以從fd[1]裡讀出資料了, 但百度的伺服器被李老闆炸了, 響應不了了, 這時, 上面的程式碼就會卡在i==0時迴圈裡的n = recv(fd[0], buf, sizeof(buf), 0)這條語句中, 直到李老闆把伺服器修好. 這就很蛋疼.

當然, 你可以用多執行緒解決這個問題, 多數情況下, 你有一個問題, 你嘗試使用多執行緒解決, 然後你多個了有問題.

上面是一個冷笑話, 多執行緒或多程式是一個解決方案, 通常情況下, 最簡單的套路是使用一個執行緒或程式去建立TCP連線, 然後連線建立成功後, 為每個連線建立獨立的執行緒或程式來進行IO讀寫. 這樣即使一個網站抽風了, 也只阻塞屬於它自己的那個讀寫執行緒或程式, 不會影響到其它網站的響應.

下面是另外一個例子程式, 這是一個服務端程式, 監聽40173埠上的TCP連線請求, 然後把客戶端傳送的資料按ROT13法再回寫給客戶端, 一次處理一行資料. 這個程式使用Unix上的fork()函式為每個客戶端的連線建立一個獨立的處理程式.

#include <netinet/in.h>     // for sockaddr_in
#include <sys/socket.h>     // for socket functions
#include <sys/errno.h>      // for errno

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

#define MAX_LINE 16384

char rot13_char(char c)
{
    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];  // extra byte for '\0'
    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;
        }

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

    int one = 1;
    setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));

    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 argc, char ** argv)
{
    run();
    return 0;
}

你可以使用下面的命令列, 通過netcat工具向本機的40713傳送資料, 來試驗一下上面的服務端程式碼:

printf "abcdefghijklmnopqrstuvwxyz\n" | nc -4 -w1 localhost 40713

多程式或多執行緒確實是一個還算是比較優雅的, 應對併發連線的解決方案. 這種解決方案的缺陷是: 程式或執行緒的建立是有開銷的, 在某些平臺上, 這個開銷還是比較大的. 這裡優化的方案是使用執行緒, 並使用執行緒池策略. 如果你的機器需要處理上千上萬的併發連線, 這就意味著你需要建立成千上萬個執行緒, 想象一下, 伺服器一般也就十幾個核心, 64個不得了了, 如果有五千併發連線, 5000個執行緒排除輪64個核心的大米, 執行緒排程肯定是個大開銷.

這個時候我們就需要了解一下非阻塞了, 通過下面的Unix呼叫, 可以將一個檔案描述符設定為"非阻塞"的. 明確一下: "非阻塞"描述的是IO函式的行為, 將一個檔案描述符設定為"非阻塞"的, 其實是指, 在這個檔案描述符上執行IO操作, 函式的行為會變成非阻塞的.

fcntl(fd, F_SETFL, O_NONBLOCK);

當這個檔案描述符是socket的檔案描述符時, 我們一般也會直接稱, "把一個socket設定為非阻塞". 將一個socket設定為非阻塞之後, 在對應的檔案描述符上, 無論是執行網路程式設計相關的函式, 還是執行IO相關的函式, 函式行為都會變成非阻塞的, 即函式在呼叫之後就立即返回: 要麼立即返回成功, 要把立即告訴呼叫者: "暫時不可用, 請稍後再試"

有了非阻塞這種手段, 我們就可以改寫我們的訪問網頁程式了: 我們這時可以正確的處理同時下載兩個網站的資料的需求了. 程式碼片斷如下:

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(int i = 0; i < n_sockets; ++i)
    {
        n = recv(fd[i], buf, sizeof(buf), 0);
        if(n == 0)
        {
            handle_close(fd[i]);        // peer was closed
        }
        else if(n < 0)
        {
            if(errno == EAGAIN)
            {
                // do nothing, the kernel didn't have any data for us to read
                // retry
            }
            else
            {
                handle_error(fd[i], errno);
            }
        }
        else
        {
            handle_input(fd[i], buf, n);    // read success
        }
    }
}

這樣寫確實解決了問題, 但是, 在對端網站還沒有成功響應的那幾百毫秒裡, 這段程式碼將會瘋狂的死迴圈, 會把你的一個核心佔滿. 這是一個很蛋疼的解決方案, 原因是: 對於真正的資料何時到達, 我們無法確定, 只能開個死迴圈輪詢.

舊式的改進方案是使用一個叫select()的系統呼叫函式. select()函式內部維護了三個集合:

  1. 有資料可供讀取的檔案描述符
  2. 可以進行寫入操作的檔案描述符
  3. 出現異常的檔案描述符

select()函式在這三個集合有至少一個集合不為空的時候返回. 如果三個集合都為空, 那麼select()函式將阻塞.

下面是使用select()改進後的程式碼片斷:

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):
    }

    select(maxfd+1, &readset, NULL, NULL, NULL);

    for(int i = 0; i < n_sockets; ++i)
    {
        if(FDD_ISSET(fd[i], &readset))
        {
            n = recv(fd[i], &readset);

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

使用select()改進了程式, 但select()蛋疼的地方在於: 它只告訴你, 三集合中有資料了, 但是: 哪個fd可讀, 哪個fd可寫, 哪個fd有異常, 這些具體的資訊, 它還是沒告訴你. 如果你的fd數量不多, OK, 上面的程式碼沒什麼問題, 但如果你持有著上千個併發連線, 那每次select()返回時, 你都需要把所有fd都輪一遍.

下面是使用select()呼叫對rot13服務端示例程式碼的重構

#include <netinet/in.h>     // for sockaddr_in
#include <sys/socket.h>     // for socket functions
#include <sys/errno.h>      // for errno
#include <fcntl.h>          // for fcntl
#include <sys/select.h>     // for select

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

#define MAX_LINE 16384

char rot13_char(char c)
{
    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(int 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);

    int one = 1;
    setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));

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

        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 argc, char ** argv)
{
    setvbuf(stdout, NULL, _IONBF, 0);

    run();

    return 0;
}

但這樣還不夠好: FD_SETSIZE是一個很大的值, 至少不小於1024. 當要監聽的fd的值比較大的時候, 就很噁心, 遍歷會遍歷很多次. 對於非阻塞IO介面來講, select是一個很粗糙的解決方案, 這個系統呼叫提供的功能比較薄弱, 只能說是夠用, 但介面確實太屎了, 不好用, 效能也堪優.

不同的作業系統平臺上提供了很多select的替代品, 它們都用於配套非阻塞IO介面來使單執行緒程式也有一定的併發能力. 這些替代品有poll(), epoll(), kqueue(), evports/dev/poll. 並且這些替代品的效能都比select()要好的多. 但比較蛋疼的是, 上面提到的所有介面, 幾乎都不是跨平臺的. epoll()是Linux獨有的, kqueue()是BSD系列(包括OS X)獨有的. evports/dev/poll是Solaris獨有的. 是的, select()屬於POSIX標準的一部分, 但就是效能捉急. 也就是說, 如果你寫的程式想跨平臺, 高效能, 你就得自己寫一層抽象, 把不同平臺對於IO多路複用的底層統一起來: 這也就是Libevent乾的事情.

libevent的低階API為IO多路複用提供了統一的介面, 其底層實現在不同的作業系統平臺上都是最高效的實現.

下面, 我們將使用libevent對上面的程式進行重構. 注意: fd_sets不見了, 取而代之的是一個叫event_base的結構體.

/* 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;
}

總之:

  1. 注意連結的時候加上 -levent
  2. 程式碼量沒有減少, 邏輯也沒有簡化. libevent只是給你提供了一個通用的多路IO介面. 或者叫事件監聽介面.
  3. evutil_socket_t型別的使用, 與evutil_make_socket_nonblocking()函式的使用, 均是為也跨平臺相容性. 使用這些型別名與工具函式, 使得在windows平臺上程式碼也能跑起來.

現在, 你看, 非同步IO+事件處理(或者叫多路IO複用), 是單執行緒單程式程式取得併發能力的最佳途徑, 而libevent則是把多平臺的IO多路複用庫給你抽象統一成一層介面了. 這樣代寫的程式碼不需要改動, 就可以執行在多個平臺上.

這樣就有了三個問題:

  1. 如果我的程式碼需要跨平臺, 或者只需要跨部分平臺(比如我只考慮Linux和BSD使用者, 完全不考慮Windows平臺), 我為什麼不自己把多路IO庫做個簡單的封裝, 為什麼要使用libevent呢? 典型的就是Redis, 用了很薄的一層封裝, 下面統一了epoll, kqueue, evport, select等. 為什麼, 我需要使用libevent呢?
  2. 如果將libevent作為一個黑盒去用, 不可避免的問題就是: 它的效能怎麼樣? 它封裝了多個多路IO庫, 在封裝上是否有效能損失?
  3. 現在是個輪子都說自己解決了跨平臺問題, 那麼libevent在windows上表現怎麼樣? 它能相容IOCP式多路IO庫嗎? 畢竟IOCP的設計思路和epoll``select``evport``kqueue等都不一樣.

答案在這裡:

  1. 你沒有任何理由非得使用libevent, redis就是一個很好的例子. libevent有不少功能, 但如果你只是跨小部分平臺, 並且只關注在多路IO複用上, 那麼真的沒什麼必要非得用libevent. 你完全可以像redis那樣, 用幾百行簡單的把多路IO庫自己封裝一下.
  2. 基本上這麼講吧: 你使用系統原生非同步IO多路複用介面的效能是多少, 使用libevent就是多少. 說實施libevent裡沒太多的抽象, 介面也沒有多麼好用, 封閉很薄, 和你使用原生介面基本一樣.
  3. libevent從版本2開始就能搞定windows了. 上面我們使用的是libevent很底層的介面, 其設計思路是遵循*nix上的事件處理模型的, 典型的就是selectepoll: 當網路可讀寫時, 通知應用程式去讀去寫. 而windows上IOCP的設計思路是: 當網路可讀可寫時不通知應用程式, 而是先完成讀與寫, 再通知應用程式, 應用程式直接拿到的就是資料. 當在libevent 2提供的bufferevents系列介面中, 它將*nix平臺下的設計, 改巴改巴改成了IOCP式的. 使用這個系列的介面不可避免的, 對*nix平臺有效能損失(這和asio封裝網路庫是一樣的做法), 但實話講, IOCP式的設計確實對程式設計師更友好, 程式碼可讀性高了不少.

總的來說, 你應該在如下的場合使用libevent

  1. 程式碼需要跨多個平臺, 甚至是windows
  2. 想在*nix平臺上使用IOCP式的事件介面程式設計
  3. 你不想自己封裝多個平臺上的多路IO介面, 並且自認為, 就算自己做, 做的也肯定沒有libevent好. libevent是一個久經考驗的很基礎的庫. 身經百戰.

下面是使用bufferevents系列介面, 以IOCP式風格對之前例子程式碼的重構, 體驗一下更人性的事件處理方式:

/* 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 ** argv)
{
    setvbuf(stdout, NULL, _IONBF, 0);

    run();
    return 0;
}

說實話也沒人性到哪裡去, 底層庫就是這樣, libevent還是太基礎了. 算不上十分友好的輪子.

3. Libevent 簡介

現在我們要正式介紹Libevent

3.1 libevent的賣點

  1. 程式碼跨平臺.
  2. 效能高. libevent在非阻塞IO+多路複用的底層實現上, 選取的是特定平臺上最快的介面. 比如Linux上用epoll, BSD上用kqueue
  3. 高併發可擴充套件. libevent就是為了那種, 需要維持成千上萬的活動socket連線的應用程式使用的.
  4. 介面友好. 雖然並沒有友好多少, 但至少比原生的epoll要好一點.

3.2 libevent下的各個子模組

  1. evutil 通用型別定義, 跨平臺相關的通用定義, 以及一些通用小函式
  2. event and event_base 核心模組. 事件庫. *nix風格的事件模型: 在socket可讀可寫時通知應用程式.
  3. bufferevent 對核心事件庫的再一層封裝, IOCP式的事件模型: 在資料已讀已寫後通知應用程式
  4. evbuffer 這是bufferevent模組內部使用的緩衝區實現.
  5. evhttp 簡單的HTTP C/S實現
  6. evdns 簡單的 DNS C/S實現
  7. evrpc 簡單的 RPC實現

總的來說, 作為使用者, 需要關心的是:

  1. evutil是需要關心的
  2. 對於主在*nix平臺上寫後臺服務端程式的人: 只需要關心 event and event_base 核心庫的用法即可.
  3. 對於跨平臺, 特別是包含win平臺的開發人員: 需要關注 buffereventevbuffer, 對於核心庫event and event_base, 可以不關心
  4. evhttp, evdns, evrpc, 如無需要, 可以不用關心

3.3 libevent下的二進位制庫

以下是在連結你的程式碼的時候, 你需要了解的二進位制庫.

  1. libevent_core 包含event and event_base, evutil, evbuffer, bufferevent中的所有函式
  2. libevent_extra 包含協議相關的函式. 包括 HTTP/DNS/RPC 等. 如果你用不到 evhttp/evdns/evrpc裡的函式, 那麼這個庫不用連結.
  3. libevent 滿清遺老, 包含了上面兩個庫裡的所有函式. 官方不建議在使用libevent 2.0以上的版本時連結這個庫. 這是個懶人庫.
  4. libevent_pthreads 如果你編寫多執行緒應用程式. 那麼這個庫裡包含了基於POSIX執行緒庫的相關函式實現. 如果你沒有用到libevent中有關的多執行緒函式, 那麼這個庫不用連結. 以前這些函式是劃分在libevent_core中的, 後來被單獨割出來了.注意: 這個庫不是全平臺的.
  5. libevent_openssl 這個庫裡的與OpenSSL相關的函式實現. 如果你沒有用到libevent中有關OpenSSL的函式, 那麼這個庫不用連結. 以前這些函式也算在libevent_core中, 最後也割出來了. 注意: 這個庫也不是全平臺的

3.4 libevent中的標頭檔案

libevent中的標頭檔案分為三類, 所有標頭檔案都位於event2目錄下. 也就是說在程式碼中你應當這樣寫:

#include <event2/xxxx>
#include <event2/xxxx>
#include <event2/xxxx>
#include <event2/xxxx>

具體有哪些標頭檔案在後續章節會詳細介紹, 目前只介紹這個分類:

  1. API 標頭檔案. 這些標頭檔案定義了libevent對外的介面. 這些標頭檔案沒有特定字首.
  2. 相容性 標頭檔案. 這些標頭檔案是為了向前相容老版本libevent存在的, 它們裡面定義了老版本的一些廢棄介面. 除非你是在做老程式碼遷移工作, 否則不建議使用這些標頭檔案.
  3. 型別定義 標頭檔案. 定義了libevent庫中相關的型別. 這些標頭檔案有共同字尾_struct.h

3.5 如何將老版本的程式碼遷移到libevent 2上

官方建議大家使用版本2, 但有時候這個世界就是不那麼讓人舒服, 如果你需要和版本1的歷史程式碼打交道, 你可以參照下面的對照表: 老標頭檔案與新標頭檔案的對照表

舊標頭檔案 新標頭檔案
event.h event2/event*.h, event2/buffer*.h, event2/bufferevent*.h, event2/tag*.h
evdns.h event2/dns*.h
evhttp.h event2/http*.h
evrpc.h event2/rpc*.h
evutil.h event2/util*.h

在當前的2.0版本中, 老的舊標頭檔案其實是不需要替換的, 這些舊標頭檔案依然存在. 但還是建議將他們替換成新標頭檔案, 因為說不定50年後libevent升級到3.0版本, 這些舊標頭檔案就被扔了.

另外還有一些點需要你注意:

  1. 在1.4版本之前, 只有一個二進位制庫檔案. libevent, 裡面是libevent的所有實現. 如今這些實現被分割到了 libevent_corelibevent_extra兩個庫中.
  2. 在2.0之前, libevent不支援鎖. 也就是說, 2.0之前如果要寫出執行緒安全的程式碼, 你只能避免線上程間共享資料例項. 沒有其它辦法.

3.6 滿清遺老

官方對待老版本是這樣建議的:

  1. 1.4.7之前的版本被正式廢棄了
  2. 1.3之前的版本有一堆bug, 用的時候看臉吧.
  3. 推薦使用2.0後的版本

我對老版本的態度是這樣的: 能幹活就好. 沒有特殊原因, 我是不會做程式碼遷移的. 並且考慮到應用場景, 有時候用老版本也挺好的.

1.4.x版本的libevent被大量專案使用, 其實挺穩定的, 官方不建議使用, 只是官方不再在1.4版本上再加特性修bug了. 1.4版本最後的一個小版本號就停止在7上不動了. 而對於1.3版本, 確實不應該再碰了.

4. 使用libevent的正確姿勢

libevent有幾項全域性設定, 如果你需要改動這幾項設定, 那麼確保在程式碼初始化的時候設定好值, 一旦你的程式碼流程開始了, 呼叫了第一個libevent中的任何函式, 後續強烈建議不要再更改設定值, 否則會引起不可預知的後果.

4.1 libevent中的日誌

libevent預設情況下將把錯誤與警告日誌寫進stderr, 並且如果你需要一些libevent內部的除錯日誌的話, 也可以通過更改設定來讓其輸出除錯日誌, 以在程式崩潰時提供更多的參考資訊. 這些行為都可以通過自行實現日誌函式進行更改. 下面是libevent相關的日誌介面.

// 以下是日誌級別
#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

// 這個是一個函式指標型別別名, 指向日誌輸出函式
// 日誌輸出函式應當是一個雙參, 無返回值的函式, 第一個引數severity為日誌級別, 第二個引數為日誌字串
typedef void (*event_log_cb)(int severity, const char *msg);

// 這是使用者自定義設定日誌處理函式的介面. 如果呼叫該函式時入參設定為NULL
// 則將採用預設行為
void event_set_log_callback(event_log_cb cb);

比如下面我需要改寫libevent記錄日誌的方式:

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

// 丟棄日誌
static void discard_cb(int severity, const char * msg)
{
    // 這個日誌函式內部什麼也不做
}

// 將日誌記錄至檔案
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 = "[DBG]";    break;
        case EVENT_LOG_MSG:     s = "[MSG]";    break;
        case EVENT_LOG_WARN:    s = "[WRN]";    break;
        case EVENT_LOG_ERR:     s = "[ERR]";    break;
        default:                s = "[???]";    break;
    }

    fprintf(logfile, "[%s][%s][%s] %s\n", __FILE__, __func__, s, msg);
}

void suppress_logging(void)
{
    event_set_log_callback(discard_cb);
}

void set_logfile(FILE * f)
{
    logfile = f;
    event_set_log_callback(write_to_file_cb);
}

注意: 在自定義的日誌輸出函式中, 不要呼叫其它libevent中的函式! 比如, 如果你要把日誌遠端輸出至網路socket上去, 你還使用了bufferevent來輸出你的日誌, 在目前的libevent版本中, 這會導致一些奇怪的bug. libevent官方也承認這是一個設計上沒有考慮到的點, 這可能在後續版本中被移除, 但截止目前的2.1.8 stable版本, 這個問題都還沒有解決. 不要作死.

預設情況下的日誌級別是EVENT_LOG_MSG, 也就是說EVENT_LOG_DEBUG級別的日誌不會呼叫至日誌輸出函式. 要讓libevent輸出除錯級別的日誌, 請使用下面的介面:

#define EVENT_DBG_NONE 0
#define EVENT_DBG_ALL 0xffffffffu

// 如果傳入 EVENT_DBG_NONE, 將保持預設狀態: 不輸出除錯日誌
// 如果傳入 EVENT_DEG_ALL, 將開啟除錯日誌的輸出
void event_enable_debug_logging(ev_uint32_t which);

除錯日誌很詳盡, 通常情況下對於libevent的使用者而言是沒有輸出的必要的. 因為要用到除錯級別日誌的場合, 是你百般無奈, 開始懷疑libevent本身有bug的時候. 雖然從巨集的命名上, 彷彿還存在著 EVENT_DGB_SOMETHING 這樣, 可以單獨控制某個模組的除錯日誌輸出的引數, 但實際上並沒有: 除錯日誌要麼全開, 要麼全關. 沒有中間地帶. 官方宣稱可能在後續的版本中細化除錯日誌的控制.

而如果你要控制其它日誌級別的輸出與否, 請自行實現日誌輸出函式. 比如忽略掉EVENT_LOG_MSG級別的日誌之類的. 上面的介面只是控制"如果產生了除錯日誌, libevent呼叫或不呼叫日誌輸出函式"而已.

上面有關日誌的介面均定義在<event2/event.h>中.

  1. 日誌輸出函式相關介面早在版本1.0時就有了.
  2. event_enable_debug_logging()介面在2.1.1版本之後才有
  3. 日誌級別巨集名, 在2.0.19之前, 是以下劃線開頭的, 即_DEBUG_LOG_XXX, 但現在已經廢棄掉了這種定義, 在新版本中請使用不帶下劃線開頭的版本.

4.2 正確處理致命錯誤

當libevent檢測到有致命的內部錯誤發生時(比如踩記憶體了之類的不可恢復的錯誤), 預設行為是呼叫exit()abort(). 出現這種情況99.99的原因是使用者自身的程式碼出現了嚴重的bug, 另外0.01%的原因是libevent自身有bug.

如果你希望在程式退出之前做點額外的事情, 寫幾行帶fxxk的日誌之類的, libevent提供了相關的入口, 這可以改寫libevent對待致命錯誤的預設行為.

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

注意, 不要試圖強行恢復這種致命錯誤, 也就是說, 雖然libevent給你提供了這麼個介面, 但不要在註冊的函式中試圖讓程式繼續執行. 因為這個時候libevent內部已經有坑了, 如果繼續強行恢復, 結果是不可預知的. 換個說法: 這個函式應該提供的是臨終遺言, 而不應該試圖救死扶傷.

這個函式也定義在 <event2/event.h>中, 在2.0.3版本之後可用.

4.3 記憶體管理

預設情況下, libevent使用的是標準C庫中的記憶體管理函式, 即malloc(), realloc(), free()等. libevent允許你使用其它的記憶體管理庫, 比如tcmallocjemalloc. 相關介面如下:

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

介面的第一個引數是記憶體分配函式指標, 第二個引數是記憶體重分配函式指標, 第三個引數是記憶體釋放函式指標.

下面是一個使用的例子:

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

union alignment
{
    size_t sz;
    void * ptr;
    double dbl;
};

#define ALIGNMENT sizeof(union alignment)

#define OUTPTR(ptr)     (((char *)ptr) + ALIGNMENT)
#define INPTR(ptr)      (((char *)ptr) - ALIGNMENT)

static size_t total_allocated = 0;

static void * my_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 * my_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 my_free(void * ptr)
{
    ptr = INPTR(ptr);
    total_allocated -= *(size_t *)ptr;
    free(ptr);
}

void start_counting_bytes(void)
{
    event_set_mem_functions(
        my_malloc, my_realloc, my_free
    );
}

上面這個例子中, 提供了一種記錄全域性記憶體使用量的簡單方案, 非執行緒安全.

對於自定義記憶體管理介面, 需要注意的有:

  1. 再次重申, 這是一個全域性設定, 一旦設定, 後續所有的libevent函式內部的記憶體操作都會受影響. 並且不要在程式碼流程中途更改設定.
  2. 自定義的記憶體管理函式, 在分配記憶體時, 返回的指標後必須確保在至少sz個位元組可用.
  3. 自定義的記憶體重分配函式, 必須正確處理realloc(NULL, sz)這種情況: 即, 使之行為等同於 malloc(sz). 也必須正確處理realloc(ptr, 0)這種情況: 即, 使之行為與free(ptr)相同且返回NULL.
  4. 自定義的記憶體釋放函式, 必須正確處理 free(NULL): 什麼也不做.
  5. 自定義的內在分配函式, 必須正確處理 malloc(0): 返回NULL.
  6. 如果你在多執行緒環境中使用libevent, 請務必確保記憶體分配函式是執行緒安全的.
  7. 如果你要釋放一個由libevent建立來的記憶體區域, 請確認你使用的free()版本與libevent內部使用的記憶體管理函式是一致的. 也就是說: 如果要操作libevent相關的記憶體區域, 請確保相關的記憶體處理函式和libevent內部使用的內在管理函式是一致的. 或者簡單一點: 如果你決定使用某個記憶體管理庫, 那麼在整個專案範圍內都使用它, 這樣最簡單, 不容易出亂子. 否則應該盡力避免在外部操作libevent建立的記憶體區域.

event_set_mem_functions()介面也定義在<event2/event.h>中, 在2.0.2版本後可用.

需要注意的是: libevent在編譯安裝的時候, 可以關閉event_set_mem_functions()這個特性. 如果關閉了這個特性, 而在專案中又使用了這個特性, 那麼在專案編譯時, 編譯將報錯. 如果要檢測當前引入的libevent庫是否啟用了這個功能, 可以通過檢測巨集EVENT_SET_MEM_FUNCTIONS_IMPLEMENTED巨集是否被定義來判斷.

4.4 執行緒與鎖

多執行緒程式設計裡的資料訪問是個大難題. 目前的版本里, libevent支援了多執行緒程式設計, 但這個支援法呢, 怎麼講呢, 使用者還是需要知道不少細節才能正確的寫出多執行緒應用. libevent中的資料結構分為三類:

  1. 有一些資料結構就是非執行緒安全的. 這是歷史遺留問題, libevent在大版本號更新為2後才支援多執行緒, 這些資料結構是從版本1一路繼承下來的, 不要在多執行緒中共享這些例項. 沒辦法.
  2. 有一些資料結構的例項可以用鎖保護起來, 以在多執行緒環境中共享. 如果你需要在多個執行緒中訪問某個例項, 那麼你需要給libevent說明這個情況, 然後libevent會為這個例項加上適當的鎖保護, 以確保你在多執行緒訪問它時是安全的. 加鎖不需要你去加, 你需要做的只是告訴libevent一聲, 如何具體操作後面再講.
  3. 有些資料結構, 天生就是帶鎖的. 如果你帶 libevent_pthreads 庫連結你的程式, 那麼這些結構的例項在多執行緒環境中一定的安全的. 你想讓它不安全都沒辦法.

雖然libevent為你寫了一些加鎖解鎖的無聊程式碼, 你不必要手動為每個物件加鎖了, 但libevent還是需要你指定加鎖的函式. 就像你可以為libevent指定其它的記憶體管理庫一樣. 注意這也是一個全域性設定, 請遵循我們一再強調的使用規則: 程式初始化時就定好, 後續不許再更改.

如果你使用的是POSIX執行緒庫, 或者標準的windows原生執行緒庫, 那麼簡單了一些. 設定加解鎖函式只需要一行函式呼叫, 介面如下:

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

#ifdef _EVENT_HAVE_PTHREADS
int evthread_use_pthreads();
#define EVTHREAD_USE_PTHREADS_IMPLEMENTED
#endif

這兩個函式在成功時都返回0, 失敗時返回-1.

這只是加解鎖. 但如果你想要自定義的是整個執行緒庫, 那麼你就需要手動指定如下的函式與結構定義

  1. 鎖的定義
  2. 加鎖函式
  3. 解鎖函式
  4. 鎖分配函式
  5. 鎖釋放函式
  6. 條件變數定義
  7. 條件變數建立函式
  8. 條件變數釋放函式
  9. 條件變數等待函式
  10. 通知/廣播條件變數的函式
  11. 執行緒定義
  12. 執行緒ID檢測函式

這裡需要注意的是: libevent並不會為你寫哪怕一行的多執行緒程式碼, libevent內部也不會去建立執行緒. 你要使用多執行緒, OK, 你用哪種執行緒庫都行, 沒問題. 但你需要將配套的鎖/條件變數/執行緒檢測函式以及相關定義告訴libevent, 這樣libevent才會知道如何在多執行緒環境中保護自己的例項, 以供你在多執行緒環境中安全的訪問.

  1. 如果你使用的是POSIX執行緒或者windows原生執行緒庫, 就方便了一點, 調一行函式的事情.
  2. 如果你在使用POSIX純種或windows原生執行緒庫時, 你不想使用POSIX配套的鎖, 那OK, 你在呼叫完evthread_use_xxx_threads()之後, 把你自己的鎖函式或者條件變數函式提供給libevent就好了. 注意這種情況下, 在你的程式的其它地方也需要使用你指定的鎖或條件變數.
  3. 而如果你使用的是其它執行緒庫, 也OK, 只不過麻煩一點, 要提供鎖的相關資訊, 要提供條件變數的相關資訊, 也要提供執行緒ID檢測函式

下面是相關的介面

// 鎖模式是 lock 與 unlock 函式的引數, 它指定了加鎖解鎖時的一些額外資訊
// 如果呼叫 lock 或 unlock 時的鎖都不滿足下面的三種模式, 引數傳0即可
#define EVTHREAD_WRITE  0x04        // 鎖模式: 僅對讀寫鎖使用: 獲取或釋放寫鎖
#define EVTHREAD_READ   0x08        // 鎖模式: 僅對讀寫鎖使用: 獲取或釋放讀鎖
#define EVTHREAD_TRY    0x10        // 鎖模式: 僅在加鎖時使用: 僅在可以立即加鎖的時候才去加鎖. 
                                    //         若當前不可加鎖, 則lock函式立即返回失敗, 而不是阻塞

// 鎖型別是 alloc 與 free 函式的引數, 它指定了建立與銷燬的鎖的型別
// 鎖型別可以是 EVTHREAD_LOCKTYPE_XXX 之一或者為0
// 所有支援的鎖型別均需要被登記在 supported_locktypes 中, 如果支援多種鎖, 則多個巨集之間用 | 連結構成該欄位的值
                                            // 當鎖型別為0時, 指的是普通的, 非遞迴鎖
#define EVTHREAD_LOCKTYPE_RECURSIVE 1       // 鎖型別: 遞迴鎖, 你必須提供一種遞迴鎖給libevent使用
#define EVTHREAD_LOCKTYPE_READWRITE 2       // 鎖型別: 讀寫鎖, 在2.0.4版本之前, libevent內部沒有使用到讀寫鎖

#define EVTHREAD_LOCK_API_VERSION 1

// 將你要用的有關鎖的所有資訊放在這個結構裡
struct evthread_lock_callbacks {
       int lock_api_version;                // 必須與巨集 EVTHREAD_LOCK_API_VERSION的值一致
       unsigned supported_locktypes;        // 必須是巨集 EVTHREAD_LOCKTYPE_XXX 的或組合, 或為0
       void *(*alloc)(unsigned locktype);               // 鎖分配, 需要指定鎖型別
       void (*free)(void *lock, unsigned locktype);     // 鎖銷燬, 需要指定鎖型別
       int (*lock)(unsigned mode, void *lock);          // 加鎖, 需要指定鎖模式
       int (*unlock)(unsigned mode, void *lock);        // 解鎖, 需要指定鎖模式
};

// 呼叫該函式以設定相關鎖函式
int evthread_set_lock_callbacks(const struct evthread_lock_callbacks *);

// 調該函式以設定執行緒ID檢測函式
void evthread_set_id_callback(unsigned long (*id_fn)(void));

// 將你要用的有關條件變數的所有資訊都放在這個結構裡
struct evthread_condition_callbacks {
        int condition_api_version;
        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 *);

要探究具體如何使用這些函式, 請看libevent原始碼中的evthread_pthread.cevthread_win32.c檔案.

對於大多數普通使用者來說, 只需要呼叫一下evthread_use_windows_threads()evthread_use_pthreads()就行了.

上面這些函式均定義在 <event2/thread.h>中. 在2.0.4版本後這些函式才可用. 2.0.1至2.0.3版本中使用了一些舊介面, event_use_pthreads()等. 有關條件變數的相關介面直至2.0.7版本才可用, 引入條件變數是為了解決之前libevent出現的死鎖問題.

libevent本身可以被編譯成不支援鎖的二進位制庫, 用這種二進位制庫連結你的多執行緒程式碼, bomshakalaka, 跑不起來. 這算是個無用知識點.

另外額外注意: 多執行緒程式, 並且還使用了POSIX執行緒庫和配套的鎖, 那麼你需要連結libevent_pthreads. windows平臺則不用.

4.5 小知識點: 有關鎖的除錯

libevent有一個額外的特性叫"鎖除錯", 開啟這種特性後, libevent將把它內部有關鎖的所有呼叫都再包裝一層, 以檢測/獲取在鎖呼叫過程中出現的錯誤, 比如:

  1. 解了一個沒有持有的鎖
  2. 對一個非遞迴鎖進行了二次加鎖

如果出現了上述錯誤, 則libevent會導致程式退出, 並附送一個斷言錯誤

要開啟這個特性, 呼叫下面的介面:

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

注意, 這也是一個全域性設定項, 請遵循: 一次設定, 初始化時就設定, 永不改動的規則.

這個特性在2.0.4版本中開始支援, 當時介面函式名拼寫錯誤了, 少寫了一個g: evthread_enable_lock_debuging(). 後來在2.1.2版本中把這個錯誤的拼寫修正過來了. 但還是相容了之前的錯誤拼寫.

這個特性吧, 很明顯是libevent內部開發時使用的. 現在開放出來估計是考慮到, 如果你的程式碼中出現了一個bug是由libevent內部加解鎖失誤導致的, 那麼用個特性可以定位到libevent內部. 否則你很難把鍋甩給libevent. 當然這種情況很少見.

4.6 小知識點: 排除不正確的使用姿勢

libevent是一個比較薄的庫, 薄的好處是效能很好, 壞處是沒有在介面上對使用者做過多的約束. 這就導致一些二把刀使用者經常會錯誤的使用libevent. 常見的智障行為有:

  1. 向相關介面傳遞了一個未初始化的事件結構例項
  2. 試圖第二次初始化一個正在被使用的事件結構例項

這種錯誤其實挺難發現的, 為了解決這個痛點, libevent額外開發了一個新特性: 在發生上述情況的時候, libevent給你報錯.

但這是一個會額外消耗資源的特性, libevent內部其實是追蹤了每個事件結構的初始化與銷燬, 所以僅在開發測試的時候開啟它, 發現問題, 解決問題. 在實際部署的時候, 不要使用這個特性. 開啟這個特性的介面如下:

void event_enable_debug_mode(void);

再不厭其煩的講一遍: 全域性設定, 初始化時設定, 一次設定, 永不更改.

這個特性開啟後, 也有一個比較蛋疼的事情: 就是如果你的程式碼裡大量使用了event_assign()來建立事件結構, 可能你的程式在這個特性下會OOM掛掉..原因是: libevent可以通過對event_new()event_free()的追蹤來檢測事件結構例項是否未被初始化, 或者被多次初始化, 或者被非法使用. 但是對於event_assign()拷貝來的事件結構, 這追蹤就無能為力了, 並且蛋疼的是event_assign()還是淺拷貝. 這樣, 如果你的程式碼裡大量的使用了event_assign(), 這就會導致內建的的追蹤功能一旦追上車就下不來了, 完事車太多就OOM掛掉了.

為了避免在這個特性下由於追蹤event_assign()建立的事件例項(或許這裡叫例項已經不合適了, 應該叫控制程式碼)而導致程式OOM, 可以呼叫下面的函式以解除對這種事件例項的追蹤, 以避免OOM

void event_debug_unassign(struct event * ev);

這樣, 除錯模式下, 相關的追蹤檢測就會放棄追蹤由event_assign建立的事件. 所以你看, 這個特性也不是萬能的, 有缺陷, 湊合用吧. 在不開啟除錯模式下, 呼叫event_debug_unassign()函式沒有任何影響

下面是一個例子:

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

#include <stdlib.h>

void cb(evutil_socket_t fd, short what, void *ptr)
{
    struct event *ev = ptr;

    if (ev)     // 通過判斷入參是否為NULL, 來確認入參攜帶的事件例項是event_new來的還是event_assign來的
        event_debug_unassign(ev); // 如果是event_assign來的, 那麼就放棄對它的追蹤
}

/*
 * 下面是一個簡單的迴圈, 等待fd1與fd2同時可讀
 */
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_new來建立堆上的例項, 並把事件回撥的入參設定為NULL
    event_assign(&event_on_stack, base, fd2, EV_READ, cb, &event_on_stack); // 通過event_assign來初始化棧上的例項, 並把事件回撥的入參設定為事件例項自身的指標

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

這個例子也寫的比較蛋疼, 湊合看吧.

另外, 除錯模式下的詳情除錯資訊, 只能通過在編譯時額外定義巨集USE_DEBUG來附加. 即在編譯時加上-DUSE_DEBUG來開啟. 加上這個編譯時的巨集定義後, libevent就會輸出一大坨有關其內部流程的詳情日誌, 包括但不限於

  1. 事件的增加
  2. 事件的刪除
  3. 與具體平臺相關的事件通知資訊

這些詳情不能通過呼叫API的方式開啟或關閉. 而開啟除錯模式的API, 在2.0.4版本後才可用.

4.7 檢測當前專案中引用的libevent的版本

介面很簡單, 如下:

#define LIBEVENT_VERSION_NUMBER 0x02000300
#define LIBEVENT_VERSION "2.0.3-alpha"
const char *event_get_version(void);        // 獲取字串形式的版本資訊
ev_uint32_t event_get_version_number(void); // 獲取值形式的版本資訊

值形式的版本資訊由一個uint32_t型別儲存, 從高位到低位, 每8位代表一個版本號. 比如 0x02000300代表的版本號就是02.00.03.00. 三級版本號後可能還有一個小版本號, 比如就存在過一個2.0.1.18的版本

下面是一個在編譯期檢查libevent版本的寫法, 若版本小於2.0.1, 則編譯不通過. 需要注意的是, 編譯期檢查的是巨集裡的值, 如果你的專案構建比較混亂, 很可能出現標頭檔案的版本, 和最終連結的二進位制庫的版本不一致的情況. 所以編譯期檢查也不一定靠譜

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

下面是一個在執行時檢查libdvent版本的寫法. 檢查執行期的版本是通過函式呼叫檢查的, 這就保證了返回的版本號一定是連結進的庫的版本號. 這個比較靠譜. 另外需要注意的是, 數值形式的版本號在libevent2.0.1之後才提供. 所以只能比較蠢的用比較字串的方式去判斷版本號

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

介面和巨集的定義位於 <event2/event.h>中, 字串形式的版本號在1.0版本就提供了, 數值形式的版本號直至2.0.1才提供

4.8 一鍵釋放所有全域性例項

就算你手動釋放了所有在程式程式碼初始化時建立的libevent物件, 在程式退出之前, 也依然有一些內建的, 對使用者不可見的libevent內部例項以及一些全域性配置例項存在著, 並且存在在堆區. 一般情況下不用管它們: 程式都退出了, 釋放不釋放有什麼區別呢? 反正作業系統會幫你清除的. 但有時你想引入一些第三方的分析工具, 比如檢測記憶體洩漏的工具時, 就會導致這些工具誤報記憶體洩漏.

你可以簡單的調一下下面這個函式, 完成一鍵完全清除:

void libevent_global_shutdown(void);

注意哦: 這個函式不會幫你釋放你自己呼叫libevent介面建立出來的物件哦! 還沒那麼智慧哦!

另外, 很顯然的一點是, 當呼叫了這個函式之後, 再去呼叫其它libevent介面, 可能會出現異常哦! 所以沒事不要呼叫它, 如果你呼叫它, 那麼一定是自殺前的最後一秒.

函式定義在<event2/event.h>中, 2.1.1版本後可用

相關文章