libuv 初窥--转
過年了,人都走光了,結(jié)果一個人活也干不了。所以我便想找點(diǎn)東西玩玩。
今天想試一下 libev 寫點(diǎn)代碼。原本在我那臺 ubuntu 機(jī)器上一點(diǎn)問題都沒有,可在 windows 機(jī)上用 mingw 編譯出來的庫一個 backend 都沒有,基本不可用。然后網(wǎng)上就有同學(xué)推薦我試一下 libuv 。
libuv 是 node.js 作者做的一個封裝庫,在 unix 環(huán)境整合的 libev ,而在 windows 下用 IOCP 另實(shí)現(xiàn)了一套。看起來挺滿足我的玩兒的需求的。所以就試了一下。
這東西沒有文檔,暫時沒看出來作者有寫文檔的打算,恐怕他是自己用為主。我 google 了一下,就是 github 上有源代碼,.h 文件里有還算比較詳細(xì)的注釋。當(dāng)然最主要是有些 test 程序,可以大概瀏覽其設(shè)計思路。
編譯倒挺順利,照著 test 寫點(diǎn)小東西也不復(fù)雜。所以我也就逐步開始了解這個東東了。
老實(shí)說,對于一個別人寫的庫,我愛用不愛用主要是考察其 API 設(shè)計。也就是該怎么用,設(shè)計的好不好,有沒有冗余設(shè)計。文檔什么的其實(shí)不太所謂,反正有代碼可以看嘛。
libuv 大體上我還算滿意,用 C 實(shí)現(xiàn)可以加很多分。不過有些小細(xì)節(jié)我覺得還是有點(diǎn)小遺憾的。(這個遺憾是指,如果我自己來設(shè)計,絕對不會像這個樣子)
最關(guān)鍵就是接口對 C 結(jié)構(gòu)體布局的依賴性。這點(diǎn)我曾經(jīng)因?yàn)?hiredis windows 版的緣故吐槽過一次了。以我自己的經(jīng)驗(yàn),似乎大多數(shù) Windows 出身的程序員都有一點(diǎn)這種壞習(xí)慣。好吧,我也不知道怎么把這點(diǎn)和 windows 聯(lián)系起來的,純粹感覺而已。因?yàn)槲易约阂郧白鲈O(shè)計也有這個習(xí)慣。
為什么我覺得這樣不好?
因?yàn)槲矣X得一個庫,若想被人當(dāng)成黑盒子去使用,以后也作用黑盒子來維護(hù),甚至可以用別的盒子去替代它。關(guān)鍵的一點(diǎn)就是接口簡單。這個簡單包括了使用最少的概念、需要最少的知識去理解它。
文檔通常是對接口無法自描述所需知識的一種補(bǔ)充。對一些例外的說明。而這些當(dāng)然是越少越好。
我傾向于不在對外接口(對于 C 庫來說,就是 .h 文件)中定義所用數(shù)據(jù)結(jié)構(gòu)的具體布局。通常只需要一個名字即可。這個名字是用來做強(qiáng)類型約束的。
過多的結(jié)構(gòu)定義導(dǎo)致了過多的概念,增加了接口復(fù)雜度。
接口的最小知識表達(dá)就是用一致的 C 函數(shù)調(diào)用約定。有明確的輸入?yún)?shù)、輸出參數(shù)。對于接口函數(shù),應(yīng)該是無全局相關(guān)狀態(tài)的。這不僅僅是為了線程安全,而是可以保證沒有隱式約定(額外的知識)。
如果某些行為需要用戶設(shè)置或讀取某個結(jié)構(gòu)體的一個特定域,我覺得就是在 C 函數(shù)調(diào)用這一方式外,增加了一種改變模塊行為的接口形式。或許這樣做的成本比 C 函數(shù)調(diào)用要來的低,但我以為得不償失。
尤其是、你的模塊如果相當(dāng)依賴這種形式:直接對結(jié)構(gòu)體的特定域賦值的形式來交換信息。這種依賴可能來至于你對性能的追求。那我覺得一般都是整個模塊的需求定義出了什么問題。一個獨(dú)立模塊需要解決的問題,通常對外界的信息交換應(yīng)該是低頻的,它應(yīng)該是可以獨(dú)立工作解決更復(fù)雜的問題的。而不應(yīng)該是不斷的要求外部告知它新的狀態(tài)變換。
ps. 對于接口中的結(jié)構(gòu)體定義問題。有另一種情況需要區(qū)分開。就是有大量的輸入?yún)?shù)或輸出參數(shù)需要一次性交換時,可以考慮定義一個結(jié)構(gòu)體來做。這樣比在 C 函數(shù)調(diào)用前壓一大堆的數(shù)據(jù)去堆棧里要干凈的多。
寫了這么多,我是想說說我初步閱讀 libuv 代碼的感受。我碰到的第一個問題就是:libuv 用了大量 callback 機(jī)制來完成異步 IO 的問題。而這些 callback 函數(shù)通常都帶有一個參數(shù)uv_stream_t或uv_req_t等。這個數(shù)據(jù)表示這次 callback 綁定的數(shù)據(jù) 。
我們知道, C 語言是沒有原生 closure 支持的。若有的話,closure 應(yīng)是 callback 機(jī)制最價解決方案。而 C 語言模擬 closure 的方法是用一個 C Function 并攜帶一個 void * ud 。此 ud 即原本應(yīng)該在 closure 中綁定的數(shù)據(jù)塊。
這里,libuv 用的uv_stream_t大致上等同于這個 ud 。
問題出來了。用戶在用這類異步 IO 庫的時候,每次 IO 事件都需要綁定的行為需要的數(shù)據(jù)不僅僅是一個 stream 。還需要一些圍繞這個 stream 做的動作所需要的一些其它數(shù)據(jù)。
我在閱讀test/echo-server.c時看到這么一段:
static void after_write(uv_write_t* req, int status) {
write_req_t* wr;
if (status) {
uv_err_t err = uv_last_error(loop);
fprintf(stderr, "uv_write error: %s
", uv_strerror(err));
ASSERT(0);
}
wr = (write_req_t*) req;
/* Free the read/write buffer and the request */
free(wr->buf.base);
free(wr);
}
這里用了一次強(qiáng)制轉(zhuǎn)換,把uv_write_t轉(zhuǎn)換為write_req_t。為什么可以這樣干,是因?yàn)?code>write_req_t被定義成:
typedef struct {
uv_write_t req;
uv_buf_t buf;
} write_req_t;
這里根據(jù) C 結(jié)構(gòu)布局,req 是第一個域,所以排在最前面。
這樣做有點(diǎn)晦澀,我只能說感覺不太好。因?yàn)槿绻s定了uv_write接口傳遞的是一個uv_write_t類型的數(shù)據(jù),這就明顯是利用 C 語言特性來夾帶私貨了。
如果這是作者推薦的慣用法的話,我則這樣理解:
libuv 其實(shí)在 API 上有個隱含約定。即回調(diào)函數(shù)的參數(shù)指向的地址偏移量為某個數(shù)值以后的數(shù)據(jù)是用戶數(shù)據(jù)。這個數(shù)值為類型的尺寸。這類似 c++ 的繼承。數(shù)據(jù)類型尺寸數(shù)值是編譯時通過編譯器來約定的。
而且,單就現(xiàn)在的用法,我認(rèn)為更嚴(yán)謹(jǐn)?shù)淖龇☉?yīng)該是類似 socket API ,顯式的把傳遞的結(jié)構(gòu)尺寸在函數(shù)接口表達(dá)出來(參考 socket connect 的接口定義中的第三個參數(shù) addrlen)。 這樣對庫的接口穩(wěn)定有好處。庫可以知道用戶有可能擴(kuò)展數(shù)據(jù),長度信息提示了庫,傳入數(shù)據(jù)體的真實(shí)大小。
btw, C++ 在用繼承來完成類似設(shè)計時,則依靠了語言對 cast 的約定。C++ 語言的知識概念太多,很難完成簡潔的模塊接口約定。在我看來,這直接導(dǎo)致了 C++ 很難設(shè)計通用庫,而只能設(shè)計專有框架。
我著一些疑惑閱讀了不少 libuv 里的實(shí)現(xiàn)代碼,尤其是 uv.h 的細(xì)節(jié)。我發(fā)現(xiàn)這樣一個結(jié)構(gòu)定義。
#define UV_HANDLE_FIELDS
/* read-only */
uv_loop_t* loop;
uv_handle_type type;
/* public */
uv_close_cb close_cb;
void* data;
/* private */
UV_HANDLE_PRIVATE_FIELDS
/* The abstract base class of all handles. */
struct uv_handle_s {
UV_HANDLE_FIELDS
};
注意這里有一個 data 域。從我的經(jīng)驗(yàn)判斷,這個域應(yīng)該就是用來在一個 handle 上夾帶用戶數(shù)據(jù)的。由于沒有文檔確認(rèn),我只能從有限的代碼閱讀中來確認(rèn)我的判斷。我很奇怪沒有定義一個明確的 api 出來綁定用戶數(shù)據(jù)。因?yàn)樵趲斓膶?shí)現(xiàn)代碼中也確實(shí)庫自己用到過這個域,所以估計也不是庫的使用者可以自由使用的。
當(dāng)然對應(yīng)的還有幾處類似設(shè)計:
#define UV_REQ_FIELDS
/* read-only */
uv_req_type type;
/* public */
void* data;
/* private */
UV_REQ_PRIVATE_FIELDS
/* Abstract base class of all requests. */
struct uv_req_s {
UV_REQ_FIELDS
};
還有
struct uv_loop_s {
UV_LOOP_PRIVATE_FIELDS
/* list used for ares task handles */
uv_ares_task_t* uv_ares_handles_;
/* Various thing for libeio. */
uv_async_t uv_eio_want_poll_notifier;
uv_async_t uv_eio_done_poll_notifier;
uv_idle_t uv_eio_poller;
/* Diagnostic counters */
uv_counters_t counters;
/* The last error */
uv_err_t last_err;
/* User data - use this for whatever. */
void* data;
};
這個struct uv_loop_s的 data 域倒是明確的注釋可以隨便使用了。
話說回來,這個綁定用戶數(shù)據(jù)的需求我在早年閱讀 Windows 的 MFC 實(shí)現(xiàn)時倒是見過另外一種解決方案。
Windows 的窗體有一個 SetWindowLong 的 API 可以讓用戶去設(shè)置一個用戶數(shù)據(jù)。這樣可以方便用戶在用 C++ 封裝的時候把一個 C++ 對象指針綁定在窗體 Handle 上。這樣在窗口消息回調(diào)函數(shù)中就可以取回這個對象指針。
MFC 封裝這些系統(tǒng) API 時,可能是為了更通用,沒有占用這個內(nèi)置域,而是自己建立了一個全局的映射表。每次窗體消息回調(diào)時,查表來找到對應(yīng)的窗體對象。這種非侵入式的方案,也湊合用吧。就是對于用 C/C++ 編寫代碼的追求性能的同學(xué)來說,或許有些小小不爽。
這就是我初步閱讀 libuv 代碼的一些簡單看法。當(dāng)然,我覺得 libuv 是個很不錯的東西,不然我也不會饒有興致的玩了一晚上。只是由于在這塊投入時間精力不多,錯誤難免。有行家看到,一笑了之吧。
總結(jié)
以上是生活随笔為你收集整理的libuv 初窥--转的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: shiro中anon配置不生效
- 下一篇: CentOS7通过SpeedTest工具