【slighttpd】基於lighttpd架構的Server專案實戰(2)—預備知識之libevent
轉載地址:https://blog.csdn.net/jiange_zh/article/details/50631393
簡介
由於本專案是純非同步的,而對於大量 socket 連線,使用 select 並不高效。(參見我的另一篇博文:epoll簡介)
事實上,大部分系統提供了處理大量 socket 連線的解決方案:
- Linux 下的 epoll()
- BSD 下的 kqueue()
- Solaris 下的 evports
- 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也許用得上,這是後話,需要用到的時候繼續查資料學習!