設計 zmq.rs ——用 Rust 實現的 ZeroMQ(二)

寫在前面:html

Rust 1.0 臨近,libgreen 因爲統一接口代價太大以及其僞輕量級的事實被降級爲不推薦的社區項目,zmq.rs 項目也面臨着一次基於 mio 的從新設計——除非更合適的協程實現能當即出現。因此呢,草稿箱裏積存了數月的「命令通道」部分再也不有意義了,但考慮到新的設計中也將有相似的概念,仍將其貼出來。git

命令通道(該設計即將刪除!!)

以前的類圖顯示了幾個重要的結構:socket 接口、不一樣類型 socket 的實現,以及他們的共享功能 SocketBase。可是,這幾個結構其實都運行在用戶的 Task 裏,也就是擁有 socket 的代碼所在的 Task。前面也說了 zmq.rs 會建立好多 Task 的,下面就介紹幾個跑在獨立 Task 中的結構。github

咱們就按當時實現的順序來吧。segmentfault

TcpListener 是用來接受鏈接請求的,每次調用 bind() 時,SocketBase 就會先建立一個 Task,而後在其中建立一個 TcpListener。咱們知道,Rust 中最經典的任務間通訊方式就是通道,無不例外,TcpListenerSocketBase 之間的數據交換也是經過通道,這個通道叫作命令通道併發

Task和命令通道示意圖

命令通道目前是單方向的,命令通常由不一樣的獨立的 Task 發給 SocketBase 對象。上述類圖中的 txrx 就是命令通道的兩個端點,SocketBase.process_commands() 會從 rx 端接收命令並執行命令;而發送端 tx 有多個副本,是一個多發單收的通道,當建立 TcpListener 的同時,SocketBase 會克隆一個 tx 出來交給新建立的 TcpListener異步

TcpListener 在單獨的 Task 中就能夠隨心所欲了。目前的實現是一個死循環,每次循環會異步阻塞在等待 bind() 給定端口上的新鏈接請求。每當有新的鏈接請求,TcpListener 都會接受,建立一個 TCP 鏈接,而後建立一個鏈接處理的任務(~~稍後提到~~),最後經過命令通道告知 SocketBaseTcpListener 會一直重複這樣的行爲,一直到……何時呢?socket

爲了避免讓 TcpListener 的死循環影響到程序的正常結束,咱們必須在 SocketBase 被銷燬時(之後若是有了 unbind 還得再單獨考慮),儘快跳出死循環。還好 Rust 的通道考慮到了這一點,提供了這樣的功能:在一個通道的接收端被銷燬後,繼續往發送端發送數據的操做將會失敗。因此咱們只要能往 tx 裏發一個空白命令,若是發送失敗就能夠跳出死循環了。所以,我在 TcpListener 的阻塞調用 self.acceptor.accept() 上加了一個 1 秒(也許會改爲 100 毫秒)的超時,而後每次循環都會發送一個空白命令。這樣,當用戶的 Task 退出以後,TcpListener 最多再存活 1 秒鐘,而後也會自我銷燬了。ide

看完了命令通道的發送端,咱們再來看一下接收端。spa

SocketBase 就沒有 TcpListener 那麼自由自在了,由於 SocketBase 是存活在用戶的 Task 中,這也就意味着,它不能無節制的阻塞,否則用戶還怎麼作本身的事情。因此呢,當用戶在執行本身的代碼時,咱們只能等着先不去接收命令,這時命令就會在命令通道里排隊。一旦 SocketBase 有機會執行了,好比用戶調用了 recv() 或是 connect(),咱們就抓緊機會,先將待處理的命令一併處理乾淨,而後再去完成用戶的請求。.net

這個設計跟 libzmq 是一致的:SocketBase.process_commands() 接受一個布爾型的參數 block,來決定是阻塞式地處理一個命令——若是沒有命令就一直阻塞、等到一個再處理,仍是非阻塞式地儘可能去處理一個命令——若是有現成的命令就處理且返回真,不然當即返回假,毫不多等。處理命令是在一個循環裏用非阻塞的方式進行的,每次循環先試圖處理一個命令,而後一樣是非阻塞地去調用底層。

翻天覆地

正如一開始所說,libgreen 已經不復存在了,因此以上全部基於輕量級協程的併發設計就不合適了,由於 Task 的開銷太大——即便是繼續使用 green-rs。關於這一段故事,我在這些幻燈片裏也記錄了,稍後我再把明天的錄像發出來,你們有興趣能夠看看。

在新的設計出來以前,這個話題就先暫告一段落了。

相關文章
相關標籤/搜索