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