[Rust] Codec 和 Stream-Sink,究竟選誰?
一週多前的 6 月 26 日我完成了第二次 Rust 培訓。這次培訓在四十度高溫下進行,由於我家沒有空調電扇,培訓過程中差點 CPU 過熱躺下,害得我只能時不時拔掉外接顯示器來給 mbp 降溫。這次培訓又創造了我個人的一個新記錄:在西雅圖「千年一遇」的高溫下,整個培訓持續了接近 6 小時(略微剪輯和 1.2 倍速後是 4 小時 44 分鐘),比預期的四個小時多了近 1/2,卻少講了大概 1/4 的內容。
培訓的視頻可以在 B 站搜索「程序君的 Rust 培訓」找到,也可以點「閱讀原文」。培訓的主要內容有:
-
回顧第一講 - copy & move,Drop 順序,Size/DST,測試,Pin 以及常見 trait
-
網絡協議
-
Live coding - kv store(基於 tokio)
-
服務端應用,GRPC,tower-service 和 tonic
-
Live coding - PoW 服務
-
網絡安全 - DH,Noise protocol
-
Live coding - 使用 noise protocol 增強 kv store 的安全性
-
P2P 應用的一般結構
-
宏編程簡述(敬請期待下一期 Rust 培訓)
在講到使用 noise protocol 增強 kv store 的安全性時,我提到了兩個方案:
-
參照 LengthDelimited Codec,實現一個 NoiseCodec,對消息進行 encode(加密) 和 decode(解密),然後使用 tokio 已有的 Framed 結構和底下的 TcpStream 或者其他 socket 交互
-
使用已有的 LengthDelimited Codec 和 Framed 包裝 TcpStream,然後在外層再創建一個結構(比如叫 Noised),實現 Noise 對消息加密解密的功能。Noised 需要實現 Stream 和 Sink,對底層的 stream 進行讀寫。
培訓後幾天,有小夥伴問:使用 Codec 和使用 Stream/Sink 有什麼優缺點?
由於我們在 live coding 裏實現了 NoiseCodec(可以參考我 github repo: tyrchen/rust-training 中的代碼),我先講講構造 Noised 實現 Stream / Sink 的思路。
首先,定義 Noised 數據結構:
#[pin_project]
pub struct Noised {
#[pin]
stream: Framed<TcpStream, LengthDelimitedCodec>,
noise: TransportState,
// add later on
buf: Vec<u8>,
}
這裏需要引入 pin_project。Noised 內部的 stream,可以是一個實現了 Stream/Sink 的類型 T,這裏爲簡單期間,我們就放 Framed<TcpStream, LengthDelimitedCodec>
。
爲 Noised 實現 Stream trait,意味着要實現 poll_next()
方法。我們可以對內部的 stream 做 poll_next,如果得到 Poll::Ready(Some(Ok(data)))
,說明拿到了一個完整 frame,可以解密之。而對於 Sink trait,我們主要需要實現 start_send()
,先把上層交過來的數據加密,然後再調用內部的 stream 做 start_send()
。
整個過程比較直觀,感興趣的同學可以自己嘗試實現一下。
實現機制
從實現的角度來說,兩種方式各有千秋,Codec 的方式,是把實現植入 tokio Framed 提供的框架內,實現新的 Codec,如下圖所示:
而 Stream/Sink 方式則在調用方和 Framed 之間插入了一層處理,需要特別注意數據結構的泛型化,否則只能處理 Framed,複用性不高:
實現難度
實現爲 Codec 意味着你需要了解 Codec 的機制,以及 Encoder / Decoder trait。實現爲 Stream/Sink 意味着你需要了解 Stream / Sink 這兩個 trait 的原理,以及與 Pin
打交道。我個人覺得 Codec 實現起來更簡單一些,因爲所有的異步處理都被 Framed 解決了。在接收數據時,Codec 只需要決定何時拿到了一個完整的 frame,然後 decode 即可;在發送數據時,Codec 只需要 encode 即可,整個過程是純粹的內存操作,不涉及任何 I/O。
如果說從單純實現的角度,二者代碼量差不多,Stream/Sink 方式甚至代碼量可能更少,但二者測試的難度區別非常大。Codec 是純粹的函數,意味着整個 Noise Protocol 的握手,以及握手成功後的加密解密,都可以通過單元測試完成,效率很高,很直觀,如果大家再去看 live coding,可以看到 unit test 的撰寫非常簡單,不存在複雜的數據結構的構建問題。
而 Stream/Sink 的測試,則要複雜不少。除非你對 Framed<S, Codec>
構造或者使用 Mock,否則很難做單元測試。即便使用 Mock,Mock 的數據結構和測試數據的構建也需要花一些心思。
結論
如果你要構建自定義協議的網絡應用,tokio 的 Framed 是一個很不錯的工具,在此之上你可以構建你自己的 Codec 來處理 frame。雖然你也可以構建一個實現了 Stream/Sink 的中間層,來處理自定義協議,但它不如 Codec 來得直觀。
賢者時刻
上次培訓時,開始前本來打算做一個投票。結果天一熱,忘記在 zoom 裏發起投票了。把投票放在這裏,感興趣的可以填一下,讓我好了解大家的偏好。對 Rust 不感興趣的,不用管它。:)
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/ObwdJhJ91TKxFv9XNxF1sA