1. 程式人生 > 程式設計 >c++ 網路庫asio的優勢

c++ 網路庫asio的優勢

如果說答案是效能,那麼肯定有人會滿不在乎。覺得效能不夠的話, 只要加機器就可以了。 然而更少的機器,意味著更低的能耗,更少的硬體投入,更少的人力資源投入去維護機器。總而言之,更低的成本。

肯定會有人說,C++的開發速度太慢了。然而這並不是絕對的。C++也可以做到非常快速的開發。有句俗語 “指令碼一時爽,重構火葬場” 說的正是指令碼語言開發的專案進入維護階段後無窮的災難。而 C++ 經過了幾十年的發展,擁有龐大的工具鏈. 不管是動態分析還是靜態分析都有大量的工具,能極大的幫助程式設計師減少錯誤. c++得益於精良的設計,嚴格的檢查,越是大型的工程,越是能降低開發成本。

但這並不意味著C++就不適合小型專案了。小型的專案,也可以快速開發。因為 C++11 開始,已經 感覺像是全新的語言了,可以完全以指令碼的形式去使用,獲得接近甚至超越指令碼語言的開發速度,同時得益於編譯優化,獲得不俗的執行時效能。 C++正是魚和熊掌得兼的語言。

為什麼要用asio這個庫?

事實上如果使用C++開發服務端程式,你有多得數不清的選擇。什麼 ACE 啦,libuv 啦,libevent 啦,libev 啦,甚至可以直接使用 epoll/iocp 這樣的系統API。 為什麼要用 asio 呢?

那些年我們用過的網路庫

在計算機史前文明時代,曾經有個世界難題,叫 “c10k problem”. 這個是繼 y2k problem 後的又一個重大攻關專案. 全世界的文藝青年都想拿下解決這個問題的榮譽,正可謂八仙過海,各顯神通.

那一年,NPTL 還沒有研究出來. 還不能建立成千上萬個執行緒

那一年,windows 還在藍屏中掙扎,無暇顧及網路.

然而,曙光還是有的. 非同步的出現帶給了人以希望. 古老的UNIX早就想到了,提供了 select() 系統呼叫供人驅使. 然而問題還是有的,select 只能支援 1024 個檔案描述符,windows 上的 select 更是劣質到只能使用64個. 就算通過修改定義強迫接受一萬個檔案描述符,也沒有解決實際的問題. select 實在是太慢了.

在這種背景下,IBM 老大哥帶領著MS老弟先搞了 IOCP . 然而開源的人有開源的做法,在 NIH 綜合症的影響下,BSD 的人敢為天下所不齒,發明了 Kqueue. 同樣在 NIH 綜合症影響下,Linux 的一群 M* 的猴子搗鼓出了 epoll.

分裂,讓人頭疼.

然而,他們都聲稱自己的新介面對 select 有質的提升,是破解 c10k 問題的不二法寶. 你用也得用,不用也得用. 為了讓自己編寫的網路程式能跨平臺,程式設計師開始了對3大各自為陣的法寶的膜拜學習.

除了需要應對多套互不相容的 API,非同步本身也需要更高階的抽象,把程式設計師從編寫非同步程式碼的地獄模式裡拯救出來. 於是程式設計師們急需一個上天入地無所不能的法寶的法寶,把這3家法寶給統御起來.

率先站出來悳瑟的是 ACE.

悳瑟的 ACE

恰亂世剛過,天下待定,C++ 委員會的老人們卻韜光養晦,不問世事. 所謂亂世出英雄,英雄出少年,歐文大學出了名秀才. 憑藉其洋洋灑灑的一片雄文 《Pattern-Oriented Software Architecture》 中舉去了首府學城,併為ACE奠定了無可撼動的地位.

ACE 的名字,也許靈感來自 Adaptive Clubbed Rod,這也是當年一位英雄少年的寶貝. 既是寶貝,必需如意. 即是後來的葫蘆娃都怕了 “如意寶貝”.

ACE 如意在什麼地方呢?如意其一,支援 IOCP/kqueu/epoll/select/you_name_it 各種介面,號曰沒有不能跨的平臺. 如意其二,支援多種模型。這些模型都在《Pattern-Oriented Software Architecture》有過詳細敘述. ACE 本身就是這篇論文的實踐,因為他知道,紙上得來終覺淺 絕知此事要躬行。 如意其三,介面和模式排列組合下,多少種,竟可不修改程式碼而適應。

然而 ACE 畢竟嫩了點,沒過幾年就失勢了. 現在除了一些老程式設計師還在用,新生代的程式設計師已經不再使用 ACE 了. 為什麼呢? 陳碩在他的部落格裡說,ACE 過於複雜,甚至比它試圖封裝的物件更復雜,程式設計師是指望用你的如意寶貝去駕馭另外那三家寶貝的,結果你比他們還難。ACE 犯了早期 C++ 庫都會犯的一個錯誤,過度設計, 過度java化。所謂 java 化, 就是以物件代替介面, 以虛擬函式代替回撥,以繼承代替組合。以虛類代替模板。物件間關係錯綜複雜,牽一髮而動全身。除了作者,已經無人能參與 ACE 的開發了。

與此同時,C語言的迴歸卻在背後悄然進行。C語言的復辟,帶來了幾個更為糟糕的替代品, libevent 和 libev,以及 乘著nodejs的盛行東風而來的 libuv。

原始的 libevent

C語言有著頑強的生命力,當然,這並不是因為C語言有多好,在後續的章節了我們還會深入的探討C++相對C的改進。C語言的頑強和人天生的懶惰和偏見是有一定關係的。這種惰性表現為隨遇而安,表現為固執己見。 非要拼命的否定C++,固守 C,對 C 的缺點視而不見,詆譭C++相對C的改進。固守的結果就是簡陋原始的 libevent . 然而因為保守黨巨大的人數優勢, libevent 應其群眾基礎良好而獲得了空前的廣泛使用。

libevent 就如名字所言,是一個非同步事件框架。從 OS 那裡獲得事件, 然後派發。派發機制就是“回撥函式”。非同步非同步,歸根結底就是處理從作業系統獲得的事件。iocp也好, epoll也罷,都只是用來獲取事件的介面。libevent 去掉了ACE華而不實的包裝,保留了非同步事件,極大的簡化了模型。不得不說軟體工程是個糟糕的發明,從來都把簡單問題複雜化。libevent把簡單問題簡單化,讓非同步網路程式設計反樸歸真,應該來說,本是一個好庫。

然而 libevent 因為設計缺陷,例如使用全域性變數,定時器無法處理時間跳變,諸如此類的設計缺陷導致了 libev 的出現。 libev 就是為了克服libevent的缺陷而誕生的。然而,libev 就一定好了嗎?

禁錮的 libev

libev 帶著對 libevent 的怨氣出世了。 吸收了 libevent 的所有缺點, 雖然承諾過改進。然而 libev 如何改進的了呢? libev 已經夠原始了,向下改進還不如讓人直接使用系統的 api,向上改進,一是會導致和libevent的重疊,二是很快就碰到了 C 語言強加的禁錮。

C 語言因其語法簡陋簡潔而著稱。然而,缺乏必要的抽象能力,導致 C 語言編寫非同步程式,就如同安迪拿著小錘子琢開肖生克監獄的牆壁一樣。能,但是要耗費巨大的精力和時間。 編寫非同步程式, 最需要的2個抽象能力, 其一為協程,其二是函式物件,包括匿名函式物件, 也就是lambda。 C統統沒有。函式物件是實現閉包比不可少的,如果沒有函式物件, 就只能通過攜帶 void 指標的形式迂迴完整,繁瑣不說,還特別容易出錯。程式裡也到處充滿了型別強轉。到處是臨時定義的型別,就為了傳給 void 使用。

儘管C 有那麼多缺點,然而 libev 還未來得及被C的缺點拖累,因為他不支援 IOCP. 於是 libuv 就出來給 libev 擦屁股了。 支援了 iocp 後的 libuv 就真的只有 C 本身的缺點了嗎?

混亂的 libuv

libuv 可以說是 C 語言的非同步庫所能達到的最高高度了。完完全全的觸碰到了C語言的自身瓶頸,好在 libuv 只是 nodejs 的底層庫,上層軟體轉移到 javascript 語言而逃避了 C 的禁錮。

真的是這樣的嗎? libuv 自身還有什麼缺點呢?

開源社群avplayer的大拿jackarain曾經說過,一個網路庫好不好,就看他有沒有正確的處理 TCP 關閉, read write 實現的ui不對。 libuv 很遺憾的是,不合格。libuv 的 uv_write 沒有返回值,允許空回撥。也就是忽略write錯誤。 網路出錯的情況下, libuv 的使用者只能稀裡糊塗的知道出錯了, 至於錯在哪?資料到底有沒有發出去了? 一概不知道。 把資料交給 uv_write 後,就是一筆糊塗賬了,大概 TCP 不可靠的說法就是從這裡傳出來的吧。

ASIO 騰空出世

在地球最大的島上,另一位少年開始拜讀 ACE 的大作。那時候,沒有 libuv 沒有 libev 更沒有 libevent . 有的只是 ACE. 然而這個南方小國的少年沒有跟風陷入 ACE 崇拜,他以敏銳的目光察覺到了 ACE 的弊病。 ACE 哪裡做的不好?又哪裡是值得借鑑的?

少年在給 c++ 委員會寫的一篇上書中說,Proactor 模型乃最優模型。而 Proactor 模型,乃 ACE 提出的 6 個模型之一。根據 IT 界贏者通吃律,一個優秀的網路庫,只需要支援 Proactor 模型即可。 支援其他次優模型都是徒勞的。ACE 試圖全盤通吃,犯了大忌。

少年在一次開發者大會的演講上,再次透露,網路庫不宜做成框架,而是要像系統的API那樣,作為一個樂高積木。ACE 做成了一個框架,同樣不妥。

你說了那麼多 ACE 不好,有本事你弄個好的啊? 批評者向來都是這麼理直氣壯。 正如 ACE 的作者實踐了 “紙上得來終覺淺 絕知此事要躬行” 一樣,這位勇敢的少年也拿出了 ASIO,“實踐出真知”,他如是說。

那還是 SARS 病毒肆虐的年代,幾乎沒有人注意到,今後顛覆C++的網路世界的 ASIO 悄然出世了。而他的父親,還只是悉尼的學子。ASIO 並沒有顯赫的家庭背景,然而英雄不問出處,它註定將有不平凡的一生。

俗語有云,三歲看老。在 asio 才三歲的時候,它父親就將 asio 引薦給了 c++ 委員會的老人們。上一次他們這麼做的時候,他們接納了 STL。 ASIO 最終被內定,然後放入 Boost 鍛鍊,經過 Boost 十餘的鍛鍊,ASIO 終於在 2017 年進入了 c++ 標準。

前攝者?為什麼是演員

在給 c++ 老人會的引薦信裡,asio 爸爸仔細闡述了asio的設計抉擇回答了圍繞 asio 的設計提出的很多問題。 為什麼 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 上實現了宇宙級非同步核心。 當然,如果 c++11 早點出現,如果 c++17 早點出現,實現 asio 的宇宙模型會更加的簡單 —— 其實這也是 c++ 的理念,增加語言特性,只是為了讓語言用起來更簡單。

緩衝區

有了閉包的支援,記憶體管理也變得輕輕鬆鬆起來。 ASIO 本身並不管理記憶體,所有的IO操作,只提交對使用者管理的記憶體的引用,稱 Buffers。asio::buffers 引用了使用者提交的記憶體,保持整個 IO 期間,這塊記憶體的有效性是使用者的責任。然而這並不難! 因為回撥是一個閉包。通過閉包持有記憶體,只要 asio 還未回撥,閉包就在,閉包在,記憶體在。asio 在呼叫完回撥後才刪除相應的閉包。因此資源管理的責任可以丟給閉包,而閉包可以通過智慧指標精確的控制記憶體。 不是 GC,勝於 GC 千百倍!益於c++的 RAII機制,再無記憶體洩漏之憂!

以上就是c++ 網路庫asio的優勢的詳細內容,更多關於c++ 網路庫asio的資料請關注我們其它相關文章!