從今年 3 月份看到有人打算用 Rust 重寫 ZeroMQ、我開始認真學習 Rust 語言,到後來 6 月份開始着手實現,再到如今 0.1
版即將達成,先後也有小半年了。今天,我打算在這裏把當前的設計總結一下,也順便試圖招募志願者一塊兒來作開發。html
項目地址:https://github.com/zeromq/zmq.rs前端
沒錯木哈哈,被收編成了 ZeroMQ 官方項目了,因此必定來一塊兒作哦。python
關於本文:8 月份的草稿啊!這都年末了,真是醉了。下個月(2015 年 1 月)北京有個 Rust 的聚會,打算分享這個項目,因此如今果斷刪掉未完成章節,把能發的先發出來。git
突然意識到以前幾篇《Rust 語言學習筆記》一直沒有介紹過 Rust 語言,這裏一併補齊。程序員
Rust 語言是幾種下一代編程語言中較爲優秀的一款系統級編程語言,最顯著的特色就是運行時幾乎不崩潰、高併發無數據競爭,還有就是跑的賊拉快。github
相較於應用級編程語言如 Java 或 Python,Rust 做爲一款系統級編程語言,天生被設計用來開發系統軟件諸如操做系統、設備驅動或編譯器等,與 D 語言、Go 語言齊名(你們對 Go 語言是不是系統級編程語言有爭議),是 C++ 繼位者的候選人。所以,Rust 理所應當是編譯型的語言。依託於 LLVM,Rust 在編譯器(Rust 編譯器前端系 Rust 語言自己實現,即所謂的自舉)上大下功夫,經過「全部權」的概念將內存管理的最佳實踐整合在了編譯期,在編譯期保證了運行時的內存安全,且沒有使用垃圾回收機制(垃圾回收是 Rust 的一種可選的額外工具),運行速度不打折扣。這種對內存管理的高要求,也讓程序員能夠更容易地用 Rust 語言編寫正確的高併發程序,天然地實現相似 Erlang 的併發模型。另外受 Haskell 的薰陶,Rust 語言對函數式編程也是很是友好的——我本身認爲要比 Python 函數式多了。另外,Rust 的語法一點也不詭異,若是您寫過 C/C++/Java/Python/Ruby,您會以爲不少語法似曾相識,上手較快。編程
很少說了,畢竟不是《半小時,介紹 Rust》——有興趣你們能夠移步這裏繼續閱讀。segmentfault
ZeroMQ 乍一看是一種消息隊列,但實際上它並非傳統意義上的消息隊列——開一個消息服務器,全部消息都通過它,能夠離線什麼的。上述傳統消息隊列主要部分只是 ZeroMQ 指南中的一個叫作泰坦尼克的模式概念,ZeroMQ 的庫中甚至都沒有這種模式的實現。其實 ZeroMQ 是一種內嵌式的網絡庫,關注於怎樣將程序連在一塊兒。簡單來講,您能夠認爲 ZeroMQ 是對普通 socket 的一種封裝和抽象,將常規的通訊工具封裝成了 ZeroMQ 的 socket。您能夠經過 ZeroMQ 的 socket 來實現通訊,ZeroMQ 的 socket 則內置了消息排隊、流量控制、收發模型、斷線重連等機制,方便您設計出本身適合的消息通訊模型。安全
介紹再細一點。服務器
每一個 ZeroMQ 的 socket 都有一個類型,決定了它內置的收發模型,好比建立一個用於發送請求的 REQ
:
pythonsocket = context.socket(zmq.REQ)
REQ
就限制了,這個 socket
必須得先發送一個消息,而後才能——且必須——接收一個消息。發一個請求、收一個響應,絕對不能亂套:
python# print(socket.recv_multipart()) -- 這會失敗 socket.send_multipart(["Hello"]) # socket.send_multipart(["Hello2"]) -- 這會失敗 print(socket.recv_multipart()) # print(socket.recv_multipart()) -- 這會失敗 socket.send_multipart(["Hello2"])
固然了,在收發消息以前,咱們得先把咱們的 socket
跟別的 socket 連上才行:
pythonsocket.connect("tcp://192.168.3.27:8868")
意思就是說,跟監聽在 192.168.3.27:8868
上的 ZeroMQ socket 創建 TCP 連接。另外,ZeroMQ 還支持進程間通訊(ipc
)、進程內通訊(inproc
)和多播。
有趣的是,ZeroMQ 容許先 connect
,再建立監聽端的 socket(由於內置了斷線重連機制):
pythonserver_socket = context.socket(zmq.REP) server_socket.bind("tcp://192.168.3.27:8868")
這個 REP
的 socket
也只是又一個普通的 ZeroMQ 的 socket,只不過 REP
的要求與 REQ
正好相反,必須得先收再發——收一個請求,返回一個響應。這裏咱們就很少作示例了。
每個 ZeroMQ 的 socket 都(特例除外)能夠屢次作 connect
或/和 bind
,只要 socket 之間創建了鏈接,他們就能夠在各自類型的約束下實現通訊。對 REQ
來講,發送時會輪流使用全部鏈接,接收時必須從上一次發送去的鏈接來接收;而對 REP
來講,全部鏈接上進來的請求都會被公平的排隊,REP
會公平地接收,而發送時則保證將響應發送回請求的來源。這些就是所謂的收發模型,這對用戶都是透明的,您只須要調用 send
或 recv
就能夠了。
除了請求-響應模式的 REQ
和 REP
,ZeroMQ 經常使用的一些 socket 類型還有:高級請求-響應模式的 DEALER
和 ROUTER
、發佈-訂閱模式的 PUB
和 SUB
、流水線模式的 PUSH
和 PULL
等。
實際應用中,咱們一般會將上述這些基本模式綜合使用,組成各類各樣的高級模式——好比一開頭提到的泰坦尼克模式——去解決不少實際中的問題。這些就很少說了,你們有興趣能夠再次移步這裏來觀摩學習。
該進入正題了。這一部分跟以前的那篇英文博文的第一部分對應的。這一部分和接下來的一部分主要作兩個關鍵的技術選型。
我對 zmq.rs 的設計「借鑑」了 libzmq 不少——爲了保留借鑑痕跡便於參照,有些名詞我連名字都沒有改(呃,這句英文博文裏沒有……)。以前的幾篇《Rust 語言學習筆記》中,其實我已經在試圖搭 zmq.rs 的架子了,有個關鍵性的問題在當時就已經出現了:
怎麼樣正確使用 Rust 的 Task
。
目前來看,針對於個人項目,C++(libzmq 的實現語言)和 Rust 有兩點重要的不一樣:
select()
接口,以及libgreen
——提供了微線程的實現。做爲一個 Gevent 的重度用戶,我天然而然地選擇了微線程模型,經過建立大量 Task
來實現併發,底層與直接使用 select()
來實現異步併發並沒有實質區別。這聽起來很是理想,但遺憾的是 libgreen
並非 Rust 的默認選項—— libnative
纔是。這樣的話,若是用戶選擇默認使用 libnative
,那麼 zmq.rs 輕輕鬆鬆就能夠幫用戶建立數百個操做系統級的線程,由於 libnative
1:1 的模型下,一個 Task
就是一個線程嘛。
這就不是一件很使人愉快的事了。那咱們先放下這個,看看另一條路吧:徹底借(zhao)鑑(ban)libzmq,Rust 裏缺什麼再補什麼,好比 select()
。1:1 的 libnative
模型天然就不會有問題了,由於咱們將要本身從新實現異步,因此開啓的 Task
數屈指可數。雖然沒有了協做式的異步編碼優點,但這條路上的代碼也能夠寫的乾淨整潔——至少不會比 libzmq 差吧。但是,這個時候假如用戶又選擇了使用 libgreen
,又會怎麼樣呢?咣噹!幾個協做式的異步 Task
在分別執行一段手動實現的異步代碼,何其詭異!由於協做式的異步 Task
(即微線程)是應該被主事件循環驅動的,而不是用來跑一個本身寫的事件循環!微線程原本就是設計用來將異步代碼同步化、提升可讀性、下降編程複雜度,而不是像如今這樣又全都搞回去了。libgreen
在底層已經使用了 libuv 做爲主循環來調度全部的 Task
,而咱們如今爲了實現 select()
,還得想辦法把 libuv 底層的接口給暴露上來。最後,咱們還得使用這些接口,從新本身實現一遍異步任務調度。
就我來看,上述方法有多是在 libgreen
下實現 select()
的最合理的方法,但這讓我以爲很是彆扭,感受好像把 1:1 的 libnative
跟 M:N 的 libgreen
作出統一的接口是個糟糕的主意。這裏不深究了,問題交給 Rust 1.0 以後打算實現 select()
的人去吧。這裏呢,我選擇了第一種方案,也就是把 Task
當微線程的方案,由於目前來看這種方案須要寫的代碼更少,並且感受更像是 Rust 指望的樣子。
補充:其實也沒有必要整個程序全都一致要求要麼 libnative
要麼 libgreen
的,爲何不能混着用呢?個人 zmq.rs 內部本身搞一個 libgreen
的 Scheduler
好了,本身強制使用微線程模型;調用的人愛咋咋地唄。因此也就不糾結了。
這個事兒也讓我糾結了好久,也彆扭了好久:Rust 裏的繼承只包括成員方法,不包括成員變量。換句話說,Rust 裏沒有類,只有接口(Trait)和數據結構(Struct),struct 能夠實現 trait,trait 能夠繼承 trait,可是 struct 不能繼承 struct。這讓用慣了 Python 的我非!常!不!爽!怎麼能這樣呢,父類里根本不能定義數據!難道父類搞個多態還得每次要數據的時候調用一個 self.getData()
嗎?這聽說仍是面向對象編程的一個演化方向呢,求高人點解啊……
這裏碰到的問題主要是定義 SocketBase
啦,人家 libzmq 裏洋洋灑灑幾個文件,輕鬆搞定了漂亮的繼承關係。到我這裏死活搞不漂亮了:做爲父類的 SocketBase
必須得本身負責一部分數據,而子類跟 SocketBase
又是息息相關的。
怎麼辦嘞?
拆!仍是順應了那句老話,先組合後繼承。如今呢,個人 socket 長這樣:
因此呢,ReqSocket
和 RepSocket
的 bind()
裏只有相同的一句:self.base.bind()
,connect()
也是相似。而對於 send()
和 recv()
的實現,兩種不一樣的 socket 則有了不一樣的實現,且調用 self.base
的函數也不盡相同。
用這種組合的方式當然解決了當下的問題,但不知之後能不能學會 Rust 裏基於接口的繼承。