1. 程式人生 > >【slighttpd】基於lighttpd架構的Server專案實戰(2)—預備知識之libevent

【slighttpd】基於lighttpd架構的Server專案實戰(2)—預備知識之libevent

轉載地址:https://blog.csdn.net/jiange_zh/article/details/50631393

簡介


由於本專案是純非同步的,而對於大量 socket 連線,使用 select 並不高效。(參見我的另一篇博文:epoll簡介

事實上,大部分系統提供了處理大量 socket 連線的解決方案:

  1. Linux 下的 epoll()
  2. BSD 下的 kqueue()
  3. Solaris 下的 evports
  4. Windows 下的 IOCP


由於各個平臺使用了不同的介面,所以想編寫跨平臺的高效能非同步程式就需要做一層跨平臺封裝。此時 Libevent 是一個不錯的選擇(當然你也可以自己來實現這個事-。-),其最底層 API(event 和 event_base API)為各個平臺實現高效能非同步程式提供了一致的介面。

一些概念


event:會繫結檔案描述符、回撥函式並表示一個或者多個條件(例如,檔案描述符可以讀、寫或者超時等)。event 表示的條件如果被觸發了,那麼 event 會變為活躍的,它繫結的回撥函式就會被執行。

event_base: 持有一組 event 並進行事件迴圈,event_base 會存在一個後端(也叫做方法),常見的後端包括 epoll、kqueue 等。

結構


元件:

evutil 用於抽象不同的平臺的網路(基礎的)實現;

event、event_base 為 Libevent 的核心,為不同的平臺下基於事件的非阻塞 I/O 提供了一套抽象的介面;

bufferevent 對 Libevent 的基於事件的核心的封裝。應用程式的讀寫請求是基於緩衝區的;

evbuffer 為 bufferevent 實現的緩衝區;

evhttp 一個簡單的 HTTP client/server 的實現;

evdns 一個簡單的 DNS client/server 的實現;

evrpc 一個簡單的 RPC 實現;

庫:

libevent_core 包括 util、event_base、evbuffer、bufferevent;

libevent_extra 包括 HTTP、DNS、RPC;

libevent 此庫由於歷史原因而存在,不要使用它;

libevent_pthreads 此庫為基於 pthread 的執行緒和鎖的實現;

libevent_openssl 此庫通過 openssl 和 bufferevent 提供了加密通訊;

標頭檔案: 
    所有的公用標頭檔案位於 event2 目錄中。

建立和銷燬 event_base


event_base 是最基本的,故需要首先被建立。

event_base 結構持有一個 event 集合。如果 event_base 被設定了使用鎖,那麼它在多個執行緒中可以安全的訪問。但是對 event_base 的迴圈只能在某個執行緒中執行。如果希望在多個執行緒中進行迴圈,那麼應該為每一個執行緒建立一個 event_base。

event_base 存在多個後端可以選擇(我們也把 event_base 後端叫做 event_base 的方法):

select
poll
epoll
kqueue
devpoll
evport
win32


在建立 event_base 時,libevent 會為我們選擇最快的後端。

建立 event_base 的函式:

// 建立成功返回一個擁有預設設定的 event base
// 建立失敗返回 NULL
struct event_base *event_base_new(void);

// 檢視 event_base 實際上使用到的後端
const char *event_base_get_method(const struct event_base *base);


event_base 的釋放使用函式:

void event_base_free(struct event_base *base);


事件迴圈(event loop)


event_base 會持有一組 event。如果我們向 event_base 中註冊了一些 event,那麼就可以讓 libevent 開始事件迴圈了:

// 指定一個 event_base 並開始事件迴圈
// 此函式內部被實現為一個不斷進行的迴圈
// 此函式返回 0 表示成功退出
// 此函式返回 -1 表示存在未處理的錯誤
int event_base_dispatch(struct event_base *base);


event_base_dispatch會在以下情況停止:

如果 base 中沒有 event,那麼事件迴圈將停止
呼叫 event_base_loopbreak(),那麼事件迴圈將停止
呼叫 event_base_loopexit(),那麼事件迴圈將停止
如果出現錯誤,那麼事件迴圈將停止


在事件迴圈中,會監聽是否存在活躍事件,若存在活躍事件,則呼叫事件對應的回撥函式。

按照上面說的,如果想停止事件迴圈:

     1.移除 event_base 中的 event。

     2.如果想在 event_base 中存在 event 的情況下停止事件迴圈,可以呼叫以下函式:

    // 這兩個函式成功返回 0 失敗返回 -1
    // 指定在 tv 時間後停止事件迴圈
    // 如果 tv == NULL 那麼將無延時的停止事件迴圈
    int event_base_loopexit(struct event_base *base,
                            const struct timeval *tv);
    // 立即停止事件迴圈(而不是無延時的停止)
    int event_base_loopbreak(struct event_base *base);


event_base_loopexit(base, NULL) :如果當前正在為多個活躍事件呼叫回撥函式,那麼不會立即退出,而是等到所有的活躍事件的回撥函式都執行完成後才退出事件迴圈;

event_base_loopbreak(base) :如果當前正在為多個活躍事件呼叫回撥函式,那麼當前正在呼叫的回撥函式會被執行,然後馬上退出事件迴圈,而並不處理其他的活躍事件了。

在事件的回撥函式中獲取當前的時間可以使用event_base_gettimeofday_cached()(你的系統可能實現 gettimeofday() 為一個系統呼叫,故用下面函式可避免系統呼叫的開銷):

// 獲取到的時間為開始執行此輪事件回撥函式的時間
// 成功返回 0 失敗返回負數
int event_base_gettimeofday_cached(struct event_base *base,
    struct timeval *tv_out);


event事件


event是一組觸發條件,例如:

     1. 一個檔案描述符可讀或者可寫;
     2. 超時;
     3. 產生訊號;
     4. 使用者觸發了一個事件。
相關術語:

一個 event 通過 event_new() 創建出來,那麼它是已初始化的,另外 event_assign() 也被用來初始化 event(但是有它特定的用法);

一個 event 被註冊到(通過 add 函式新增到)一個 event_base 中,其為 pending 狀態;

如果 pending event 表示的條件被觸發了,那麼此 event 會變為活躍的,其為 active 狀態,則 event 相關的回撥函式被呼叫;

event 可以是 persistent(持久的)也可以是非 persistent 的。event 相關的回撥函式被呼叫之後,只有 persistent event 會繼續轉為 pending 狀態。對於非 persistent 的 event 你可以在 event 相關的事件回撥函式中呼叫 event_add() 使得此 event 轉為 pending 狀態,否則該事件將會被刪除(即該事件是一次性的)。

常用 API:

    // 定義了各種條件
    // 超時
    #define EV_TIMEOUT      0x01
    // 檔案描述符可讀
    #define EV_READ         0x02
    // 檔案描述符可寫
    #define EV_WRITE        0x04
    // 用於訊號檢測
    #define EV_SIGNAL       0x08
    // 用於指定 event 為 persistent
    #define EV_PERSIST      0x10
    // 用於指定 event 會被邊緣觸發
    #define EV_ET           0x20

    // event 的回撥函式
    // 引數 evutil_socket_t --- event 關聯的檔案描述符
    // 引數 short --- 當前發生的條件型別
    // 引數 void* --- 其為 event_new 函式中的 arg 引數
    typedef void (*event_callback_fn)(evutil_socket_t, short, void *);

    // 建立 event
    // base --- 使用此 event 的 event_base
    // what --- 指定 event 關心的條件
    // fd --- 檔案描述符
    // cb --- event 相關的回撥函式
    // arg --- 使用者自定義資料
    // 函式執行失敗返回 NULL
    struct event *event_new
    (struct event_base *base, 
     evutil_socket_t fd,
     short what, 
     event_callback_fn cb,
     void *arg);

    // 釋放 event(真正釋放記憶體,對應 event_new 使用)
    // 可以用來釋放由 event_new 分配的 event
    // 若 event 處於 pending 或者 active 狀態釋放也不會存在問題
    void event_free(struct event *event);

    // 清理 event(並不是真正釋放記憶體)
    // 可用於已經初始化的、pending、active 的 event
    // 此函式會將 event 轉換為非 pending、非 active 狀態的
    // 函式返回 0 表示成功 -1 表示失敗
    int event_del(struct event *event);

    // 用於向 event_base 中註冊 event
    // tv 用於指定超時時間,為 NULL 表示無超時時間
    // 函式返回 0 表示成功 -1 表示失敗
    int event_add(struct event *ev, const struct timeval *tv);


訊號 event 相關的函式:

 

// base --- event_base
// signum --- 訊號,例如 SIGHUP
// callback --- 訊號出現時呼叫的回撥函式
// arg --- 使用者自定義資料
#define evsignal_new(base, signum, callback, arg) \
    event_new(base, signum, EV_SIGNAL|EV_PERSIST, cb, arg)

// 將訊號 event 註冊到 event_base
#define evsignal_add(ev, tv) \
    event_add((ev),(tv))

// 清理訊號 event
#define evsignal_del(ev) \
    event_del(ev)


注意,一個程序中 Libevent 只能允許一個 event_base 監聽訊號。

重用 event ,避免 event 在堆上的頻繁分配和釋放:

    // 此函式用於初始化 event(包括可以初始化棧上和靜態儲存區中的 event)
    // event_assign() 和 event_new() 除了 event 引數之外,使用了一樣的引數
    // event 引數用於指定一個未初始化的且需要初始化的 event
    // 函式成功返回 0 失敗返回 -1
    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 使用
    #define evsignal_assign(event, base, signum, callback, arg) \
        event_assign(event, base, signum, EV_SIGNAL|EV_PERSIST, callback, arg)


已經初始化或者處於 pending 的 event,首先需要呼叫 event_del() 後才能呼叫 event_assign()。

event_new() 實際上完成了兩件事情:

通過記憶體分配函式在堆上分配了 event
使用 event_assign() 初始化了此 event


event_free() 實際上完成了兩件事情:

呼叫 event_del() 進行 event 的清理工作
通過記憶體分配函式在堆上釋放此 event


常用基本資料型別

evutil_socket_t 用於儲存 socket
ev_uint64_t 取值範圍 [0, EV_UINT64_MAX]
ev_int64_t 取值範圍 [EV_INT64_MIN, EV_INT64_MAX]
ev_uint32_t 取值範圍 [0, EV_UINT32_MAX]
ev_int32_t 取值範圍 [EV_INT32_MIN, EV_INT32_MAX]
ev_uint16_t 取值範圍 [0, EV_UINT16_MAX]
ev_int16_t 取值範圍 [EV_INT16_MIN, EV_INT16_MAX]
ev_uint8_t 取值範圍 [0, EV_UINT8_MAX]
ev_int8_t 取值範圍 [EV_INT8_MIN, EV_INT8_MAX]
ev_ssize_type(signed size_t)取值範圍 [EV_SSIZE_MIN, EV_SSIZE_MAX]


時間相關


// 用於加或者減前兩個引數,結果被儲存在第三個引數中
#define evutil_timeradd(tvp, uvp, vvp) /* ... */
#define evutil_timersub(tvp, uvp, vvp) /* ... */

// 清除 timeval 將其值設定為 0
#define evutil_timerclear(tvp) /* ... */
// 判斷 timeval 是否為 0,如果是 0 返回 false,否則返回 true
#define evutil_timerisset(tvp) /* ... */

// 比較兩個 timeval
// 使用的時候這樣用:
// evutil_timercmp(t1, t2, <=) 含義為判斷 t1 <= t2 是否成立
// cmp 為所有的 C 關係操作符
#define evutil_timercmp(tvp, uvp, cmp)

// 獲取當前時間並儲存到 tv
// tz 目前無用
int evutil_gettimeofday(struct timeval *tv, struct timezone *tz);


Socket相關

// 用於關閉一個 socket
int evutil_closesocket(evutil_socket_t s);
#define EVUTIL_CLOSESOCKET(s) evutil_closesocket(s)

// 返回當前執行緒的最後一次 socket 操作的錯誤碼
#define EVUTIL_SOCKET_ERROR()
// 改變當前 socket 的錯誤碼
#define EVUTIL_SET_SOCKET_ERROR(errcode)
// 返回特定的 sock 的錯誤碼
#define evutil_socket_geterror(sock)
// 通過 socket 錯誤碼獲取到一個字串描述
#define evutil_socket_error_to_string(errcode)

// 設定 sock 為非阻塞的 socket
int evutil_make_socket_nonblocking(evutil_socket_t sock);

// 設定 sock 的地址可重用
int evutil_make_listen_socket_reuseable(evutil_socket_t sock);


字串相關

// 它們對應於標準的 snprintf 和 vsnprintf
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);


結語


以上是libevent的基本介紹,在本次的專案中,會使用到最基本的event以及訊號event。

對於資料、緩衝區的管理,bufferevent也許用得上,這是後話,需要用到的時候繼續查資料學習!