s2n-quic: 終於有一個比較好用的 QUIC 實現了
QUIC 是一種爲性能而設計的加密傳輸協議,它是尚處在襁褓之中的 HTTP/3(最新是 draft-34)的基礎。根據維基百科的資料顯示,QUIC 在 2012 年就被部署到 Google 內部,並於 2013 年對外發布。2021 年 5 月,IETF 在 RFC 9000 中對 QUIC 的基本功能進行了標準化,並在 RFC 9001 中標準化瞭如何使用 TLS 保護 QUIC,以及 RFC 9002 中標準化了 QUIC 的擁塞控制。QUIC 通過使用在 QUIC 傳輸中承載的通過 TLS 建立的加密和身份驗證密鑰來保護其 UDP 數據報文。它旨在通過提供改進的首字節延遲,多路複用,以及解決諸如線頭阻塞、移動性和數據丟失檢測等問題來改進 TCP。
我們知道,即便在 TLS 1.3 中改進了握手的效率,一個 HTTPS(TCP + TLS)完整的握手過程需要 2 個 roundtrip,包括 TCP 三次握手頭兩個包 SYN - SYN/ACK 一個 roundtrip,以及後續的 ACK/HELLO - ACK/HELLO/CERT/Finished 一個 roundtrip:
所以,Web 應用的延遲還是非常可觀。如果使用 QUIC,QUIC/TLS 握手可以同步進行,於是可以省掉 TCP 三次握手的一次 roundtrip,一個 roundtrip 就可以建立安全的加密信道:
這就使得 Web 應用程序能夠更快地執行,尤其是在網絡較差的情況下。
除了更短的延遲外,QUIC 還有一個很重要的特性是傳輸層的多路複用,也就是在同一個連接中打開多個互不干擾的 stream(流)。以前我們要支持多路複用,需要 TCP + Yamux,現在可以用 QUIC 更高效地在傳輸層完成這個功能,解決了隊頭阻塞的問題。
s2n-quic 是什麼?
上週 Amazon 開源了 s2n-quic 這個 QUIC 協議的軟件包。s2n-quic 用 Rust 撰寫,可見 Amazon/AWS 對 Rust 不斷投入的決心。s2n 這個名字是 Signal to Noise 的縮寫,是對我們生活中無處不在的信息加密的致敬 —— 因爲加密是一種將有意義的信號(Signal)僞裝成看似隨機的噪聲(Noise)的行爲。Amazon 有好幾個 s2n 打頭的開源軟件,包括 s2n-tls 和 s2n-bignum。s2n-tls 是用 C 撰寫的 TLS 實現,它被 s2n-quic 默認使用(s2n-quic 也可以用 Rustls)。
別看 Rust 是一門相對年輕的語言,但很多新興的基礎領域的代碼,都在用 Rust 撰寫。比如哈希算法中比較新的 blake3,就是用 Rust 撰寫並移植到其它語言。對於 QUIC 來說,Rust 下已經有了 Cloudflare 的 quiche,社區開發的 quinn,還有今天發佈的 s2n-quic。三者中 s2n-quic 的接口最爲易用,非常值得一試。
如何使用 s2n-quic?
我們看如何實現一個簡單的 echo clien/server。首先是服務器實現:
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let mut listener = Server::builder()
.with_tls((CERT_PEM, KEY_PEM))?
.with_io("127.0.0.1:4433")?
.start()?;
while let Some(mut conn) = listener.accept().await {
// spawn a new task for the connection
tokio::spawn(async move {
eprintln!("Connection accepted from {:?}", conn.remote_addr());
while let Ok(Some(mut stream)) = conn.accept_bidirectional_stream().await {
// spawn a new task for the stream
tokio::spawn(async move {
eprintln!("Stream opened from {:?}", stream.connection().remote_addr());
// echo any data back to the stream
while let Ok(Some(data)) = stream.receive().await {
stream.send(data).await.expect("stream should be open");
}
});
}
});
}
Ok(())
}
這和 Tokio TCP 的使用方法幾乎一樣,比 Tokio TCP + TLS 還要簡單一些。值得注意的是,由於 QUIC 支持多路複用,所以 accept 一個 conn 之後,這個 conn 並不是一個 Stream,而是需要進一步調用 conn.accept_bidirectional_stream()
得到一個可以收發數據的 stream。
客戶端的實現也非常類似:
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let client = Client::builder()
.with_tls(CERT_PEM)?
.with_io("0.0.0.0:0")?
.start()?;
let addr: SocketAddr = "127.0.0.1:4433".parse()?;
let connect = Connect::new(addr).with_server_name("localhost");
let mut conn = client.connect(connect).await?;
// ensure the connection doesn't time out with inactivity
conn.keep_alive(true)?;
// open a new stream and split the receiving and sending sides
let stream = conn.open_bidirectional_stream().await?;
let (mut rx, mut tx) = stream.split();
// spawn a task that copies responses from the server to stdout
tokio::spawn(async move {
let mut stdout = tokio::io::stdout();
let _ = tokio::io::copy(&mut rx, &mut stdout).await;
});
// copy data from stdin and send it to the server
let mut stdin = tokio::io::stdin();
tokio::io::copy(&mut stdin, &mut tx).await?;
Ok(())
}
注意這裏 client 和 conn 是分開的概念,一個 client 可以建立多個 conn。然後一個 conn 又可以打開多個 stream 進行收發。
在體驗了簡單的 echo client/server 後,我感覺 s2n-quic 把 QUIC 協議的使用門檻大大降低,我們可以用和處理 TCP client/server 相同結構的代碼,來處理 QUIC。
爲了進一步體驗 QUIC 和已有項目的結合,我嘗試着把我之前爲極客時間的《Rust 第一課》做的示範性的類似 redis 的 kv server(github 下 tyrchen/simple-kv)添加了 QUIC 的支持。我發現,只消添加兩三百行代碼,我的 simple-kv 就能很好地在已有支持 TCP + TLS + Yamux 的基礎上,擴展支持 QUIC。如果你感興趣的話,可以去看看那個 repo,代碼我已提交。
如果你覺得文字描述代碼比較生硬,你也可以去 B 站看我的視頻。我做了兩個關於 s2n-quic 的視頻,週二晚和週四晚會發布在合集 —— Rust crate 大巡禮 中。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/YdRMFNNL4ld0ymXY_SmM6A