c++網路庫發展
C++也可以做到非常快速的開發。有句俗語 * “指令碼一時爽,重構火葬場” * 說的正是指令碼語言開發的專案進入維護階段後無窮的災難。
C++11 開始,已經 感覺像是全新的語言了,可以完全以指令碼的形式去使用。
(1)select-> epoll kqueue
UNIX提供了 select() 系統呼叫供人驅使.然而只能支援 1024 個檔案描述符, windows 上的 select 更是隻能使用64個.
IBM 搞了 IOCP . BSD 發明了 Kqueue. Linux 搗鼓出了 epoll.
《Pattern-Oriented Software Architecture》ACE 犯了早期 C++ 庫都會犯的一個錯誤,過度設計, 過度java化。所謂 java 化, 就是以物件代替介面, 以虛擬函式代替回撥,以繼承代替組合。以虛類代替模板。物件間關係錯綜複雜,牽一髮而動全身。
(2) C語言的復辟,帶來了幾個更為糟糕的替代品, libevent 和 libev,以及 乘著nodejs的盛行東風而來的 libuv。
a 簡陋原始的 libevent . 從 OS 那裡獲得事件, 然後派發。派發機制就是“回撥函式”。非同步,歸根結底就是處理從作業系統獲得的事件。iocp epoll 都只是用來獲取事件的介面。libevent 去掉了ACE華而不實的包裝,保留了非同步事件,極大的簡化了模型。
b 禁錮的 libev
libev吸收了 libevent 的所有缺點,
向下改進還不如直接使用系統的 api, 向上改進,很快就碰到了 C 語言強加的禁錮。
編寫非同步程式, 最需要的2個抽象能力, 其一為協程,其二是函式物件,包括匿名函式物件, 也就是lambda。
函式物件是實現閉包比不可少的,如果沒有函式物件, 就只能通過攜帶 void*
指標的形式迂迴完整,繁瑣不說,還特別容易出錯。程式裡也到處充滿了型別強轉。到處是臨時定義的型別,就為了傳給 void*
使用。
c libuv 就出來給 libev 擦屁股了。 C 語言的非同步庫所能達到的最高高度了。完完全全的觸碰到了C語言的自身瓶頸,好在 libuv 只是 nodejs 的底層庫,上層軟體轉移到 javascript 語言而逃避了 C 的禁錮。
網路出錯的情況下, libuv 的使用者只能稀裡糊塗的知道出錯
(3)ASIO
網路庫不宜做成框架,而是要像系統的API那樣,ACE 做成了一個框架,同樣不妥。
ASIO 終於在 2017 年進入了 c++ 標準。
為什麼 Proactor 會是最佳模型?
- 跨平臺 許多作業系統都有非同步API,即便是沒有非同步API的Linux, 通過 epoll 也能模擬 Proactor 模式。
- 支援回撥函式組合 將一系列非同步操作進行組合,封裝成對外的一個非同步呼叫。這個只有Proactor能做到,Reactor 做不到。意味著如果asio使用Reactor模式,就對不起他“庫” 之名。
- 相比 Reactor 可以實現 Zero-copy
- 和執行緒解耦 長時間執行的過程總是有系統非同步完成,應用程式無需為此開啟執行緒
Proactor 也並非全無缺點,缺點就是記憶體佔用比 Reactor 大。Proactor 需要先分配記憶體而後處理IO, 而 Reactor 是先等待 IO 而後分配記憶體。相對的Proactor卻獲得了Zero-copy好處。因為記憶體已經分配好了,因此作業系統可以將接受到的網路資料直接從網路介面拷貝到應用程式記憶體,而無需經過核心中轉。
Proactor 模式需要一個 loop ,這個 loop asio 將其封裝為 io_service.他不僅是 asio的核心,更是一切基於asio設計的程式的核心。
宇宙級非同步核心
io_service 脫胎於 IO 但不僅用於 IO. Christopher Kohlhoff 在給委員會的另一份編號 N3747 的信上上說它是 ~~宇宙級非同步模型~~ Universal Asynchronous Model。在宇宙級非同步模型裡,一個非同步操作由三部分構成
- 發起 按照 asio 的編碼規範,所有的發起操作都使用 async_ 字首,使用 async_動詞 的形式作為函式名。
- 執行 非同步過程在發起的時候被executor執行(系統可以是支援 AIO 的核心,不支援 AIO 的系統則是 aiso 使用者層模擬)
- 完成並回調 在發起 async_* 操作的時候,總是攜帶一個回撥的閉包。asio使用閉包作為非同步事件完成的處理回撥,沒而不是C式的回撥函式。asio的宇宙非同步模型裡,回撥總是在執行 io_service::run 的執行緒裡執行。asio絕不會在內部執行緒裡呼叫回撥。
在回撥裡發起新的非同步操作,一輪套一輪。整個程式就圍繞著 io_service::run 運轉起來了。
io_service 不僅僅能用於非同步 IO ,還可以用來投遞任意閉包。實現作為執行緒池的功能。這一通用型非同步模型徹底擊敗微軟 PPL 提案,致使微軟轉而研究協程。然而微軟在協程上同樣面臨 asio 的絞殺。
閉包和協程
宇宙級 asio 使用閉包作為回撥,而 C 庫只能使用函式+void*, ACE 雖然使用的 C++語言,卻不知道閉包為何物,使用的是 虛擬函式作為回撥。需要大量的從 ACE 的物件繼承。以閉包為回撥,asio更是支援了一種叫“無棧協程”的強悍武器。
asio的無棧協程,僅僅通過庫的形式,不論是在效能上,還是易用性上,還是簡潔性上,甚至是B格上,都超過了微軟易於修改語言而得的 await提案。
微軟,乃至 ACE ,並不是不知道閉包,而是在c++裡實現閉包的宇宙級executor —— 也就是 io_service,需要對模板技術的精通。
asio 以地球人無法理解的方式硬是在 c++98 上實現了宇宙級非同步核心。
buffers
有了閉包的支援,記憶體管理也變得輕輕鬆鬆起來。
ASIO 本身並不管理記憶體,所有的IO操作,只提交對使用者管理的記憶體的引用,稱 Buffers。asio::buffers 引用了使用者提交的記憶體,保持整個 IO 期間,這塊記憶體的有效性是使用者的責任。然而這並不難!
因為回撥是一個閉包。通過閉包持有記憶體,只要 asio 還未回撥,閉包就在,閉包在,記憶體在。asio 在呼叫完回撥後才刪除相應的閉包。因此資源管理的責任可以丟給閉包,而閉包可以通過智慧指標精確的控制記憶體。
不是 GC , 勝於 GC 千百倍!益於c++的 RAII機制,再無記憶體洩漏之憂!