libevent詳解與實踐

文章目錄


libevent文檔

官方網站

官方文檔

官方GitHub

libevent-2.1.11-stable.tar.gz

前言

Libevent是用於開發可伸縮網絡服務器的事件通知庫。Libevent API提供了一種機制,在文件描述符上發生特定事件或達到超時後執行回調函數。此外,Libevent還支持由於信號或定期超時而進行的回調。

Libevent用於取代在事件驅動的網絡服務器中的事件的循環。應用程序只需調用event_base_dispatch(),然後動態添加或刪除事件,而無需更改事件循環。

目前,Libevent支持/dev/poll、kqueue(2)、select(2)、poll(2)、epoll(4)和evports。內部事件機制完全獨立於公開的事件API,簡單的Libevent更新可以提供新的功能,而無需重新設計應用程序。因此,Libevent允許進行可移植的應用程序開發,並提供操作系統上可用的最可伸縮的事件通知機制。Libevent也可以用於多線程程序。Libevent應該在Linux、*BSD、Mac OS X、Solaris和Windows上編譯。

設計目標是:

  • 可移植性:使用 libevent 編寫的程序應該可以在 libevent 支持的所有平臺上工作。即使 沒有好的方式進行非阻塞 IO,libevent 也應該支持一般的方式,讓程序可以在受限的環境中運行。

  • 速度:libevent 嘗試使用每個平臺上最高速的非阻塞 IO 實現,並且不引入太多的額外開銷。

  • 可擴展性:libevent 被設計爲程序即使需要上萬個活動套接字的時候也可以良好工作。

  • 方便:無論何時,最自然的使用 libevent 編寫程序的方式應該是穩定的、可移植的。

libevent 由下列組件構成

  • evutil:用於抽象不同平臺網絡實現差異的通用功能。
  • event 和 event_base:libevent 的核心,爲各種平臺特定的、基於事件的非阻塞 IO 後 端提供抽象 API,讓程序可以知道套接字何時已經準備好,可以讀或者寫,並且處理基 本的超時功能,檢測 OS 信號。
  • bufferevent:爲 libevent 基於事件的核心提供使用更方便的封裝。除了通知程序套接字 已經準備好讀寫之外,還讓程序可以請求緩衝的讀寫操作,可以知道何時 IO 已經真正 發生。
  • evbuffer:在 bufferevent 層之下實現了緩衝功能,並且提供了方便有效的訪問函數。
  • evhttp:一個簡單的 HTTP 客戶端/服務器實現。
  • evdns:一個簡單的 DNS 客戶端/服務器實現。
  • evrpc:一個簡單的 RPC 實現。

編譯

linux

$ ./configure
$ make
$ make verify   # (optional)
$ sudo make install

ARM

$ ./configure --host=arm-linux --prefix=/usr/local/libevent_arm CC=arm-none-linux-gnueabi-gcc CXX=arm-none-linux-gnueabi-g++
$ make
$ make verify   # (optional)
$ sudo make install

Building and installing Libevent

生成庫

創建 libevent 時,默認安裝下列庫:

  • ibevent_core:所有核心的事件和緩衝功能,包含了所有的 event_base、evbuffer、 bufferevent 和工具函數。

  • ibevent_extra:定義了程序可能需要,也可能不需要的協議特定功能,包括 HTTP、 DNS 和 RPC。

  • libevent:這個庫因爲歷史原因而存在,它包含 libevent_core 和 libevent_extra 的內容。 不應該使用這個庫,未來版本的 libevent 可能去掉這個庫。

某些平臺上可能安裝下列庫:

  • libevent_pthreads:添加基於 pthread 可移植線程庫的線程和鎖定實現。它獨立於 libevent_core,這樣程序使用 libevent 時就不需要鏈接到 pthread,除非實際上是以多線程的方式使用libevent。

  • libevent_openssl:這個庫爲使用 bufferevent 和 OpenSSL 進行加密的通信提供支持。 它獨立於 libevent_core,這樣程序使用 libevent 時就不需要鏈接到 OpenSSL,除非實際使用的是加密連接。

概述

標準用法

使用Libevent的每個程序都必須包含<event2/event.h>頭文件,並將-levent傳遞給鏈接器。(如果只需要主事件和緩衝的基於IO的代碼,而不想鏈接任何協議代碼,則可以改爲鏈接-levent_core。)

庫設置

在調用任何其他Libevent函數之前,需要設置庫。如果要在多線程應用程序的多個線程中使用Libevent,則需要初始化線程支持:通常使用evthread_use_pthreads()evthread_use_windows_threads()。有關更多信息,請參見<event2/thread.h>

這也是可以用event_set_mem_functions替換Libevent的內存管理函數,並用event_enable_debug_mode()啓用調試模式的地方。

創建event base

接下來,需要使用event_base_new()event_base_new_with_config()創建一個event_base結構體。event_base負責跟蹤哪些事件是“待處理的”(也就是說,被監視以查看它們是否變爲活動的)以及哪些事件是“活動的”。每個事件都與單個event_base相關聯。

事件通知

對於要監視的每個文件描述符,必須使用event_new()創建事件結構。(您也可以聲明一個事件結構並調用event_assign()來初始化該結構的成員。)要啓用通知,您可以通過調用event_add()將該結構添加到監視的事件列表中。只要事件結構處於活動狀態,它就必須保持分配狀態,因此通常應該在堆上分配它。

調度事件。

最後,調用event_base_dispatch()循環和分派事件。您還可以使用event_base_loop()進行更細粒度的控制。

目前,一次只能有一個線程調度給定的event_base 。如果希望一次在多個線程中運行事件,可以有一個將工作添加到工作隊列中的event_base ,也可以創建多個event_base對象。

I/O緩衝區

Libevent在常規事件回調的基礎上提供了緩衝I/O抽象。這個抽象稱爲bufferevent。bufferevent提供自動填充和移除的輸入和輸出緩衝區。緩衝事件的用戶不再直接處理I/O,而是讀取輸入和寫入輸出緩衝區。

一旦通過bufferevent_socket_new()初始化,bufferevent結構就可以與bufferevent_enable()bufferevent_disable()一起重複使用。您將調用bufferevent_read()bufferevent_write(),而不是直接讀取和寫入套接字。

啓用讀取後,bufferevent將嘗試從文件描述符讀取並調用讀取回調。每當輸出緩衝區被排放到寫低水位線(默認爲0)以下時,就會執行寫回調。

有關更多信息,請參閱<event2/bufferevent*.h>。

計時器

Libevent還可以用來創建計時器,在一定時間過期後調用回調。evtimer_new()宏返回要用作計時器的事件結構。要激活計時器,請調用evtimer_add()。可以通過調用evtimer_del()來停用計時器。(這些宏是圍繞event_new()event_add()event_del()的精簡封裝;您也可以使用它們。)

異步DNS解析

Libevent提供了一個異步DNS解析器,應該使用它來代替標準DNS解析器函數。有關更多詳細信息,請參閱<event2/dns.h>函數。

事件驅動的HTTP服務器

Libevent提供了一個非常簡單的事件驅動HTTP服務器,可以嵌入到程序中並用於服務HTTP請求。

要使用此功能,您需要在程序中包含<event2/http.h>頭。有關詳細信息,請參閱該標題。

RPC服務器和客戶機的框架

Libevent提供了一個創建RPC服務器和客戶端的框架。它負責對所有數據結構進行編組和解組。

API參考

要瀏覽libevent API的完整文檔,請單擊以下任何鏈接。

event2/event.h :主libevent頭

event2/thread.h :多線程程序

event2/buffer.hevent2/bufferevent.h :網絡讀寫的緩衝區管理

event2/util.h : 可移植非阻塞網絡代碼的實用函數

event2/dns.h :異步DNS解析

event2/http.h :基於libevent的嵌入式HTTP服務器

event2/rpc.h :創建RPC服務器和客戶端的框架

event2/watch.h :“準備”和“檢查”觀察者

詳細說明

基於Libevent 2.0+,在C語言中編寫快速可移植的異步網絡IO程序。

一、設置libevent庫

Libevent具有一些在整個進制中共享的會影響整個庫的全局設置。

在調用Libevent庫的任何其他部分之前,必須對這些設置進行任何更改。 如果您不這樣做,Libevent可能會處於不一致狀態。

1. Libevent中的日誌消息

Libevent可以記錄內部錯誤和警告。 如果它是在日誌支持下編譯的,它還會記錄調試消息。 默認情況下,這些消息將寫入stderr。 可以通過提供自己的日誌記錄功能來覆蓋此行爲。

接口

#define EVENT_LOG_DEBUG 0
#define EVENT_LOG_MSG   1
#define EVENT_LOG_WARN  2
#define EVENT_LOG_ERR   3

/* Deprecated; see note at the end of this section */
#define _EVENT_LOG_DEBUG EVENT_LOG_DEBUG
#define _EVENT_LOG_MSG   EVENT_LOG_MSG
#define _EVENT_LOG_WARN  EVENT_LOG_WARN
#define _EVENT_LOG_ERR   EVENT_LOG_ERR

typedef void (*event_log_cb)(int severity, const char *msg);

void event_set_log_callback(event_log_cb cb);

要覆蓋Libevent的日誌記錄行爲,請編寫與event_log_cb參數匹配的自己的函數,並將其作爲參數傳遞給event_set_log_callback()。 只要Libevent想要記錄一條消息,它將把它傳遞給該函數。 可以通過以NULL作爲參數再次調用event_set_log_callback()來使Libevent恢復其默認行爲。

示例

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

static void discard_cb(int severity, const char *msg)
{
    /* This callback does nothing. */
}

static FILE *logfile = NULL;
static void write_to_file_cb(int severity, const char *msg)
{
    const char *s;
    if (!logfile)
        return;
    switch (severity) {
        case _EVENT_LOG_DEBUG: s = "debug"; break;
        case _EVENT_LOG_MSG:   s = "msg";   break;
        case _EVENT_LOG_WARN:  s = "warn";  break;
        case _EVENT_LOG_ERR:   s = "error"; break;
        default:               s = "?";     break; /* never reached */
    }
    fprintf(logfile, "[%s] %s\n", s, msg);
}

/* Turn off all logging from Libevent. */
void suppress_logging(void)
{
    event_set_log_callback(discard_cb);
}

/* Redirect all Libevent log messages to the C stdio file 'f'. */
void set_logfile(FILE *f)
{
    logfile = f;
    event_set_log_callback(write_to_file_cb);
}

注意

在用戶提供的event_log_cb回調中調用Libevent函數是不安全的! 例如,如果您嘗試編寫一個使用bufferevents向網絡套接字發送警告消息的日誌回調,則很可能會遇到奇怪且難以診斷的錯誤。 在將來的Libevent版本中,某些功能可能會取消此限制。

通常,調試日誌不會啓用,並且不會發送到日誌回調。 如果Libevent要支持它們,則可以手動將其打開。

接口

#define EVENT_DBG_NONE 0
#define EVENT_DBG_ALL 0xffffffffu

void event_enable_debug_logging(ev_uint32_t which);

調試日誌很冗長,在大多數情況下不一定有用。 使用EVENT_DBG_NONE調用event_enable_debug_logging()將獲得默認行爲。 使用EVENT_DBG_ALL調用它會打開所有受支持的調試日誌。 將來的版本中可能會支持更多細緻的選項。

這些函數在<event2 / event.h>中聲明。 它們首先出現在Libevent 1.0c中,除event_enable_debug_logging()首次出現在Libevent 2.1.1-alpha中。

兼容性說明

在Libevent 2.0.19-穩定版本之前,EVENT_LOG_ *宏的名稱以下劃線開頭:_EVENT_LOG_DEBUG,_EVENT_LOG_MSG,_EVENT_LOG_WARN和_EVENT_LOG_ERR。 這些較早的名稱已被棄用,僅應用於與Libevent 2.0.18-stable和更早版本的向後兼容。 在將來的Libevent版本中可能會刪除它們。

2. 處理致命錯誤

當Libevent檢測到不可恢復的內部錯誤(例如,損壞的數據結構)時,其默認行爲是調用exit()或abort()退出當前運行的進程。 這些錯誤幾乎總是意味着某個地方存在bug:在你的代碼中,或者在Libevent本身中。

如果您希望應用程序更優雅地處理致命錯誤,則可以通過提供Libevent代替退出而調用的函數來覆蓋Libevent的行爲。

接口

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

要使用這些函數,首先定義一個新函數,遇到致命錯誤時Libevent應該調用該函數,然後將其傳遞給event_set_fatal_callback()。以後,如果Libevent遇到致命錯誤,它將調用您提供的函數。

該函數不應將控制權返回給Libevent。這樣做可能會導致不確定的行爲,並且Libevent可能仍會退出以避免崩潰。一旦調用了函數,就不應調用任何其他Libevent函數。

這些函數在<event2 / event.h>中聲明。首先出現在Libevent 2.0.3-alpha中。

3. 內存管理

默認情況下,Libevent使用C庫的內存管理功能從堆中分配內存。您可以通過提供自己的malloc,realloc和free替換項來使Libevent使用另一個內存管理器。如果有想要使用Libevent的更高效的分配器,或者如果想要使用Libevent的檢測化分配器來查找內存泄漏,則可能需要這樣做。

接口

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

這是一個簡單的示例,該示例將Libevent的分配函數替換爲對已分配的字節總數進行計數的變體。 實際上,您可能希望在此處添加鎖,以防止Libevent在多個線程中運行時出錯。

示例

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

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

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

static size_t total_allocated = 0;
static void *replacement_malloc(size_t sz)
{
    void *chunk = malloc(sz + ALIGNMENT);
    if (!chunk) return chunk;
    total_allocated += sz;
    *(size_t*)chunk = sz;
    return OUTPTR(chunk);
}
static void *replacement_realloc(void *ptr, size_t sz)
{
    size_t old_size = 0;
    if (ptr) {
        ptr = INPTR(ptr);
        old_size = *(size_t*)ptr;
    }
    ptr = realloc(ptr, sz + ALIGNMENT);
    if (!ptr)
        return NULL;
    *(size_t*)ptr = sz;
    total_allocated = total_allocated - old_size + sz;
    return OUTPTR(ptr);
}
static void replacement_free(void *ptr)
{
    ptr = INPTR(ptr);
    total_allocated -= *(size_t*)ptr;
    free(ptr);
}
void start_counting_bytes(void)
{
    event_set_mem_functions(replacement_malloc,
                            replacement_realloc,
                            replacement_free);
}

注意

  • 替換內存管理功能會影響以後所有從Libevent分配,調整大小或釋放內存的調用。因此,在調用任何其他Libevent函數之前,需要確保已替換函數。否則,Libevent將使用你的free版本釋放從C庫的malloc版本分配的內存。

  • 你的malloc和realloc函數需要返回與C庫相同的內存塊。

  • 你的realloc函數需要正確處理realloc(NULL,sz)(即,將其視爲malloc(sz))。

  • 你的realloc函數需要正確處理realloc(ptr,0)(也就是說,將其視爲free(ptr))。

  • 你的free函數不需要處理free(NULL)。

  • 你的malloc函數不需要處理malloc(0)。

  • 如果從多個線程中使用Libevent,則替換後的內存管理功能必須是線程安全的。

  • Libevent將使用這些函數來分配返回給您的內存。因此,如果要釋放由Libevent函數分配和返回的內存,並且已經替換了malloc和realloc函數,則可能必須使用替換free函數來釋放它。

在<event2 / event.h>中聲明瞭event_set_mem_functions()函數。它首先出現在Libevent 2.0.1-alpha中。

可以在禁用event_set_mem_functions()的情況下構建Libevent。如果是這樣,則使用event_set_mem_functions的程序將不會編譯或鏈接。在Libevent 2.0.2-alpha和更高版本中,您可以通過檢查是否已定義EVENT_SET_MEM_FUNCTIONS_IMPLEMENTED宏來檢測是否存在event_set_mem_functions()。

4. 鎖和線程

編寫多線程程序,在多個線程同時訪問相同的數據並不總是安全的。

Libevent結構通常可以在多線程中以三種方式工作。

  • 有些結構本質上是單線程的:從多個線程同時使用它們永遠是不安全的。

  • 某些結構是有可選的鎖:可以告訴每個對象Libevent是否需要在多線程使用每個對象。

  • 某些結構始終是鎖定的:如果Libevent在鎖定支持下運行,則始終可以安全地多個線程使用它們。

爲獲取鎖,在調用分配需要在多個線程間共享的結構體的 libevent 函數之前,必須告知 libevent 使用哪個鎖函數。

如果使用 pthreads 庫,或者使用 Windows 本地線程代碼,那麼已經有設置 libevent 使用正確的 pthreads 或者 Windows 函數的預定義函數。

接口

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

這些函數在成功時都返回0,失敗時返回-1。

如果使用不同的線程庫,則需要一些額外的工作,必須使用你的線程庫來定義函數去實現:

  • 鎖定

  • 解鎖

  • 分配鎖

  • 析構鎖

  • 條件變量

  • 創建條件變量

  • 析構條件變量

  • 等待條件變量

  • 觸發/廣播某條件變量

  • 線程

  • 線程 ID 檢測

使用 evthread_set_lock_callbacks 和 evthread_set_id_callback 接口告知 libevent 這些函

數。

接口

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

#define EVTHREAD_LOCKTYPE_RECURSIVE 1
#define EVTHREAD_LOCKTYPE_READWRITE 2

#define EVTHREAD_LOCK_API_VERSION 1

struct evthread_lock_callbacks {
       int lock_api_version;
       unsigned supported_locktypes;
       void *(*alloc)(unsigned locktype);
       void (*free)(void *lock, unsigned locktype);
       int (*lock)(unsigned mode, void *lock);
       int (*unlock)(unsigned mode, void *lock);
};

int evthread_set_lock_callbacks(const struct evthread_lock_callbacks *);

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

struct evthread_condition_callbacks {
        int condition_api_version;
        void *(*alloc_condition)(unsigned condtype);
        void (*free_condition)(void *cond);
        int (*signal_condition)(void *cond, int broadcast);
        int (*wait_condition)(void *cond, void *lock,
            const struct timeval *timeout);
};

int evthread_set_condition_callbacks(
        const struct evthread_condition_callbacks *);

evthread_lock_callbacks 結 構 體 描 述 的 鎖 回 調 函 數 及 其 能 力 。 對 於 上 述 版 本 ,

lock_api_version 字 段 必 須 設 置 爲 EVTHREAD_LOCK_API_VERSION 。 必 須 設 置

supported_locktypes 字段爲 EVTHREAD_LOCKTYPE_*常量的組合以描述支持的鎖類型

( 在 2.0.4-alpha 版 本 中 , EVTHREAD_LOCK_RECURSIVE 是 必 須 的 ,

EVTHREAD_LOCK_READWRITE 則沒有使用)。alloc 函數必須返回指定類型的新鎖;free

函數必須釋放指定類型鎖持有的所有資源;lock 函數必須試圖以指定模式請求鎖定,如果成

功則返回0,失敗則返回非零;unlock 函數必須試圖解鎖,成功則返回0,否則返回非零。

可識別的鎖類型有:

  • 0:通常的,不必遞歸的鎖。

  • EVTHREAD_LOCKTYPE_RECURSIVE:不會阻塞已經持有它的線程的鎖。一旦持有它的線程進行原來鎖定次數的解鎖,其他線程立刻就可以請求它了。

  • EVTHREAD_LOCKTYPE_READWRITE:可以讓多個線程同時因爲讀而持有它,但是任何時刻只有一個線程因爲寫而持有它。寫操作排斥所有讀操作。

可識別的鎖模式有:

  • EVTHREAD_READ:僅用於讀寫鎖:爲讀操作請求或者釋放鎖

  • EVTHREAD_WRITE:僅用於讀寫鎖:爲寫操作請求或者釋放鎖

  • EVTHREAD_TRY:僅用於鎖定:僅在可以立刻鎖定的時候才請求鎖定

id_fn 參數必須是一個函數,它返回一個無符號長整數,標識調用此函數的線程。對於相同線程,這個函數應該總是返回同樣的值;而對於同時調用該函數的不同線程,必須返回不同的值。evthread_condition_callbacks 結構體描述了與條件變量相關的回調函數。對於上述版本,condition_api_version 字 段 必 須 設 置 爲EVTHREAD_CONDITION_API_VERSION 。alloc_condition 函數必須返回到新條件變量的指針。它接受0作爲其參數。free_condition 函數必須釋放條件變量持有的存儲器和資源。wait_condition 函數要求三個參數:一個由alloc_condition 分配的條件變量,一個由你提供的 evthread_lock_callbacks.alloc 函數分配的鎖,以及一個可選的超時值。調用本函數時,必須已經持有參數指定的鎖;本函數應該釋放指定的鎖,等待條件變量成爲授信狀態,或者直到指定的超時時間已經流逝(可選 )。wait_condition 應該在錯誤時返回-1,條件變量授信時返回0,超時時返回1。返回之前,函數應該確定其再次持有鎖。最後,signal_condition 函數應該喚醒等待該條件變量的某個線程(broadcast 參數爲 false 時),或者喚醒等待條件變量的所有線程(broadcast 參數爲 true 時)。只有在持有與條件變量相關的鎖的時候,才能夠進行這些操作。

關於條件變量的更多信息,請查看 pthreads 的 pthread_cond_*函數文檔,或者 Windows的 CONDITION_VARIABLE(Windows Vista 新引入的)函數文檔。

示例

關於使用這些函數的示例,請查看 Libevent 源代碼發佈版本中的 evthread_pthread.c 和 evthread_win32.c 文件。

這些函數在<event2/thread.h>中聲明,其中大多數在 2.0.4-alpha 版本中首次出現。2.0.1-alpha 到2.0.3-alpha 使用較老版本的鎖函數。event_use_pthreads 函數要求程序鏈接event_pthreads 庫。

條件變量函數是2.0.7-rc 版本新引入的,用於解決某些棘手的死鎖問題。

可以創建禁止鎖支持的 libevent。這時候已創建的使用上述線程相關函數的程序將不能運行。

5. 調試鎖的使用

爲幫助調試鎖的使用,libevent 有一個可選的“鎖調試”特徵。這個特徵包裝了鎖調用,以便捕獲典型的鎖錯誤,包括:

  • 解鎖並沒有持有的鎖

  • 重新鎖定一個非遞歸鎖

如果發生這些錯誤中的某一個,libevent 將給出斷言失敗並且退出。

接口

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

注意

必須在創建或者使用任何鎖之前調用這個函數。爲安全起見,請在設置完線程函數後立即調用這個函數。

此功能是Libevent 2.0.4-alpha中的新增功能,拼寫錯誤的名稱爲“ evthread_enable_lock_debuging()”。 拼寫固定爲2.1.2-alpha中的evthread_enable_lock_debugging(); 目前都支持這兩個名稱。

6. 調試事件的使用

libevent 可以檢測使用事件時的一些常見錯誤並且進行報告。這些錯誤包括:

  • 將未初始化的 event 結構體當作已經初始化的

  • 試圖重新初始化未決的 event 結構體

跟蹤哪些事件已經初始化需要使用額外的內存和處理器時間,所以只應該在真正調試程序的時候才啓用調試模式。

接口

void event_enable_debug_mode(void);

必須在創建任何 event_base 之前調用這個函數。

如果在調試模式下使用大量由 event_assign(而不是 event_new)創建的事件,程序可能會耗盡內存,這是因爲沒有方式可以告知 libevent 由 event_assign 創建的事件不會再被使用了(可以調用 event_free 告知由 event_new 創建的事件已經無效了)。如果想在調試時避免耗盡內存,可以顯式告知 libevent 這些事件不再被當作已分配的了:

接口

void event_debug_unassign(struct event *ev);

沒有啓用調試的時候調用 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)
{
    /* We pass 'NULL' as the callback pointer for the heap allocated
     * event, and we pass the event itself as the callback pointer
     * for the stack-allocated event. */
    struct event *ev = ptr;

    if (ev)
        event_debug_unassign(ev);
}

/* Here's a simple mainloop that waits until fd1 and fd2 are both
 * ready to read. */
void mainloop(evutil_socket_t fd1, evutil_socket_t fd2, int debug_mode)
{
    struct event_base *base;
    struct event event_on_stack, *event_on_heap;

    if (debug_mode)
       event_enable_debug_mode();

    base = event_base_new();

    event_on_heap = event_new(base, fd1, EV_READ, cb, NULL);
    event_assign(&event_on_stack, base, fd2, EV_READ, cb, &event_on_stack);

    event_add(event_on_heap, NULL);
    event_add(&event_on_stack, NULL);

    event_base_dispatch(base);

    event_free(event_on_heap);
    event_base_free(base);
}

詳細的事件調試是一項只能在編譯時使用CFLAGS環境變量“ -DUSE_DEBUG”啓用的功能。 啓用此標誌後,針對Libevent編譯的任何程序都將輸出非常詳細的日誌,詳細說明後端的低級活動。 這些日誌包括但不限於以下內容:

  • 活動添加

  • 事件刪除

  • 平臺特定的事件通知信息

無法通過API調用啓用或禁用此功能,因此只能在開發人員內部使用。

這些調試功能已添加到Libevent 2.0.4-alpha中。

7.檢測libevent的版本

新版本的 libevent 會添加特徵,移除 bug。有時候需要檢測 libevent 的版本,以便:

  • 檢測已安裝的 libevent 版本是否可用於創建你的程序

  • 爲調試顯示 libevent 的版本

  • 檢測 libevent 的版本,以便向用戶警告 bug,或者提示要做的工作

接口

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

宏返回編譯時的 libevent 版本;函數返回運行時的 libevent 版本。注意:如果動態鏈接到libevent,這兩個版本可能不同。

可以獲取兩種格式的 libevent 版本:用於顯示給用戶的字符串版本,或者用於數值比較的4字節整數版本。整數格式使用高字節表示主版本,低字節表示副版本,第三字節表示修正版本,最低字節表示發佈狀態:0表示發佈,非零表示某特定發佈版本的後續開發序列。

所以,libevent 2.0.1-alpha 發佈版本的版本號是[02 00 01 00],或者說0x02000100。2.0.1-alpha 和2.0.2-alpha 之間的開發版本可能是[02 00 01 08],或者說0x02000108。

示例:編譯時檢測

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

示例:運行時檢測

#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>中。event_get_version 函數首次出現在1.0c版本;其他的首次出現在2.0.1-alpha 版本。

8. 釋放全局的Libevent結構

即使釋放了使用Libevent分配的所有對象,也將剩下一些全局分配的結構。通常這不是問題:退出流程後,無論如何都將對其進行清理。但是擁有這些結構可能會使某些調試工具誤以爲Libevent正在泄漏資源。如果需要確保Libevent已發佈所有內部庫全局數據結構,則可以調用:

接口

void libevent_global_shutdown(void;

此函數不會釋放Libevent函數返回給您的任何結構。如果要在退出之前釋放所有內容,則需要自己釋放所有事件,event_bases,bufferevents等。

調用libevent_global_shutdown()將使其他Libevent函數的行爲無法預期。除了作爲您的程序調用的最後一個Libevent函數外,不要調用它。一個例外是libevent_global_shutdown()是冪等(idempotent)的:可以調用它,即使它已經被調用也可以。

此函數在<event2 / event.h>中聲明。它是在Libevent 2.1.1-alpha中引入的。

二、創建event_base

使用 libevent 函數之前需要分配一個或者多個 event_base 結構體。每個 event_base 結構體持有一個事件集合,可以檢測以確定哪個事件是激活的。

如果設置 event_base 使用鎖,則可以安全地在多個線程中訪問它。然而,其事件循環只能運行在一個線程中。如果需要用多個線程檢測 IO,則需要爲每個線程使用一個 event_base。

每個 event_base 都有一種用於檢測哪種事件已經就緒的“方法”,或者說後端。可以識別的方法有:

  • select

  • poll

  • epoll

  • kqueue

  • devpoll

  • evport

  • win32

用戶可以用環境變量禁止某些特定的後端。比如說,要禁止 kqueue 後端,可以設置 EVENT_NOKQUEUE 環境變量 。 如果要用編程的方法禁止後端 , 請看下面關於event_config_avoid_method()的說明。

1. 建立默認的 event_base

event_base_new()函數分配並且返回一個新的具有默認設置的 event_base。函數會檢測環境變量,返回一個到 event_base 的指針。如果發生錯誤,則返回 NULL。選擇各種方法時,函數會選擇 OS 支持的最快方法。

接口

struct event_base *event_base_new(void);

大多數程序使用這個函數就夠了。

event_base_new()函數聲明在<event2/event.h>中,首次出現在 libevent 1.4.3版。

2. 創建複雜的 event_base

要對取得什麼類型的 event_base 有更多的控制,就需要使用 event_config。event_config是一個容納event_base 配置信息的不透明結構體。需要 event_base 時,將 event_config傳遞給 event_base_new_with_config()。

接口

struct event_config *event_config_new(void);
struct event_base *event_base_new_with_config(const struct event_config *cfg);
void event_config_free(struct event_config *cfg);

要使用這些函數分配 event_base,先調用 event_config_new()分配一個 event_config。 然後,對event_config 調用其它函數,設置所需要的 event_base 特徵。最後,調用 event_base_new_with_config()獲取新的 event_base。完成工作後,使用 event_config_free()釋放 event_config。

接口

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

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

enum event_base_config_flag {
    EVENT_BASE_FLAG_NOLOCK = 0x01,
    EVENT_BASE_FLAG_IGNORE_ENV = 0x02,
    EVENT_BASE_FLAG_STARTUP_IOCP = 0x04,
    EVENT_BASE_FLAG_NO_CACHE_TIME = 0x08,
    EVENT_BASE_FLAG_EPOLL_USE_CHANGELIST = 0x10,
    EVENT_BASE_FLAG_PRECISE_TIMER = 0x20
};
int event_config_set_flag(struct event_config *cfg,

調用 event_config_avoid_method()可以通過名字讓 libevent 避免使用特定的可用後端。

調用 event_config_require_feature()讓 libevent 不要使用不能提供所有功能的後端。

調用 event_config_set_flag()讓 libevent 在創建 event_base 時設置一個或者多個將在下面介紹的運行時標誌。

event_config_require_features()可識別的特徵值有:

  • EV_FEATURE_ET:要求支持邊沿觸發的後端

  • EV_FEATURE_O1:要求添加、刪除單個事件,或者確定哪個事件激活的操作是 O(1)複雜度的後端

  • EV_FEATURE_FDS:要求支持任意文件描述符,而不僅僅是套接字的後端

event_config_set_flag()可識別的選項值有:

  • EVENT_BASE_FLAG_NOLOCK:不要爲 event_base 分配鎖。設置這個選項可以爲event_base 節省一點用於鎖定和解鎖的時間,但是讓在多個線程中訪問 event_base成爲不安全的。

  • EVENT_BASE_FLAG_IGNORE_ENV:選擇使用的後端時,不要檢測 EVENT_*環境變量。使用這個標誌需要三思:這會讓用戶更難調試你的程序與 libevent 的交互。

  • EVENT_BASE_FLAG_STARTUP_IOCP:僅用於 Windows,讓 libevent 在啓動時就啓用任何必需的 IOCP 分發邏輯,而不是按需啓用。

  • EVENT_BASE_FLAG_NO_CACHE_TIME:不是在事件循環每次準備執行超時回調時檢測當前時間,而是在每次超時回調後進行檢測。注意:這會消耗更多的 CPU 時間。

  • EVENT_BASE_FLAG_EPOLL_USE_CHANGELIST:告訴 libevent,如果決定使用epoll 後端,可以安全地使用更快的基於 changelist 的後端。epoll-changelist 後端可以在後端的分發函數調用之間,同樣的 fd 多次修改其狀態的情況下,避免不必要的系統調用。但是如果傳遞任何使用 dup()或者其變體克隆的 fd 給 libevent,epoll-changelist後端會觸發一個內核 bug,導致不正確的結果。在不使用 epoll 後端的情況下,這個標誌是沒有效果的。也可以通過設置 EVENT_EPOLL_USE_CHANGELIST 環境變量來打開 epoll-changelist 選項。

  • EVENT_BASE_FLAG_PRECISE_TIMER: 默認情況下,Libevent嘗試使用操作系統提供的最快的可用計時機制。 如果存在較慢的計時機制,可以提供更精細的計時精度,則此標誌告訴Libevent改用該計時機制。 如果操作系統不提供這種慢速但精確的機制,則此標誌無效。

上述操作 event_config 的函數都在成功時返回0,失敗時返回-1。

注意 設置 event_config,請求 OS 不能提供的後端是很容易的。比如說,對於 libevent 2.0.1-alpha,在 Windows 中是沒有 O(1)後端的;在 Linux 中也沒有同時提供 EV_FEATURE_FDS 和EV_FEATURE_O1 特 徵 的 後 端 。 如 果 創 建 了 libevent 不 能 滿 足 的 配 置 ,event_base_new_with_config()會返回 NULL

接口

int event_config_set_num_cpus_hint(struct event_config *cfg, int cpus)

這個函數當前僅在 Windows 上使用 IOCP 時有用,雖然將來可能在其他平臺上有用。這個函數告訴 event_config 在生成多線程 event_base 的時候,應該試圖使用給定數目的 CPU。注意這僅僅是一個提示:event_base 使用的CPU 可能比你選擇的要少。

接口

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

此功能通過限制在檢查更多高優先級事件之前可以調用多少個低優先級事件回調來防止優先級倒置。 如果max_interval爲非null,則事件循環將檢查每個回調之後的時間,如果max_interval已超過,則重新掃描高優先級事件。 如果max_callbacks爲非負數,則在調用max_callbacks回調後,事件循環還會檢查更多事件。 這些規則適用於min_priority或更高級別的任何事件。

示例:優先使用邊緣觸發的後端

struct event_config *cfg;
struct event_base *base;
int i;

/* My program wants to use edge-triggered events if at all possible.  So
   I'll try to get a base twice: Once insisting on edge-triggered IO, and
   once not. */
for (i=0; i<2; ++i) {
    cfg = event_config_new();

    /* I don't like select. */
    event_config_avoid_method(cfg, "select");

    if (i == 0)
        event_config_require_features(cfg, EV_FEATURE_ET);

    base = event_base_new_with_config(cfg);
    event_config_free(cfg);
    if (base)
        break;

    /* If we get here, event_base_new_with_config() returned NULL.  If
       this is the first time around the loop, we'll try again without
       setting EV_FEATURE_ET.  If this is the second time around the
       loop, we'll give up. */
}

示例:避免優先級倒置

struct event_config *cfg;
struct event_base *base;

cfg = event_config_new();
if (!cfg)
   /* Handle error */;

/* I'm going to have events running at two priorities.  I expect that
   some of my priority-1 events are going to have pretty slow callbacks,
   so I don't want more than 100 msec to elapse (or 5 callbacks) before
   checking for priority-0 events. */
struct timeval msec_100 = { 0, 100*1000 };
event_config_set_max_dispatch_interval(cfg, &msec_100, 5, 1);

base = event_base_new_with_config(cfg);
if (!base)
   /* Handle error */;

event_base_priority_init(base, 2);

這些函數和類型在<event2 / event.h>中聲明。

EVENT_BASE_FLAG_IGNORE_ENV標誌首先出現在Libevent 2.0.2-alpha中。 EVENT_BASE_FLAG_PRECISE_TIMER標誌首先出現在Libevent 2.1.2-alpha中。 event_config_set_num_cpus_hint()函數是Libevent 2.0.7-rc中的新增功能,而event_config_set_max_dispatch_interval()是2.1.1-alpha中的新增功能。 本節中的所有其他內容首先出現在Libevent 2.0.1-alpha中。

3. 檢查 event_base 的後端方法

有時候需要檢查 event_base 支持哪些特徵,或者當前使用哪種方法。

接口

const char **event_get_supported_methods(void);

event_get_supported_methods()函數返回一個指針,指向 libevent 支持的方法名字數組。這個數組的最後一個元素是 NULL。

示例

int i;
const char **methods = event_get_supported_methods();
printf("Starting Libevent %s.  Available methods are:\n",
    event_get_version());
for (i=0; methods[i] != NULL; ++i) {
    printf("    %s\n", methods[i]);
}

注意這個函數返回 libevent 被編譯以支持的方法列表。然而 libevent 運行的時候,操作系統可能 不能支持所有方法。比如說,可能 OS X 版本中的 kqueue 的 bug 太多,無法使用。

接口

const char *event_base_get_method(const struct event_base *base);
enum event_method_feature event_base_get_features(const struct event_base *base);

event_base_get_method()返回 event_base 正在使用的方法。

event_base_get_features()返回 event_base 支持的特徵的比特掩碼。

示例

struct event_base *base;
enum event_method_feature f;

base = event_base_new();
if (!base) {
    puts("Couldn't get an event_base!");
} else {
    printf("Using Libevent with backend method %s.",
        event_base_get_method(base));
    f = event_base_get_features(base);
    if ((f & EV_FEATURE_ET))
        printf("  Edge-triggered events are supported.");
    if ((f & EV_FEATURE_O1))
        printf("  O(1) event notification is supported.");
    if ((f & EV_FEATURE_FDS))
        printf("  All FD types are supported.");
    puts("");
}

這個函數定義在<event2/event.h>中。event_base_get_method()首次出現在1.4.3版本中,其他函數首次出現在2.0.1-alpha 版本中。

4. 釋放 event_base

使用完 event_base 之後,使用 event_base_free()進行釋放。
接口

void event_base_free(struct event_base *base);

注意:這個函數不會釋放當前與 event_base 關聯的任何事件,或者關閉他們的套接字,或者釋放任何指針。

event_base_free()定義在<event2/event.h>中,首次由 libevent 1.2實現。

5. 設置 event_base 的優先級

libevent 支持爲事件設置多個優先級。然而,event_base 默認只支持單個優先級。可以調用event_base_priority_init()設置 event_base 的優先級數目。

接口

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

成功時這個函數返回0,失敗時返回-1。base 是要修改的 event_base,n_priorities 是要支持的優先級數目,這個數目至少是1。每個新的事件可用的優先級將從0(最高)到n_priorities-1(最低)。
常量 EVENT_MAX_PRIORITIES 表示 n_priorities 的上限。調用這個函數時爲 n_priorities給出更大的值是錯誤的。

注意
必須在任何事件激活之前調用這個函數,最好在創建 event_base 後立刻調用。

要查找某個數據庫當前支持的優先級數量,可以調用event_base_getnpriorities()。

接口

int event_base_get_npriorities(struct event_base *base);

返回值等於基礎中配置的優先級數。 因此,如果event_base_get_npriorities()返回3,則允許的優先級值爲0、1和2。

示例
關於示例,請看 event_priority_set 的文檔。
默認情況下,與 event_base 相關聯的事件將被初始化爲具有優先級 n_priorities / 2。event_base_priority_init()函數定義在<event2/event.h>中,從 libevent 1.0版就可用了。

6. 在 fork()之後重新初始化 event_base

不是所有事件後端都在調用 fork()之後可以正確工作。所以,如果在使用 fork()或者其
他相關係統調用啓動新進程之後,希望在新進程中繼續使用 event_base,就需要進行重新
初始化。
接口

int event_reinit(struct event_base *base);

成功時這個函數返回0,失敗時返回-1。
示例

struct event_base *base = event_base_new();

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

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

event_reinit()定義在<event2/event.h>中,在 libevent 1.4.3-alpha 版中首次可用。

三、創建event loop

1. 運行循環

一旦有了一個已經註冊了某些事件的 event_base(關於如何創建和註冊事件請看下一節),就需要讓 libevent 等待事件並且通知事件的發生。

接口

#define EVLOOP_ONCE             0x01
#define EVLOOP_NONBLOCK         0x02
#define EVLOOP_NO_EXIT_ON_EMPTY 0x04

int event_base_loop(struct event_base *base, int flags);

默認情況下,event_base_loop()函數將運行一個event_base,直到其中沒有註冊更多事件爲止。爲了運行循環,它反覆檢查是否已觸發任何已註冊的事件(例如,讀取事件的文件描述符是否已準備好讀取,或者超時事件的超時是否已準備就緒)。一旦發生這種情況,它將所有觸發的事件標記爲“活動”,並開始運行它們。

可以通過在event_base_loop()的flags參數中設置一個或多個標誌來更改其行爲。

如果設置了EVLOOP_ONCE,則循環將等待,直到某些事件變爲活動狀態,然後運行活動事件,直到沒有其他要運行的狀態,然後返回。

如果設置了EVLOOP_NONBLOCK,則該循環將不等待事件觸發:它將僅檢查是否有任何事件準備立即觸發,並在可能時運行其回調。

通常,一旦沒有掛起或活動事件,循環將立即退出。您可以通過傳遞EVLOOP_NO_EXIT_ON_EMPTY標誌來覆蓋此行爲-例如,如果要從其他線程添加事件。如果您確實設置了EVLOOP_NO_EXIT_ON_EMPTY,則循環將一直運行,直到有人調用event_base_loopbreak()或調用event_base_loopexit()或發生錯誤爲止。

完成後,如果event_base_loop()正常退出,則返回0;如果由於後端發生一些未處理的錯誤而退出,則返回-1;如果由於沒有更多pending 或active 事件而退出,則返回1。

爲了幫助理解,以下是event_base_loop算法的大致摘要:

僞代碼

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

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

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

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

爲方便起見,也可以調用:

接口

int event_base_dispatch(struct event_base *base);

event_base_dispatch ( ) 等 同 於 沒 有 設 置 標 志 的 event_base_loop ( )。

event_base_dispatch ()將一直運行,直到沒有已經註冊的事件了,或者調用 了event_base_loopbreak()或者 event_base_loopexit()爲止。

這些函數定義在<event2/event.h>中,從 libevent 1.0版就存在了。

2. 停止循環

如果想在移除所有已註冊的事件之前停止活動的事件循環,可以調用兩個稍有不同的函數。 接口

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

event_base_loopexit()讓 event_base 在給定時間之後停止循環。如果 tv 參數爲 NULL,event_base 會立即停止循環,沒有延時。如果 event_base 當前正在執行任何激活事件的回調,則回調會繼續運行,直到運行完所有激活事件的回調之才退出。

event_base_loopbreak ( ) 讓 event_base 立 即 退 出 循 環 。 它 與 event_base_loopexit(base,NULL)的不同在於,如果 event_base 當前正在執行激活事件的回調,它將在執行完當前正在處理的事件後立即退出

注意 event_base_loopexit(base,NULL)和 event_base_loopbreak(base)在事件循環沒有運行時的行爲不同:前者安排下一次事件循環在下一輪迴調完成後立即停止(就好像 帶EVLOOP_ONCE 標誌調用一樣);後者卻僅僅停止當前正在運行的循環,如果事件循環沒有運行,則沒有任何效果。

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

示例:立即關閉

#include <event2/event.h>

/* Here's a callback function that calls loopbreak */
void cb(int sock, short what, void *arg)
{
    struct event_base *base = arg;
    event_base_loopbreak(base);
}

void main_loop(struct event_base *base, evutil_socket_t watchdog_fd)
{
    struct event *watchdog_event;

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

    event_add(watchdog_event, NULL);

    event_base_dispatch(base);
}

示例:執行事件循環10秒,然後退出

#include <event2/event.h>

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

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

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

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

有時候需要知道對 event_base_dispatch()或者 event_base_loop()的調用是正常退出的,還是因爲調用 event_base_loopexit()或者 event_base_break()而退出的。可以調用下述函數來確定是否調用了 loopexit 或者 break 函數。

接口

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

這兩個函數分別會在因爲調用 event_base_loopexit()或者 event_base_break()而退出循環的時候返回 true,否則返回 false。下次啓動事件循環的時候,這些值會被重設。

這些函數聲明在<event2/event.h>中。event_break_loopexit()函數首次在 libevent 1.0c 版本中實現;event_break_loopbreak()首次在 libevent 1.4.3版本中實現。

3. 檢查內部時間緩存

有時候需要在事件回調中獲取當前時間的近似視圖,但不想調用 gettimeofday()(可能是因爲 OS 將gettimeofday()作爲系統調用實現,而你試圖避免系統調用的開銷)。

在回調中,可以請求 libevent 開始本輪迴調時的當前時間視圖。

接口

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

如果當前正在執行回調,event_base_gettimeofday_cached()函數將 tv_out 參數的值置爲緩存的時間。否則,函數調用 evutil_gettimeofday()獲取真正的當前時間。成功時函數返回0,失敗時返回負數。

注意,因爲 libevent 在開始執行回調的時候緩存時間值,所以這個值至少是有一點不精確的。如果回調執行很長時間,這個值將非常不精確。

要強制立即更新緩存,可以調用以下函數:

接口

int event_base_update_cache_time(struct event_base *base);

如果成功,則返回0,失敗則返回-1,如果event base未運行其事件循環,則無效。

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

4. 轉儲 event_base 的狀態

接口

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

爲幫助調試程序(或者調試 libevent),有時候可能需要加入到 event_base 的事件及其狀態的完整列表。調用 event_base_dump_events()可以將這個列表輸出到指定的文件中。

這個列表是人可讀的,未來版本的 libevent 將會改變其格式。

這個函數在 libevent 2.0.1-alpha 版本中引入。

四、創建event

libevent 的基本操作單元是事件。每個事件代表一組條件的集合,這些條件包括:

  • 文件描述符已經就緒,可以讀取或者寫入

  • 文件描述符變爲就緒狀態,可以讀取或者寫入(僅對於邊沿觸發 IO)

  • 超時事件

  • 發生某信號

  • 用戶觸發事件

所有事件具有相似的生命週期。調用 libevent 函數設置事件並且關聯到 event_base 之後,事件進入“已初始化(initialized)”狀態。此時可以將事件添加到 event_base 中,這使之進入“未決(pending)”狀態。在未決狀下,如果觸發事件的條件發生(比如說,文件描述符的狀態改變,或者超時時間到達),則事件進入“激活(active)”狀態,(用戶提供的)事件回調函數將被執行。如果配置爲“持久的(persistent)”,事件將保持爲未決狀態。否則,執行完回調後,事件不再是未決的。刪除操作可以讓未決事件成爲**非未決(已初始化)**的;添加操作可以讓非未決事件再次成爲未決的。

1. 構造事件對象

1.1 創建事件

使用 event_new()接口創建事件。

接口

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

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

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

void event_free(struct event *event);

event_new()試圖分配和構造一個用於 base 的新的事件。what 參數是上述標誌的集合。如果 fd 非負,則它是將被觀察其讀寫事件的文件。事件被激活時,libevent 將調用 cb 函數,傳遞這些參數:

  • 文件描述符 fd,表示所有被觸發事件的位字段,

  • 以及構造事件時的 arg 參數。

發生內部錯誤,或者傳入無效參數時,event_new()將返回 NULL。

所有新創建的事件都處於已初始化和非未決狀態,調用 event_add()可以使其成爲未決的。

要釋放事件,調用 event_free()。對未決或者激活狀態的事件調用 event_free()是安全的:在釋放事件之前,函數將會使事件成爲非激活和非未決的。

示例

#include <event2/event.h>

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

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

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

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

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

上 述 函 數 定 義 在 <event2/event.h> 中 , 首 次 出 現 在 libevent 2.0.1-alpha 版 本 中 。event_callback_fn 類型首次在2.0.4-alpha 版本中作爲 typedef 出現。

1.2 事件標誌
  • EV_TIMEOUT

這個標誌表示某超時時間流逝後事件成爲激活的。構造事件的時候,EV_TIMEOUT 標誌是被忽略的:可以在添加事件的時候設置超時,也可以不設置。超時發生時,回調函數的 what參數將帶有這個標誌。

  • EV_READ

表示指定的文件描述符已經就緒,可以讀取的時候,事件將成爲激活的。

  • EV_WRITE

表示指定的文件描述符已經就緒,可以寫入的時候,事件將成爲激活的。

  • EV_SIGNAL

用於實現信號檢測,請看下面的“構造信號事件”章節。

  • EV_PERSIST

表示事件是“持久的”,請看下面的“關於事件持久性”章節。

  • EV_ET

表示如果底層的 event_base 後端支持邊沿觸發事件,則事件應該是邊沿觸發的。這個標誌影響 EV_READ 和 EV_WRITE 的語義。

從2.0.1-alpha 版本開始,可以有任意多個事件因爲同樣的條件而未決。比如說,可以有兩 個事件因爲某個給定的 fd 已經就緒,可以讀取而成爲激活的。這種情況下,多個事件回調 被執行的次序是不確定的。

這些標誌定義在<event2/event.h>中。除了 EV_ET 在2.0.1-alpha 版本中引入外,所有標誌從1.0版本開始就存在了。

1.3 關於事件持久性

默認情況下,每當未決事件成爲激活的(因爲 fd 已經準備好讀取或者寫入,或者因爲超時),事件將在其回調被執行前成爲非未決的。如果想讓事件再次成爲未決的,可以在回調函數中再次對其調用 event_add()。
然而,如果設置了 EV_PERSIST 標誌,事件就是持久的。這意味着即使其回調被激活,事件還是會保持爲未決狀態。如果想在回調中讓事件成爲非未決的,可以對其調用 event_del()。
每次執行事件回調的時候 ,持久事件的超時值會被複位 。因此如果具有EV_READ|EV_PERSIST 標誌,以及5秒的超時值,則事件將在以下情況下成爲激活的:

  • 套接字已經準備好被讀取的時候
  • 從最後一次成爲激活的開始,已經逝去5秒
1.4 創建事件作爲其的回調參數

通常,可能要創建一個將自身作爲回調參數接收的事件。 但是,您不能僅將指向事件的指針作爲event_new()的參數傳遞,因爲它尚不存在。 要解決此問題,可以使用event_self_cbarg()。

接口

void *event_self_cbarg();

event_self_cbarg()函數返回一個“魔術”指針,該指針在作爲事件回調參數傳遞時,告訴event_new()創建一個將自身作爲其回調參數接收的事件。

示例

#include <event2/event.h>

static int n_calls = 0;

void cb_func(evutil_socket_t fd, short what, void *arg)
{
    struct event *me = arg;

    printf("cb_func called %d times so far.\n", ++n_calls);

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

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

此函數也可以與event_new(),evtimer_new(),evsignal_new(),event_assign(),evtimer_assign()和evsignal_assign()一起使用。 但是,它不能用作非事件的回調參數

在libbevent 2.1.1-alpha中引入了event_self_cbarg()函數。

1.5 只有超時的事件

爲使用方便,libevent 提供了一些以 evtimer_開頭的宏,用於替代 event_*調用來操作純超
時事件。使用這些宏能改進代碼的清晰性。

接口

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

除了 evtimer_new()首次出現在2.0.1-alpha 版本中之外,這些宏從0.6版本就存在了。

1.6 構造信號事件

libevent 也可以監測 POSIX 風格的信號。要構造信號處理器,使用:

接口

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

除了提供一個信號編號代替文件描述符之外,各個參數與 event_new()相同。

示例

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

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

注意:信號回調是信號發生後在事件循環中被執行的,所以可以安全地調用通常不能在POSIX 風格信號處理器中使用的函數。

警告:不要在信號事件上設置超時,這可能是不被支持的。[待修正:真是這樣的嗎?]

libevent 也提供了一組方便使用的宏用於處理信號事件:

接口

#define evsignal_add(ev, tv) \
    event_add((ev),(tv))
#define evsignal_del(ev) \
    event_del(ev)
#define evsignal_pending(ev, what, tv_out) \
    event_pending((ev), (what), (tv_out))

evsignal_*宏從2.0.1-alpha 版本開始存在。先前版本中這些宏叫做 signal_add()、signal_del ()等等。

關於信號的警告
在當前版本的 libevent 和大多數後端中,每個進程任何時刻只能有一個 event_base 可以監聽信號。如果同時向兩個 event_base 添加信號事件,即使是不同的信號,也只有一個event_base 可以取得信號。kqueue 後端沒有這個限制。

1.7 設置不使用堆分配的事件

出於性能考慮或者其他原因,有時需要將事件作爲一個大結構體的一部分。對於每個事件的

使用,這可以節省:

  • 內存分配器在堆上分配小對象的開銷

  • 對 event 結構體指針取值的時間開銷

  • 如果事件不在緩存中,因爲可能的額外緩存丟失而導致的時間開銷

使用此方法可能會破壞與其他版本的Libevent的二進制兼容性,這些版本的事件結構可能具有不同的大小。

這是非常小的成本,對於大多數應用來說都沒有關係。 除非知道使用堆分配事件會嚴重降低性能,否則應該堅持使用event_new()。 如果將來的Libevent版本使用的事件結構比構建時使用的事件結構大,則使用event_assign()可能會導致難以診斷的錯誤。

接口

int event_assign(struct event *event, struct event_base *base,
    evutil_socket_t fd, short what,
    void (*callback)(evutil_socket_t, short, void *), void *arg);

除了 event 參數必須指向一個未初始化的事件之外,event_assign()的參數與 event_new ()的參數相同。成功時函數返回0,如果發生內部錯誤或者使用錯誤的參數,函數返回-1。

示例

#include <event2/event.h>
/* Watch out!  Including event_struct.h means that your code will not
 * be binary-compatible with future versions of Libevent. */
#include <event2/event_struct.h>
#include <stdlib.h>

struct event_pair {
         evutil_socket_t fd;
         struct event read_event;
         struct event write_event;
};
void readcb(evutil_socket_t, short, void *);
void writecb(evutil_socket_t, short, void *);
struct event_pair *event_pair_new(struct event_base *base, evutil_socket_t fd)
{
        struct event_pair *p = malloc(sizeof(struct event_pair));
        if (!p) return NULL;
        p->fd = fd;
        event_assign(&p->read_event, base, fd, EV_READ|EV_PERSIST, readcb, p);
        event_assign(&p->write_event, base, fd, EV_WRITE|EV_PERSIST, writecb, p);
        return p;
}

也可以用 event_assign()初始化棧上分配的,或者靜態分配的事件。

警告

不要對已經在 event_base 中未決的事件調用 event_assign(),這可能會導致難以診斷的錯誤。如果已經初始化和成爲未決的,調用 event_assign()之前需要調用 event_del()。libevent 提供了方便的宏將event_assign()用於僅超時事件或者信號事件。

接口

#define evtimer_assign(event, base, callback, arg) \
    event_assign(event, base, -1, 0, callback, arg)
#define evsignal_assign(event, base, signum, callback, arg) \
    event_assign(event, base, signum, EV_SIGNAL|EV_PERSIST, callback, arg)

如果需要使用 event_assign(),又要保持與將來版本 libevent 的二進制兼容性,可以請求libevent 告知 struct event 在運行時應該有多大:

接口

size_t event_get_struct_event_size(void);

這個函數返回需要爲 event 結構體保留的字節數。再次強調,只有在確信堆分配是一個嚴重的性能問題時才應該使用這個函數,因爲這個函數讓代碼難以閱讀和編寫。

注意,將來版本的 event_get_struct_event_size()的返回值可能比 sizeof(struct event)小,這表示 event 結構體末尾的額外字節僅僅是保留用於將來版本 libevent 的填充字節。

下面這個例子跟上面的那個相同,但是不依賴於 event_struct.h 中的 event 結構體的大小,而是使用 event_get_struct_size()來獲取運行時的正確大小。

示例

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

/* When we allocate an event_pair in memory, we'll actually allocate
 * more space at the end of the structure.  We define some macros
 * to make accessing those events less error-prone. */
struct event_pair {
         evutil_socket_t fd;
};

/* Macro: yield the struct event 'offset' bytes from the start of 'p' */
#define EVENT_AT_OFFSET(p, offset) \
            ((struct event*) ( ((char*)(p)) + (offset) ))
/* Macro: yield the read event of an event_pair */
#define READEV_PTR(pair) \
            EVENT_AT_OFFSET((pair), sizeof(struct event_pair))
/* Macro: yield the write event of an event_pair */
#define WRITEEV_PTR(pair) \
            EVENT_AT_OFFSET((pair), \
                sizeof(struct event_pair)+event_get_struct_event_size())

/* Macro: yield the actual size to allocate for an event_pair */
#define EVENT_PAIR_SIZE() \
            (sizeof(struct event_pair)+2*event_get_struct_event_size())

void readcb(evutil_socket_t, short, void *);
void writecb(evutil_socket_t, short, void *);
struct event_pair *event_pair_new(struct event_base *base, evutil_socket_t fd)
{
        struct event_pair *p = malloc(EVENT_PAIR_SIZE());
        if (!p) return NULL;
        p->fd = fd;
        event_assign(READEV_PTR(p), base, fd, EV_READ|EV_PERSIST, readcb, p);
        event_assign(WRITEEV_PTR(p), base, fd, EV_WRITE|EV_PERSIST, writecb, p);
        return p;
}

event_assign()定義在<event2/event.h>中,從2.0.1-alpha 版本開始就存在了。從2.0.3-alpha 版本開始,函數返回 int,在這之前函數返回 void。event_get_struct_event_size()在2.0.4-alpha 版本中引入。event 結構體定義在<event2/event_struct.h>中。

2. 讓事件未決和非未決

構造事件之後,在將其添加到 event_base 之前實際上是不能對其做任何操作的。使用event_add()將事件添加到 event_base。

接口

int event_add(struct event *ev, const struct timeval *tv);

在非未決的事件上調用 event_add()將使其在配置的 event_base 中成爲未決的。成功時函數返回0,失敗時返回-1。如果 tv 爲 NULL,添加的事件不會超時。否則,tv 以秒和微秒指定超時值。

如果對已經未決的事件調用 event_add(),事件將保持未決狀態,並在指定的超時時間被重新調度。

注 意 : 不 要 設 置 tv 爲 希 望 超 時 事 件 執 行 的 時 間 。 如 果 在 2010 年 1 月 1 日 設 置 “tv->tv_sec=time(NULL)+10;”,超時事件將會等待40年,而不是10秒。

接口

int event_remove_timer(struct event *ev);

對已經初始化的事件調用 event_del()將使其成爲非未決和非激活的。如果事件不是未決 的或者激活的,調用將沒有效果。成功時函數返回0,失敗時返回-1。

注意:如果在事件激活後,其回調被執行前刪除事件,回調將不會執行。

這些函數定義在<event2/event.h>中,從0.1版本就存在了。

3. 帶優先級的事件

多個事件同時觸發時,libevent 沒有定義各個回調的執行次序。可以使用優先級來定義某些事件比其他事件更重要。
在前一章討論過,每個 event_base 有與之相關的一個或者多個優先級。在初始化事件之後,但是在添加到 event_base 之前,可以爲其設置優先級。
接口

int event_priority_set(struct event *event, int priority);

事件的優先級是一個在0和 event_base 的優先級減去1之間的數值。成功時函數返回0,失敗時返回-1。
多個不同優先級的事件同時成爲激活的時候,低優先級的事件不會運行。libevent 會執行高優先級的事件,然後重新檢查各個事件。只有在沒有高優先級的事件是激活的時候,低優先級的事件纔會運行。

示例

#include <event2/event.h>

void read_cb(evutil_socket_t, short, void *);
void write_cb(evutil_socket_t, short, void *);

void main_loop(evutil_socket_t fd)
{
  struct event *important, *unimportant;
  struct event_base *base;

  base = event_base_new();
  event_base_priority_init(base, 2);
  /* Now base has priority 0, and priority 1 */
  important = event_new(base, fd, EV_WRITE|EV_PERSIST, write_cb, NULL);
  unimportant = event_new(base, fd, EV_READ|EV_PERSIST, read_cb, NULL);
  event_priority_set(important, 0);
  event_priority_set(unimportant, 1);

  /* Now, whenever the fd is ready for writing, the write callback will
     happen before the read callback.  The read callback won't happen at
     all until the write callback is no longer active. */
}

如果不爲事件設置優先級,則默認的優先級將會是 event_base 的優先級數目除以2。
這個函數聲明在<event2/event.h>中,從1.0版本就存在了。

4. 檢查事件狀態

有時候需要了解事件是否已經添加,檢查事件代表什麼。
接口

int event_pending(const struct event *ev, short what, struct timeval *tv_out);

#define event_get_signal(ev) /* ... */
evutil_socket_t event_get_fd(const struct event *ev);
struct event_base *event_get_base(const struct event *ev);
short event_get_events(const struct event *ev);
event_callback_fn event_get_callback(const struct event *ev);
void *event_get_callback_arg(const struct event *ev);
int event_get_priority(const struct event *ev);

void event_get_assignment(const struct event *event,
        struct event_base **base_out,
        evutil_socket_t *fd_out,
        short *events_out,
        event_callback_fn *callback_out,
        void **arg_out);

event_pending()函數確定給定的事件是否是未決的或者激活的。如果是,而且 what 參數設置了 EV_READ、EV_WRITE、EV_SIGNAL 或者 EV_TIMEOUT 等標誌,則函數會返回事件當前爲之未決或者激活的所有標誌。如果提供了 tv_out 參數,並且 what 參數中設置了 EV_TIMEOUT 標誌,而事件當前正因超時事件而未決或者激活,則 tv_out 會返回事件的超時值。

event_get_fd()和 event_get_signal()返回爲事件配置的文件描述符或者信號值。

event_get_base()返回爲事件配置的 event_base。

event_get_events()返回事件的標誌(EV_READ、EV_WRITE 等)。

event_get_callback()和 event_get_callback_arg()返回事件的回調函數及其參數指針。

event_get_assignment()複製所有爲事件分配的字段到提供的指針中。任何爲 NULL 的參數會被忽略。

示例

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

/* Change the callback and callback_arg of 'ev', which must not be
 * pending. */
int replace_callback(struct event *ev, event_callback_fn new_callback,
    void *new_callback_arg)
{
    struct event_base *base;
    evutil_socket_t fd;
    short events;

    int pending;

    pending = event_pending(ev, EV_READ|EV_WRITE|EV_SIGNAL|EV_TIMEOUT,
                            NULL);
    if (pending) {
        /* We want to catch this here so that we do not re-assign a
         * pending event.  That would be very very bad. */
        fprintf(stderr,
                "Error! replace_callback called on a pending event!\n");
        return -1;
    }

    event_get_assignment(ev, &base, &fd, &events,
                         NULL /* ignore old callback */ ,
                         NULL /* ignore old callback argument */);

    event_assign(ev, base, fd, events, new_callback, new_callback_arg);
    return 0;
}

這些函數聲明在<event2/event.h>中。event_pending()函數從0.1版就存在了。2.0.1-alpha 版引入了event_get_fd()和 event_get_signal()。2.0.2-alpha 引入了 event_get_base()。其他的函數在2.0.4-alpha 版中引入。

5. 查找當前正在運行的事件

爲了調試或其他目的,您可以獲取當前運行事件的指針。

接口

struct event *event_base_get_running_event(struct event_base *base);

請注意,僅在從提供的event_base循環中調用此函數時,才定義該函數的行爲。 不支持從另一個線程調用它,這可能導致未定義的行爲。

此函數在<event2 / event.h>中聲明。 它是在Libevent 2.1.1-alpha中引入的。

6. 配置一次觸發事件

如果不需要多次添加一個事件,或者要在添加後立即刪除事件,而事件又不需要是持久的,則可以使用 event_base_once()。

接口

int event_base_once(struct event_base *, evutil_socket_t, short,
  void (*)(evutil_socket_t, short, void *), void *, const struct timeval *);

除了不支持 EV_SIGNAL 或者 EV_PERSIST 之外,這個函數的接口與 event_new()相同。安排的事件將以默認的優先級加入到 event_base 並執行。回調被執行後,libevent 內部將會釋放 event 結構。成功時函數返回0,失敗時返回-1。

不能刪除或者手動激活使用 event_base_once()插入的事件:如果希望能夠取消事件,應該使用event_new()或者 event_assign()。

另請注意,在Libevent 2.0之前的版本中,如果從未觸發該事件,則將永遠不會釋放用於保存該事件的內部存儲器。 從Libevent 2.1.2-alpha開始,釋放event_base時將釋放這些事件,即使它們尚未激活,但仍要注意:如果有一些與其回調參數相關聯的存儲,則除非釋放該存儲,否則除非 您的程序做了一些跟蹤和發佈的操作。

7. 手動激活事件

極少數情況下,需要在事件的條件沒有觸發的時候讓事件成爲激活的。

接口

void event_active(struct event *ev, int what, short ncalls);

此功能使事件ev帶有標誌what(EV_READ,EV_WRITE和EV_TIMEOUT的組合)變爲活動狀態。 事件不需要已經處於未決狀態,激活事件也不會讓它成爲未決的。

警告:在同一事件上遞歸調用event_active()可能會導致資源耗盡。 以下代碼段是如何錯誤使用event_active的示例。

錯誤的例子:使用event_active()進行無限循環

struct event *ev;

static void cb(int sock, short which, void *arg) {
        /* Whoops: Calling event_active on the same event unconditionally
           from within its callback means that no other events might not get
           run! */

        event_active(ev, EV_WRITE, 0);
}

int main(int argc, char **argv) {
        struct event_base *base = event_base_new();

        ev = event_new(base, -1, EV_PERSIST | EV_READ, cb, NULL);

        event_add(ev, NULL);

        event_active(ev, EV_WRITE, 0);

        event_base_loop(base, 0);

        return 0;
}

這將導致事件循環僅執行一次並永遠調用函數“ cb”的情況。

示例:使用計時器解決上述問題的替代方法

struct event *ev;
struct timeval tv;

static void cb(int sock, short which, void *arg) {
   if (!evtimer_pending(ev, NULL)) {
       event_del(ev);
       evtimer_add(ev, &tv);
   }
}

int main(int argc, char **argv) {
   struct event_base *base = event_base_new();

   tv.tv_sec = 0;
   tv.tv_usec = 0;

   ev = evtimer_new(base, cb, NULL);

   evtimer_add(ev, &tv);

   event_base_loop(base, 0);

   return 0;
}

示例:使用event_config_set_max_dispatch_interval()解決上述問題的替代解決方案

struct event *ev;

static void cb(int sock, short which, void *arg) {
        event_active(ev, EV_WRITE, 0);
}

int main(int argc, char **argv) {
        struct event_config *cfg = event_config_new();
        /* Run at most 16 callbacks before checking for other events. */
        event_config_set_max_dispatch_interval(cfg, NULL, 16, 0);
        struct event_base *base = event_base_new_with_config(cfg);
        ev = event_new(base, -1, EV_PERSIST | EV_READ, cb, NULL);

        event_add(ev, NULL);

        event_active(ev, EV_WRITE, 0);

        event_base_loop(base, 0);

        return 0;
}

這個函數定義在<event2/event.h>中,從0.3版本就存在了。

8. 優化公用超時

當前版本的 libevent 使用二進制堆算法跟蹤未決事件的超時值,這讓添加和刪除事件超時值具有 O(logN)性能。對於隨機分佈的超時值集合,這是優化的,但對於大量具有相同超時值的事件集合,則不是。

比如說,假定有10000個事件,每個都需要在添加後5秒觸發超時事件。這種情況下,使用雙鏈隊列實現纔可以取得 O(1)性能。

自然地,不希望爲所有超時值使用隊列,因爲隊列僅對常量超時值更快。如果超時值或多或少地隨機分佈,則向隊列添加超時值的性能將是 O(n),這顯然比使用二進制堆糟糕得多。

libevent 通過放置一些超時值到隊列中,另一些到二進制堆中來解決這個問題。要使用這個機制,需要向 libevent 請求一個“公用超時(common timeout)”值,然後使用它來添加事件。如果有大量具有單個公用超時值的事件,使用這個優化應該可以改進超時處理性能。

接口

const struct timeval *event_base_init_common_timeout(
    struct event_base *base, const struct timeval *duration);

這個函數需要 event_base 和要初始化的公用超時值作爲參數。函數返回一個到特別的timeval 結構體的指針,可以使用這個指針指示事件應該被添加到 O(1)隊列,而不是 O (logN)堆。可以在代碼中自由地複製這個特的 timeval 或者進行賦值,但它僅對用於構造它的特定 event_base 有效。不能依賴於其實際內容:libevent 使用這個內容來告知自身使用哪個隊列。

示例

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

/* We're going to create a very large number of events on a given base,
 * nearly all of which have a ten-second timeout.  If initialize_timeout
 * is called, we'll tell Libevent to add the ten-second ones to an O(1)
 * queue. */
struct timeval ten_seconds = { 10, 0 };

void initialize_timeout(struct event_base *base)
{
    struct timeval tv_in = { 10, 0 };
    const struct timeval *tv_out;
    tv_out = event_base_init_common_timeout(base, &tv_in);
    memcpy(&ten_seconds, tv_out, sizeof(struct timeval));
}

int my_event_add(struct event *ev, const struct timeval *tv)
{
    /* Note that ev must have the same event_base that we passed to
       initialize_timeout */
    if (tv && tv->tv_sec == 10 && tv->tv_usec == 0)
        return event_add(ev, &ten_seconds);
    else
        return event_add(ev, tv);
}

與所有優化函數一樣,除非確信適合使用,應該避免使用公用超時功能。

這個函數由2.0.4-alpha 版本引入。

9.從已清除的內存識別事件

libevent 提供了函數,可以從已經通過設置爲0(比如說,通過 calloc()分配的,或者使用 memset()或者 bzero()清除了的)而清除的內存識別出已初始化的事件。

接口

int event_initialized(const struct event *ev);

#define evsignal_initialized(ev) event_initialized(ev)
#define evtimer_initialized(ev) event_initialized(ev)

警告

這個函數不能可靠地從沒有初始化的內存塊中識別出已經初始化的事件。除非知道被查詢的 內存要麼是已清除的,要麼是已經初始化爲事件的,才能使用這個函數。

除非編寫一個非常特別的應用,通常不需要使用這個函數。event_new()返回的事件總是 已經初始化的。

示例

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

struct reader {
    evutil_socket_t fd;
};

#define READER_ACTUAL_SIZE() \
    (sizeof(struct reader) + \
     event_get_struct_event_size())

#define READER_EVENT_PTR(r) \
    ((struct event *) (((char*)(r))+sizeof(struct reader)))

struct reader *allocate_reader(evutil_socket_t fd)
{
    struct reader *r = calloc(1, READER_ACTUAL_SIZE());
    if (r)
        r->fd = fd;
    return r;
}

void readcb(evutil_socket_t, short, void *);
int add_reader(struct reader *r, struct event_base *b)
{
    struct event *ev = READER_EVENT_PTR(r);
    if (!event_initialized(ev))
        event_assign(ev, b, r->fd, EV_READ, readcb, r);
    return event_add(ev, NULL);
}

從Libevent 0.3開始,已經提供了event_initialized()函數。

五、輔助類型和函數

<event2/util.h>定義了很多在實現可移植應用時有用的函數,libevent 內部也使用這些類型和函數。

1. 基本類型

1.1 evutil_socket_t

在除 Windows 之外的大多數地方,套接字是個整數,操作系統按照數值次序進行處理。然而,使用 Windows 套接字 API 時,socket 具有類型 SOCKET,它實際上是個類似指針的句柄,收到這個句柄的次序是未定義的。在 Windows 中,libevent 定義 evutil_socket_t 類型爲整型指針,可以處理 socket()或者 accept()的輸出,而沒有指針截斷的風險。

定義

#ifdef WIN32
#define evutil_socket_t intptr_t
#else
#define evutil_socket_t int
#endif

這個類型在2.0.1-alpha 版本中引入。

1.2 標準整數類型

落後於21世紀的 C 系統常常沒有實現 C99標準規定的 stdint.h 頭文件。考慮到這種情況,libevent 定義了來自於 stdint.h 的、位寬度確定(bit-width-specific)的整數類型:

Type Width Signed Maximum Minimum
ev_uint64_t 64 No EV_UINT64_MAX 0
ev_int64_t 64 Yes EV_INT64_MAX EV_INT64_MIN
ev_uint32_t 32 No EV_UINT32_MAX 0
ev_int32_t 32 Yes EV_INT32_MAX EV_INT32_MIN
ev_uint16_t 16 No EV_UINT16_MAX 0
ev_int16_t 16 Yes EV_INT16_MAX EV_INT16_MIN
ev_uint8_t 8 No EV_UINT8_MAX 0
ev_int8_t 8 Yes EV_INT8_MAX EV_INT8_MIN

跟 C99標準一樣,這些類型都有明確的位寬度。

這些類型由1.4.0-alpha 版本引入。MAX/MIN 常量首次出現在2.0.4-alpha 版本。

1.3 各種兼容性類型

在有 ssize_t(有符號的 size_t)類型的平臺上,ev_ssize_t 定義爲 ssize_t;而在沒有的平臺上,則定義爲某合理的默認類型。ev_ssize_t 類型的最大可能值是 EV_SSIZE_MAX;最小可能值是 EV_SSIZE_MIN。(在平臺沒有定義 SIZE_MAX 的時候,size_t 類型的最大可能值是 EV_SIZE_MAX)

ev_off_t 用於代表文件或者內存塊中的偏移量。在有合理 off_t 類型定義的平臺,它被定義爲 off_t;在 Windows 上則定義爲 ev_int64_t。

某些套接字 API 定義了 socklen_t 長度類型,有些則沒有定義。在有這個類型定義的平臺中,ev_socklen_t 定義爲 socklen_t,在沒有的平臺上則定義爲合理的默認類型。

ev_intptr_t 是一個有符號整數類型,足夠容納指針類型而不會產生截斷;而 ev_uintptr_t 則是相應的無符號類型。

ev_ssize_t 類型由2.0.2-alpha 版本加入。ev_socklen_t 類型由2.0.3-alpha 版本加入。ev_intptr_t 與 ev_uintptr_t 類型,以及 EV_SSIZE_MAX/MIN 宏定義由2.0.4-alpha 版本加入。ev_off_t 類型首次出現在2.0.9-rc 版本。

2. 定時器可移植函數

不是每個平臺都定義了標準 timeval 操作函數,所以 libevent 也提供了自己的實現。

接口

#define evutil_timeradd(tvp, uvp, vvp) /* ... */
#define evutil_timersub(tvp, uvp, vvp) /* ... */

這些宏分別對前兩個參數進行加或者減運算,將結果存放到第三個參數中。

接口

#define evutil_timerclear(tvp) /* ... */
#define evutil_timerisset(tvp) /* ... */

清除 timeval 會將其值設置爲0。evutil_timerisset 宏檢查 timeval 是否已經設置,如果已經設置爲非零值,返回 ture,否則返回 false。

接口

#define evutil_timercmp(tvp, uvp, cmp)

evutil_timercmp 宏比較兩個 timeval,如果其關係滿足 cmp 關係運算符,返回 true。比如說,evutil_timercmp(t1,t2,<=)的意思是“是否 t1<=t2?”。注意:與某些操作系統版本不同的是,libevent 的時間比較支持所有 C 關係運算符(也就是<、>、==、!=、<=和>=)。

接口

int evutil_gettimeofday(struct timeval *tv, struct timezone *tz);

evutil_gettimeofdy()函數設置 tv 爲當前時間,tz 參數未使用。

示例

struct timeval tv1, tv2, tv3;

/* Set tv1 = 5.5 seconds */
tv1.tv_sec = 5; tv1.tv_usec = 500*1000;

/* Set tv2 = now */
evutil_gettimeofday(&tv2, NULL);

/* Set tv3 = 5.5 seconds in the future */
evutil_timeradd(&tv1, &tv2, &tv3);

/* all 3 should print true */
if (evutil_timercmp(&tv1, &tv1, ==))  /* == "If tv1 == tv1" */
   puts("5.5 sec == 5.5 sec");
if (evutil_timercmp(&tv3, &tv2, >=))  /* == "If tv3 >= tv2" */
   puts("The future is after the present.");
if (evutil_timercmp(&tv1, &tv2, <))   /* == "If tv1 < tv2" */
   puts("It is no longer the past.");

除 evutil_gettimeofday()由2.0版本引入外,這些函數由1.4.0-beta 版本引入。

注意:在1.4.4之前的版本中使用<=或者>=是不安全的。

3. 套接字 API 兼容性

本節由於歷史原因而存在:Windows 從來沒有以良好兼容的方式實現 Berkeley 套接字 API。

接口

int evutil_closesocket(evutil_socket_t s);

#define EVUTIL_CLOSESOCKET(s) evutil_closesocket(s)

這個接口用於關閉套接字。在 Unix 中,它是 close()的別名;在 Windows 中,它調用closesocket()。(在 Windows 中不能將 close()用於套接字,也沒有其他系統定義 了closesocket())

evutil_closesocket() 函 數 在 2.0.5-alpha 版 本 引 入 。 在 此 之 前 , 需 要 使 用EVUTIL_CLOSESOCKET 宏。

接口

#define EVUTIL_SOCKET_ERROR()
#define EVUTIL_SET_SOCKET_ERROR(errcode)
#define evutil_socket_geterror(sock)
#define evutil_socket_error_to_string(errcode)

這些宏訪問和操作套接字錯誤代碼。

EVUTIL_SOCKET_ERROR()返回本線程最後一次套接字操作的全局錯誤號

evutil_socket_geterror()則返回某特定套接字的錯誤號。(在類 Unix 系統中都是 errno)

EVUTIL_SET_SOCKET_ERROR()修改當前套接字錯誤號(與設置 Unix 中的 errno 類似)

evutil_socket_error_to_string()返回代表某給定套接字錯誤號的字符串(與 Unix 中的 strerror()類似)。

(因爲對於來自套接字函數的錯誤,Windows 不使用 errno,而是使用 WSAGetLastError(),
所以需要這些函數。)

注意:Windows 套接字錯誤與從 errno 看到的標準 C 錯誤是不同的。

接口

int evutil_make_socket_nonblocking(evutil_socket_t sock);

用 於 對 套 接 字 進 行 非 阻 塞 IO 的 調 用 也 不 能 移 植 到 Windows 中 。evutil_make_socket_nonblocking()函數要求一個套接字(來自 socket()或者 accept())作爲參數,將其設置爲非阻塞的。(設置Unix中O_NONBLOCK標誌和Windows中的FIONBIO標誌)

接口

int evutil_make_listen_socket_reuseable(evutil_socket_t sock);

這個函數確保關閉監聽套接字後,它使用的地址可以立即被另一個套接字使用 。(在 Unix中它設置 SO_REUSEADDR 標誌,在 Windows 中則不做任何操作。不能在 Windows 中使用 SO_REUSEADDR 標誌:它有另外不同的含義)(多個套接字綁定到相同地址)。

接口

int evutil_make_socket_closeonexec(evutil_socket_t sock);

這個函數告訴操作系統,如果調用了 exec(),應該關閉指定的套接字。在 Unix 中函數設置 FD_CLOEXEC 標誌,在 Windows 上則沒有操作。

接口

int evutil_socketpair(int family, int type, int protocol, evutil_socket_t sv[2]);

這個函數的行爲跟 Unix 的 socketpair()調用相同:創建兩個相互連接起來的套接字,可對其使用普通套接字 IO 調用。函數將兩個套接字存儲在 sv[0]和 sv[1]中,成功時返回0,失敗時返回-1。

在 Windows 中,這個函數僅能支持 AF_INET 協議族、SOCK_STREAM 類型和0協議的套接字。注意:在防火牆軟件明確阻止127.0.0.1,禁止主機與自身通話的情況下,函數可能失敗。

除了 evutil_make_socket_closeonexec()由2.0.4-alpha 版本引入外,這些函數都由1.4.0-alpha 版本引入。

4. 可移植的字符串操作函數

接口

ev_int64_t evutil_strtoll(const char *s, char **endptr, int base);

這個函數與 strtol 行爲相同,只是用於64位整數。在某些平臺上,僅支持十進制。

接口

int evutil_snprintf(char *buf, size_t buflen, const char *format, ...);
int evutil_vsnprintf(char *buf, size_t buflen, const char *format, va_list ap);

這些 snprintf 替代函數的行爲與標準 snprintf 和 vsnprintf 接口相同。函數返回在緩衝區足夠長的情況下將寫入的字節數,不包括結尾的 NULL 字節。(這個行爲遵循 C99的 snprintf()標準,但與 Windows 的_snprintf()相反:如果字符串無法放入緩衝區,_snprintf()會返回負數)

evutil_strtoll()從1.4.2-rc 版本就存在了,其他函數首次出現在1.4.5版本中。

5. 區域無關的字符串操作函數

實現基於 ASCII 的協議時,可能想要根據字符類型的 ASCII 記號來操作字符串,而不管當前的區域設置。libevent 爲此提供了一些函數:

接口

int evutil_ascii_strcasecmp(const char *str1, const char *str2);
int evutil_ascii_strncasecmp(const char *str1, const char *str2, size_t n);

這些函數與 strcasecmp()和 strncasecmp()的行爲類似,只是它們總是使用 ASCII 字符集進行比較,而不管當前的區域設置。這兩個函數首次在2.0.3-alpha 版本出現。

6. IPv6輔助和兼容性函數

接口

const char *evutil_inet_ntop(int af, const void *src, char *dst, size_t len);
int evutil_inet_pton(int af, const char *src, void *dst);

這些函數根據 RFC 3493的規定解析和格式化 IPv4與 IPv6地址,與標準 inet_ntop()和inet_pton()函數行爲相同。

要格式化 IPv4地址,調用 evutil_inet_ntop(),設置 af 爲 AF_INET,src 指向 in_addr 結構體,dst 指向大小爲 len 的字符緩衝區。對於 IPv6地址,af 應該是AF_INET6,src 則指向 in6_addr 結構體。

要解析 IP 地址,調用 evutil_inet_pton(),設置af 爲 AF_INET 或者 AF_INET6,src 指向要解析的字符串,dst 指向一個 in_addr 或者in_addr6結構體。

失敗時 evutil_inet_ntop()返回 NULL,成功時返回到 dst 的指針。成功時 evutil_inet_pton()返回0,失敗時返回-1。

接口

int evutil_parse_sockaddr_port(const char *str, struct sockaddr *out, int *outlen);

這個接口解析來自 str 的地址,將結果寫入到 out 中。outlen 參數應該指向一個表示 out 中 可用字節數的整數;函數返回時這個整數將表示實際使用了的字節數。成功時函數返回0, 失敗時返回-1。

函數識別下列地址格式:

  • ipv6(如 ffff::)

  • ipv6(如[ffff::])

  • ipv4:端口號(如1.2.3.4:80)

  • ipv4(如1.2.3.4)

如果沒有給出端口號,結果中的端口號將被設置爲0。

接口

int evutil_sockaddr_cmp(const struct sockaddr *sa1,
                        const struct sockaddr *sa2, int include_port);

evutil_sockaddr_cmp()函數比較兩個地址,如果 sa1在 sa2前面,返回負數;如果二者相等,則返回0;如果 sa2在 sa1前面,則返回正數。函數可用於 AF_INET 和 AF_INET6地址;對於其他地址,返回值未定義。函數確保考慮地址的完整次序,但是不同版本中的次序可能不同。

如果 include_port 參數爲 false,而兩個地址只有端口號不同,則它們被認爲是相等的。否則,具有不同端口號的地址被認爲是不等的。

除 evutil_sockaddr_cmp()在2.0.3-alpha 版本引入外,這些函數在2.0.1-alpha 版本中引入。

7. 結構體可移植性函數

接口

#define evutil_offsetof(type, field) /* ... */

跟標準 offsetof 宏一樣,這個宏返回從 type 類型開始處到 field 字段的字節數。

這個宏由2.0.1-alpha 版本引入,但2.0.3-alpha 版本之前是有 bug 的。

8. 安全隨機數發生器

很多應用(包括 evdns)爲了安全考慮需要很難預測的隨機數。

接口

void evutil_secure_rng_get_bytes(void *buf, size_t n);

這個函數用隨機數據填充 buf 處的 n 個字節。

如果所在平臺提供了 arc4random(),libevent 會使用這個函數。否則,libevent 會使用自己的 arc4random()實現,種子則來自操作系統的熵池(entropy pool)(Windows 中的CryptGenRandom,其他平臺中的/dev/urandom)

接口

int evutil_secure_rng_init(void);
void evutil_secure_rng_add_bytes(const char *dat, size_t datlen);

不需要手動初始化安全隨機數發生器,但是如果要確認已經成功初始化,可以調 用evutil_secure_rng_init()。函數會播種 RNG(如果沒有播種過),並在成功時返回0。函數返回-1則表示 libevent 無法在操作系統中找到合適的熵源(source of entropy),如果不自己初始化 RNG,就無法安全使用 RNG 了。

如果程序可能會放棄特權的環境中運行(例如,通過運行chroot()),則應先調evutil_secure_rng_init()。

您可以自己調用evutil_secure_rng_add_bytes();將更多的隨機字節添加到熵池中。 通常這不是必需的。

這些功能是Libevent 2.0.4-alpha中的新增功能。

六:bufferevent:概念和入門

很多時候,除了響應事件之外,應用還希望做一定的數據緩衝。比如說,寫入數據的時候, 通常的運行模式是:

  • 決定要向連接寫入一些數據,把數據放入到緩衝區中

  • 等待連接可以寫入

  • 寫入儘量多的數據

  • 記住寫入了多少數據,如果還有更多數據要寫入,等待連接再次可以寫入

這種緩衝 IO 模式很通用,libevent 爲此提供了一種通用機制,即 bufferevent。bufferevent 由一個底層的傳輸端口(如套接字),一個讀取緩衝區和一個寫入緩衝區組成。與通常的事件在底層傳輸端口已經就緒,可以讀取或者寫入的時候執行回調不同的是,bufferevent 在讀取或者寫入了足夠量的數據之後調用用戶提供的回調。

有多種共享公用接口的 bufferevent 類型,編寫本文時已存在以下類型:

  • 基於套接字的 bufferevent (socket-based bufferevents):使用 event_*接口作爲後端,通過底層流式套接字發送或者 接收數據的 bufferevent

  • 異步 IO bufferevent (asynchronous-IO bufferevents):使用 Windows IOCP 接口,通過底層流式套接字發送或者接收數 據的 bufferevent(僅用於 Windows,試驗中)

  • 過濾型 bufferevent (filtering bufferevents):將數據傳輸到底層 bufferevent 對象之前,處理輸入或者輸出數據 的 bufferevent:比如說,爲了壓縮或者轉換數據。

  • 成對的 bufferevent (paired bufferevents):相互傳輸數據的兩個 bufferevent。

**注意:**截止2.0.2-alpha版,這裏列出的bufferevent接口還沒有完全正交於所有的bufferevent 類型。也就是說,下面將要介紹的接口不是都能用於所有 bufferevent 類型。libevent 開發者在未來版本中將修正這個問題。

也請注意:當前 bufferevent 只能用於像 TCP 這樣的面向流的協議,將來纔可能會支持像UDP 這樣的面向數據報的協議。

本節描述的所有函數和類型都在 event2/bufferevent.h 中聲明。特別提及的關於 evbuffer 的函數聲明在 event2/buffer.h 中,詳細信息請參考下一章。

1. bufferevent 和 evbuffer

每個 bufferevent 都有一個輸入緩衝區和一個輸出緩衝區,它們的類型都是“struct evbuffer”。

有數據要寫入到 bufferevent 時,添加數據到輸出緩衝區;bufferevent 中有數據供讀取的時候,從輸入緩衝區抽取(drain)數據。

evbuffer 接口支持很多種操作,後面的章節將討論這些操作。

2. 回調和水位

每個 bufferevent 有兩個數據相關的回調:一個讀取回調和一個寫入回調。默認情況下,從底層傳輸端口讀取了任意量的數據之後會調用讀取回調;輸出緩衝區中足夠量的數據被清空到底層傳輸端口後寫入回調會被調用。通過調整 bufferevent 的讀取和寫入 “水位(watermarks)”可以覆蓋這些函數的默認行爲。

每個 bufferevent 有四個水位:

  • 讀取低水位:讀取操作使得輸入緩衝區的數據量在此級別或者更高時,讀取回調將被調用。默認值爲0,所以每個讀取操作都會導致讀取回調被調用。
  • 讀取高水位:輸入緩衝區中的數據量達到此級別後,bufferevent 將停止讀取,直到輸入緩衝區中足夠量的數據被抽取,使得數據量低於此級別。默認值是無限,所以永遠不會因爲輸入緩衝區的大小而停止讀取。
  • 寫入低水位:寫入操作使得輸出緩衝區的數據量達到或者低於此級別時,寫入回調將被調用。默認值是0,所以只有輸出緩衝區空的時候纔會調用寫入回調。
  • 寫入高水位:bufferevent 沒有直接使用這個水位。它在 bufferevent 用作另外一個bufferevent 的底層傳輸端口時有特殊意義。請看後面關於過濾型 bufferevent 的介紹。

bufferevent 也有“錯誤”或者“事件”回調,用於嚮應用通知非面向數據的事件,如連接已經關閉或者發生錯誤。定義了下列事件標誌:

  • BEV_EVENT_READING:讀取操作時發生某事件,具體是哪種事件請看其他標誌。
  • BEV_EVENT_WRITING:寫入操作時發生某事件,具體是哪種事件請看其他標誌。
  • BEV_EVENT_ERROR : 操作時發生錯誤 。關於錯誤的更多信息 ,EVUTIL_SOCKET_ERROR()。
  • BEV_EVENT_TIMEOUT:發生超時。
  • BEV_EVENT_EOF:遇到文件結束指示。
  • BEV_EVENT_CONNECTED:請求的連接過程已經完成。

上述標誌由2.0.2-alpha 版新引入。

3. 延遲迴調

默認情況下,bufferevent 的回調在相應的條件發生時立即被執行。(evbuffer 的回調也是這樣的,隨後會介紹)在依賴關係複雜的情況下,這種立即調用會製造麻煩。比如說,假如某個回調在 evbuffer A 空的時候向其中移入數據,而另一個回調在 evbuffer A 滿的時候從中取出數據。這些調用都是在棧上發生的,在依賴關係足夠複雜的時候,有棧溢出的風險。

要解決此問題,可以請求 bufferevent(或者 evbuffer)延遲其回調。條件滿足時,延遲迴調不會立即調用,而是在 event_loop()調用中被排隊,然後在通常的事件回調之後執行。

延遲迴調由 libevent 2.0.1-alpha 版引入。

4. bufferevent 的選項標誌

創建 bufferevent 時可以使用一個或者多個標誌修改其行爲。可識別的標誌有:

  • BEV_OPT_CLOSE_ON_FREE:釋放 bufferevent 時關閉底層傳輸端口。這將關閉底層套接字,釋放底層 bufferevent 等。
  • BEV_OPT_THREADSAFE:自動爲 bufferevent 分配鎖,這樣就可以安全地在多個線程中使用 bufferevent。
  • BEV_OPT_DEFER_CALLBACKS:設置這個標誌時,bufferevent 延遲所有回調,如上所述。
  • BEV_OPT_UNLOCK_CALLBACKS:默認情況下,如果設置 bufferevent 爲線程安全的,則 bufferevent 會在調用用戶提供的回調時進行鎖定。設置這個選項會讓 libevent在執行回調的時候不進行鎖定。

BEV_OPT_UNLOCK_CALLBACKS 由2.0.5-beta 版引入,其他選項由2.0.1-alpha 版引入。

5. 與基於套接字的 bufferevent 一起工作

基於套接字的 bufferevent 是最簡單的,它使用 libevent 的底層事件機制來檢測底層網絡套接字是否已經就緒,可以進行讀寫操作,並且使用底層網絡調用(如 readv、writev、WSASend、WSARecv)來發送和接收數據。

5.1 創建基於套接字的 bufferevent

可以使用 bufferevent_socket_new()創建基於套接字的 bufferevent。

接口

struct bufferevent *bufferevent_socket_new(
    struct event_base *base,
    evutil_socket_t fd,
    enum bufferevent_options options);

base 是 event_base,options 是表示 bufferevent 選項(BEV_OPT_CLOSE_ON_FREE 等)的位掩碼,fd 是一個可選的表示套接字的文件描述符。如果想以後設置文件描述符,可以設置 fd 爲-1。

注意:
[請確保您提供給bufferevent_socket_new的套接字處於非阻塞模式。 Libevent爲此提供了便捷方法evutil_make_socket_nonblocking。]

成功時函數返回一個 bufferevent,失敗則返回 NULL。

bufferevent_socket_new()函數由2.0.1-alpha 版新引入。

5.2 在基於套接字的 bufferevent 上啓動連接

如果 bufferevent 的套接字還沒有連接上,可以啓動新的連接。

接口

int bufferevent_socket_connect(struct bufferevent *bev,
    struct sockaddr *address, int addrlen);

address 和 addrlen 參數跟標準調用 connect()的參數相同。如果還沒有爲 bufferevent 設置套接字,調用函數將爲其分配一個新的流套接字,並且設置爲非阻塞的。

如果已經爲 bufferevent 設置套接字,調用 bufferevent_socket_connect()將告知 libevent套接字還未連接,直到連接成功之前不應該對其進行讀取或者寫入操作。

連接完成之前可以向輸出緩衝區添加數據。

如果連接成功啓動,函數返回0;如果發生錯誤則返回-1。

示例

#include <event2/event.h>
#include <event2/bufferevent.h>
#include <sys/socket.h>
#include <string.h>

void eventcb(struct bufferevent *bev, short events, void *ptr)
{
    if (events & BEV_EVENT_CONNECTED) {
         /* We're connected to 127.0.0.1:8080.   Ordinarily we'd do
            something here, like start reading or writing. */
    } else if (events & BEV_EVENT_ERROR) {
         /* An error occured while connecting. */
    }
}

int main_loop(void)
{
    struct event_base *base;
    struct bufferevent *bev;
    struct sockaddr_in sin;

    base = event_base_new();

    memset(&sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = htonl(0x7f000001); /* 127.0.0.1 */
    sin.sin_port = htons(8080); /* Port 8080 */

    bev = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE);

    bufferevent_setcb(bev, NULL, NULL, eventcb, NULL);

    if (bufferevent_socket_connect(bev,
        (struct sockaddr *)&sin, sizeof(sin)) < 0) {
        /* Error starting connection */
        bufferevent_free(bev);
        return -1;
    }

    event_base_dispatch(base);
    return 0;
}

bufferevent_socket_connect()函數由2.0.2-alpha 版引入。在此之前,必須自己手動在套接字上調用 connect(),連接完成時,bufferevent 將報告寫入事件。

注 意 : 如 果 使 用 bufferevent_socket_connect() 發 起 連 接 , 將 只 會 收 到BEV_EVENT_CONNECTED 事件。如果自己調用 connect(),則連接上將被報告爲寫入事件。

如果想自己調用connect(),但在連接成功時仍然會收到BEV_EVENT_CONNECTED事件,請在connect()返回errno等於EAGAIN或EINPROGRESS的-1之後,調用bufferevent_socket_connect(bev,NULL,0)。

此功能在Libevent 2.0.2-alpha中引入。

5.3 通過主機名啓動連接

常常需要將解析主機名和連接到主機合併成單個操作,libevent 爲此提供了:

接口

int bufferevent_socket_connect_hostname(struct bufferevent *bev,
    struct evdns_base *dns_base, int family, const char *hostname,
    int port);
int bufferevent_socket_get_dns_error(struct bufferevent *bev);

這 個 函 數 解 析 名 字 hostname , 查 找 其 family 類 型 的 地 址 (允許的地址族類型有AF_INET,AF_INET6和 AF_UNSPEC)。如果名字解析失敗,函數將調用事件回調,報告錯誤事件。如果解析成功,函數將啓動連接請求,就像 bufferevent_socket_connect()一樣。

dns_base 參數是可選的:如果爲 NULL,等待名字查找完成期間調用線程將被阻塞,而這通常不是期望的行爲;如果提供 dns_base 參數,libevent 將使用它來異步地查詢主機名。關於 DNS 的更多信息,請看第九章。

跟 bufferevent_socket_connect()一樣,函數告知 libevent,bufferevent 上現存的套接字還沒有連接,在名字解析和連接操作成功完成之前,不應該對套接字進行讀取或者寫入操作。

函數返回的錯誤可能是DNS主機名查詢錯誤,可以調用bufferevent_socket_get_dns_error()來獲取最近的錯誤。返回值0表示沒有檢測到 DNS 錯誤。

示例:簡單的 HTTP v0客戶端

/* Don't actually copy this code: it is a poor way to implement an
   HTTP client.  Have a look at evhttp instead.
*/
#include <event2/dns.h>
#include <event2/bufferevent.h>
#include <event2/buffer.h>
#include <event2/util.h>
#include <event2/event.h>

#include <stdio.h>

void readcb(struct bufferevent *bev, void *ptr)
{
    char buf[1024];
    int n;
    struct evbuffer *input = bufferevent_get_input(bev);
    while ((n = evbuffer_remove(input, buf, sizeof(buf))) > 0) {
        fwrite(buf, 1, n, stdout);
    }
}

void eventcb(struct bufferevent *bev, short events, void *ptr)
{
    if (events & BEV_EVENT_CONNECTED) {
         printf("Connect okay.\n");
    } else if (events & (BEV_EVENT_ERROR|BEV_EVENT_EOF)) {
         struct event_base *base = ptr;
         if (events & BEV_EVENT_ERROR) {
                 int err = bufferevent_socket_get_dns_error(bev);
                 if (err)
                         printf("DNS error: %s\n", evutil_gai_strerror(err));
         }
         printf("Closing\n");
         bufferevent_free(bev);
         event_base_loopexit(base, NULL);
    }
}

int main(int argc, char **argv)
{
    struct event_base *base;
    struct evdns_base *dns_base;
    struct bufferevent *bev;

    if (argc != 3) {
        printf("Trivial HTTP 0.x client\n"
               "Syntax: %s [hostname] [resource]\n"
               "Example: %s www.google.com /\n",argv[0],argv[0]);
        return 1;
    }

    base = event_base_new();
    dns_base = evdns_base_new(base, 1);

    bev = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE);
    bufferevent_setcb(bev, readcb, NULL, eventcb, base);
    bufferevent_enable(bev, EV_READ|EV_WRITE);
    evbuffer_add_printf(bufferevent_get_output(bev), "GET %s\r\n", argv[2]);
    bufferevent_socket_connect_hostname(
        bev, dns_base, AF_UNSPEC, argv[1], 80);
    event_base_dispatch(base);
    return 0;
}

bufferevent_socket_connect_hostname()函數是Libevent 2.0.3-alpha中的新增功能;

bufferevent_socket_get_dns_error()是2.0.5-beta中的新增功能。

6. 通用 bufferevent 操作

本節描述的函數可用於多種 bufferevent 實現。

6.1 釋放 bufferevent

接口

void bufferevent_free(struct bufferevent *bev);

這個函數釋放 bufferevent。bufferevent 內部具有引用計數,所以,如果釋放 bufferevent 時還有未決的延遲迴調,則在回調完成之前 bufferevent 不會被刪除。

但是,bufferevent_free()函數確實會嘗試儘快釋放bufferevent。 如果存在要在bufferevent上寫入的待處理數據,則在釋放bufferevent之前可能不會刷新該數據。

如果設置了 BEV_OPT_CLOSE_ON_FREE 標誌,並且 bufferevent 有一個套接字或者底層bufferevent 作爲其傳輸端口,則釋放 bufferevent 將關閉這個傳輸端口。

這個函數由 libevent 0.8版引入。

6.2 操作回調、水位和啓用/禁用

接口

typedef void (*bufferevent_data_cb)(struct bufferevent *bev, void *ctx);
typedef void (*bufferevent_event_cb)(struct bufferevent *bev,
    short events, void *ctx);

void bufferevent_setcb(struct bufferevent *bufev,
    bufferevent_data_cb readcb, bufferevent_data_cb writecb,
    bufferevent_event_cb eventcb, void *cbarg);

void bufferevent_getcb(struct bufferevent *bufev,
    bufferevent_data_cb *readcb_ptr,
    bufferevent_data_cb *writecb_ptr,
    bufferevent_event_cb *eventcb_ptr,
    void **cbarg_ptr);

bufferevent_setcb()函數修改 bufferevent 的一個或者多個回調。readcb、writecb 和 eventcb 函數將分別在已經讀取足夠的數據、已經寫入足夠的數據,或者發生錯誤時被調用。每個回調 函 數 的 第 一 個 參 數 都 是 發 生 了 事 件 的 bufferevent , 最 後 一 個參數都是調用bufferevent_setcb()時用戶提供的 cbarg 參數:可以通過它向回調傳遞數據。事件回調的events 參數是一個表示事件標誌的位掩碼:請看前面的“回調和水位”節。

要禁用回調,傳遞NULL而不是回調函數。注意:bufferevent的所有回調函數共享單個cbarg, 所以修改它將影響所有回調函數。

可以通過將指針傳遞給bufferevent_getcb()來檢索bufferevent的當前設置的回調,該指針將* readcb_ptr設置爲當前的讀回調,將* writecb_ptr設置爲當前的寫回調,將* eventcb_ptr設置爲當前事件的回調,並將* cbarg_ptr設置爲當前的回調 回調參數字段。 這些指針中的任何一個設置爲NULL都將被忽略。

Libevent 1.4.4中引入了bufferevent_setcb()函數。 在Libevent 2.0.2-alpha中,類型名稱“ bufferevent_data_cb”和“ bufferevent_event_cb”是新的。 在2.1.1-alpha中添加了bufferevent_getcb()函數。

接口

void bufferevent_enable(struct bufferevent *bufev, short events);
void bufferevent_disable(struct bufferevent *bufev, short events);

short bufferevent_get_enabled(struct bufferevent *bufev);

可以啓用或者禁用 bufferevent 上的 EV_READ、EV_WRITE 或者 EV_READ | EV_WRITE 事件。沒有啓用讀取或者寫入事件時,bufferevent 將不會試圖進行數據讀取或者寫入。

沒有必要在輸出緩衝區空時禁用寫入事件:bufferevent 將自動停止寫入,然後在有數據等待寫入時重新開始。

類似地,沒有必要在輸入緩衝區高於高水位時禁用讀取事件:bufferevent 將自動停止讀取,然後在有空間用於讀取時重新開始讀取。

默認情況下,新創建的 bufferevent 的寫入是啓用的,但是讀取沒有啓用。

可以調用 bufferevent_get_enabled()確定 bufferevent 上當前啓用的事件。

除了 bufferevent_get_enabled()由2.0.3-alpha 版引入外,這些函數都由0.8版引入。

接口

void bufferevent_setwatermark(struct bufferevent *bufev, short events,
    size_t lowmark, size_t highmark);

bufferevent_setwatermark()函數調整單個 bufferevent 的讀取水位、寫入水位,或者同時調整二者。(如果events參數設置了EV_READ,調整讀取水位。如果events設置了EV_WRITE標誌,調整寫入水位)

對於高水位,0表示“無限”。

這個函數首次出現在1.4.4版。

示例

#include <event2/event.h>
#include <event2/bufferevent.h>
#include <event2/buffer.h>
#include <event2/util.h>

#include <stdlib.h>
#include <errno.h>
#include <string.h>

struct info {
    const char *name;
    size_t total_drained;
};

void read_callback(struct bufferevent *bev, void *ctx)
{
    struct info *inf = ctx;
    struct evbuffer *input = bufferevent_get_input(bev);
    size_t len = evbuffer_get_length(input);
    if (len) {
        inf->total_drained += len;
        evbuffer_drain(input, len);
        printf("Drained %lu bytes from %s\n",
             (unsigned long) len, inf->name);
    }
}

void event_callback(struct bufferevent *bev, short events, void *ctx)
{
    struct info *inf = ctx;
    struct evbuffer *input = bufferevent_get_input(bev);
    int finished = 0;

    if (events & BEV_EVENT_EOF) {
        size_t len = evbuffer_get_length(input);
        printf("Got a close from %s.  We drained %lu bytes from it, "
            "and have %lu left.\n", inf->name,
            (unsigned long)inf->total_drained, (unsigned long)len);
        finished = 1;
    }
    if (events & BEV_EVENT_ERROR) {
        printf("Got an error from %s: %s\n",
            inf->name, evutil_socket_error_to_string(EVUTIL_SOCKET_ERROR()));
        finished = 1;
    }
    if (finished) {
        free(ctx);
        bufferevent_free(bev);
    }
}

struct bufferevent *setup_bufferevent(void)
{
    struct bufferevent *b1 = NULL;
    struct info *info1;

    info1 = malloc(sizeof(struct info));
    info1->name = "buffer 1";
    info1->total_drained = 0;

    /* ... Here we should set up the bufferevent and make sure it gets
       connected... */

    /* Trigger the read callback only whenever there is at least 128 bytes
       of data in the buffer. */
    bufferevent_setwatermark(b1, EV_READ, 128, 0);

    bufferevent_setcb(b1, read_callback, NULL, event_callback, info1);

    bufferevent_enable(b1, EV_READ); /* Start reading. */
    return b1;
}
6.3 操作 bufferevent 中的數據

如果只是通過網絡讀取或者寫入數據,而不能觀察操作過程,是沒什麼好處的。bufferevent提供了下列函數用於觀察要寫入或者讀取的數據。

接口

struct evbuffer *bufferevent_get_input(struct bufferevent *bufev);
struct evbuffer *bufferevent_get_output(struct bufferevent *bufev);

這兩個函數提供了非常強大的基礎:它們分別返回輸入和輸出緩衝區。關於可以對 evbuffer類型進行的所有操作的完整信息,請看下一章。

請注意,應用程序只能刪除(不添加)輸入緩衝區中的數據,並且只能添加(不刪除)輸出緩衝區中的數據。

如果寫入操作因爲數據量太少而停止(或者讀取操作因爲太多數據而停止),則向輸出緩衝區添加數據(或者從輸入緩衝區移除數據)將自動重啓操作。

這些函數由2.0.1-alpha 版引入。

接口

int bufferevent_write(struct bufferevent *bufev,
    const void *data, size_t size);
int bufferevent_write_buffer(struct bufferevent *bufev,
    struct evbuffer *buf);

這些函數向 bufferevent 的輸出緩衝區添加數據。

bufferevent_write()將內存中從 data 處開始的 size 字節數據添加到輸出緩衝區的末尾。

bufferevent_write_buffer()移除 buf 的所有內容,將其放置到輸出緩衝區的末尾。成功時這些函數都返回0,發生錯誤時則返回-1。

這些函數從0.8版就存在了。

接口

size_t bufferevent_read(struct bufferevent *bufev, void *data, size_t size);
int bufferevent_read_buffer(struct bufferevent *bufev,
    struct evbuffer *buf);

這些函數從 bufferevent 的輸入緩衝區移除數據。

bufferevent_read()至多從輸入緩衝區移除size 字 節 的 數 據 , 將 其 存 儲 到 內 存 中 data 處 。函 數 返 回 實 際 移 除 的 字 節 數 。

bufferevent_read_buffer()函數抽空輸入緩衝區的所有內容,將其放置到 buf 中,成功時返回0,失敗時返回-1。

注意,對於 bufferevent_read(),data 處的內存塊必須有足夠的空間容納 size 字節數據。

bufferevent_read()函數從0.8版就存在了;bufferevnet_read_buffer()由2.0.1-alpha 版引入。

示例

#include <event2/bufferevent.h>
#include <event2/buffer.h>

#include <ctype.h>

void
read_callback_uppercase(struct bufferevent *bev, void *ctx)
{
        /* This callback removes the data from bev's input buffer 128
           bytes at a time, uppercases it, and starts sending it
           back.

           (Watch out!  In practice, you shouldn't use toupper to implement
           a network protocol, unless you know for a fact that the current
           locale is the one you want to be using.)
         */

        char tmp[128];
        size_t n;
        int i;
        while (1) {
                n = bufferevent_read(bev, tmp, sizeof(tmp));
                if (n <= 0)
                        break; /* No more data. */
                for (i=0; i<n; ++i)
                        tmp[i] = toupper(tmp[i]);
                bufferevent_write(bev, tmp, n);
        }
}

struct proxy_info {
        struct bufferevent *other_bev;
};
void
read_callback_proxy(struct bufferevent *bev, void *ctx)
{
        /* You might use a function like this if you're implementing
           a simple proxy: it will take data from one connection (on
           bev), and write it to another, copying as little as
           possible. */
        struct proxy_info *inf = ctx;

        bufferevent_read_buffer(bev,
            bufferevent_get_output(inf->other_bev));
}

struct count {
        unsigned long last_fib[2];
};

void
write_callback_fibonacci(struct bufferevent *bev, void *ctx)
{
        /* Here's a callback that adds some Fibonacci numbers to the
           output buffer of bev.  It stops once we have added 1k of
           data; once this data is drained, we'll add more. */
        struct count *c = ctx;

        struct evbuffer *tmp = evbuffer_new();
        while (evbuffer_get_length(tmp) < 1024) {
                 unsigned long next = c->last_fib[0] + c->last_fib[1];
                 c->last_fib[0] = c->last_fib[1];
                 c->last_fib[1] = next;

                 evbuffer_add_printf(tmp, "%lu", next);
        }

        /* Now we add the whole contents of tmp to bev. */
        bufferevent_write_buffer(bev, tmp);

        /* We don't need tmp any longer. */
        evbuffer_free(tmp);
}
6.4 讀寫超時

跟其他事件一樣,可以要求在一定量的時間已經流逝,而沒有成功寫入或者讀取數據的時候調用一個超時回調。

接口

void bufferevent_set_timeouts(struct bufferevent *bufev,
    const struct timeval *timeout_read, const struct timeval *timeout_write);

設置超時爲 NULL 會移除超時回調。

試圖讀取數據的時候,如果至少等待了 timeout_read 秒,則讀取超時事件將被觸發。試圖寫入數據的時候,如果至少等待了 timeout_write 秒,則寫入超時事件將被觸發。

注意,只有在讀取或者寫入的時候纔會計算超時。也就是說,如果 bufferevent 的讀取被禁止,或者輸入緩衝區滿(達到其高水位),則讀取超時被禁止。類似的,如果寫入被禁止,或者沒有數據待寫入,則寫入超時被禁止。

讀取或者寫入超時發生時,相應的讀取或者寫入操作被禁止,然後超時事件回調被調用,帶有標誌BEV_EVENT_TIMEOUT | BEV_EVENT_READING或者BEV_EVENT_TIMEOUT | BEV_EVENT_WRITING。

這個函數從2.0.1-alpha 版就存在了,但是直到2.0.4-alpha 版纔對於各種 bufferevent 類型行爲一致。

6.5 對 bufferevent 發起清空操作

接口

int bufferevent_flush(struct bufferevent *bufev,
    short iotype, enum bufferevent_flush_mode state);

清空 bufferevent 要求 bufferevent 強制從底層傳輸端口讀取或者寫入儘可能多的數據,而忽略其他可能保持數據不被寫入的限制條件。函數的細節功能依賴於 bufferevent 的具體類型。

iotype 參數應該是 EV_READ、EV_WRITE 或者 EV_READ | EV_WRITE,用於指示應該處理讀取、寫入,還是二者都處理。state 參數可以是 BEV_NORMAL、BEV_FLUSH 或者BEV_FINISHED。BEV_FINISHED 指示應該告知另一端,沒有更多數據需要發送了; 而BEV_NORMAL 和 BEV_FLUSH 的區別依賴於具體的 bufferevent 類型。失敗時 bufferevent_flush()返回-1,如果沒有數據被清空則返回0,有數據被清空則返回1。

當前(2.0.5-beta 版)僅有一些 bufferevent 類型實現了 bufferevent_flush()。特別是,基於套接字bufferevent 沒有實現。

7. 類型特定的 bufferevent 函數

這些 bufferevent 函數不能支持所有 bufferevent 類型。

接口

int bufferevent_priority_set(struct bufferevent *bufev, int pri);
int bufferevent_get_priority(struct bufferevent *bufev);

這個函數調整 bufev 的優先級爲 pri。關於優先級的更多信息請看 event_priority_set()。

成功時函數返回0,失敗時返回-1。這個函數僅能用於基於套接字的 bufferevent。

這個函數由Libevent 1.0中引入了bufferevent_priority_set()函數; 直到Libevent 2.1.2-alpha纔出現bufferevent_get_priority()。

接口

int bufferevent_setfd(struct bufferevent *bufev, evutil_socket_t fd);
evutil_socket_t bufferevent_getfd(struct bufferevent *bufev);

這些函數設置或者返回基於 fd 的事件的文件描述符。只有基於套接字的 bufferevent 支持setfd()。兩個函數都在失敗時返回-1;setfd()成功時返回0。

bufferevent_setfd()函數由1.4.4版引入;bufferevent_getfd()函數由2.0.2-alpha 版引入。

接口

struct event_base *bufferevent_get_base(struct bufferevent *bev);

這個函數返回 bufferevent 的 event_base,由2.0.9-rc 版引入。

接口

struct bufferevent *bufferevent_get_underlying(struct bufferevent *bufev);

這個函數返回作爲 bufferevent 底層傳輸端口的另一個 bufferevent。關於這種情況,請看關於過濾型 bufferevent 的介紹。

這個函數由2.0.2-alpha 版引入。

8. 手動鎖定和解鎖

有時候需要確保對 bufferevent 的一些操作是原子地執行的。爲此,libevent 提供了手動鎖定和解鎖 bufferevent 的函數。

接口

void bufferevent_lock(struct bufferevent *bufev);
void bufferevent_unlock(struct bufferevent *bufev);

注意:如果創建 bufferevent 時沒有指定 BEV_OPT_THREADSAFE 標誌,或者沒有激活libevent 的線程支持,則鎖定操作是沒有效果的。

用這個函數鎖定 bufferevent 將自動同時鎖定相關聯的 evbuffer。這些函數是遞歸的:鎖定已經持有鎖的 bufferevent 是安全的。當然,對於每次鎖定都必須進行一次解鎖。

這些函數由2.0.6-rc 版引入。

七、Bufferevent:高級話題

本章介紹了Libevent的bufferevent實現的一些高級功能,這些功能對於典型用途不是必需的。 如果您只是在學習如何使用bufferevents,則應該暫時跳過本章,然後繼續閱讀evbuffer章。

1. 成對的 bufferevent

有時候網絡程序需要與自身通信。比如說,通過某些協議對用戶連接進行隧道操作的程序,有時候也需要通過同樣的協議對自身的連接進行隧道操作。當然,可以通過打開一個到自身監聽端口的連接,讓程序使用這個連接來達到這種目標。但是,通過網絡棧來與自身通信比較浪費資源。

替代的解決方案是,創建一對成對的 bufferevent。這樣,寫入到一個 bufferevent 的字節都被另一個接收(反過來也是),但是不需要使用套接字。

接口

int bufferevent_pair_new(struct event_base *base, int options,
    struct bufferevent *pair[2]);

調用 bufferevent_pair_new()會設置 pair[0]和 pair[1]爲一對相互連接的 bufferevent。除了 BEV_OPT_CLOSE_ON_FREE 無效、BEV_OPT_DEFER_CALLBACKS 總是打開的之外, 所有通常的選項都是支持的。

爲什麼 bufferevent 對需要帶延遲迴調運行?通常某一方上的操作會調用一個通知另一方的回調,從而調用另一方的回調,如此這樣進行很多步。如果不延遲迴調,這種調用鏈常常會導致棧溢出或者餓死其他連接,而且還要求所有的回調是可重入的。

成對的 bufferevent 支持 flush:設置模式參數爲 BEV_NORMAL 或者 BEV_FLUSH 會強制要求所有相關數據從對中的一個 bufferevent 傳輸到另一箇中,而忽略可能會限制傳輸的水位設置。增加 BEV_FINISHED 到模式參數中還會讓對端的 bufferevent 產生 EOF 事件。

釋放對中的任何一個成員不會自動釋放另一個,也不會產生 EOF 事件。釋放僅僅會使對中的另一個成員成爲斷開的。bufferevent 一旦斷開,就不能再成功讀寫數據或者產生任何事件了。

接口

struct bufferevent *bufferevent_pair_get_partner(struct bufferevent *bev)

有時可能只需要給一個緩衝事件對中的另一個成員。 爲此,您可以調用bufferevent_pair_get_partner()函數。 如果bev是該對中的一個成員,並且另一個成員仍然存在,它將返回該對中的另一個成員。 否則,它返回NULL。

bufferevent 對由2.0.1-alpha 版本引入,而 bufferevent_pair_get_partner()函數由2.0.6版本引入。

2. 過濾 bufferevent

有時候需要轉換傳遞給某 bufferevent 的所有數據,這可以通過添加一個壓縮層,或者將協議包裝到另一個協議中進行傳輸來實現。

接口

enum bufferevent_filter_result {
        BEV_OK = 0,
        BEV_NEED_MORE = 1,
        BEV_ERROR = 2
};
typedef enum bufferevent_filter_result (*bufferevent_filter_cb)(
    struct evbuffer *source, struct evbuffer *destination, ev_ssize_t dst_limit,
    enum bufferevent_flush_mode mode, void *ctx);


struct bufferevent *bufferevent_filter_new(struct bufferevent *underlying,
        bufferevent_filter_cb input_filter,
        bufferevent_filter_cb output_filter,
        int options,
        void (*free_context)(void *),
        void *ctx);

bufferevent_filter_new()函數創建一個封裝現有的“底層”bufferevent 的過濾 bufferevent。所有通過底層 bufferevent 接收的數據在到達過濾 bufferevent 之前都會經過“輸入”過濾器的轉換;所有通過底層 bufferevent 發送的數據在被髮送到底層 bufferevent 之前都會經過“輸出”過濾器的轉換。

向底層 bufferevent 添加過濾器將替換其回調函數。可以向底層 bufferevent 的 evbuffer 添加回調函數,但是如果想讓過濾器正確工作,就不能再設置 bufferevent 本身的回調函數。

input_filter 和 output_filter 函數將隨後描述。options 參數支持所有通常的選項。如果設置了 BEV_OPT_CLOSE_ON_FREE , 那 麼 釋 放 過 濾 bufferevent 也會同時釋放底層bufferevent。ctx 參數是傳遞給過濾函數的任意指針;如果提供了 free_context,則在釋放 ctx 之前它會被調用。

底層輸入緩衝區有數據可讀時,輸入過濾器函數會被調用;過濾器的輸出緩衝區有新的數據待寫入時,輸出過濾器函數會被調用。兩個過濾器函數都有一對 evbuffer 參數:從 source 讀取數據;向 destination 寫入數據,而 dst_limit 參數描述了可以寫入 destination 的字節數 上限。過濾器函數可以忽略這個參數,但是這樣可能會違背高水位或者速率限制。如果dst_limit 是-1,則沒有限制。mode 參數向過濾器描述了寫入的方式。值 BEV_NORMAL 表示應該在方便轉換的基礎上寫入儘可能多的數據;而 BEV_FLUSH 表示寫入儘可能多的數據;BEV_FINISHED 表示過濾器函數應該在流的末尾執行額外的清理操作。最後,過濾器函數的 ctx 參數就是傳遞給 bufferevent_filter_new()函數的指針(ctx 參數)。

如果成功向目標緩衝區寫入了任何數據,過濾器函數應該返回 BEV_OK;如果不獲得更多的輸入,或者不使用不同的清空(flush)模式,就不能向目標緩衝區寫入更多的數據,則應該返回 BEV_NEED_MORE;如果過濾器上發生了不可恢復的錯誤,則應該返回BEV_ERROR。

創建過濾器將啓用底層 bufferevent 的讀取和寫入。隨後就不需要自己管理讀取和寫入了:過濾器在不想讀取的時候會自動掛起底層 bufferevent 的讀取。從2.0.8-rc 版本開始,可以在過濾器之外獨立地啓用/禁用底層 bufferevent 的讀取和寫入。然而,這樣可能會讓過濾器不能成功取得所需要的數據。

不需要同時指定輸入和輸出過濾器:沒有給定的過濾器將被一個不進行數據轉換的過濾器取代。

3. 限制最大單讀/寫大小

默認情況下,每次事件循環調用時,bufferevent都不會讀取或寫入最大可能的字節數; 這樣做會導致奇怪的不公平行爲和資源匱乏。 另一方面,默認值可能並非在所有情況下都合理。

接口

int bufferevent_set_max_single_read(struct bufferevent * bev,size_t size);
int bufferevent_set_max_single_write(struct bufferevent * bev,size_t size);

ev_ssize_t bufferevent_get_max_single_read(struct bufferevent * bev);
ev_ssize_t bufferevent_get_max_single_write(struct bufferevent * bev);

這兩個“設置”功能分別替換了當前的最大讀取和寫入最大值。 如果大小值爲0或大於EV_SSIZE_MAX,則將最大值設置爲默認值。 這些函數成功返回0,失敗返回-1。

這兩個“ get”函數分別返回當前的每個循環讀取和寫入最大值。

這些功能已添加到2.1.1-alpha中。

4. bufferevent 和速率限制

某些程序需要限制單個或者一組 bufferevent 使用的帶寬。2.0.4-alpha 和2.0.5-alpha 版本添加了爲單個或者一組 bufferevent 設置速率限制的基本功能。

4.1 速率限制模型

libevent 的速率限制使用記號存儲器(token bucket)算法確定在某時刻可以寫入或者讀取多少字節。每個速率限制對象在任何給定時刻都有一個“讀存儲器(read bucket)”和一個“寫存儲器(write bucket)”,其大小決定了對象可以立即讀取或者寫入多少字節。每個存儲器有一個填充速率,一個最大突發尺寸,和一個時間單位,或者說“滴答(tick)”。一個時間單位流逝後,存儲器被填充一些字節(決定於填充速率)——但是如果超過其突發尺寸,則超出的字節會丟失。
因此,填充速率決定了對象發送或者接收字節的最大平均速率,而突發尺寸決定了在單次突發中可以發送或者接收的最大字節數;時間單位則確定了傳輸的平滑程度。

4.2 爲 bufferevent 設置速率限制

接口

#define EV_RATE_LIMIT_MAX EV_SSIZE_MAX
struct ev_token_bucket_cfg;
struct ev_token_bucket_cfg *ev_token_bucket_cfg_new(
        size_t read_rate, size_t read_burst,
        size_t write_rate, size_t write_burst,
        const struct timeval *tick_len);
void ev_token_bucket_cfg_free(struct ev_token_bucket_cfg *cfg);
int bufferevent_set_rate_limit(struct bufferevent *bev,
    struct ev_token_bucket_cfg *cfg);

ev_token_bucket_cfg 結構體代表用於限制單個或者一組 bufferevent 的一對記號存儲器的配置值。要創建 ev_token_bucket_cfg,調用 ev_token_bucket_cfg_new 函數,提供最大平均讀取速率、最大突發讀取量、最大平均寫入速率、最大突發寫入量,以及一個滴答的長度。如果 tick_len 參數爲 NULL,則默認的滴答長度爲一秒。如果發生錯誤,函數會返回 NULL。

注意:read_rate 和 write_rate 參數的單位是字節每滴答。也就是說,如果滴答長度是十分之一秒,read_rate 是300,則最大平均讀取速率是3000字節每秒。此外,不支持大於EV_RATE_LIMIT_MAX 的速率或者突發量。

要 限 制 bufferevent的傳輸速率,使用一個 ev_token_bucket_cfg , 對其調用bufferevent_set_rate_limit()。成功時函數返回0,失敗時返回-1。可以對任意數量的bufferevent 使 用 相 同 的 ev_token_bucket_cfg 。 要 移 除 速 率 限 制 , 可 以 調 用bufferevent_set_rate_limit(),傳遞 NULL 作爲 cfg 參數值。

調用 ev_token_bucket_cfg_free()可以釋放 ev_token_bucket_cfg。注意:當前在沒有任何 bufferevent 使用 ev_token_bucket_cfg 之前進行釋放是不安全的。

4.3 爲一組 bufferevent 設置速率限制

如果要限制一組 bufferevent 總的帶寬使用,可以將它們分配到一個速率限制組中。

接口

struct bufferevent_rate_limit_group;

struct bufferevent_rate_limit_group *bufferevent_rate_limit_group_new(
        struct event_base *base,
        const struct ev_token_bucket_cfg *cfg);
int bufferevent_rate_limit_group_set_cfg(
        struct bufferevent_rate_limit_group *group,
        const struct ev_token_bucket_cfg *cfg);
void bufferevent_rate_limit_group_free(struct bufferevent_rate_limit_group *);
int bufferevent_add_to_rate_limit_group(struct bufferevent *bev,
    struct bufferevent_rate_limit_group *g);
int bufferevent_remove_from_rate_limit_group(struct bufferevent *bev);

要創建速率限制組,使用一個 event_base 和一個已經初始化的 ev_token_bucket_cfg 作爲參數調用 bufferevent_rate_limit_group_new 函 數 。使用bufferevent_add_to_rate_limit_group 將 bufferevent 添 加 到 組中 ;使用bufferevent_remove_from_rate_limit_group 從組中刪除 bufferevent。這些函數成功時返回
0,失敗時返回-1。

單個 bufferevent 在某時刻只能是一個速率限制組的成員。bufferevent 可以同時有單獨的速率限制(通過 bufferevent_set_rate_limit 設置)和組速率限制。設置了這兩個限制時,對每個 bufferevent,較低的限制將被應用。

調用 bufferevent_rate_limit_group_set_cfg 修改組的速率限制。函數成功時返回0,失敗時返回-1。bufferevent_rate_limit_group_free 函數釋放速率限制組,移除所有成員。

在2.0版本中,組速率限制試圖實現總體的公平,但是具體實現可能在小的時間範圍內並不公平。如果你強烈關注調度的公平性,請幫助提供未來版本的補丁。

4.4 檢查當前速率限制

有時候需要得知應用到給定 bufferevent 或者組的速率限制,爲此,libevent 提供了函數:

接口

ev_ssize_t bufferevent_get_read_limit(struct bufferevent *bev);
ev_ssize_t bufferevent_get_write_limit(struct bufferevent *bev);
ev_ssize_t bufferevent_rate_limit_group_get_read_limit(
        struct bufferevent_rate_limit_group *);
ev_ssize_t bufferevent_rate_limit_group_get_write_limit(
        struct bufferevent_rate_limit_group *);

上述函數返回以字節爲單位的 bufferevent 或者組的讀寫記號存儲器大小。注意:如果bufferevent 已經被強制超過其配置(清空(flush)操作就會這樣),則這些值可能是負數。

接口

ev_ssize_t bufferevent_get_max_to_read(struct bufferevent *bev);
ev_ssize_t bufferevent_get_max_to_write(struct bufferevent *bev);

這些函數返回在考慮了應用到 bufferevent 或者組(如果有)的速率限制,以及一次最大讀寫數據量的情況下,現在可以讀或者寫的字節數。

接口

void bufferevent_rate_limit_group_get_totals(
    struct bufferevent_rate_limit_group *grp,
    ev_uint64_t *total_read_out, ev_uint64_t *total_written_out);
void bufferevent_rate_limit_group_reset_totals(
    struct bufferevent_rate_limit_group *grp);

每個 bufferevent_rate_limit_group 跟蹤經過其發送的總的字節數,這可用於跟蹤組中所有bufferevent 總的使用情況。對一個組調用 bufferevent_rate_limit_group_get_totals 會分別設置 total_read_out 和 total_written_out 爲組的總讀取和寫入字節數。組創建的時候這些計數從0開始,調用 bufferevent_rate_limit_group_reset_totals 會復位計數爲0。

4.5 手動調整速率限制

對於有複雜需求的程序,可能需要調整記號存儲器的當前值。比如說,如果程序不通過使用bufferevent 的方式產生一些通信量時。

接口

int bufferevent_decrement_read_limit(struct bufferevent *bev, ev_ssize_t decr);
int bufferevent_decrement_write_limit(struct bufferevent *bev, ev_ssize_t decr);
int bufferevent_rate_limit_group_decrement_read(
        struct bufferevent_rate_limit_group *grp, ev_ssize_t decr);
int bufferevent_rate_limit_group_decrement_write(
        struct bufferevent_rate_limit_group *grp, ev_ssize_t decr);

這些函數減小某個 bufferevent 或者速率限制組的當前讀或者寫存儲器。注意:減小是有符號的。如果要增加存儲器,就傳入負值。

4.6 設置速率限制組的最小可能共享

通常,不希望在每個滴答中爲速率限制組中的所有 bufferevent 平等地分配可用的字節。比如說,有一個含有10000個活動 bufferevent 的速率限制組,它在每個滴答中可以寫入10000字節,那麼,因爲系統調用和 TCP 頭部的開銷,讓每個 bufferevent 在每個滴答中僅寫入1字節是低效的。

爲解決此問題,速率限制組有一個“最小共享(minimum share)”的概念。在上述情況下,不是允許每個 bufferevent 在每個滴答中寫入1字節,而是在每個滴答中允許某個 bufferevent寫入一些(最小共享)字節,而其餘的 bufferevent 將不允許寫入。允許哪個 bufferevent寫入將在每個滴答中隨機選擇。

默認的最小共享值具有較好的性能,當前(2.0.6-rc 版本)其值爲64。可以通過這個函數調整最小共享值:

接口

int bufferevent_rate_limit_group_set_min_share(
        struct bufferevent_rate_limit_group *group, size_t min_share);

設置 min_share 爲0將會完全禁止最小共享。

速率限制功能從引入開始就具有最小共享了,而修改最小共享的函數在2.0.6-rc 版本首次引入。

4.7 速率限制實現的限制

2.0版本的 libevent 的速率限制具有一些實現上的限制:

  • 不是每種 bufferevent 類型都良好地或者說完整地支持速率限制。
  • bufferevent 速率限制組不能嵌套,一個 bufferevent 在某時刻只能屬於一個速率限制組。
  • 速率限制實現僅計算 TCP 分組傳輸的數據,不包括 TCP 頭部。
  • 讀速率限制實現依賴於 TCP 棧通知應用程序僅僅以某速率消費數據,並且在其緩衝區
    滿的時候將數據推送到 TCP 連接的另一端。
  • 某些 bufferevent 實現(特別是 Windows 中的 IOCP 實現)可能調撥過度。
  • 存儲器開始於一個滴答的通信量。這意味着 bufferevent 可以立即開始讀取或者寫入,
    而不用等待一個滴答的時間。但是這也意味着速率被限制爲 N.1個滴答的 bufferevent
    可能傳輸 N+1個滴答的通信量。
  • 滴答不能小於1毫秒,毫秒的小數部分都被忽略。

5. bufferevent 和 SSL

bufferevent 可以使用 OpenSSL 庫實現 SSL/TLS 安全傳輸層。因爲很多應用不需要或者不想鏈接 OpenSSL,這部分功能在單獨的 libevent_openssl 庫中實現。未來版本的 libevent可能會添加其他 SSL/TLS 庫,如 NSS 或者 GnuTLS,但是當前只有 OpenSSL。

OpenSSL 功能在2.0.3-alpha 版本引入,然而直到2.0.5-beta 和2.0.6-rc 版本才能良好工作。

這一節不包含對 OpenSSL、SSL/TLS 或者密碼學的概述。

這一節描述的函數都在 event2/bufferevent_ssl.h 中聲明。

創建和使用基於 OpenSSL 的 bufferevent

接口

enum bufferevent_ssl_state {
        BUFFEREVENT_SSL_OPEN = 0,
        BUFFEREVENT_SSL_CONNECTING = 1,
        BUFFEREVENT_SSL_ACCEPTING = 2
};

struct bufferevent *
bufferevent_openssl_filter_new(struct event_base *base,
    struct bufferevent *underlying,
    SSL *ssl,
    enum bufferevent_ssl_state state,
    int options);

struct bufferevent *
bufferevent_openssl_socket_new(struct event_base *base,
    evutil_socket_t fd,
    SSL *ssl,
    enum bufferevent_ssl_state state,
    int options);

可以創建兩種SSL bufferevent:基於過濾器的bufferevent,該事件通過另一個基礎bufferevent進行通信,或者基於套接字的bufferevent,通知OpenSSL直接通過網絡與網絡進行通信。無論哪種情況,都必須提供SSL對象和SSL對象狀態的描述。如果SSL當前正在作爲客戶端進行協商,則狀態應爲BUFFEREVENT_SSL_CONNECTING;如果SSL當前正在作爲服務器進行協商,則狀態應爲BUFFEREVENT_SSL_ACCEPTING;如果SSL握手已完成,則狀態應爲BUFFEREVENT_SSL_OPEN。

接受常規選項; BEV_OPT_CLOSE_ON_FREE使當關閉openssl bufferevent本身時,SSL對象和基礎fd或bufferevent關閉。

握手完成後,將使用標誌中的BEV_EVENT_CONNECTED調用新的bufferevent的事件回調。

如果您要創建基於套接字的bufferEvent,並且SSL對象已經設置了套接字,則無需自己提供套接字:只需傳遞-1。您還可以稍後使用bufferevent_setfd()設置fd。

/// TODO:bufferevent_shutdown()API完成後,請刪除此代碼。

請注意,在SSL緩衝區事件中設置BEV_OPT_CLOSE_ON_FREE時,將不會在SSL連接上執行乾淨關閉。這有兩個問題:首先,連接似乎已被另一端“斷開”,而不是被完全關閉:另一方將無法告知您是否關閉了連接,或斷開了連接攻擊者或第三方。其次,OpenSSL將會話視爲“不良”會話,並從會話緩存中刪除。這可能導致負載下的SSL應用程序性能顯着下降。

當前,唯一的解決方法是手動關閉惰性SSL。雖然這破壞了TLS RFC,但可以確保會話一旦關閉就將保留在緩存中。下面的代碼實現此解決方法。

示例

SSL *ctx = bufferevent_openssl_get_ssl(bev);

/*
 * SSL_RECEIVED_SHUTDOWN tells SSL_shutdown to act as if we had already
 * received a close notify from the other end.  SSL_shutdown will then
 * send the final close notify in reply.  The other end will receive the
 * close notify and send theirs.  By this time, we will have already
 * closed the socket and the other end's real close notify will never be
 * received.  In effect, both sides will think that they have completed a
 * clean shutdown and keep their sessions valid.  This strategy will fail
 * if the socket is not ready for writing, in which case this hack will
 * lead to an unclean shutdown and lost session on the other end.
 */
SSL_set_shutdown(ctx, SSL_RECEIVED_SHUTDOWN);
SSL_shutdown(ctx);
bufferevent_free(bev);

接口

SSL *bufferevent_openssl_get_ssl(struct bufferevent *bev);

這個函數返回 OpenSSL bufferevent 使用的 SSL 對象。如果 bev 不是一個基於 OpenSSL 的 bufferevent,則返回 NULL。

接口

unsigned long bufferevent_get_openssl_error(struct bufferevent *bev);

這個函數返回給定 bufferevent 的第一個未決的 OpenSSL 錯誤;如果沒有未決的錯誤,則返回0。錯誤值的格式與 openssl 庫中的 ERR_get_error()返回的相同。

接口

int bufferevent_ssl_renegotiate(struct bufferevent *bev);

調用這個函數要求 SSL 重新協商,bufferevent 會調用合適的回調函數。這是個高級功能,通常應該避免使用,除非你確實知道自己在做什麼,特別是有些 SSL 版本具有與重新協商相關的安全問題。

接口

int bufferevent_openssl_get_allow_dirty_shutdown(struct bufferevent *bev);
void bufferevent_openssl_set_allow_dirty_shutdown(struct bufferevent *bev,
    int allow_dirty_shutdown);

SSL協議的所有良好版本(即SSLv3和所有TLS版本)都支持經過身份驗證的關閉操作,該操作使各方可以區分下意識緩衝區中的故意關閉與意外或惡意引發的終止。 默認情況下,我們將正確關閉以外的所有內容都視爲連接錯誤。 但是,如果allow_dirty_shutdown標誌設置爲1,則將連接中的關閉視爲BEV_EVENT_EOF。

Libevent 2.1.1-alpha中添加了allow_dirty_shutdown函數。

示例:一個簡單的基於SSL的回顯服務器

#include <event2/listener.h>
#include <event2/bufferevent.h>
#include <event2/buffer.h>

#include <arpa/inet.h>

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

static void
echo_read_cb(struct bufferevent *bev, void *ctx)
{
        /* This callback is invoked when there is data to read on bev. */
        struct evbuffer *input = bufferevent_get_input(bev);
        struct evbuffer *output = bufferevent_get_output(bev);

        /* Copy all the data from the input buffer to the output buffer. */
        evbuffer_add_buffer(output, input);
}

static void
echo_event_cb(struct bufferevent *bev, short events, void *ctx)
{
        if (events & BEV_EVENT_ERROR)
                perror("Error from bufferevent");
        if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
                bufferevent_free(bev);
        }
}

static void
accept_conn_cb(struct evconnlistener *listener,
    evutil_socket_t fd, struct sockaddr *address, int socklen,
    void *ctx)
{
        /* We got a new connection! Set up a bufferevent for it. */
        struct event_base *base = evconnlistener_get_base(listener);
        struct bufferevent *bev = bufferevent_socket_new(
                base, fd, BEV_OPT_CLOSE_ON_FREE);

        bufferevent_setcb(bev, echo_read_cb, NULL, echo_event_cb, NULL);

        bufferevent_enable(bev, EV_READ|EV_WRITE);
}

static void
accept_error_cb(struct evconnlistener *listener, void *ctx)
{
        struct event_base *base = evconnlistener_get_base(listener);
        int err = EVUTIL_SOCKET_ERROR();
        fprintf(stderr, "Got an error %d (%s) on the listener. "
                "Shutting down.\n", err, evutil_socket_error_to_string(err));

        event_base_loopexit(base, NULL);
}

int
main(int argc, char **argv)
{
        struct event_base *base;
        struct evconnlistener *listener;
        struct sockaddr_in sin;

        int port = 9876;

        if (argc > 1) {
                port = atoi(argv[1]);
        }
        if (port<=0 || port>65535) {
                puts("Invalid port");
                return 1;
        }

        base = event_base_new();
        if (!base) {
                puts("Couldn't open event base");
                return 1;
        }

        /* Clear the sockaddr before using it, in case there are extra
         * platform-specific fields that can mess us up. */
        memset(&sin, 0, sizeof(sin));
        /* This is an INET address */
        sin.sin_family = AF_INET;
        /* Listen on 0.0.0.0 */
        sin.sin_addr.s_addr = htonl(0);
        /* Listen on the given port. */
        sin.sin_port = htons(port);

        listener = evconnlistener_new_bind(base, accept_conn_cb, NULL,
            LEV_OPT_CLOSE_ON_FREE|LEV_OPT_REUSEABLE, -1,
            (struct sockaddr*)&sin, sizeof(sin));
        if (!listener) {
                perror("Couldn't create listener");
                return 1;
        }
        evconnlistener_set_error_cb(listener, accept_error_cb);

        event_base_dispatch(base);
        return 0;
}

八、evbuffer:緩衝 IO 實用功能

libevent 的 evbuffer 實現了爲向後面添加數據和從前面移除數據而優化的字節隊列。

evbuffer 用於處理緩衝網絡 IO 的“緩衝”部分。它不提供調度 IO 或者當 IO 就緒時觸發 IO 的功能:這是 bufferevent 的工作。

除非特別說明,本章描述的函數都在 event2/buffer.h 中聲明。

1. 創建和釋放 evbuffer

接口

struct evbuffer *evbuffer_new(void);
void evbuffer_free(struct evbuffer *buf);

這 兩 個 函 數 的 功 能 很 簡 明 : evbuffer_new() 分 配 和 返 回 一 個 新 的 空 evbuffer ; 而evbuffer_free()釋放 evbuffer 和其內容。

這兩個函數從 libevent 0.8版就存在了。

2. evbuffer 與線程安全

接口

int evbuffer_enable_locking(struct evbuffer *buf, void *lock);
void evbuffer_lock(struct evbuffer *buf);
void evbuffer_unlock(struct evbuffer *buf);

默認情況下,在多個線程中同時訪問 evbuffer 是不安全的。如果需要這樣的訪問,可以調用 evbuffer_enable_locking() 。如果lock參數爲NULL ,libevent會使用evthread_set_lock_creation_callback 提供的鎖創建函數創建一個鎖。否則,libevent 將 lock參數用作鎖。

evbuffer_lock()和 evbuffer_unlock()函數分別請求和釋放 evbuffer 上的鎖。可以使用這兩個函數讓一系列操作是原子的。如果 evbuffer 沒有啓用鎖,這兩個函數不做任何操作。

注意:對於單個操作,不需要調用 evbuffer_lock()和 evbuffer_unlock():如果 evbuffer啓用了鎖,單個操作就已經是原子的。只有在需要多個操作連續執行,不讓其他線程介入的時候,才需要手動鎖定 evbuffer

這些函數都在2.0.1-alpha 版本中引入。

3. 檢查evbuffer

接口

size_t evbuffer_get_length(const struct evbuffer *buf);

這個函數返回 evbuffer 存儲的字節數,它在2.0.1-alpha 版本中引入。

接口

size_t evbuffer_get_contiguous_space(const struct evbuffer *buf);

這個函數返回連續地存儲在 evbuffer 前面的字節數。evbuffer 中的數據可能存儲在多個分隔開的內存塊中,這個函數返回當前第一個塊中的字節數。

這個函數在2.0.1-alpha 版本引入。

4. 向 evbuffer 添加數據:基礎

接口

int evbuffer_add(struct evbuffer *buf, const void *data, size_t datlen);

這個函數添加 data 處的 datalen 字節到 buf 的末尾,成功時返回0,失敗時返回-1。

接口

int evbuffer_add_printf(struct evbuffer *buf, const char *fmt, ...)
int evbuffer_add_vprintf(struct evbuffer *buf, const char *fmt, va_list ap);

這些函數添加格式化的數據到 buf 末尾。格式參數和其他參數的處理分別與 C 庫函數 printf 和 vprintf 相同。函數返回添加的字節數。

接口

int evbuffer_expand(struct evbuffer *buf, size_t datlen);

這個函數修改緩衝區的最後一塊,或者添加一個新的塊,使得緩衝區足以容納 datlen 字節,而不需要更多的內存分配。

示例

/* Here are two ways to add "Hello world 2.0.1" to a buffer. */
/* Directly: */
evbuffer_add(buf, "Hello world 2.0.1", 17);

/* Via printf: */
evbuffer_add_printf(buf, "Hello %s %d.%d.%d", "world", 2, 0, 1);

evbuffer_add()和 evbuffer_add_printf()函數在 libevent 0.8版本引入;evbuffer_expand()首次出現在0.9版本,而 evbuffer_add_printf()首次出現在1.1版本。

5. 將數據從一個 evbuffer 移動到另一個

爲提高效率,libevent 具有將數據從一個 evbuffer 移動到另一個的優化函數。

接口

int evbuffer_add_buffer(struct evbuffer *dst, struct evbuffer *src);
int evbuffer_remove_buffer(struct evbuffer *src, struct evbuffer *dst,
    size_t datlen);

evbuffer_add_buffer()將 src 中的所有數據移動到 dst 末尾,成功時返回0,失敗時返回-1。

evbuffer_remove_buffer()函數從 src 中移動 datlen 字節到 dst 末尾,儘量少進行復制。如果字節數小於 datlen,所有字節被移動。函數返回移動的字節數。

evbuffer_add_buffer()在0.8版本引入;evbuffer_remove_buffer()是2.0.1-alpha 版本新增加的。

6. 添加數據到 evbuffer 前面

接口

int evbuffer_prepend(struct evbuffer *buf, const void *data, size_t size);
int evbuffer_prepend_buffer(struct evbuffer *dst, struct evbuffer* src);

除了將數據移動到目標緩衝區前面之外,這兩個函數的行爲分別與 evbuffer_add()和evbuffer_add_buffer()相同。

使用這些函數時要當心,永遠不要對與 bufferevent 共享的 evbuffer 使用。這些函數是2.0.1-alpha 版本新添加的。

7. 重新排列 evbuffer 的內部佈局

有時候需要取出 evbuffer 前面的 N 字節,將其看作連續的字節數組。要做到這一點,首先必須確保緩衝區的前面確實是連續的。

接口

unsigned char *evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size);

evbuffer_pullup()函數“線性化”buf 前面的 size 字節,必要時將進行復制或者移動,以保證這些字節是連續的,佔據相同的內存塊。如果 size 是負的,函數會線性化整個緩衝區。如果 size 大於緩衝區中的字節數,函數返回 NULL。否則,evbuffer_pullup()返回指向 buf 中首字節的指針。

調用 evbuffer_pullup()時使用較大的 size 參數可能會非常慢,因爲這可能需要複製整個緩衝區的內容。

示例

#include <event2/buffer.h>
#include <event2/util.h>

#include <string.h>

int parse_socks4(struct evbuffer *buf, ev_uint16_t *port, ev_uint32_t *addr)
{
    /* Let's parse the start of a SOCKS4 request!  The format is easy:
     * 1 byte of version, 1 byte of command, 2 bytes destport, 4 bytes of
     * destip. */
    unsigned char *mem;

    mem = evbuffer_pullup(buf, 8);

    if (mem == NULL) {
        /* Not enough data in the buffer */
        return 0;
    } else if (mem[0] != 4 || mem[1] != 1) {
        /* Unrecognized protocol or command */
        return -1;
    } else {
        memcpy(port, mem+2, 2);
        memcpy(addr, mem+4, 4);
        *port = ntohs(*port);
        *addr = ntohl(*addr);
        /* Actually remove the data from the buffer now that we know we
           like it. */
        evbuffer_drain(buf, 8);
        return 1;
    }
}

提示

使用 evbuffer_get_contiguous_space()返回的值作爲尺寸值調用 evbuffer_pullup()不會導致任何數據複製或者移動。

evbuffer_pullup()函數由2.0.1-alpha 版本新增加:先前版本的 libevent 總是保證 evbuffer 中的數據是連續的,而不計開銷。

8. 從 evbuffer 中移除數據

接口

int evbuffer_drain(struct evbuffer *buf, size_t len);
int evbuffer_remove(struct evbuffer *buf, void *data, size_t datlen);

evbuffer_remove()函數從 buf 前面複製和移除 datlen 字節到 data 處的內存中。如果可用字節少於 datlen,函數複製所有字節。失敗時返回-1,否則返回複製了的字節數。

evbuffer_drain()函數的行爲與 evbuffer_remove()相同,只是它不進行數據複製:而只是將數據從緩衝區前面移除。成功時返回0,失敗時返回-1。

evbuffer_drain()由0.8版引入,evbuffer_remove()首次出現在0.9版。

9. 從 evbuffer 中複製出數據

有時候需要獲取緩衝區前面數據的副本,而不清除數據。比如說,可能需要查看某特定類型的記錄是否已經完整到達,而不清除任何數據(像 evbuffer_remove 那樣),或者在內部重新排列緩衝區(像 evbuffer_pullup那樣)。

接口

ev_ssize_t evbuffer_copyout(struct evbuffer *buf, void *data, size_t datlen);
ev_ssize_t evbuffer_copyout_from(struct evbuffer *buf,
     const struct evbuffer_ptr *pos,
     void *data_out, size_t datlen);

evbuffer_copyout()的行爲與 evbuffer_remove()相同,但是它不從緩衝區移除任何數據。也就是說,它從 buf 前面複製 datlen 字節到 data 處的內存中。如果可用字節少於 datlen,函數會複製所有字節。失敗時返回-1,否則返回複製的字節數。

evbuffer_copyout_from()函數的行爲類似於evbuffer_copyout(),但不是從緩衝區的開頭複製字節,而是從pos中提供的位置開始複製字節。 有關evbuffer_ptr結構的信息,請參見下面的“在evbuffer中搜索”。

如果從緩衝區複製數據太慢,可以使用 evbuffer_peek()。

示例

#include <event2/buffer.h>
#include <event2/util.h>
#include <stdlib.h>
#include <stdlib.h>

int get_record(struct evbuffer *buf, size_t *size_out, char **record_out)
{
    /* Let's assume that we're speaking some protocol where records
       contain a 4-byte size field in network order, followed by that
       number of bytes.  We will return 1 and set the 'out' fields if we
       have a whole record, return 0 if the record isn't here yet, and
       -1 on error.  */
    size_t buffer_len = evbuffer_get_length(buf);
    ev_uint32_t record_len;
    char *record;

    if (buffer_len < 4)
       return 0; /* The size field hasn't arrived. */

   /* We use evbuffer_copyout here so that the size field will stay on
       the buffer for now. */
    evbuffer_copyout(buf, &record_len, 4);
    /* Convert len_buf into host order. */
    record_len = ntohl(record_len);
    if (buffer_len < record_len + 4)
        return 0; /* The record hasn't arrived */

    /* Okay, _now_ we can remove the record. */
    record = malloc(record_len);
    if (record == NULL)
        return -1;

    evbuffer_drain(buf, 4);
    evbuffer_remove(buf, record, record_len);

    *record_out = record;
    *size_out = record_len;
    return 1;
}

evbuffer_copyout()函數首先出現在Libevent 2.0.5-alpha中; evbuffer_copyout_from()已添加到Libevent 2.1.1-alpha中。

10. 面向行的輸入

接口

enum evbuffer_eol_style {
        EVBUFFER_EOL_ANY,
        EVBUFFER_EOL_CRLF,
        EVBUFFER_EOL_CRLF_STRICT,
        EVBUFFER_EOL_LF,
        EVBUFFER_EOL_NUL
};
char *evbuffer_readln(struct evbuffer *buffer, size_t *n_read_out,
    enum evbuffer_eol_style eol_style);

很多互聯網協議使用基於行的格式。evbuffer_readln()函數從 evbuffer 前面取出一行,用一個新分配的空字符結束的字符串返回這一行。如果 n_read_out 不是 NULL,則它被設置爲返回的字符串的字節數。如果沒有整行供讀取,函數返回空。返回的字符串不包括行結束符。

evbuffer_readln()理解5種行結束格式:

  • EVBUFFER_EOL_LF

    行尾是單個換行符(也就是\n,ASCII 值是0x0A)

  • EVBUFFER_EOL_CRLF_STRICT

    行尾是一個回車符,後隨一個換行符(也就是\r\n,ASCII 值是0x0D 0x0A)

  • EVBUFFER_EOL_CRLF

    行尾是一個可選的回車,後隨一個換行符(也就是說,可以是\r\n 或者\n)。這種格式對於解析基於文本的互聯網協議很有用,因爲標準通常要求\r\n 的行結束符,而不遵循標準的客戶端有時候只使用\n。

  • EVBUFFER_EOL_ANY

    行尾是任意數量、任意次序的回車和換行符。這種格式不是特別有用。它的存在主要是爲了向後兼容。

  • EVBUFFER_EOL_NUL

    行尾是一個值爲0的單個字節,即ASCII NUL。

注意,如果使用 event_se_mem_functions()覆蓋默認的 malloc,則 evbuffer_readln 返回的字符串將由你指定的 malloc 替代函數分配

示例

char *request_line;
size_t len;

request_line = evbuffer_readln(buf, &len, EVBUFFER_EOL_CRLF);
if (!request_line) {
    /* The first line has not arrived yet. */
} else {
    if (!strncmp(request_line, "HTTP/1.0 ", 9)) {
        /* HTTP 1.0 detected ... */
    }
    free(request_line);
}

evbuffer_readln()接口在Libevent 1.4.14-stable和更高版本中可用。 EVBUFFER_EOL_NUL已添加到Libevent 2.1.1-alpha中。

11. 在 evbuffer 中搜索

evbuffer_ptr 結構體指示 evbuffer 中的一個位置,包含可用於在 evbuffer 中迭代的數據。

接口

struct evbuffer_ptr {
        ev_ssize_t pos;
        struct {
                /* internal fields */
        } _internal;
};

pos 是唯一的公有字段,用戶代碼不應該使用其他字段。pos 指示 evbuffer 中的一個位置,以到開始處的偏移量表示。

接口

struct evbuffer_ptr evbuffer_search(struct evbuffer *buffer,
    const char *what, size_t len, const struct evbuffer_ptr *start);
struct evbuffer_ptr evbuffer_search_range(struct evbuffer *buffer,
    const char *what, size_t len, const struct evbuffer_ptr *start,
    const struct evbuffer_ptr *end);
struct evbuffer_ptr evbuffer_search_eol(struct evbuffer *buffer,
    struct evbuffer_ptr *start, size_t *eol_len_out,
    enum evbuffer_eol_style eol_style);

evbuffer_search()函數在緩衝區中查找含有 len 個字符的字符串 what。函數返回包含字符串位置,或者在沒有找到字符串時包含-1的 evbuffer_ptr 結構體。如果提供了 start 參數,則 從指定的位置開始搜索;否則,從開始處進行搜索。

evbuffer_search_range()函數和 evbuffer_search 行爲相同,只是它只考慮在 end 之前出現的 what。

evbuffer_search_eol()函數像 evbuffer_readln()一樣檢測行結束,但是不復制行,而是返回指向行結束符的 evbuffer_ptr。如果 eol_len_out 非空,則它被設置爲 EOL 字符串長度。

接口

enum evbuffer_ptr_how {
        EVBUFFER_PTR_SET,
        EVBUFFER_PTR_ADD
};
int evbuffer_ptr_set(struct evbuffer *buffer, struct evbuffer_ptr *pos,
    size_t position, enum evbuffer_ptr_how how);

evbuffer_ptr_set函數操作 buffer 中的位置 pos。如果 how 等於 EVBUFFER_PTR_SET,指針被移動到緩衝區中的絕對位置 position;如果等於 EVBUFFER_PTR_ADD,則向前移動position 字節。成功時函數返回0,失敗時返回-1。

示例

#include <event2/buffer.h>
#include <string.h>

/* Count the total occurrences of 'str' in 'buf'. */
int count_instances(struct evbuffer *buf, const char *str)
{
    size_t len = strlen(str);
    int total = 0;
    struct evbuffer_ptr p;

    if (!len)
        /* Don't try to count the occurrences of a 0-length string. */
        return -1;

    evbuffer_ptr_set(buf, &p, 0, EVBUFFER_PTR_SET);

    while (1) {
         p = evbuffer_search(buf, str, len, &p);
         if (p.pos < 0)
             break;
         total++;
         evbuffer_ptr_set(buf, &p, 1, EVBUFFER_PTR_ADD);
    }

    return total;
}

警告

任何修改 evbuffer 或者其佈局的調用都會使得 evbuffer_ptr 失效,不能再安全地使用。這些接口是2.0.1-alpha 版本新增加的。

12. 檢測數據而不復制

有時候需要讀取 evbuffer 中的數據而不進行復制(像 evbuffer_copyout()那樣),也不重新排列內部內存佈局(像 evbuffer_pullup()那樣)。有時候可能需要查看 evbuffer 中間的數據。

接口

struct evbuffer_iovec {
        void *iov_base;
        size_t iov_len;
};

int evbuffer_peek(struct evbuffer *buffer, ev_ssize_t len,
    struct evbuffer_ptr *start_at,
    struct evbuffer_iovec *vec_out, int n_vec);

調用 evbuffer_peek()的時候,通過 vec_out 給定一個 evbuffer_iovec 數組,數組的長度是n_vec。函數會讓每個結構體包含指向 evbuffer 內部內存塊的指針(iov_base)和塊中數據長度。

如果 len 小於0,evbuffer_peek()會試圖填充所有 evbuffer_iovec 結構體。否則,函數會進行填充,直到使用了所有結構體,或者見到 len 字節爲止。如果函數可以給出所有請求的數據,則返回實際使用的結構體個數;否則,函數返回給出所有請求數據所需的結構體個數。

如果 ptr 爲 NULL,函數從緩衝區開始處進行搜索。否則,從 ptr 處開始搜索。

示例

{
    /* Let's look at the first two chunks of buf, and write them to stderr. */
    int n, i;
    struct evbuffer_iovec v[2];
    n = evbuffer_peek(buf, -1, NULL, v, 2);
    for (i=0; i<n; ++i) { /* There might be less than two chunks available. */
        fwrite(v[i].iov_base, 1, v[i].iov_len, stderr);
    }
}

{
    /* Let's send the first 4906 bytes to stdout via write. */
    int n, i, r;
    struct evbuffer_iovec *v;
    size_t written = 0;

    /* determine how many chunks we need. */
    n = evbuffer_peek(buf, 4096, NULL, NULL, 0);
    /* Allocate space for the chunks.  This would be a good time to use
       alloca() if you have it. */
    v = malloc(sizeof(struct evbuffer_iovec)*n);
    /* Actually fill up v. */
    n = evbuffer_peek(buf, 4096, NULL, v, n);
    for (i=0; i<n; ++i) {
        size_t len = v[i].iov_len;
        if (written + len > 4096)
            len = 4096 - written;
        r = write(1 /* stdout */, v[i].iov_base, len);
        if (r<=0)
            break;
        /* We keep track of the bytes written separately; if we don't,
           we may write more than 4096 bytes if the last chunk puts
           us over the limit. */
        written += len;
    }
    free(v);
}

{
    /* Let's get the first 16K of data after the first occurrence of the
       string "start\n", and pass it to a consume() function. */
    struct evbuffer_ptr ptr;
    struct evbuffer_iovec v[1];
    const char s[] = "start\n";
    int n_written;

    ptr = evbuffer_search(buf, s, strlen(s), NULL);
    if (ptr.pos == -1)
        return; /* no start string found. */

    /* Advance the pointer past the start string. */
    if (evbuffer_ptr_set(buf, &ptr, strlen(s), EVBUFFER_PTR_ADD) < 0)
        return; /* off the end of the string. */

    while (n_written < 16*1024) {
        /* Peek at a single chunk. */
        if (evbuffer_peek(buf, -1, &ptr, v, 1) < 1)
            break;
        /* Pass the data to some user-defined consume function */
        consume(v[0].iov_base, v[0].iov_len);
        n_written += v[0].iov_len;

        /* Advance the pointer so we see the next chunk next time. */
        if (evbuffer_ptr_set(buf, &ptr, v[0].iov_len, EVBUFFER_PTR_ADD)<0)
            break;
    }
}

注意

  • 修改 evbuffer_iovec 所指的數據會導致不確定的行爲

  • 如果任何函數修改了 evbuffer,則 evbuffer_peek()返回的指針會失效

  • 如果在多個線程中使用 evbuffer ,確保在調用evbuffer_peek()之前使用evbuffer_lock(),在使用完 evbuffer_peek()給出的內容之後進行解鎖

這個函數是2.0.2-alpha 版本新增加的。

13. 直接向 evbuffer 添加數據

有時候需要能夠直接向 evbuffer 添加數據,而不用先將數據寫入到字符數組中,然後再使用 evbuffer_add()進行復制。有一對高級函數可以完成這種功能:evbuffer_reserve_space() 和 evbuffer_commit_space()。跟 evbuffer_peek()一樣,這兩個函數使用 evbuffer_iovec 結構體來提供對 evbuffer 內部內存的直接訪問。

接口

int evbuffer_reserve_space(struct evbuffer *buf, ev_ssize_t size,
    struct evbuffer_iovec *vec, int n_vecs);
int evbuffer_commit_space(struct evbuffer *buf,
    struct evbuffer_iovec *vec, int n_vecs);

evbuffer_reserve_space()函數給出 evbuffer 內部空間的指針。函數會擴展緩衝區以至少提供 size 字節的空間。到擴展空間的指針,以及其長度,會存儲在通過 vec 傳遞的向量數組中,n_vec 是數組的長度。

n_vec 的值必須至少是1。如果只提供一個向量,libevent 會確保請求的所有連續空間都在 單個擴展區中,但是這可能要求重新排列緩衝區,或者浪費內存。爲取得更好的性能,應該至少提供2個向量。函數返回提供請求的空間所需的向量數。

寫入到向量中的數據不會是緩衝區的一部分,直到調用 evbuffer_commit_space(),使得寫入的數據進入緩衝區。如果需要提交少於請求的空間,可以減小任何 evbuffer_iovec 結構體的 iov_len 字段,也可以提供較少的向量。函數成功時返回0,失敗時返回-1。

提示和警告

  • 調用任何重新排列 evbuffer 或者向其添加數據的函數都將使從evbuffer_reserve_space()獲取的指針失效。

  • 當前實現中,不論用戶提供多少個向量,evbuffer_reserve_space()從不使用多於兩個。未來版本可能會改變這一點。

  • 如果在多個線程中使用 evbuffer,確保在調用 evbuffer_reserve_space()之前使用evbuffer_lock()進行鎖定,然後在提交後解除鎖定。

示例

/* Suppose we want to fill a buffer with 2048 bytes of output from a
   generate_data() function, without copying. */
struct evbuffer_iovec v[2];
int n, i;
size_t n_to_add = 2048;

/* Reserve 2048 bytes.*/
n = evbuffer_reserve_space(buf, n_to_add, v, 2);
if (n<=0)
   return; /* Unable to reserve the space for some reason. */

for (i=0; i<n && n_to_add > 0; ++i) {
   size_t len = v[i].iov_len;
   if (len > n_to_add) /* Don't write more than n_to_add bytes. */
      len = n_to_add;
   if (generate_data(v[i].iov_base, len) < 0) {
      /* If there was a problem during data generation, we can just stop
         here; no data will be committed to the buffer. */
      return;
   }
   /* Set iov_len to the number of bytes we actually wrote, so we
      don't commit too much. */
   v[i].iov_len = len;
}

/* We commit the space here.  Note that we give it 'i' (the number of
   vectors we actually used) rather than 'n' (the number of vectors we
   had available. */
if (evbuffer_commit_space(buf, v, i) < 0)
   return; /* Error committing */

不好的示例

/* Here are some mistakes you can make with evbuffer_reserve().
   DO NOT IMITATE THIS CODE. */
struct evbuffer_iovec v[2];

{
  /* Do not use the pointers from evbuffer_reserve_space() after
     calling any functions that modify the buffer. */
  evbuffer_reserve_space(buf, 1024, v, 2);
  evbuffer_add(buf, "X", 1);
  /* WRONG: This next line won't work if evbuffer_add needed to rearrange
     the buffer's contents.  It might even crash your program. Instead,
     you add the data before calling evbuffer_reserve_space. */
  memset(v[0].iov_base, 'Y', v[0].iov_len-1);
  evbuffer_commit_space(buf, v, 1);
}

{
  /* Do not modify the iov_base pointers. */
  const char *data = "Here is some data";
  evbuffer_reserve_space(buf, strlen(data), v, 1);
  /* WRONG: The next line will not do what you want.  Instead, you
     should _copy_ the contents of data into v[0].iov_base. */
  v[0].iov_base = (char*) data;
  v[0].iov_len = strlen(data);
  /* In this case, evbuffer_commit_space might give an error if you're
     lucky */
  evbuffer_commit_space(buf, v, 1);
}

這個函數及其提出的接口從2.0.2-alpha 版本就存在了。

14. 使用 evbuffer 的網絡 IO

libevent 中 evbuffer 的最常見使用場合是網絡 IO。將 evbuffer 用於網絡 IO 的接口是:

接口

int evbuffer_write(struct evbuffer *buffer, evutil_socket_t fd);
int evbuffer_write_atmost(struct evbuffer *buffer, evutil_socket_t fd,
        ev_ssize_t howmuch);
int evbuffer_read(struct evbuffer *buffer, evutil_socket_t fd, int howmuch);

evbuffer_read()函數從套接字 fd 讀取至多 howmuch 字節到 buffer 末尾。成功時函數返回讀取的字節數,0表示 EOF,失敗時返回-1。注意,錯誤碼可能指示非阻塞操作不能立即成功,應該檢查錯誤碼 EAGAIN(或者 Windows 中的 WSAWOULDBLOCK)。如果 howmuch 爲負,evbuffer_read()試圖猜測要讀取多少數據。

evbuffer_write_atmost()函數試圖將 buffer 前面至多 howmuch 字節寫入到套接字 fd 中。成功時函數返回寫入的字節數,失敗時返回-1。跟 evbuffer_read()一樣,應該檢查錯誤碼,看是真的錯誤,還是僅僅指示非阻塞 IO 不能立即完成。如果爲 howmuch 給出負值,函數會試圖寫入 buffer 的所有內容。

調用 evbuffer_write()與使用負的 howmuch 參數調用 evbuffer_write_atmost()一樣:函數會試圖儘量清空 buffer 的內容。

在 Unix 中,這些函數應該可以在任何支持 read 和 write 的文件描述符上正確工作。在 Windows 中,僅僅支持套接字。

注意,如果使用 bufferevent,則不需要調用這些函數,bufferevent 的代碼已經爲你調用了。

evbuffer_write_atmost()函數在2.0.1-alpha 版本中引入。

15. evbuffer 和回調

evbuffer 的用戶常常需要知道什麼時候向 evbuffer 添加了數據,什麼時候移除了數據。爲支持這個,libevent 爲 evbuffer 提高了通用回調機制。

接口

struct evbuffer_cb_info {
        size_t orig_size;
        size_t n_added;
        size_t n_deleted;
};

typedef void (*evbuffer_cb_func)(struct evbuffer *buffer,
    const struct evbuffer_cb_info *info, void *arg);

向 evbuffer 添加數據,或者從中移除數據的時候,回調函數會被調用。函數收到緩衝區指針、一個 evbuffer_cb_info 結構體指針,和用戶提供的參數。evbuffer_cb_info 結構體的orig_size 字段指示緩衝區改變大小前的字節數,n_added 字段指示向緩衝區添加了多少字節;n_deleted 字段指示移除了多少字節。

接口

struct evbuffer_cb_entry;
struct evbuffer_cb_entry *evbuffer_add_cb(struct evbuffer *buffer,
    evbuffer_cb_func cb, void *cbarg);

evbuffer_add_cb()函數爲 evbuffer 添加一個回調函數,返回一個不透明的指針,隨後可用於代表這個特定的回調實例。cb 參數是將被調用的函數,cbarg 是用戶提供的將傳給這個函數的指針。

可以爲單個 evbuffer 設置多個回調,添加新的回調不會移除原來的回調。

示例

#include <event2/buffer.h>
#include <stdio.h>
#include <stdlib.h>

/* Here's a callback that remembers how many bytes we have drained in
   total from the buffer, and prints a dot every time we hit a
   megabyte. */
struct total_processed {
    size_t n;
};
void count_megabytes_cb(struct evbuffer *buffer,
    const struct evbuffer_cb_info *info, void *arg)
{
    struct total_processed *tp = arg;
    size_t old_n = tp->n;
    int megabytes, i;
    tp->n += info->n_deleted;
    megabytes = ((tp->n) >> 20) - (old_n >> 20);
    for (i=0; i<megabytes; ++i)
        putc('.', stdout);
}

void operation_with_counted_bytes(void)
{
    struct total_processed *tp = malloc(sizeof(*tp));
    struct evbuffer *buf = evbuffer_new();
    tp->n = 0;
    evbuffer_add_cb(buf, count_megabytes_cb, tp);

    /* Use the evbuffer for a while.  When we're done: */
    evbuffer_free(buf);
    free(tp);
}

注意:釋放非空 evbuffer 不會清空其數據,釋放 evbuffer 也不會爲回調釋放用戶提供的數據指針。

如果不想讓緩衝區上的回調永遠激活,可以移除或者禁用回調:

接口

int evbuffer_remove_cb_entry(struct evbuffer *buffer,
    struct evbuffer_cb_entry *ent);
int evbuffer_remove_cb(struct evbuffer *buffer, evbuffer_cb_func cb,
    void *cbarg);

#define EVBUFFER_CB_ENABLED 1
int evbuffer_cb_set_flags(struct evbuffer *buffer,
                          struct evbuffer_cb_entry *cb,
                          ev_uint32_t flags);
int evbuffer_cb_clear_flags(struct evbuffer *buffer,
                          struct evbuffer_cb_entry *cb,
                          ev_uint32_t flags);

可以通過添加回調時候的 evbuffer_cb_entry 來移除回調,也可以通過回調函數和參數指針來移除。成功時函數返回0,失敗時返回-1。

evbuffer_cb_set_flags()和 evbuffer_cb_clear_flags()函數分別爲回調函數設置或者清除給定的標誌。當前只有一個標誌是用戶可見的:EVBUFFER_CB_ENABLED。這個標誌默認是打開的。如果清除這個標誌,對 evbuffer 的修改不會調用回調函數。

接口

int evbuffer_defer_callbacks(struct evbuffer *buffer, struct event_base *base);

跟 bufferevent 回調一樣,可以讓 evbuffer 回調不在 evbuffer 被修改時立即運行,而是延遲到某 event_base 的事件循環中執行。如果有多個 evbuffer,它們的回調潛在地讓數據添加到 evbuffer 中,或者從中移除,又要避免棧崩潰,延遲迴調是很有用的。

如果回調被延遲,則最終執行時,它可能是多個操作結果的總和。

與 bufferevent 一樣,evbuffer 具有內部引用計數的,所以即使還有未執行的延遲迴調,釋放 evbuffer 也是安全的。

整 個 回 調 系 統 是 2.0.1-alpha 版 本 新 引 入 的 。 evbuffer_cb_(set|clear)_flags() 函 數 從2.0.2-alpha 版本開始存在。

16. 爲基於 evbuffer 的 IO 避免數據複製

真正高速的網絡編程通常要求儘量少的數據複製,libevent 爲此提供了一些機制:

接口

typedef void (*evbuffer_ref_cleanup_cb)(const void *data,
    size_t datalen, void *extra);

int evbuffer_add_reference(struct evbuffer *outbuf,
    const void *data, size_t datlen,
    evbuffer_ref_cleanup_cb cleanupfn, void *extra);

這個函數通過引用向 evbuffer 末尾添加一段數據。不會進行復制:evbuffer 只會存儲一個到data 處的 datlen 字節的指針。因此,在 evbuffer 使用這個指針期間,必須保持指針是有效的。evbuffer 會在不再需要這部分數據的時候調用用戶提供的 cleanupfn 函數,帶有提供的data 指針、datlen 值和 extra 指針參數。函數成功時返回0,失敗時返回-1。

示例

#include <event2/buffer.h>
#include <stdlib.h>
#include <string.h>

/* In this example, we have a bunch of evbuffers that we want to use to
   spool a one-megabyte resource out to the network.  We do this
   without keeping any more copies of the resource in memory than
   necessary. */

#define HUGE_RESOURCE_SIZE (1024*1024)
struct huge_resource {
    /* We keep a count of the references that exist to this structure,
       so that we know when we can free it. */
    int reference_count;
    char data[HUGE_RESOURCE_SIZE];
};

struct huge_resource *new_resource(void) {
    struct huge_resource *hr = malloc(sizeof(struct huge_resource));
    hr->reference_count = 1;
    /* Here we should fill hr->data with something.  In real life,
       we'd probably load something or do a complex calculation.
       Here, we'll just fill it with EEs. */
    memset(hr->data, 0xEE, sizeof(hr->data));
    return hr;
}

void free_resource(struct huge_resource *hr) {
    --hr->reference_count;
    if (hr->reference_count == 0)
        free(hr);
}

static void cleanup(const void *data, size_t len, void *arg) {
    free_resource(arg);
}

/* This is the function that actually adds the resource to the
   buffer. */
void spool_resource_to_evbuffer(struct evbuffer *buf,
    struct huge_resource *hr)
{
    ++hr->reference_count;
    evbuffer_add_reference(buf, hr->data, HUGE_RESOURCE_SIZE,
        cleanup, hr);
}

一些操作系統提供了將文件寫入到網絡,而不需要將數據複製到用戶空間的方法。如果存在,可以使用下述接口訪問這種機制:

17. 將文件添加到evbuffer

一些操作系統提供了無需將數據複製到用戶空間即可將文件寫入網絡的方法。 您可以使用簡單的界面訪問以下機制:

接口

int evbuffer_add_file(struct evbuffer *output, int fd, ev_off_t offset,
    size_t length);

evbuffer_add_file()要求一個打開的可讀文件描述符 fd(注意:不是套接字)。函數將文件中offset 處開始的length 字節添加到 output 末尾。成功時函數返回0,失敗時返回-1。

在2.0.2-alpha 版中,對於使用這種方式添加的數據的可靠操作只有:通過 evbuffer_write*()將其發送到網絡、使用 evbuffer_drain()清空數據,或者使用 evbuffer_*_buffer()將其移動到另一個 evbuffer 中。不能使用evbuffer_remove()取出數據,使用 evbuffer_pullup()進行線性化等。

如果操作系統支持 splice()或者 sendfile(),則調用 evbuffer_write()時 libevent 會直接使用這些函數來將來自 fd 的數據發送到網絡中,而根本不將數據複製到用戶內存中。如果不存在splice()和 sendfile(),但是支持 mmap(),libevent 將進行文件映射,而內核將意識到永遠不需要將數據複製到用戶空間。否則,libevent 會將數據從磁盤讀取到內存。

從evbuffer刷新數據後或釋放evbuffer時,文件描述符將關閉。 如果這不是想要的,或者想要對文件進行更細粒度的控制,請參見下面的file_segment功能。

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

18. 帶有文件段的細粒度控制

evbuffer_add_file()接口無法多次添加同一文件,因爲它擁有文件所有權。

接口

struct evbuffer_file_segment;

struct evbuffer_file_segment *evbuffer_file_segment_new(
        int fd, ev_off_t offset, ev_off_t length, unsigned flags);
void evbuffer_file_segment_free(struct evbuffer_file_segment *seg);
int evbuffer_add_file_segment(struct evbuffer *buf,
    struct evbuffer_file_segment *seg, ev_off_t offset, ev_off_t length);

evbuffer_file_segment_new()函數創建並返回一個新的evbuffer_file_segment對象,以表示存儲在fd中的基礎文件的一部分,該文件從offset開始幷包含長度字節。 錯誤時,它將返回NULL。

文件段可以根據需要使用sendfile,splice,mmap,CreateFileMapping或malloc()和read()來實現。 它們是使用受支持程度最輕的機制創建的,並根據需要過渡到較重的機制。 (例如,如果操作系統支持sendfile和mmap,則可以僅使用sendfile來實現文件段,直到嘗試實際檢查其內容爲止。此時,需要對它進行mmap()處理。)可以控制 具有以下標誌的文件段的細粒度行爲:

  • EVBUF_FS_CLOSE_ON_FREE
    如果設置了此標誌,則使用evbuffer_file_segment_free()釋放文件段將關閉基礎文件。

  • EVBUF_FS_DISABLE_MMAP
    如果設置了此標誌,則file_segment將永遠不會對此文件使用映射內存樣式的後端(CreateFileMapping,mmap),即使這樣做比較合適。

  • EVBUF_FS_DISABLE_SENDFILE
    如果設置了此標誌,則file_segment將永遠不會爲此文件使用sendfile樣式的後端(sendfile,splice),即使那是適當的。

  • EVBUF_FS_DISABLE_LOCKING
    如果設置了此標誌,則不會爲文件段分配任何鎖:以任何方式被多個線程看到的方式使用它都是不安全的。

有了evbuffer_file_segment後,可以使用evbuffer_add_file_segment()將部分或全部添加到evbuffer中。 這裏的offset參數是指文件段內的偏移量,而不是文件本身內的偏移量。

當不再希望使用文件段時,可以使用evbuffer_file_segment_free()釋放它。 直到沒有evbuffer不再保存對文件段的引用之前,纔會釋放實際的存儲空間。

接口

typedef void (*evbuffer_file_segment_cleanup_cb)(
    struct evbuffer_file_segment const *seg, int flags, void *arg);

void evbuffer_file_segment_add_cleanup_cb(struct evbuffer_file_segment *seg,
        evbuffer_file_segment_cleanup_cb cb, void *arg);

可以在文件段中添加一個回調函數,當釋放對該文件段的最終引用並且該文件段即將被釋放時,將調用該函數。 此回調不得嘗試重新激活文件段,將其添加到任何緩衝區等。

這些文件段功能首先出現在Libevent 2.1.1-alpha中; evbuffer_file_segment_add_cleanup_cb()已添加到2.1.2-alpha中。

19. 通過引用將evbuffer添加到另一個evbuffer

可以通過引用將一個evbuffer添加到另一個evbuffer:不必刪除一個緩衝區的內容並將其添加到另一個緩衝區,而是給一個evbuffer引用另一個緩衝區,並且它的行爲就好像已複製了所有字節。

接口

int evbuffer_add_buffer_reference(struct evbuffer *outbuf,
    struct evbuffer *inbuf);

evbuffer_add_buffer_reference()函數的行爲就像已將所有數據從outbuf複製到inbuf一樣,但是不執行任何不必要的複製。 如果成功則返回0,失敗則返回-1。

請注意,對inbuf內容的後續更改不會反映在outbuf中:此函數通過引用而不是evbuffer本身添加evbuffer的當前內容。

還要注意,不能嵌套緩衝區引用:已經是一個evbuffer_add_buffer_reference調用的緩衝區的緩衝區不能是另一個evbuffer_add_buffer_reference調用的緩衝區。

此功能在Libevent 2.1.1-alpha中引入。

20. 讓 evbuffer 只能添加或者只能移除

接口

int evbuffer_freeze(struct evbuffer *buf, int at_front);
int evbuffer_unfreeze(struct evbuffer *buf, int at_front);

可以使用這些函數暫時禁止修改 evbuffer 的開頭或者末尾。bufferevent 的代碼在內部使用這些函數阻止對輸出緩衝區頭部,或者輸入緩衝區尾部的意外修改。

evbuffer_freeze()函數是2.0.1-alpha 版本引入的。

九、連接監聽器:接受TCP連接

evconnlistener 機制提供了監聽和接受 TCP 連接的方法。

本章的所有函數和類型都在 event2/listener.h 中聲明,除非特別說明,它們都在2.0.2-alpha 版本中首次出現。

1. 創建和釋放 evconnlistener

接口

struct evconnlistener *evconnlistener_new(struct event_base *base,
    evconnlistener_cb cb, void *ptr, unsigned flags, int backlog,
    evutil_socket_t fd);
struct evconnlistener *evconnlistener_new_bind(struct event_base *base,
    evconnlistener_cb cb, void *ptr, unsigned flags, int backlog,
    const struct sockaddr *sa, int socklen);
void evconnlistener_free(struct evconnlistener *lev);

兩個 evconnlistener_new*()函數都分配和返回一個新的連接監聽器對象。連接監聽器使用event_base 來得知什麼時候在給定的監聽套接字上有新的 TCP 連接。新連接到達時,監聽器調用你給出的回調函數。

兩個函數中,base 參數都是監聽器用於監聽連接的 event_base。cb 是收到新連接時要調用的回調函數;如果 cb 爲 NULL,則監聽器是禁用的,直到設置了回調函數爲止。ptr 指針將傳遞給回調函數。flags 參數控制回調函數的行爲,下面會更詳細論述。backlog 是任何時刻網絡棧允許處於還未接受狀態的最大未決連接數。更多細節請查看系統的 listen()函數文檔。如果 backlog 是負的,libevent 會試圖挑選一個較好的值;如果爲0,libevent 認爲已 經對提供的套接字調用了 listen()。

兩個函數的不同在於如何建立監聽套接字。evconnlistener_new()函數假定已經將套接字綁定到要監聽的端口,然後通過 fd 傳入這個套接字。如果要 libevent 分配和綁定套接字,可以調用 evconnlistener_new_bind(),傳輸要綁定到的地址和地址長度。

提示: 使用evconnlistener_new時,通過使用evutil_make_socket_nonblocking或手動設置正確的套接字選項,確保偵聽套接字處於非阻止模式。 當偵聽套接字處於阻塞模式時,可能會發生未定義的行爲。

要釋放連接監聽器,調用 evconnlistener_free()。

可識別的標誌

可以給 evconnlistener_new()函數的 flags 參數傳入一些標誌。可以用或(OR)運算任意連接

下述標誌:

  • LEV_OPT_LEAVE_SOCKETS_BLOCKING

    默認情況下,連接監聽器接收新套接字後,會將其設置爲非阻塞的,以便將其用於 libevent。如果不想要這種行爲,可以設置這個標誌。

  • LEV_OPT_CLOSE_ON_FREE

    如果設置了這個選項,釋放連接監聽器會關閉底層套接字。

  • LEV_OPT_CLOSE_ON_EXEC

    如果設置了這個選項,連接監聽器會爲底層套接字設置 close-on-exec 標誌。更多信息請查看 fcntl 和 FD_CLOEXEC 的平臺文檔。

  • LEV_OPT_REUSEABLE

    某些平臺在默認情況下,關閉某監聽套接字後,要過一會兒其他套接字纔可以綁定到同一個端口。設置這個標誌會讓 libevent 標記套接字是可重用的,這樣一旦關閉,可以立即打開其他套接字,在相同端口進行監聽。

  • LEV_OPT_THREADSAFE

    爲監聽器分配鎖,這樣就可以在多個線程中安全地使用了。這是2.0.8-rc 的新功能。

  • LEV_OPT_DISABLED
    初始化監聽器以將其禁用,而不是啓用。 您可以使用evconnlistener_enable()手動將其打開。 Libevent 2.1.1-alpha中的新功能。

  • LEV_OPT_DEFERRED_ACCEPT
    如果可能的話,告訴內核在接收到套接字上的某些數據並且可以讀取之前,不要宣佈套接字已被接受。 如果您的協議不是從客戶端傳輸數據開始的,則不要使用此選項,因爲在這種情況下,此選項有時會導致內核從不告訴您有關連接的信息。 並非所有操作系統都支持此選項:在不支持的操作系統上,此選項無效。 Libevent 2.1.1-alpha中的新功能。

連接監聽器回調

接口

typedef void (*evconnlistener_cb)(struct evconnlistener *listener,
    evutil_socket_t sock, struct sockaddr *addr, int len, void *ptr);

接收到新連接會調用提供的回調函數。listener 參數是接收連接的連接監聽器。sock 參數是新接收的套接字 。 addr 和 len 參數是接收連接的地址和地址長度 。ptr是調用evconnlistener_new()時用戶提供的指針。

2. 啓用和禁用 evconnlistener

接口

int evconnlistener_disable(struct evconnlistener *lev);
int evconnlistener_enable(struct evconnlistener *lev);

這兩個函數暫時禁止或者重新允許監聽新連接。

3. 調整 evconnlistener 的回調函數

接口

void evconnlistener_set_cb(struct evconnlistener *lev,
    evconnlistener_cb cb, void *arg);

函數調整 evconnlistener 的回調函數和其參數。它是2.0.9-rc 版本引入的。

4. 檢測 evconnlistener

接口

evutil_socket_t evconnlistener_get_fd(struct evconnlistener *lev);
struct event_base *evconnlistener_get_base(struct evconnlistener *lev);

這些函數分別返回監聽器關聯的套接字和 event_base。

evconnlistener_get_fd()函數首次出現在2.0.3-alpha 版本。

5. 偵測錯誤

可以設置一個一旦監聽器上的 accept()調用失敗就被調用的錯誤回調函數。對於一個不解決就會鎖定進程的錯誤條件,這很重要。

接口

typedef void (*evconnlistener_errorcb)(struct evconnlistener *lis, void *ptr);
void evconnlistener_set_error_cb(struct evconnlistener *lev,
    evconnlistener_errorcb errorcb);

如果使用 evconnlistener_set_error_cb()爲監聽器設置了錯誤回調函數,則監聽器發生錯誤時回調函數就會被調用。第一個參數是監聽器,第二個參數是調用 evconnlistener_new() 時傳入的 ptr。

這個函數在2.0.8-rc 版本引入。

6. 示例代碼:回顯服務器

示例

#include <event2/listener.h>
#include <event2/bufferevent.h>
#include <event2/buffer.h>

#include <arpa/inet.h>

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

static void
echo_read_cb(struct bufferevent *bev, void *ctx)
{
        /* This callback is invoked when there is data to read on bev. */
        struct evbuffer *input = bufferevent_get_input(bev);
        struct evbuffer *output = bufferevent_get_output(bev);

        /* Copy all the data from the input buffer to the output buffer. */
        evbuffer_add_buffer(output, input);
}

static void
echo_event_cb(struct bufferevent *bev, short events, void *ctx)
{
        if (events & BEV_EVENT_ERROR)
                perror("Error from bufferevent");
        if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
                bufferevent_free(bev);
        }
}

static void
accept_conn_cb(struct evconnlistener *listener,
    evutil_socket_t fd, struct sockaddr *address, int socklen,
    void *ctx)
{
        /* We got a new connection! Set up a bufferevent for it. */
        struct event_base *base = evconnlistener_get_base(listener);
        struct bufferevent *bev = bufferevent_socket_new(
                base, fd, BEV_OPT_CLOSE_ON_FREE);

        bufferevent_setcb(bev, echo_read_cb, NULL, echo_event_cb, NULL);

        bufferevent_enable(bev, EV_READ|EV_WRITE);
}

static void
accept_error_cb(struct evconnlistener *listener, void *ctx)
{
        struct event_base *base = evconnlistener_get_base(listener);
        int err = EVUTIL_SOCKET_ERROR();
        fprintf(stderr, "Got an error %d (%s) on the listener. "
                "Shutting down.\n", err, evutil_socket_error_to_string(err));

        event_base_loopexit(base, NULL);
}

int
main(int argc, char **argv)
{
        struct event_base *base;
        struct evconnlistener *listener;
        struct sockaddr_in sin;

        int port = 9876;

        if (argc > 1) {
                port = atoi(argv[1]);
        }
        if (port<=0 || port>65535) {
                puts("Invalid port");
                return 1;
        }

        base = event_base_new();
        if (!base) {
                puts("Couldn't open event base");
                return 1;
        }

        /* Clear the sockaddr before using it, in case there are extra
         * platform-specific fields that can mess us up. */
        memset(&sin, 0, sizeof(sin));
        /* This is an INET address */
        sin.sin_family = AF_INET;
        /* Listen on 0.0.0.0 */
        sin.sin_addr.s_addr = htonl(0);
        /* Listen on the given port. */
        sin.sin_port = htons(port);

        listener = evconnlistener_new_bind(base, accept_conn_cb, NULL,
            LEV_OPT_CLOSE_ON_FREE|LEV_OPT_REUSEABLE, -1,
            (struct sockaddr*)&sin, sizeof(sin));
        if (!listener) {
                perror("Couldn't create listener");
                return 1;
        }
        evconnlistener_set_error_cb(listener, accept_error_cb);

        event_base_dispatch(base);
        return 0;
}

十、使用 libevent DNS:高層和底層功能

libevent 提供了少量用於解析 DNS 名字的 API,以及用於實現簡單 DNS 服務器的機制。

我們從用於名字查詢的高層機制開始介紹,然後介紹底層機制和服務器機制。

注意
libevent 當前的 DNS 客戶端實現存在限制:不支持 TCP 查詢、DNSSec 以及任意記錄類型。未來版本的 libevent 會修正這些限制。

1. 預備:可移植的阻塞式名字解析

爲移植已經使用阻塞式名字解析的程序,libevent 提供了標準 getaddrinfo()接口的可移植實現。對於需要運行在沒有 getaddrinfo()函數,或者 getaddrinfo()不像我們的替代函數那樣遵循標準的平臺上的程序,這個替代實現很有用。

getaddrinfo()接口由 RFC 3493的6.1節定義。關於 libevent 如何不滿足其一致性實現的概述,請看下面的“兼容性提示”節。

接口

struct evutil_addrinfo {
    int ai_flags;
    int ai_family;
    int ai_socktype;
    int ai_protocol;
    size_t ai_addrlen;
    char *ai_canonname;
    struct sockaddr *ai_addr;
    struct evutil_addrinfo *ai_next;
};

#define EVUTIL_AI_PASSIVE     /* ... */
#define EVUTIL_AI_CANONNAME   /* ... */
#define EVUTIL_AI_NUMERICHOST /* ... */
#define EVUTIL_AI_NUMERICSERV /* ... */
#define EVUTIL_AI_V4MAPPED    /* ... */
#define EVUTIL_AI_ALL         /* ... */
#define EVUTIL_AI_ADDRCONFIG  /* ... */

int evutil_getaddrinfo(const char *nodename, const char *servname,
    const struct evutil_addrinfo *hints, struct evutil_addrinfo **res);
void evutil_freeaddrinfo(struct evutil_addrinfo *ai);
const char *evutil_gai_strerror(int err);

evutil_getaddrinfo()函數試圖根據 hints 給出的規則,解析指定的 nodename 和 servname,建立一個 evutil_addrinfo 結構體鏈表,將其存儲在*res 中。成功時函數返回0,失敗時返回非零的錯誤碼。

必須至少提供 nodename 和 servname 中的一個。如果提供了 nodename,則它是 IPv4字面地址(如127.0.0.1)、IPv6字面地址(如::1),或者是 DNS 名字(如 www.example.com)。

如果提供了 servname,則它是某網絡服務的符號名(如 https),或者是一個包含十進制端口號的字符串(如443)。

如果不指定 servname,則 res 中的端口號將是零。如果不指定 nodename,則 res 中的地址要麼是 localhost(默認),要麼是“任意”(如果設置了 EVUTIL_AI_PASSIVE)。hints 的 ai_flags 字段指示 evutil_getaddrinfo 如何進行查詢,它可以包含0個或者多個以或運算連接的下述標誌:

  • EVUTIL_AI_PASSIVE
    這個標誌指示將地址用於監聽,而不是連接。通常二者沒有差別,除非 nodename 爲空:對於連接,空的 nodename 表示 localhost(127.0.0.1或者::1);而對於監聽,空的 nodename表示任意(0.0.0.0或者::0)。

  • EVUTIL_AI_CANONNAME
    如果設置了這個標誌,則函數試圖在 ai_canonname 字段中報告標準名稱。

  • EVUTIL_AI_NUMERICHOST
    如果設置了這個標誌,函數僅僅解析數值類型的 IPv4和 IPv6地址;如果 nodename 要求名字查詢,函數返回 EVUTIL_EAI_NONAME 錯誤。

  • EVUTIL_AI_NUMERICSERV
    如果設置了這個標誌,函數僅僅解析數值類型的服務名。如果 servname 不是空,也不是十進制整數,函數返回 EVUTIL_EAI_NONAME 錯誤。

  • EVUTIL_AI_V4MAPPED
    這個標誌表示,如果 ai_family 是 AF_INET6,但是找不到 IPv6地址,則應該以 v4映射(v4-mapped)型 IPv6地址的形式返回結果中的 IPv4地址。當前 evutil_getaddrinfo()不支持這個標誌,除非操作系統支持它。

  • EVUTIL_AI_ALL
    如果設置了這個標誌和 EVUTIL_AI_V4MAPPED,則無論結果是否包含 IPv6地址,IPv4地址都應該以 v4映射型 IPv6地址的形式返回。當前 evutil_getaddrinfo()不支持這個標誌,除非操作系統支持它。

  • EVUTIL_AI_ADDRCONFIG
    如果設置了這個標誌,則只有系統擁有非本地的 IPv4地址時,結果才包含 IPv4地址;只有系統擁有非本地的 IPv6地址時,結果才包含 IPv6地址。

hints 的 ai_family 字段指示 evutil_getaddrinfo()應該返回哪個地址。字段值可以是 AF_INET,表示只請求 IPv4地址;也可以是 AF_INET6,表示只請求 IPv6地址;或者用 AF_UNSPEC表示請求所有可用地址。

hints 的 ai_socktype 和 ai_protocol 字段告知 evutil_getaddrinfo()將如何使用返回的地址。這兩個字段值的意義與傳遞給 socket()函數的 socktype 和 protocol 參數值相同。

成功時函數新建一個 evutil_addrinfo 結構體鏈表,存儲在*res 中,鏈表的每個元素通過ai_next 指針指向下一個元素。因爲鏈表是在堆上分配的,所以需要調用 evutil_freeaddrinfo()進行釋放。

如果失敗,函數返回數值型的錯誤碼:

  • EVUTIL_EAI_ADDRFAMILY
    請求的地址族對 nodename 沒有意義。

  • EVUTIL_EAI_AGAIN
    名字解析中發生可以恢復的錯誤,請稍後重試。

  • EVUTIL_EAI_FAIL
    名字解析中發生不可恢復的錯誤:解析器或者 DNS 服務器可能已經崩潰。

  • EVUTIL_EAI_BADFLAGS
    hints 中的 ai_flags 字段無效。

  • EVUTIL_EAI_FAMILY
    不支持 hints 中的 ai_family 字段。

  • EVUTIL_EAI_MEMORY
    迴應請求的過程耗盡內存。

  • EVUTIL_EAI_NODATA
    請求的主機不存在。

  • EVUTIL_EAI_SERVICE
    請求的服務不存在。

  • EVUTIL_EAI_SOCKTYPE
    不支持請求的套接字類型,或者套接字類型與 ai_protocol 不匹配。

  • EVUTIL_EAI_SYSTEM
    名字解析中發生其他系統錯誤,更多信息請檢查 errno。

  • EVUTIL_EAI_CANCEL
    應用程序在解析完成前請求取消。evutil_getaddrinfo()函數從不產生這個錯誤,但是後面描述的 evdns_getaddrinfo()可能產生這個錯誤。

調用 evutil_gai_strerror()可以將上述錯誤值轉化成描述性的字符串。

注意如果操作系統定義了 addrinfo 結構體,則 evutil_addrinfo 僅僅是操作系統內置的addrinfo 結構體的別名。類似地,如果操作系統定義了 AI_*標誌,則相應的 EVUTIL_AI_*標誌僅僅是本地標誌的別名;如果操作系統定義了 EAI_*錯誤,則相應的 EVUTIL_EAI_*只是本地錯誤碼的別名。

示例:解析主機名,建立阻塞的連接

#include <event2/util.h>

#include <sys/socket.h>
#include <sys/types.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>

evutil_socket_t
get_tcp_socket_for_host(const char *hostname, ev_uint16_t port)
{
    char port_buf[6];
    struct evutil_addrinfo hints;
    struct evutil_addrinfo *answer = NULL;
    int err;
    evutil_socket_t sock;

    /* Convert the port to decimal. */
    evutil_snprintf(port_buf, sizeof(port_buf), "%d", (int)port);

    /* Build the hints to tell getaddrinfo how to act. */
    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_UNSPEC; /* v4 or v6 is fine. */
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP; /* We want a TCP socket */
    /* Only return addresses we can use. */
    hints.ai_flags = EVUTIL_AI_ADDRCONFIG;

    /* Look up the hostname. */
    err = evutil_getaddrinfo(hostname, port_buf, &hints, &answer);
    if (err != 0) {
          fprintf(stderr, "Error while resolving '%s': %s",
                  hostname, evutil_gai_strerror(err));
          return -1;
    }

    /* If there was no error, we should have at least one answer. */
    assert(answer);
    /* Just use the first answer. */
    sock = socket(answer->ai_family,
                  answer->ai_socktype,
                  answer->ai_protocol);
    if (sock < 0)
        return -1;
    if (connect(sock, answer->ai_addr, answer->ai_addrlen)) {
        /* Note that we're doing a blocking connect in this function.
         * If this were nonblocking, we'd need to treat some errors
         * (like EINTR and EAGAIN) specially. */
        EVUTIL_CLOSESOCKET(sock);
        return -1;
    }

    return sock;
}

上述函數和常量是2.0.3-alpha 版本新增加的,聲明在 event2/util.h 中。

2. 使用 evdns_getaddrinfo()的非阻塞式名字解析

通常的 getaddrinfo(),以及上面的 evutil_getaddrinfo()的問題是,它們是阻塞的:調用線程必須等待函數查詢 DNS 服務器,等待迴應。對於 libevent,這可能不是期望的行爲。對於非阻塞式應用,libevent 提供了一組函數用於啓動 DNS 請求,讓 libevent 等待服務器迴應。

接口

typedef void (*evdns_getaddrinfo_cb)(
    int result, struct evutil_addrinfo *res, void *arg);
struct evdns_getaddrinfo_request;

struct evdns_getaddrinfo_request *evdns_getaddrinfo(
    struct evdns_base *dns_base,
    const char *nodename, const char *servname,
    const struct evutil_addrinfo *hints_in,
    evdns_getaddrinfo_cb cb, void *arg);

void evdns_getaddrinfo_cancel(struct evdns_getaddrinfo_request *req);

除了不會阻塞在 DNS 查詢上,而是使用 libevent 的底層 DNS 機制進行查詢外,evdns_getaddrinfo()和 evutil_getaddrinfo()是一樣的。因爲函數不是總能立即返回結果,所以需要提供一個 evdns_getaddrinfo_cb 類型的回調函數,以及一個給回調函數的可選的用戶參數。

此外,調用 evdns_getaddrinfo()還要求一個 evdns_base 指針。evdns_base 結構體爲libevent 的 DNS 解析器保持狀態和配置。關於如何獲取 evdns_base 指針,請看下一節。

如果失敗或者立即成功,函數返回 NULL。否則,函數返回一個 evdns_getaddrinfo_request指針。在解析完成之前可以隨時使用 evdns_getaddrinfo_cancel()和這個指針來取消解析。

注意:不論 evdns_getaddrinfo()是否返回 NULL,是否調用了 evdns_getaddrinfo_cancel(),回調函數總是會被調用。

evdns_getaddrinfo()內部會複製 nodename、servname 和 hints 參數,所以查詢進行過程中不必保持這些參數有效。

示例:使用evdns_getaddrinfo()的非阻塞查詢

#include <event2/dns.h>
#include <event2/util.h>
#include <event2/event.h>

#include <sys/socket.h>

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

int n_pending_requests = 0;
struct event_base *base = NULL;

struct user_data {
    char *name; /* the name we're resolving */
    int idx; /* its position on the command line */
};

void callback(int errcode, struct evutil_addrinfo *addr, void *ptr)
{
    struct user_data *data = ptr;
    const char *name = data->name;
    if (errcode) {
        printf("%d. %s -> %s\n", data->idx, name, evutil_gai_strerror(errcode));
    } else {
        struct evutil_addrinfo *ai;
        printf("%d. %s", data->idx, name);
        if (addr->ai_canonname)
            printf(" [%s]", addr->ai_canonname);
        puts("");
        for (ai = addr; ai; ai = ai->ai_next) {
            char buf[128];
            const char *s = NULL;
            if (ai->ai_family == AF_INET) {
                struct sockaddr_in *sin = (struct sockaddr_in *)ai->ai_addr;
                s = evutil_inet_ntop(AF_INET, &sin->sin_addr, buf, 128);
            } else if (ai->ai_family == AF_INET6) {
                struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)ai->ai_addr;
                s = evutil_inet_ntop(AF_INET6, &sin6->sin6_addr, buf, 128);
            }
            if (s)
                printf("    -> %s\n", s);
        }
        evutil_freeaddrinfo(addr);
    }
    free(data->name);
    free(data);
    if (--n_pending_requests == 0)
        event_base_loopexit(base, NULL);
}

/* Take a list of domain names from the command line and resolve them in
 * parallel. */
int main(int argc, char **argv)
{
    int i;
    struct evdns_base *dnsbase;

    if (argc == 1) {
        puts("No addresses given.");
        return 0;
    }
    base = event_base_new();
    if (!base)
        return 1;
    dnsbase = evdns_base_new(base, 1);
    if (!dnsbase)
        return 2;

    for (i = 1; i < argc; ++i) {
        struct evutil_addrinfo hints;
        struct evdns_getaddrinfo_request *req;
        struct user_data *user_data;
        memset(&hints, 0, sizeof(hints));
        hints.ai_family = AF_UNSPEC;
        hints.ai_flags = EVUTIL_AI_CANONNAME;
        /* Unless we specify a socktype, we'll get at least two entries for
         * each address: one for TCP and one for UDP. That's not what we
         * want. */
        hints.ai_socktype = SOCK_STREAM;
        hints.ai_protocol = IPPROTO_TCP;

        if (!(user_data = malloc(sizeof(struct user_data)))) {
            perror("malloc");
            exit(1);
        }
        if (!(user_data->name = strdup(argv[i]))) {
            perror("strdup");
            exit(1);
        }
        user_data->idx = i;

        ++n_pending_requests;
        req = evdns_getaddrinfo(
                          dnsbase, argv[i], NULL /* no service name given */,
                          &hints, callback, user_data);
        if (req == NULL) {
          printf("    [request for %s returned immediately]\n", argv[i]);
          /* No need to free user_data or decrement n_pending_requests; that
           * happened in the callback. */
        }
    }

    if (n_pending_requests)
      event_base_dispatch(base);

    evdns_base_free(dnsbase, 0);
    event_base_free(base);

    return 0;
}

上述函數是2.0.3-alpha 版本新增加的,聲明在 event2/dns.h 中。

3. 創建和配置 evdns_base

使用 evdns 進行非阻塞 DNS 查詢之前需要配置一個 evdns_base。evdns_base 存儲名字服務器列表和 DNS 配置選項,跟蹤活動的、進行中的 DNS 請求。

接口

struct evdns_base *evdns_base_new(struct event_base *event_base,
       int initialize);
void evdns_base_free(struct evdns_base *base, int fail_requests);

成功時 evdns_base_new()返回一個新建的 evdns_base,失敗時返回 NULL。如果 initialize參數爲 true,函數試圖根據操作系統的默認值配置 evdns_base;否則,函數讓 evdns_base爲空,不配置名字服務器和選項。

可以用 evdns_base_free()釋放不再使用的 evdns_base。如果 fail_request 參數爲 true,函數會在釋放 evdns_base 前讓所有進行中的請求使用取消錯誤碼調用其回調函數。

3.1 使用系統配置初始化 evdns

如果需要更多地控制 evdns_base 如何初始化,可以爲 evdns_base_new()的 initialize 參數傳遞0,然後調用下述函數。

接口

#define DNS_OPTION_SEARCH 1
#define DNS_OPTION_NAMESERVERS 2
#define DNS_OPTION_MISC 4
#define DNS_OPTION_HOSTSFILE 8
#define DNS_OPTIONS_ALL 15
int evdns_base_resolv_conf_parse(struct evdns_base *base, int flags,
                                 const char *filename);

#ifdef WIN32
int evdns_base_config_windows_nameservers(struct evdns_base *);
#define EVDNS_BASE_CONFIG_WINDOWS_NAMESERVERS_IMPLEMENTED
#endif

evdns_base_resolv_conf_parse()函數掃描 resolv.conf 格式的文件 filename,從中讀取 flags
指示的選項(關於 resolv.conf 文件的更多信息,請看 Unix 手冊)。

  • DNS_OPTION_SEARCH
    請求從 resolv.conf 文件讀取 domain 和 search 字段以及 ndots 選項,使用它們來確定使用哪個域(如果存在)來搜索不是全限定的主機名。

  • DNS_OPTION_NAMESERVERS
    請求從 resolv.conf 中讀取名字服務器地址。

  • DNS_OPTION_MISC
    請求從 resolv.conf 文件中讀取其他配置選項。

  • DNS_OPTION_HOSTSFILE
    請求從/etc/hosts 文件讀取主機列表。

  • DNS_OPTION_ALL
    請求從 resolv.conf 文件獲取儘量多的信息。

Windows 中 沒 有 可 以 告 知 名 字 服 務 器 在 哪 裏 的 resolv.conf 文 件 , 但 可 以 用evdns_base_config_windows_nameservers()函數從註冊表(或者 NetworkParams,或者其他隱藏的地方)讀取名字服務器。

3.2 resolv.conf 文件格式

resolv.conf 是一個文本文件,每一行要麼是空行,要麼包含以#開頭的註釋,要麼由一個跟
隨零個或者多個參數的標記組成。可以識別的標記有:

  • nameserver

    必須後隨一個名字服務器的 IP 地址。作爲一個擴展,libevent 允許使用 IP:Port 或者IPv6:port 語法爲名字服務器指定非標準端口。

  • domain

    本地域名

  • search

    解析本地主機名時要搜索的名字列表。如果不能正確解析任何含有少於“ndots”個點的本地名字,則在這些域名中進行搜索。比如說,如果“search”字段值爲 example.com,“ndots” 爲1,則用戶請求解析“www”時,函數認爲那是“www.example.com”。

  • options

    空格分隔的選項列表。選項要麼是空字符串,要麼具有格式 option:value(如果有參數)。

    可識別的選項有:

    • ndots:INTEGER
      用於配置搜索,請參考上面的“search”,默認值是1。

    • timeout:FLOAT
      等待 DNS 服務器響應的時間,單位是秒。默認值爲5秒。

    • max-timeouts:INT
      名字服務器響應超時幾次才認爲服務器當機?默認是3次。

    • max-inflight:INT
      最多允許多少個未決的 DNS 請求?(如果試圖發出多於這麼多個請求,則過多的請求將被延遲,直到某個請求被響應或者超時)。默認值是 XXX。

    • attempts:INT
      在放棄之前重新傳輸多少次 DNS 請求?默認值是 XXX。

    • randomize-case:INT
      如果非零,evdns 會爲發出的 DNS 請求設置隨機的事務 ID,並且確認迴應具有同樣的隨機事務 ID 值。這種稱作 “0x20 hack” 的機制可以在一定程度上阻止對 DNS 的簡單激活事件攻擊。這個選項的默認值是1。

    • bind-to:ADDRESS
      如果提供,則向名字服務器發送數據之前綁定到給出的地址。對於2.0.4-alpha 版本,這個設置僅應用於後面的名字服務器條目。

    • initial-probe-timeout:FLOAT
      確定名字服務器當機後,libevent 以指數級降低的頻率探測服務器以判斷服務器是否恢復。這個選項配置(探測時間間隔)序列中的第一個超時,單位是秒。默認值是10。

    • getaddrinfo-allow-skew:FLOAT
      同時請求IPv4和IPv6地址時,evdns_getaddrinfo()用單獨的DNS請求包分別請求兩種地址,因爲有些服務器不能在一個包中同時處理兩種請求。服務器迴應一種地址類型後,函數等待一段時間確定另一種類型的地址是否到達。這個選項配置等待多長時間,單位是秒。默認值是3秒。

      不識別的字段和選項會被忽略。

3.3 手動配置 evdns

如果需要更精細地控制 evdns 的行爲,可以使用下述函數:

接口

int evdns_base_nameserver_sockaddr_add(struct evdns_base *base,
                                 const struct sockaddr *sa, ev_socklen_t len,
                                 unsigned flags);
int evdns_base_nameserver_ip_add(struct evdns_base *base,
                                 const char *ip_as_string);
int evdns_base_load_hosts(struct evdns_base *base, const char *hosts_fname);

void evdns_base_search_clear(struct evdns_base *base);
void evdns_base_search_add(struct evdns_base *base, const char *domain);
void evdns_base_search_ndots_set(struct evdns_base *base, int ndots);

int evdns_base_set_option(struct evdns_base *base, const char *option,
    const char *val);

int evdns_base_count_nameservers(struct evdns_base *base);

evdns_base_nameserver_sockaddr_add()函數通過地址向 evdns_base 添加名字服務器。當前忽略 flags 參數,爲向前兼容考慮,應該傳入0。成功時函數返回0,失敗時返回負值。(這個函數在2.0.7-rc 版本加入)

evdns_base_nameserver_ip_add()函數向 evdns_base 加入字符串表示的名字服務器,格式可以是 IPv4地址、IPv6地址、帶端口號的 IPv4地址(IPv4:Port),或者帶端口號的 IPv6地址(IPv6:Port)。成功時函數返回0,失敗時返回負值。

evdns_base_load_hosts()函數從 hosts_fname 文件中載入主機文件(格式與/etc/hosts 相 同)。成功時函數返回0,失敗時返回負值。

evdns_base_search_clear()函數從 evdns_base 中移除所有(通過 search 配置的)搜索後綴;evdns_base_search_add()則添加後綴。

evdns_base_set_option()函數設置 evdns_base 中某選項的值。選項和值都用字符串表示。 (2.0.3版本之前,選項名後面必須有一個冒號)

解析一組配置文件後,可以使用 evdns_base_count_nameservers()查看添加了多少個名字服務器。

3.4 庫端配置

有一些爲 evdns 模塊設置庫級別配置的函數:

接口

typedef void (*evdns_debug_log_fn_type)(int is_warning, const char *msg);
void evdns_set_log_fn(evdns_debug_log_fn_type fn);
void evdns_set_transaction_id_fn(ev_uint16_t (*fn)(void));

因爲歷史原因,evdns 子系統有自己單獨的日誌。evdns_set_log_fn()可以設置一個回調函數,以便在丟棄日誌消息前做一些操作。

爲安全起見,evdns 需要一個良好的隨機數發生源:使用0x20 hack 的時候,evdns 通過這個源來獲取難以猜測(hard-to-guess)的事務 ID 以隨機化查詢(請參考“randomize-case”選項)。然而,較老版本的 libevent 沒有自己的安全的 RNG(隨機數發生器)。此時可以通過調用 evdns_set_transaction_id_fn(),傳入一個返回難以預測(hard-to-predict)的兩字節無符號整數的函數,來爲 evdns 設置一個更好的隨機數發生器。

2.0.4-alpha 以 及 後 續 版 本 中 , libevent 有 自 己 內 置 的 安 全 的 RNG ,evdns_set_transaction_id_fn()就沒有效果了。

4. 底層 DNS 接口

有時候需要啓動能夠比從 evdns_getaddrinfo()獲取的 DNS 請求進行更精細控制的特別的DNS 請求,libevent 也爲此提供了接口。

缺少的特徵

當前 libevent 的 DNS 支持缺少其他底層 DNS 系統所具有的一些特徵,如支持任意請求類型和 TCP 請求。如果需要 evdns 所不具有的特徵,歡迎貢獻一個補丁。也可以看看其他全特徵的 DNS 庫,如 c-ares。

接口

#define DNS_QUERY_NO_SEARCH /* ... */

#define DNS_IPv4_A         /* ... */
#define DNS_PTR            /* ... */
#define DNS_IPv6_AAAA      /* ... */

typedef void (*evdns_callback_type)(int result, char type, int count,
    int ttl, void *addresses, void *arg);

struct evdns_request *evdns_base_resolve_ipv4(struct evdns_base *base,
    const char *name, int flags, evdns_callback_type callback, void *ptr);
struct evdns_request *evdns_base_resolve_ipv6(struct evdns_base *base,
    const char *name, int flags, evdns_callback_type callback, void *ptr);
struct evdns_request *evdns_base_resolve_reverse(struct evdns_base *base,
    const struct in_addr *in, int flags, evdns_callback_type callback,
    void *ptr);
struct evdns_request *evdns_base_resolve_reverse_ipv6(
    struct evdns_base *base, const struct in6_addr *in, int flags,
    evdns_callback_type callback, void *ptr);

這些解析函數爲一個特別的記錄發起 DNS 請求。每個函數要求一個 evdns_base 用於發起請求、一個要查詢的資源(正向查詢時的主機名,或者反向查詢時的地址)、一組用以確定如何進行查詢的標誌、一個查詢完成時調用的回調函數,以及一個用戶提供的傳給回調函數的指針。

flags 參數可以是0,也可以用 DNS_QUERY_NO_SEARCH 明確禁止原始查詢失敗時在搜索列表中進行搜索。DNS_QUERY_NO_SEARCH 對反向查詢無效,因爲反向查詢不進行搜索。

請求完成(不論是否成功)時回調函數會被調用。回調函數的參數是指示成功或者錯誤碼(參看下面的 DNS 錯誤表)的 result、一個記錄類型(DNS_IPv4_A、DNS_IPv6_AAAA,或者 DNS_PTR)、addresses 中的記錄數、以秒爲單位的存活時間、地址(查詢結果),以及用戶提供的指針。

發生錯誤時傳給回調函數的 addresses 參數爲 NULL。沒有錯誤時:對於 PTR 記錄,addresses 是空字符結束的字符串;對於 IPv4記錄,則是網絡字節序的四字節地址值數組;對於 IPv6記錄,則是網絡字節序的16字節記錄數組。(注意:即使沒有錯誤,addresses 的個數也可能是0。名字存在,但是沒有請求類型的記錄時就會出現這種情況)

可能傳遞給回調函數的錯誤碼如下:

Code Meaning
DNS_ERR_NONE 沒有錯誤
DNS_ERR_FORMAT 服務器不識別查詢請求DNS_ERR_SERVERFAILED 服務器內部錯誤
DNS_ERR_SERVERFAILED 服務器報告內部錯誤
DNS_ERR_NOTEXIST 沒有給定名字的記錄
DNS_ERR_NOTIMPL 服務器不識別這種類型的查詢
DNS_ERR_REFUSED 因爲策略設置,服務器拒絕查詢
DNS_ERR_TRUNCATED DNS 記錄不適合 UDP 分組
DNS_ERR_UNKNOWN 未知的內部錯誤
DNS_ERR_TIMEOUT 等待超時
DNS_ERR_SHUTDOWN 用戶請求關閉 evdns 系統
DNS_ERR_CANCEL 用戶請求取消查詢
DNS_ERR_NODATA 相應回覆了,但沒有答案

(DNS_ERR_NODATA是2.0.15穩定的新版本。)

您可以使用以下命令將這些錯誤代碼解碼爲人類可讀的字符串:

接口

const char *evdns_err_to_string(int err);

每個解析函數都返回不透明的 evdns_request 結構體指針。回調函數被調用前的任何時候都可以用這個指針來取消請求:

接口

void evdns_cancel_request(struct evdns_base *base,
    struct evdns_request *req);

用這個函數取消請求將使得回調函數被調用,帶有錯誤碼 DNS_ERR_CANCEL

掛起DNS客戶端操作,更換名字服務器

有時候需要重新配置或者關閉 DNS 子系統,但不能影響進行中的 DNS 請求。

接口

int evdns_base_clear_nameservers_and_suspend(struct evdns_base *base);
int evdns_base_resume(struct evdns_base *base);

evdns_base_clear_nameservers_and_suspend()會移除所有名字服務器,但未決的請求會被保留,直到隨後重新添加名字服務器,調用 evdns_base_resume()。

這些函數成功時返回0,失敗時返回-1。它們在2.0.1-alpha 版本引入。

5. DNS 服務器接口

libevent 爲實現不重要的 DNS 服務器,響應通過 UDP 傳輸的 DNS 請求提供了簡單機制。

本節要求讀者對 DNS 協議有一定的瞭解。

5.1 創建和關閉 DNS 服務器

接口

struct evdns_server_port *evdns_add_server_port_with_base(
    struct event_base *base,
    evutil_socket_t socket,
    int flags,
    evdns_request_callback_fn_type callback,
    void *user_data);

typedef void (*evdns_request_callback_fn_type)(
    struct evdns_server_request *request,
    void *user_data);

void evdns_close_server_port(struct evdns_server_port *port);

要開始監聽 DNS 請求,調用 evdns_add_server_port_with_base()。函數要求用於事件處理的 event_base、用於監聽的 UDP 套接字、可用的標誌(現在總是0)、一個收到 DNS查詢時要調用的回調函數 ,以及要傳遞給回調函數的用戶數據指針 。函數返回 evdns_server_port 對象。

使用 DNS 服務器完成工作後,需要調用 evdns_close_server_port()。

evdns_add_server_port_with_base() 是 2.0.1-alpha 版本引入的 ,而evdns_close_server_port()則由1.3版本引入。

5.2 檢測 DNS 請求

不幸的是,當前 libevent 沒有提供較好的獲取 DNS 請求的編程接口,用戶需要包含

event2/dns_struct.h 文件,查看 evdns_server_request 結構體。

未來版本的 libevent 應該會提供更好的方法。

接口

struct evdns_server_request {
        int flags;
        int nquestions;
        struct evdns_server_question **questions;
};
#define EVDNS_QTYPE_AXFR 252
#define EVDNS_QTYPE_ALL  255
struct evdns_server_question {
        int type;
        int dns_question_class;
        char name[1];
};

flags 字段包含請求中設置的 DNS 標誌;nquestions 字段是請求中的問題數;questions 是evdns_server_question 結構體指針數組。每個 evdns_server_question 包含請求的資源類型(請看下面的 EVDNS_*_TYPE 宏列表)、請求類別(通常爲 EVDNS_CLASS_INET),以及請求的主機名。

這些結構體在1.3版本中引入,但是1.4版之前的名字是 dns_question_class。名字中的“class”會讓 C++用戶迷惑。仍然使用原來的“class”名字的 C 程序將不能在未來發布版本中正確工作。

接口

int evdns_server_request_get_requesting_addr(struct evdns_server_request *req,
        struct sockaddr *sa, int addr_len);

有時想知道哪個地址發出了特定的DNS請求。可以通過在其上調用evdns_server_request_get_requesting_addr()進行檢查。 您應該傳入一個具有足夠存儲空間的sockaddr來保存該地址:建議使用struct sockaddr_storage。

此功能在Libevent 1.3c中引入。

5.3 響應 DNS 請求

DNS 服務器收到每個請求後,會將請求傳遞給用戶提供的回調函數,還帶有用戶數據指針。

回調函數必須響應請求或者忽略請求,或者確保請求最終會被回答或者忽略。迴應請求前可以向迴應中添加一個或者多個答案:

接口

int evdns_server_request_add_a_reply(struct evdns_server_request *req,
    const char *name, int n, const void *addrs, int ttl);
int evdns_server_request_add_aaaa_reply(struct evdns_server_request *req,
    const char *name, int n, const void *addrs, int ttl);
int evdns_server_request_add_cname_reply(struct evdns_server_request *req,
    const char *name, const char *cname, int ttl);

上述函數爲請求 req 的 DNS 迴應的結果節添加一個 RR(類型分別爲 A、AAAA 和 CNAME)。 各個函數中,name 是要爲之添加結果的主機名,ttl 是以秒爲單位的存活時間。對於 A 和 AAAA 記錄,n 是要添加的地址個數,addrs 是到原始地址的指針:對於 A 記錄,是以 n4 字節序列格式給出的 IPv4地址;對於 AAAA 記錄,是以 n16字節序列格式給出的 IPv6地址。

成功時函數返回0,失敗時返回-1。

接口

int evdns_server_request_add_ptr_reply(struct evdns_server_request *req,
    struct in_addr *in, const char *inaddr_name, const char *hostname,
    int ttl);

這個函數爲請求的結果節添加一個 PTR 記錄。參數 req 和 ttl 跟上面的函數相同。必須提供 in(一個 IPv4地址)和 inaddr_name(一個 arpa 域的地址)中的一個,而且只能提供一個, 以指示爲迴應提供哪種地址。hostname 是 PTR 查詢的答案。

接口

#define EVDNS_ANSWER_SECTION 0
#define EVDNS_AUTHORITY_SECTION 1
#define EVDNS_ADDITIONAL_SECTION 2

#define EVDNS_TYPE_A       1
#define EVDNS_TYPE_NS      2
#define EVDNS_TYPE_CNAME   5
#define EVDNS_TYPE_SOA     6
#define EVDNS_TYPE_PTR    12
#define EVDNS_TYPE_MX     15
#define EVDNS_TYPE_TXT    16
#define EVDNS_TYPE_AAAA   28

#define EVDNS_CLASS_INET   1

int evdns_server_request_add_reply(struct evdns_server_request *req,
    int section, const char *name, int type, int dns_class, int ttl,
    int datalen, int is_name, const char *data);

這個函數爲請求 req 的 DNS 迴應添加任意 RR。section 字段指示添加到哪一節,其值應該是某個 EVDNS_SECTION。name 參數是 RR 的名字字段。type 參數是 RR 的類型字段,其值應該是某個 EVDNS_TYPE。dns_class 參數是 RR 的類別字段。RR 的 rdata 和 rdlength 字段將從 data 處的 datalen 字節中產生。如果 is_name 爲 true,data 將被編碼成 DNS 名字(例如,使用 DNS 名字壓縮)。否則,data 將被直接包含到 RR 中。

接口

int evdns_server_request_respond(struct evdns_server_request *req, int err);
int evdns_server_request_drop(struct evdns_server_request *req);

evdns_server_request_respond()函數爲請求發送 DNS 迴應,帶有用戶添加的所有 RR,以及錯誤碼 err。如果不想回應某個請求,可以調用 evdns_server_request_drop()來忽略請求,釋放請求關聯的內存和結構體。

接口

#define EVDNS_FLAGS_AA  0x400
#define EVDNS_FLAGS_RD  0x080

void evdns_server_request_set_flags(struct evdns_server_request *req,
                                    int flags);

如果要爲迴應消息設置任何標誌,可以在發送迴應前的任何時候調用這個函數。

除了evdns_server_request_set_flags()首次在2.0.1-alpha 版本中出現外,本節描述的所有 函數都在1.3版本中引入。

5.4 DNS 服務器示例

示例:普通的DNS響應器

#include <event2/dns.h>
#include <event2/dns_struct.h>
#include <event2/util.h>
#include <event2/event.h>

#include <sys/socket.h>

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

/* Let's try binding to 5353.  Port 53 is more traditional, but on most
   operating systems it requires root privileges. */
#define LISTEN_PORT 5353

#define LOCALHOST_IPV4_ARPA "1.0.0.127.in-addr.arpa"
#define LOCALHOST_IPV6_ARPA ("1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0."         \
                             "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa")

const ev_uint8_t LOCALHOST_IPV4[] = { 127, 0, 0, 1 };
const ev_uint8_t LOCALHOST_IPV6[] = { 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1 };

#define TTL 4242

/* This toy DNS server callback answers requests for localhost (mapping it to
   127.0.0.1 or ::1) and for 127.0.0.1 or ::1 (mapping them to localhost).
 */
void server_callback(struct evdns_server_request *request, void *data)
{
    int i;
    int error=DNS_ERR_NONE;
    /* We should try to answer all the questions.  Some DNS servers don't do
       this reliably, though, so you should think hard before putting two
       questions in one request yourself. */
    for (i=0; i < request->nquestions; ++i) {
        const struct evdns_server_question *q = request->questions[i];
        int ok=-1;
        /* We don't use regular strcasecmp here, since we want a locale-
           independent comparison. */
        if (0 == evutil_ascii_strcasecmp(q->name, "localhost")) {
            if (q->type == EVDNS_TYPE_A)
                ok = evdns_server_request_add_a_reply(
                       request, q->name, 1, LOCALHOST_IPV4, TTL);
            else if (q->type == EVDNS_TYPE_AAAA)
                ok = evdns_server_request_add_aaaa_reply(
                       request, q->name, 1, LOCALHOST_IPV6, TTL);
        } else if (0 == evutil_ascii_strcasecmp(q->name, LOCALHOST_IPV4_ARPA)) {
            if (q->type == EVDNS_TYPE_PTR)
                ok = evdns_server_request_add_ptr_reply(
                       request, NULL, q->name, "LOCALHOST", TTL);
        } else if (0 == evutil_ascii_strcasecmp(q->name, LOCALHOST_IPV6_ARPA)) {
            if (q->type == EVDNS_TYPE_PTR)
                ok = evdns_server_request_add_ptr_reply(
                       request, NULL, q->name, "LOCALHOST", TTL);
        } else {
            error = DNS_ERR_NOTEXIST;
        }
        if (ok<0 && error==DNS_ERR_NONE)
            error = DNS_ERR_SERVERFAILED;
    }
    /* Now send the reply. */
    evdns_server_request_respond(request, error);
}

int main(int argc, char **argv)
{
    struct event_base *base;
    struct evdns_server_port *server;
    evutil_socket_t server_fd;
    struct sockaddr_in listenaddr;

    base = event_base_new();
    if (!base)
        return 1;

    server_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (server_fd < 0)
        return 2;
    memset(&listenaddr, 0, sizeof(listenaddr));
    listenaddr.sin_family = AF_INET;
    listenaddr.sin_port = htons(LISTEN_PORT);
    listenaddr.sin_addr.s_addr = INADDR_ANY;
    if (bind(server_fd, (struct sockaddr*)&listenaddr, sizeof(listenaddr))<0)
        return 3;
    /*The server will hijack the event loop after receiving the first request if the socket is blocking*/
    if(evutil_make_socket_nonblocking(server_fd)<0)
        return 4;
    server = evdns_add_server_port_with_base(base, server_fd, 0,
                                             server_callback, NULL);

    event_base_dispatch(base);

    evdns_close_server_port(server);
    event_base_free(base);

    return 0;
}

示例

Timer

示例

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

static int numCalls = 0;
static int numCalls_now = 0;
struct timeval lasttime;
struct timeval lasttime_now;

static void timeout_cb(evutil_socket_t fd, short event, void *arg)
{
	struct event *ev = (struct event *)arg;
	struct timeval newtime,tv_diff;
	double elapsed;

	evutil_gettimeofday(&newtime, NULL);
	evutil_timersub(&newtime, &lasttime, &tv_diff);

	elapsed = tv_diff.tv_sec +  (tv_diff.tv_usec / 1.0e6);

	lasttime = newtime;
	printf("[%.3f]timeout_cb %d \n",elapsed,++numCalls);
	
}

static void now_timeout_cb(evutil_socket_t fd, short event, void *arg)
{
	struct event *ev = (struct event *)arg;
	struct timeval tv = {3,0};
	struct timeval newtime,tv_diff;
	double elapsed;

	evutil_gettimeofday(&newtime, NULL);
	evutil_timersub(&newtime, &lasttime_now, &tv_diff);
	elapsed = tv_diff.tv_sec +  (tv_diff.tv_usec / 1.0e6);

	lasttime_now = newtime;
	printf("[%.3f]now_timeout_cb %d \n",elapsed,++numCalls_now);

	//每次回調都將當前先del,再次添加
	//if (!event_pending(ev,EV_PERSIST|EV_TIMEOUT,NULL))
	//{
	//	printf("if\n");
	//	event_del(ev);
	//	event_add(ev, &tv);
	//}
	//添加新的event
	if (!event_pending(ev,EV_PERSIST|EV_TIMEOUT,NULL))
	{
		struct event_base *evBase = event_get_base(ev);
		event_del(ev);
		event_free(ev);
	
		ev = event_new(evBase, -1, EV_PERSIST|EV_TIMEOUT, now_timeout_cb, event_self_cbarg());
		event_add(ev, &tv);
	}
}

int main(int argc, char **argv)
{
	struct event_base *evBase = NULL;
	struct event_config *evConf = NULL;
	struct event *ev_now_timeout = NULL;
	struct event *ev_timeout = NULL;
	struct timeval tv = {0,0};
	struct timeval tv_now = {0,0};
	//創建簡單的event_base
	evBase = event_base_new();

	//創建帶配置的event_base
	evConf = event_config_new();//創建event_config
	evBase = event_base_new_with_config(evConf);

	//創建event
	//傳遞自己event_self_cbarg()
	ev_now_timeout = evtimer_new(evBase, now_timeout_cb, event_self_cbarg());

	//設置時間
	tv.tv_sec = 1;
	tv.tv_usec = 500 * 1000;
	ev_timeout = event_new(evBase, -1, EV_PERSIST|EV_TIMEOUT, timeout_cb, event_self_cbarg());

	//添加event
	event_add(ev_now_timeout, &tv_now);//立即執行一次,然後定時
	event_add(ev_timeout, &tv);

	//獲取時間
    evutil_gettimeofday(&lasttime, NULL);
	evutil_gettimeofday(&lasttime_now, NULL);
	//循環
	//event_base_loop(evBase, 0);
	event_base_dispatch(evBase);
	
	//釋放
	event_free(ev_timeout);
	event_free(ev_now_timeout);
	event_config_free(evConf);
	event_base_free(evBase);
}

實現了一個Timer定時器,添加了2個event,ev_now_timeout立即執行一次,然後按3s週期執行;ev_timeout以1.5s週期執行。

TCP

Server

#include <string.h>
#include <unistd.h>
#include <event2/event.h>
#include <event2/listener.h>
#include <event2/bufferevent.h>
#include <event2/buffer.h> 
// 讀緩衝區回調
static void read_cb(struct bufferevent *bev, void *arg)
{
	//讀到buff
    char buf[1024] = {0};   
    bufferevent_read(bev, buf, sizeof(buf));

	//讀取evbuffer
	//struct evbuffer *input = bufferevent_get_input(bev);
	//size_t size = evbuffer_get_length(input);
	//
	//char *buf = (char *)malloc(size);   
	//evbuffer_remove(input, buf, size);

	printf("[server]rece client data\n");
	printf("[server]client say: %s\n", buf);
 
}
 
// 寫緩衝區回調
static void write_cb(struct bufferevent *bev, void *arg)
{
    printf("[server]write_cb\n"); 
}
 
// 事件
static void event_cb(struct bufferevent *bev, short events, void *arg)
{
    if (events & BEV_EVENT_EOF)
    {
        printf("[server]connection close\n");  
    }
    else if(events & BEV_EVENT_ERROR)   
    {
        printf("[server]connection error\n");
    }else if(events & BEV_EVENT_CONNECTED)
    {
        printf("[server]connection success\n");
        return;
    }
    
    bufferevent_free(bev);    
    printf("[server]bufferevent free\n"); 
}
 
static void send_cb(evutil_socket_t fd, short what, void *arg)
{
    char buf[1024] = {0}; 
    struct bufferevent* bev = (struct bufferevent*)arg;
	
    read(fd, buf, sizeof(buf));

	//struct evbuffer *output = bufferevent_get_output(bev);
	//evbuffer_add(output, buf, strlen(buf)+1);

    bufferevent_write(bev, buf, strlen(buf)+1);
}

static void cb_listener(
        struct evconnlistener *listener, 
        evutil_socket_t fd, 
        struct sockaddr *addr, 
        int len, void *ptr)
{
   printf("[server]connect new client\n");
 
   struct event_base* base = (struct event_base*)ptr;
   // 通信操作
   // 添加新事件
   struct bufferevent *bev;
   bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
 
   if (!bev) {
		fprintf(stderr, "[server]error constructing bufferevent!");
		event_base_loopbreak(base);
		return;
	}

   // 給bufferevent緩衝區設置回調
   bufferevent_setcb(bev, read_cb, write_cb, event_cb, NULL);

   bufferevent_enable(bev, EV_READ);
   
   // 創建一個事件
   struct event* ev = event_new(base, STDIN_FILENO, 
                                 EV_READ | EV_PERSIST, 
                                 send_cb, bev);
   event_add(ev, NULL);
   
}

static void accept_error_cb(struct evconnlistener *listener, void *ctx)
{
	struct event_base *base = evconnlistener_get_base(listener);
	int err = EVUTIL_SOCKET_ERROR();
	printf("[Server]Got an error %d (%s) on the listener.Shutting down.\n", 
		err, evutil_socket_error_to_string(err));
	event_base_loopexit(base, NULL);
}

 
int main(int argc, const char* argv[])
{
    struct sockaddr_in serv;
	struct event_base* base;
	struct evconnlistener* listener;

	// init server 
    memset(&serv, 0, sizeof(serv));
    serv.sin_family = AF_INET;
    serv.sin_port = htons(6666);
    serv.sin_addr.s_addr = htonl(INADDR_ANY);
    
    base = event_base_new();

	if (!base) {
		printf("[server]Couldn't open event base\n");
		return 1;
	}

    // 創建套接字
    // 綁定
    // 接收連接請求  
	//LEV_OPT_CLOSE_ON_FREE 如果設置了這個選項,釋放連接監聽器會關閉底層套接字。 
	//LEV_OPT_REUSEABLE 某些平臺在默認情況下,關閉某監聽套接字後,要過一會兒其他套接字纔可以綁定到同一個端口。
	//設置這個標誌會讓 libevent 標記套接字是可重用的,這樣一旦關閉,可以立即打開其他套接字,在相同端口進行監聽。
    listener = evconnlistener_new_bind(base, cb_listener, base, 
                                  LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, 
                                  36, (struct sockaddr*)&serv, sizeof(serv));
 
	if (!listener) {
		perror("[server]Couldn't create listener");
		return 1;
	}

	//error處理
	evconnlistener_set_error_cb(listener, accept_error_cb);

    event_base_dispatch(base);
 
	//釋放
    evconnlistener_free(listener);
    event_base_free(base);
 
    return 0;
}

實現了一個TCP server,監聽6666端口,與客戶端建立連接,以後可以互相發送消息。

Client

實現了一個TCP client,連接本地6666端口,與客戶端建立連接,以後可以互相發送消息。

#include <unistd.h>
#include <string.h>
#include <event2/event.h>
#include <event2/util.h>
#include <event2/bufferevent.h>
#include <event2/buffer.h> 

void read_cb(struct bufferevent *bev, void *arg)
{
    char buf[1024] = {0}; 
    bufferevent_read(bev, buf, sizeof(buf));
	printf("[client]rece server data\n");
    printf("[client]server say: %s\n", buf);
}
 
void write_cb(struct bufferevent *bev, void *arg)
{
   printf("[client]write_cb\n"); 
}
 
void event_cb(struct bufferevent *bev, short events, void *arg)
{
    if (events & BEV_EVENT_EOF)
    {
        printf("[client]connection close\n");  
    }
    else if(events & BEV_EVENT_ERROR)   
    {
        printf("[client]connection error\n");
    }
    else if(events & BEV_EVENT_CONNECTED)
    {
        printf("[client]connection success\n");
        return;
    }
    
    bufferevent_free(bev);
    printf("[client]bufferevent free\n");
}
 
void send_cb(evutil_socket_t fd, short what, void *arg)
{
    char buf[1024] = {0}; 
    struct bufferevent* bev = (struct bufferevent*)arg;
    read(fd, buf, sizeof(buf));
    bufferevent_write(bev, buf, strlen(buf)+1);
}

int main(int argc, const char* argv[])
{
	struct event_base *base;
	struct bufferevent* bev;
	struct sockaddr_in serv;
	struct event* ev;

	base = event_base_new();

	if (!base) {
		printf("[client]Couldn't open event base\n");
		return 1;
	}

	bev = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE);

	//連接服務器
    memset(&serv, 0, sizeof(serv));
    serv.sin_family = AF_INET;
    serv.sin_port = htons(6666);
	//解析 IP 地址
    evutil_inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr.s_addr);
	//連接
    bufferevent_socket_connect(bev, (struct sockaddr*)&serv, sizeof(serv));
	//設置回調
    bufferevent_setcb(bev, read_cb, write_cb, event_cb, NULL);
    bufferevent_enable(bev, EV_READ);

	// 創建一個事件
	//STDIN_FILENO:接收鍵盤的輸入
    ev = event_new(base, STDIN_FILENO, EV_READ | EV_PERSIST, 
                                 send_cb, bev);
	event_add(ev, NULL);
    
    event_base_dispatch(base);

	//釋放
	bufferevent_free(bev);
	event_free(ev);
	event_base_free(base);

}

HTTP

Server

實現了一個http server。

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

#include <sys/types.h>
#include <sys/stat.h>

#include <sys/stat.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>

#include <signal.h>

#include <event2/event.h>
#include <event2/http.h>
#include <event2/listener.h>
#include <event2/buffer.h>
#include <event2/util.h>
#include <event2/keyvalq_struct.h>

char uri_root[512];

static const struct table_entry {
	const char *extension;
	const char *content_type;
} content_type_table[] = {
	{ "txt", "text/plain" },
	{ "c", "text/plain" },
	{ "h", "text/plain" },
	{ "html", "text/html" },
	{ "htm", "text/htm" },
	{ "css", "text/css" },
	{ "gif", "image/gif" },
	{ "jpg", "image/jpeg" },
	{ "jpeg", "image/jpeg" },
	{ "png", "image/png" },
	{ "pdf", "application/pdf" },
	{ "ps", "application/postscript" },
	{ NULL, NULL },
};

/* 嘗試猜測“path”的內容類型 */
static const char *
guess_content_type(const char *path)
{
	const char *last_period, *extension;
	const struct table_entry *ent;
	last_period = strrchr(path, '.');
	if (!last_period || strchr(last_period, '/'))
		goto not_found; /* no exension */
	extension = last_period + 1;
	for (ent = &content_type_table[0]; ent->extension; ++ent) {
		if (!evutil_ascii_strcasecmp(ent->extension, extension))
			return ent->content_type;
	}

not_found:
	return "application/misc";
}

/* 用於/test URI請求的回調 */
static void
dump_request_cb(struct evhttp_request *req, void *arg)
{
	const char *cmdtype;
	struct evkeyvalq *headers;
	struct evkeyval *header;
	struct evbuffer *buf_in;
	struct evbuffer *buf_out;
	struct evhttp_uri *decoded = NULL;
    struct evkeyvalq params;
	char *decoded_path;
	const char *path;
	char cbuf[1024] = {0};
	const char *uri = evhttp_request_get_uri(req);

	switch (evhttp_request_get_command(req)) {
	case EVHTTP_REQ_GET: cmdtype = "GET"; break;
	case EVHTTP_REQ_POST: cmdtype = "POST"; break;
	case EVHTTP_REQ_HEAD: cmdtype = "HEAD"; break;
	case EVHTTP_REQ_PUT: cmdtype = "PUT"; break;
	case EVHTTP_REQ_DELETE: cmdtype = "DELETE"; break;
	case EVHTTP_REQ_OPTIONS: cmdtype = "OPTIONS"; break;
	case EVHTTP_REQ_TRACE: cmdtype = "TRACE"; break;
	case EVHTTP_REQ_CONNECT: cmdtype = "CONNECT"; break;
	case EVHTTP_REQ_PATCH: cmdtype = "PATCH"; break;
	default: cmdtype = "unknown"; break;
	}

	printf("Received a %s request for %s\nHeaders:\n",
	    cmdtype, uri);

	headers = evhttp_request_get_input_headers(req);

	for (header = headers->tqh_first; header;
	    header = header->next.tqe_next) {
		printf("  %s: %s\n", header->key, header->value);
	}

	/*********************************/
	/* 解析 URI */
	decoded = evhttp_uri_parse(uri);
	if (!decoded) {
		printf("It's not a good URI. Sending BADREQUEST\n");
		evhttp_send_error(req, HTTP_BADREQUEST, 0);
		return;
	}

	/* 獲取path */
	path = evhttp_uri_get_path(decoded);
	if (!path){
		evhttp_send_error(req, HTTP_BADREQUEST, 0);
        return;
	}
	printf("path: %s\n", path);

	
    //解析URI的參數
    //將URL數據封裝成key-value格式,q=value1, s=value2
    evhttp_parse_query(uri, &params);
	//得到a所對應的value
	const char *a_data = evhttp_find_header(&params, "a");

    printf("a=%s\n",a_data);
	/*********************************/
	if (strcmp(cmdtype,"POST") == 0)
	{
		//獲取POST方法的數據
		buf_in = evhttp_request_get_input_buffer(req);

		if (buf_in==NULL)
		{
			printf("evBuf null, err\n");
			goto err;
		}
		//獲取長度
		int buf_in_len = evbuffer_get_length(buf_in);
	
		printf("evBuf len:%d\n",buf_in_len);
    
		if(buf_in_len <= 0)
		{
			goto err;
		}
		//將數據從evbuff中移動到char *
		int str_len = evbuffer_remove(buf_in,cbuf,sizeof(cbuf));

		if (str_len <= 0)
		{
			printf("post parameter null err\n");
			goto err;
		}
		printf("str_len:%d cbuf:%s\n",str_len,cbuf);
	}
	/*********************************/
	buf_out = evbuffer_new();
	if(!buf_out)
    {
        puts("failed to create response buffer \n");
        return;
    }
	evbuffer_add_printf(buf_out,"%s","success");
	evhttp_send_reply(req, 200, "OK", buf_out);
	return;
err:
    evhttp_send_error(req, HTTP_INTERNAL, 0);
}

static void
send_document_cb(struct evhttp_request *req, void *arg)
{
	evhttp_send_error(req, 404, "url was not found");
}

static void
do_term(int sig, short events, void *arg)
{
	struct event_base *base = (struct event_base *)arg;
	event_base_loopbreak(base);
	fprintf(stderr, "Got %i, Terminating\n", sig);
}

static int
display_listen_sock(struct evhttp_bound_socket *handle)
{
	struct sockaddr_storage ss;
	evutil_socket_t fd;
	ev_socklen_t socklen = sizeof(ss);
	char addrbuf[128];
	void *inaddr;
	const char *addr;
	int got_port = -1;

	fd = evhttp_bound_socket_get_fd(handle);
	memset(&ss, 0, sizeof(ss));
	if (getsockname(fd, (struct sockaddr *)&ss, &socklen)) {
		perror("getsockname() failed");
		return 1;
	}

	if (ss.ss_family == AF_INET) {
		got_port = ntohs(((struct sockaddr_in*)&ss)->sin_port);
		inaddr = &((struct sockaddr_in*)&ss)->sin_addr;
	} else if (ss.ss_family == AF_INET6) {
		got_port = ntohs(((struct sockaddr_in6*)&ss)->sin6_port);
		inaddr = &((struct sockaddr_in6*)&ss)->sin6_addr;
	}
	else {
		fprintf(stderr, "Weird address family %d\n",
		    ss.ss_family);
		return 1;
	}

	addr = evutil_inet_ntop(ss.ss_family, inaddr, addrbuf,
	    sizeof(addrbuf));
	if (addr) {
		printf("Listening on %s:%d\n", addr, got_port);
		evutil_snprintf(uri_root, sizeof(uri_root),
		    "http://%s:%d",addr,got_port);
	} else {
		fprintf(stderr, "evutil_inet_ntop failed\n");
		return 1;
	}

	return 0;
}

int
main(int argc, char **argv)
{

	struct event_base *base = NULL;
	struct evhttp *http = NULL;
	struct evhttp_bound_socket *handle = NULL;
	struct evconnlistener *lev = NULL;
	struct event *term = NULL;
	int ret = 0;

	if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) {
		ret = 1;
		goto err;
	}

	//event_base
	base = event_base_new();

	if (!base) {
		fprintf(stderr, "Couldn't create an event_base: exiting\n");
		ret = 1;
	}

	/* 創建一個新的evhttp對象來處理請求。 */
	http = evhttp_new(base);
	if (!http) {
		fprintf(stderr, "couldn't create evhttp. Exiting.\n");
		ret = 1;
	}

	/* / test URI將所有請求轉儲到stdout並說200 OK。 */
	evhttp_set_cb(http, "/test", dump_request_cb, NULL);

	/*要接受任意請求,需要設置一個“通用”cb。 還可以爲特定路徑添加回調。 */
	evhttp_set_gencb(http, send_document_cb, NULL);

	//綁定socket
	handle = evhttp_bind_socket_with_handle(http, "0.0.0.0", 8888);

	if (!handle) {
		fprintf(stderr, "couldn't bind to port %d. Exiting.\n", 8888);
		ret = 1;
		goto err;
	}
	
	//監聽socket
	if (display_listen_sock(handle)) {
		ret = 1;
		goto err;
	}

	//終止信號
	term = evsignal_new(base, SIGINT, do_term, base);
	if (!term)
		goto err;
	if (event_add(term, NULL))
		goto err;

	//事件分發
	event_base_dispatch(base);

err:

	if (http)
		evhttp_free(http);
	if (term)
		event_free(term);
	if (base)
		event_base_free(base);

	return ret;
}

Client

實現了一個http client。

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <event2/bufferevent.h>
#include <event2/buffer.h>
#include <event2/listener.h>
#include <event2/util.h>
#include <event2/http.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h> 

static int ignore_cert = 0;

static void
http_request_done(struct evhttp_request *req, void *ctx)
{
	char buffer[256];
	int nread;
	struct evbuffer *buf_in;
	char cbuf[1024] = {0};

	//錯誤處理,打印
	if (!req || !evhttp_request_get_response_code(req)) {
		
		struct bufferevent *bev = (struct bufferevent *) ctx;
		unsigned long oslerr;
		int printed_err = 0;
		int errcode = EVUTIL_SOCKET_ERROR();
		fprintf(stderr, "some request failed - no idea which one though!\n");

		/* 嘗試打印 */
		if (! printed_err)
			fprintf(stderr, "socket error = %s (%d)\n",
				evutil_socket_error_to_string(errcode),
				errcode);
		return;
	}

	fprintf(stderr, "Response line: %d %s\n",
	    evhttp_request_get_response_code(req),
	    evhttp_request_get_response_code_line(req));

	//獲取數據
	buf_in = evhttp_request_get_input_buffer(req);

	if (buf_in==NULL)
	{
		printf("evBuf null, err\n");
	}
	//獲取長度
	int buf_in_len = evbuffer_get_length(buf_in);
	
	printf("evBuf len:%d\n",buf_in_len);
    
	if(buf_in_len <= 0)
	{
	}
	//將數據從evbuff中移動到char *
	int str_len = evbuffer_remove(buf_in,cbuf,sizeof(cbuf));

	if (str_len <= 0)
	{
		printf("post parameter null err\n");
	}
	printf("str_len:%d cbuf:%s\n",str_len,cbuf);
}

static void
err(const char *msg)
{
	fputs(msg, stderr);
}

int
main(int argc, char **argv)
{
	int r;
	struct event_base *base = NULL;
	struct evhttp_uri *http_uri = NULL;
	const char *url = NULL, *data_file = NULL;
	const char *scheme, *host, *path, *query;
	char uri[256];
	int port;
	int retries = 0;
	int timeout = -1;

	struct bufferevent *bev;
	struct evhttp_connection *evcon = NULL;
	struct evhttp_request *req;
	struct evkeyvalq *output_headers;
	struct evbuffer *output_buffer;

	int i;
	int ret = 0;
	
	//初始化url
	url = "http://127.0.0.1:8888/test?a=123";
	if (!url) {
		goto error;
	}

	http_uri = evhttp_uri_parse(url);
	if (http_uri == NULL) {
		err("malformed url");
		goto error;
	}

	scheme = evhttp_uri_get_scheme(http_uri);
	//忽略大小寫比較字符串
	if (scheme == NULL || strcasecmp(scheme, "http") != 0) {
		err("url must be http");
		goto error;
	}

	host = evhttp_uri_get_host(http_uri);
	if (host == NULL) {
		err("url must have a host");
		goto error;
	}

	port = evhttp_uri_get_port(http_uri);

	if (port == -1) {
		port = 80;
	}

	path = evhttp_uri_get_path(http_uri);
	if (strlen(path) == 0) {
		path = "/";
	}

	query = evhttp_uri_get_query(http_uri);

	if (query == NULL) {
		//將可變參數 “…” 按照format的格式格式化爲字符串,然後再將其拷貝至str中。
		snprintf(uri, sizeof(uri) - 1, "%s", path);
	} else {
		snprintf(uri, sizeof(uri) - 1, "%s?%s", path, query);
	}

	uri[sizeof(uri) - 1] = '\0';

	// 創建 event base
	base = event_base_new();
	if (!base) {
		perror("event_base_new()");
		goto error;
	}

	if (strcasecmp(scheme, "http") == 0) {
		bev = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE);
	} 

	if (bev == NULL) {
		fprintf(stderr, "bufferevent_socket_new() failed\n");
		goto error;
	}

	evcon = evhttp_connection_base_bufferevent_new(base, NULL, bev, host, port);
	if (evcon == NULL) {
		fprintf(stderr, "evhttp_connection_base_bufferevent_new() failed\n");
		goto error;
	}

	//重試
	if (retries > 0) {
		evhttp_connection_set_retries(evcon, retries);
	}

	//超時
	if (timeout >= 0) {
		evhttp_connection_set_timeout(evcon, timeout);
	}

	//回調
	req = evhttp_request_new(http_request_done, bev);

	if (req == NULL) {
		fprintf(stderr, "evhttp_request_new() failed\n");
		goto error;
	}

	output_headers = evhttp_request_get_output_headers(req);
	evhttp_add_header(output_headers, "Host", host);
	evhttp_add_header(output_headers, "Connection", "close");

	//文件路徑
	data_file = "/home/zza/libevent/demo/test.txt";

	if (data_file) {

		char buf[1024];
		output_buffer = evhttp_request_get_output_buffer(req);

		//post file傳統複製
		//FILE* f = fopen(data_file, "rb");		
		//size_t s;
		//size_t bytes = 0;
		//if (!f) {
		//	goto error;
		//}		
		//
		//while ((s = fread(buf, 1, sizeof(buf), f)) > 0) {
		//	evbuffer_add(output_buffer, buf, s);
		//	bytes += s;
		//}
		//evutil_snprintf(buf, sizeof(buf)-1, "%lu", (unsigned long)bytes);
		//evhttp_add_header(output_headers, "Content-Length", buf);
		//fclose(f);

		//************************************************************
		//post file使用evbuffer_add_file()或 
		//evbuffer_add_file_segment(),以避免不必要的複製
		//int fd = open(data_file, O_RDONLY);

		//if (fd == -1)
		//{
		//	fprintf(stderr, "open %s failed\n", data_file);
		//	goto error;
		//}

		//evbuffer_add_file(output_buffer,fd,0,-1);
		//ev_ssize_t size =  evbuffer_copyout(output_buffer, buf, sizeof(buf));		
		//evhttp_add_header(output_headers, "Content-Length", buf);

		//************************************************************
		//http post json
		//sprintf(buf,"%s","{\"a\":\"b\"}");
		//evbuffer_add(output_buffer, buf, strlen(buf));
		//
		//evhttp_add_header(output_headers, "Content-Type", "application/json;charset=UTF-8");

		//************************************************************
		//http post format
		sprintf(buf,"%s\r\n%s\r\n\r\n%s\r\n%s\r\n",
			"--------------------------123",
			"Content-Disposition: form-data; name=\"hello\"",
			"world!!!!",
			"--------------------------123");
		evbuffer_add(output_buffer, buf, strlen(buf));
		
		evhttp_add_header(output_headers, "Content-Type", 
			"multipart/form-data; boundary=--------------------------123");
		//************************************************************
	}
	//文件路徑爲NULL時爲get請求,非空post
	//請求
	r = evhttp_make_request(evcon, req, data_file ? EVHTTP_REQ_POST : EVHTTP_REQ_GET, uri);

	if (r != 0) {
		fprintf(stderr, "evhttp_make_request() failed\n");
		goto error;
	}

	event_base_dispatch(base);

	goto cleanup;

error:
	printf("error stop");
	ret = 1;
cleanup:
	if (evcon)
		evhttp_connection_free(evcon);
	if (http_uri)
		evhttp_uri_free(http_uri);
	if (base)
		event_base_free(base);

	return ret;
}

HTTPS

Client

/*
  This is an example of how to hook up evhttp with bufferevent_ssl

  It just GETs an https URL given on the command-line and prints the response
  body to stdout.

  Actually, it also accepts plain http URLs to make it easy to compare http vs
  https code paths.

  Loosely based on le-proxy.c.
 */

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

#include <sys/socket.h>
#include <netinet/in.h>

#include <event2/bufferevent_ssl.h>
#include <event2/bufferevent.h>
#include <event2/buffer.h>
#include <event2/listener.h>
#include <event2/util.h>
#include <event2/http.h>

#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/rand.h>

static int ignore_cert = 0;

static void
http_request_done(struct evhttp_request *req, void *ctx)
{
	char buffer[256];
	int nread;

	if (!req || !evhttp_request_get_response_code(req)) {
		/* If req is NULL, it means an error occurred, but
		 * sadly we are mostly left guessing what the error
		 * might have been.  We'll do our best... */
		struct bufferevent *bev = (struct bufferevent *) ctx;
		unsigned long oslerr;
		int printed_err = 0;
		int errcode = EVUTIL_SOCKET_ERROR();
		fprintf(stderr, "some request failed - no idea which one though!\n");
		/* Print out the OpenSSL error queue that libevent
		 * squirreled away for us, if any. */
		while ((oslerr = bufferevent_get_openssl_error(bev))) {
			ERR_error_string_n(oslerr, buffer, sizeof(buffer));
			fprintf(stderr, "%s\n", buffer);
			printed_err = 1;
		}
		/* If the OpenSSL error queue was empty, maybe it was a
		 * socket error; let's try printing that. */
		if (! printed_err)
			fprintf(stderr, "socket error = %s (%d)\n",
				evutil_socket_error_to_string(errcode),
				errcode);
		return;
	}

	fprintf(stderr, "Response line: %d %s\n",
	    evhttp_request_get_response_code(req),
	    evhttp_request_get_response_code_line(req));

	while ((nread = evbuffer_remove(evhttp_request_get_input_buffer(req),
		    buffer, sizeof(buffer)))
	       > 0) {
		/* These are just arbitrary chunks of 256 bytes.
		 * They are not lines, so we can't treat them as such. */
		fwrite(buffer, nread, 1, stdout);
	}
}

static void
err(const char *msg)
{
	fputs(msg, stderr);
}

static void
err_openssl(const char *func)
{
	fprintf (stderr, "%s failed:\n", func);

	/* This is the OpenSSL function that prints the contents of the
	 * error stack to the specified file handle. */
	ERR_print_errors_fp (stderr);

	exit(1);
}

int
main(int argc, char **argv)
{
	int r;
	struct event_base *base = NULL;
	struct evhttp_uri *http_uri = NULL;
	const char *url = NULL, *data_file = NULL;
	const char *crt = NULL;
	const char *scheme, *host, *path, *query;
	char uri[256];
	int port;
	int retries = 0;
	int timeout = -1;

	SSL_CTX *ssl_ctx = NULL;
	SSL *ssl = NULL;
	struct bufferevent *bev;
	struct evhttp_connection *evcon = NULL;
	struct evhttp_request *req;
	struct evkeyvalq *output_headers;
	struct evbuffer *output_buffer;

	int i;
	int ret = 0;
	enum { HTTP, HTTPS } type = HTTP;
		
	//初始化url
	url = "https://127.0.0.1:8888/login";

	if (!url) {
		goto error;
	}

	http_uri = evhttp_uri_parse(url);
	if (http_uri == NULL) {
		err("malformed url");
		goto error;
	}

	scheme = evhttp_uri_get_scheme(http_uri);
	if (scheme == NULL || (strcasecmp(scheme, "https") != 0 &&
	                       strcasecmp(scheme, "http") != 0)) {
		err("url must be http or https");
		goto error;
	}

	host = evhttp_uri_get_host(http_uri);
	if (host == NULL) {
		err("url must have a host");
		goto error;
	}

	port = evhttp_uri_get_port(http_uri);
	if (port == -1) {
		port = (strcasecmp(scheme, "http") == 0) ? 80 : 443;
	}

	path = evhttp_uri_get_path(http_uri);
	if (strlen(path) == 0) {
		path = "/";
	}

	query = evhttp_uri_get_query(http_uri);
	if (query == NULL) {
		snprintf(uri, sizeof(uri) - 1, "%s", path);
	} else {
		snprintf(uri, sizeof(uri) - 1, "%s?%s", path, query);
	}
	uri[sizeof(uri) - 1] = '\0';

#if (OPENSSL_VERSION_NUMBER < 0x10100000L) || \
	(defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x20700000L)
	// Initialize OpenSSL
	SSL_library_init();
	ERR_load_crypto_strings();
	SSL_load_error_strings();
	OpenSSL_add_all_algorithms();
#endif

	/* This isn't strictly necessary... OpenSSL performs RAND_poll
	 * automatically on first use of random number generator. */
	r = RAND_poll();
	if (r == 0) {
		err_openssl("RAND_poll");
		goto error;
	}

	/* Create a new OpenSSL context */
	ssl_ctx = SSL_CTX_new(SSLv23_method());
	if (!ssl_ctx) {
		err_openssl("SSL_CTX_new");
		goto error;
	}
	// Create event base
	base = event_base_new();
	if (!base) {
		perror("event_base_new()");
		goto error;
	}

	// Create OpenSSL bufferevent and stack evhttp on top of it
	ssl = SSL_new(ssl_ctx);
	if (ssl == NULL) {
		err_openssl("SSL_new()");
		goto error;
	}

	#ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME
	// Set hostname for SNI extension
	SSL_set_tlsext_host_name(ssl, host);
	#endif

	if (strcasecmp(scheme, "http") == 0) {
		bev = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE);
	} else {
		type = HTTPS;
		bev = bufferevent_openssl_socket_new(base, -1, ssl,
			BUFFEREVENT_SSL_CONNECTING,
			BEV_OPT_CLOSE_ON_FREE|BEV_OPT_DEFER_CALLBACKS);
	}

	if (bev == NULL) {
		fprintf(stderr, "bufferevent_openssl_socket_new() failed\n");
		goto error;
	}

	bufferevent_openssl_set_allow_dirty_shutdown(bev, 1);

	// For simplicity, we let DNS resolution block. Everything else should be
	// asynchronous though.
	evcon = evhttp_connection_base_bufferevent_new(base, NULL, bev,
		host, port);
	if (evcon == NULL) {
		fprintf(stderr, "evhttp_connection_base_bufferevent_new() failed\n");
		goto error;
	}

	if (retries > 0) {
		evhttp_connection_set_retries(evcon, retries);
	}
	if (timeout >= 0) {
		evhttp_connection_set_timeout(evcon, timeout);
	}

	// Fire off the request
	req = evhttp_request_new(http_request_done, bev);
	if (req == NULL) {
		fprintf(stderr, "evhttp_request_new() failed\n");
		goto error;
	}

	output_headers = evhttp_request_get_output_headers(req);
	evhttp_add_header(output_headers, "Host", host);
	evhttp_add_header(output_headers, "Connection", "close");

	if (data_file) {
		/* NOTE: In production code, you'd probably want to use
		 * evbuffer_add_file() or evbuffer_add_file_segment(), to
		 * avoid needless copying. */
		FILE * f = fopen(data_file, "rb");
		char buf[1024];
		size_t s;
		size_t bytes = 0;

		if (!f) {
			goto error;
		}

		output_buffer = evhttp_request_get_output_buffer(req);
		while ((s = fread(buf, 1, sizeof(buf), f)) > 0) {
			evbuffer_add(output_buffer, buf, s);
			bytes += s;
		}
		evutil_snprintf(buf, sizeof(buf)-1, "%lu", (unsigned long)bytes);
		evhttp_add_header(output_headers, "Content-Length", buf);
		fclose(f);
	}

	r = evhttp_make_request(evcon, req, data_file ? EVHTTP_REQ_POST : EVHTTP_REQ_GET, uri);
	if (r != 0) {
		fprintf(stderr, "evhttp_make_request() failed\n");
		goto error;
	}

	event_base_dispatch(base);
	goto cleanup;

error:
	ret = 1;
cleanup:
	if (evcon)
		evhttp_connection_free(evcon);
	if (http_uri)
		evhttp_uri_free(http_uri);
	if (base)
		event_base_free(base);

	if (ssl_ctx)
		SSL_CTX_free(ssl_ctx);
	if (type == HTTP && ssl)
		SSL_free(ssl);
#if (OPENSSL_VERSION_NUMBER < 0x10100000L) || \
	(defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x20700000L)
	EVP_cleanup();
	ERR_free_strings();

#if OPENSSL_VERSION_NUMBER < 0x10000000L
	ERR_remove_state(0);
#else
	ERR_remove_thread_state(NULL);
#endif

	CRYPTO_cleanup_all_ex_data();

	sk_SSL_COMP_free(SSL_COMP_get_compression_methods());
#endif /* (OPENSSL_VERSION_NUMBER < 0x10100000L) || \
	(defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x20700000L) */

#ifdef _WIN32
	WSACleanup();
#endif

	return ret;
}

Server

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <signal.h>

#include <sys/types.h>
#include <sys/stat.h>
 
#include <sys/stat.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <netinet/in.h>

#include <openssl/ssl.h>
#include <openssl/err.h>
 
#include <event2/bufferevent.h>
#include <event2/bufferevent_ssl.h>
#include <event2/event.h>
#include <event2/http.h>
#include <event2/buffer.h>
#include <event2/util.h>
#include <event2/keyvalq_struct.h>
 
unsigned short serverPort = 8888;

void die_most_horribly_from_openssl_error (const char *func)
{ 
	fprintf (stderr, "%s failed:\n", func);

	/* This is the OpenSSL function that prints the contents of the
	* error stack to the specified file handle. */
	ERR_print_errors_fp (stderr);

	exit (EXIT_FAILURE);
}

/* This callback gets invoked when we get any http request that doesn't match
 * any other callback.  Like any evhttp server callback, it has a simple job:
 * it must eventually call evhttp_send_error() or evhttp_send_reply().
 */
static void
login_cb (struct evhttp_request *req, void *arg)
{ 
    struct evbuffer *evb = NULL;
    const char *uri = evhttp_request_get_uri (req);
    struct evhttp_uri *decoded = NULL;
 
    /* 判斷 req 是否是GET 請求 */
    if (evhttp_request_get_command (req) == EVHTTP_REQ_GET)
    {
        struct evbuffer *buf = evbuffer_new();
        if (buf == NULL) return;
        evbuffer_add_printf(buf, "Requested: %s\n", uri);
        evhttp_send_reply(req, HTTP_OK, "OK", buf);
        return;
    }
 
    /* Get請求,直接return 200 OK  */
    if (evhttp_request_get_command (req) != EVHTTP_REQ_POST)
    { 
        evhttp_send_reply (req, 200, "OK", NULL);
        return;
    }

}
 
/**
 *	該回調負責創建新的SSL連接並將其包裝在OpenSSL bufferevent中。
 *	這是我們實現https服務器而不是普通的http服務器的方式。
 */
static struct bufferevent* bevcb(struct event_base *base, void *arg)
{ 
    struct bufferevent* r;
    SSL_CTX *ctx = (SSL_CTX *) arg;
 
    r = bufferevent_openssl_socket_new (base,
            -1,
            SSL_new (ctx),
            BUFFEREVENT_SSL_ACCEPTING,
            BEV_OPT_CLOSE_ON_FREE);
    return r;
}
 
static void server_setup_certs (SSL_CTX *ctx,
        const char *certificate_chain,
        const char *private_key)
{ 
    printf ("Loading certificate chain from '%s'\n"
            "and private key from '%s'\n",
            certificate_chain, private_key);
 
    if (1 != SSL_CTX_use_certificate_chain_file (ctx, certificate_chain))
        die_most_horribly_from_openssl_error ("SSL_CTX_use_certificate_chain_file");
 
    if (1 != SSL_CTX_use_PrivateKey_file (ctx, private_key, SSL_FILETYPE_PEM))
        die_most_horribly_from_openssl_error ("SSL_CTX_use_PrivateKey_file");
 
    if (1 != SSL_CTX_check_private_key (ctx))
        die_most_horribly_from_openssl_error ("SSL_CTX_check_private_key");
}
 

static int
display_listen_sock(struct evhttp_bound_socket *handle)
{
	struct sockaddr_storage ss;
	evutil_socket_t fd;
	ev_socklen_t socklen = sizeof(ss);
	char addrbuf[128];
	void *inaddr;
	const char *addr;
	int got_port = -1;

	fd = evhttp_bound_socket_get_fd(handle);
	memset(&ss, 0, sizeof(ss));
	if (getsockname(fd, (struct sockaddr *)&ss, &socklen)) {
		perror("getsockname() failed");
		return 1;
	}

	if (ss.ss_family == AF_INET) {
		got_port = ntohs(((struct sockaddr_in*)&ss)->sin_port);
		inaddr = &((struct sockaddr_in*)&ss)->sin_addr;
	} else if (ss.ss_family == AF_INET6) {
		got_port = ntohs(((struct sockaddr_in6*)&ss)->sin6_port);
		inaddr = &((struct sockaddr_in6*)&ss)->sin6_addr;
	}
	else {
		fprintf(stderr, "Weird address family %d\n",
		    ss.ss_family);
		return 1;
	}

	addr = evutil_inet_ntop(ss.ss_family, inaddr, addrbuf,
	    sizeof(addrbuf));
	if (addr) {
		printf("Listening on %s:%d\n", addr, got_port);
	} else {
		fprintf(stderr, "evutil_inet_ntop failed\n");
		return 1;
	}

	return 0;
}


static int serve_some_http (void)
{ 
    struct event_base *base;
    struct evhttp *http;
    struct evhttp_bound_socket *handle;
 
	//創建event_base
    base = event_base_new ();
    if (! base)
    { 
        fprintf (stderr, "Couldn't create an event_base: exiting\n");
        return 1;
    }
 
    /* 創建一個 evhttp 句柄,去處理用戶端的requests請求 */
    http = evhttp_new (base);
    if (! http)
    {
		fprintf (stderr, "couldn't create evhttp. Exiting.\n");
        return 1;
    }
	/******************************************/
    /* 創建SSL上下文環境 ,可以理解爲 SSL句柄 */
    SSL_CTX *ctx = SSL_CTX_new (SSLv23_server_method ());
    SSL_CTX_set_options (ctx,
            SSL_OP_SINGLE_DH_USE |
            SSL_OP_SINGLE_ECDH_USE |
            SSL_OP_NO_SSLv2);
 
    /* Cheesily pick an elliptic curve to use with elliptic curve ciphersuites.
     * We just hardcode a single curve which is reasonably decent.
     * See http://www.mail-archive.com/[email protected]/msg30957.html */
    EC_KEY *ecdh = EC_KEY_new_by_curve_name (NID_X9_62_prime256v1);
    if (! ecdh)
        die_most_horribly_from_openssl_error ("EC_KEY_new_by_curve_name");
    if (1 != SSL_CTX_set_tmp_ecdh (ctx, ecdh))
        die_most_horribly_from_openssl_error ("SSL_CTX_set_tmp_ecdh");
 
    /* 選擇服務器證書 和 服務器私鑰. */
    const char *certificate_chain = "server-certificate-chain.pem";
    const char *private_key = "server-private-key.pem";
    /* 設置服務器證書 和 服務器私鑰 到 
     OPENSSL ctx上下文句柄中 */
    server_setup_certs (ctx, certificate_chain, private_key);
	
    /* 
        使我們創建好的evhttp句柄 支持 SSL加密
        實際上,加密的動作和解密的動作都已經幫
        我們自動完成,我們拿到的數據就已經解密之後的

		設置用於爲與給定evhttp對象的連接創建新的bufferevent的回調。
		您可以使用它來覆蓋默認的bufferevent類型,
		例如,使此evhttp對象使用SSL緩衝區事件而不是未加密的事件。
		新的緩衝區事件必須在未設置fd的情況下進行分配。
	*/
    evhttp_set_bevcb (http, bevcb, ctx);
	/******************************************/
    /* 設置http回調函數 */
    //默認回調
    //evhttp_set_gencb (http, send_document_cb, NULL);
    //專屬uri路徑回調
    evhttp_set_cb(http, "/login", login_cb, NULL);
 
    /* 設置監聽IP和端口 */
    handle = evhttp_bind_socket_with_handle (http, "0.0.0.0", serverPort);
    if (! handle)
    { 
        fprintf (stderr, "couldn't bind to port %d. Exiting.\n",(int) serverPort);
        return 1;
    }

 	//監聽socket
	if (display_listen_sock(handle)) {
		return 1;
	}
 
    /* 開始阻塞監聽 (永久執行) */
    event_base_dispatch (base);
 
 
    return 0;
}
 
int main (int argc, char **argv)
{ 
    /*OpenSSL 初始化 */
	signal (SIGPIPE, SIG_IGN);

	SSL_library_init ();
	SSL_load_error_strings ();
	OpenSSL_add_all_algorithms ();

	printf ("Using OpenSSL version \"%s\"\nand libevent version \"%s\"\n",
			SSLeay_version (SSLEAY_VERSION),
			event_get_version ());
    /* now run http server (never returns) */
    return serve_some_http ();
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章