[Rust] Codec 和 Stream-Sink,究竟選誰?

一週多前的 6 月 26 日我完成了第二次 Rust 培訓。這次培訓在四十度高溫下進行,由於我家沒有空調電扇,培訓過程中差點 CPU 過熱躺下,害得我只能時不時拔掉外接顯示器來給 mbp 降溫。這次培訓又創造了我個人的一個新記錄:在西雅圖「千年一遇」的高溫下,整個培訓持續了接近 6 小時(略微剪輯和 1.2 倍速後是 4 小時 44 分鐘),比預期的四個小時多了近 1/2,卻少講了大概 1/4 的內容。

培訓的視頻可以在 B 站搜索「程序君的 Rust 培訓」找到,也可以點「閱讀原文」。培訓的主要內容有:

在講到使用 noise protocol 增強 kv store 的安全性時,我提到了兩個方案:

  1. 參照 LengthDelimited Codec,實現一個 NoiseCodec,對消息進行 encode(加密) 和 decode(解密),然後使用 tokio 已有的 Framed 結構和底下的 TcpStream 或者其他 socket 交互

  2. 使用已有的 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