Rust 語言以其記憶體安全和高效能特性,近年來備受關注。本以 Rust 開發 Redis 克隆專案為切入點,引導讀者深入理解 Rust 在網路程式設計、資料函式庫開發等領域的應用。從基礎的 TCP 伺服器搭建,到 RESP 協定的解析與實作,逐步講解如何處理客戶端連線、解析指令、儲存資料等核心功能。此外,也涵蓋了鍵值過期機制和 Actor 模型的應用,展現 Rust 在處理複雜平行任務方面的優勢。
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
fn main() {
let listener = TcpListener::bind("127.0.0.1:6379").unwrap();
for stream in listener.incoming() {
match stream {
Ok(mut stream) => {
handle_connection(&mut stream);
}
Err(e) => {
println!("error: {}", e);
}
}
}
}
fn handle_connection(stream: &mut TcpStream) {
let mut buffer = [0; 512];
loop {
match stream.read(&mut buffer) {
Ok(size) if size != 0 => {
let response = "+PONG\r\n";
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
Ok(_) => {
println!("連線已關閉");
break;
}
Err(e) => {
println!("錯誤:{}", e);
break;
}
}
}
}
用 Rust 開發 Redis 克隆專案:從零開始的實踐
本文源自於作者對 Rust 程式語言的深入探索,以及透過實際專案來學習新知的渴望。作為一個對多領域充滿興趣的人,作者發現自己總是處於初學者的角色,不斷涉獵新的概念與技術。在面對新挑戰時,無論是程式語言還是藝術技法,作者深刻體會到「經驗是最佳的老師」。雖然觀看專業人士的教學影片很有幫助,但真正的學習發生在親自實踐的過程中。
本文的寫作動機源於作者認為,透過書寫可以整理自己的思緒、檢視過去的決策,並深化對某些主題的理解。對於讀者而言,本文可以作為入門,幫助讀者熟悉 Rust 語言;也可以作為對照參考,讓讀者比較不同的實作方法;或是跟著書中的範例程式碼一起實作,並深入研究相關的 Rust 檔案。
為什麼選擇 Rust 和 Redis 克隆專案?
Rust 是一種系統程式語言,以其記憶體安全性和效能著稱。Redis 則是一種流行的記憶體資料函式庫,以其高效能和多樣化的資料結構支援而聞名。透過使用 Rust 來開發 Redis 的克隆版本,作者旨在展示如何利用 Rust 的強大功能來建立一個高效能的資料函式庫系統。
本文的內容結構
本文分為多個章節,每個章節聚焦於 Redis 克隆專案的不同方面。以下是本文的主要章節:
- 初始步驟:介紹如何設定開發環境,以及如何使用 Rust 的網路函式庫來建立一個簡單的伺服器。
- RESP 協定:探討 Redis 使用的 RESP(Redis Serialization Protocol)協定,並實作一個簡單的 RESP 解析器。
- GET 和 SET 指令:實作 Redis 的核心指令,包括 GET 和 SET,並介紹如何使用 Rust 的集合資料結構來儲存資料。
- 鍵值過期:介紹如何實作鍵值的過期機制,包括建立一個定期執行任務的機制。
- 使用 Actor 模型進行並發處理:探討如何使用 Rust 的 Actor 模型來處理並發連線請求,並提高伺服器的效能。
本文的目標讀者
本文適合對 Rust 程式語言和 Redis 資料函式庫感興趣的開發者。無論是初學者還是有經驗的開發者,都可以透過本文深入瞭解如何使用 Rust 來建立一個高效能的資料函式庫系統。
結語
本文不僅是一本關於 Rust 和 Redis 的技術,也是一本關於如何透過實踐來學習新知的。希望讀者能夠透過本文,深入瞭解 Rust 和 Redis,並在實際專案中應用所學的知識。
內容解密:
本文介紹了使用 Rust 語言開發 Redis 克隆專案的過程,從初始設定到實作 RESP 協定、GET 和 SET 指令、鍵值過期機制,以及使用 Actor 模型進行並發處理。本文旨在幫助讀者深入瞭解 Rust 和 Redis,並提供實踐經驗。書中內容結構清晰,適合不同程度的開發者閱讀。
開發環境設定與 Redis 複製專案介紹
本文旨在指導讀者從零開始建立一個 Redis 的克隆版本,稱為 “Sider”。為了跟隨本專案,您需要一個編輯器(請選擇您喜歡的並確保它支援 Rust 程式語言)以及 Rust 編譯器。您可以依照官方在您的系統上安裝 Rust。
建立 Rust 專案
使用 CodeCrafters 建立專案
如果您使用 CodeCrafters,請遵循他們提供的指示。您將會複製一個由他們提供的儲存函式庫,其中包含 Rust 專案和一個執行伺服器的指令碼。
手動建立 Rust 專案
要建立專案,建議使用 Cargo:
$ cargo new sider
$ cd sider
這將建立一個名為 sider 的目錄,其中包含基本 Rust 專案的標準檔案和目錄。您可以使用以下命令測試一切是否正常運作:
$ cargo run
現在,請用您喜歡的編輯器開啟 src/main.rs 檔案並刪除範例程式碼。
建立 spawn_redis_server.sh 指令碼
此外,請建立一個名為 spawn_redis_server.sh 的檔案,其中包含以下程式碼:
#!/bin/bash
exec cargo run \
--quiet \
--release \
--manifest-path $(dirname $0)/Cargo.toml \
-- "$@"
內容解密:
此指令碼是一個包裝器,用於執行 cargo run 命令,並將引數傳遞給它。這將在稍後用於執行測試。
為何選擇 CodeCrafters
CodeCrafters 提供了一系列挑戰,引導您從頭開始實作重要的軟體專案。對於資料函式庫愛好者,他們提供了 “Build your own Redis” 挑戰。無論您想建立自己的語言還是深入網路協定,CodeCrafters 都能提供幫助。您可以選擇您喜歡的程式語言來解決挑戰,並檢視其他程式設計師的解決方案。
本文的免費性質
本文的寫作動機是為了與他人分享我的發現,並感謝那些無私分享知識的人。本文是對免費軟體運動的一種回饋。如果您願意承認我的努力,您可以透過實驗、學習並分享您所學到的東西來參與這個分享知識的過程。
排版慣例
本文中使用了一些排版慣例。普通文字以正常字型顯示,而行內程式碼則以特殊字型顯示。當程式碼涉及函式庫時,會提供指向檔案的連結。此外,本文還使用了旁白來提供額外的資訊或連結。
原始碼與進度追蹤
本文的原始碼可在儲存函式庫中取得。目前步驟將以特殊框標示出來。
設定開發環境的下一步
接下來,您需要組態一個 Git 儲存函式庫以使用 CodeCrafters。您可以按照挑戰頁面上的指示進行操作。如果您選擇手動建立專案,請確保您的 Rust 環境設定正確,並能夠執行 Cargo 命令。
本文並非 100% 完整,未來將會新增更多章節,並完成一些進階挑戰,如複製、交易和持久化。歡迎您與我一起踏上這段充滿發現和滿足感的旅程。雖然本文可能存在打字錯誤和錯誤,但我希望您能像我一樣享受它。
如何測試程式碼(CodeCrafters)
如果你正在使用CodeCrafters,每次你提交並推播程式碼時,測試都會自動執行。請務必閱讀官方檔案,特別是當你想使用CodeCrafters CLI時。
手動測試程式碼
警告:本文的指示並非官方提供,CodeCrafters指出倉函式庫的結構未來可能會發生變化,請注意這一點!
你也可以在本地機器上手動執行CodeCrafters測試。複製測試倉函式庫並修改internal/test_helpers/pass_all/spawn_redis_server.sh檔案,將redis-server替換為你專案中指令碼的完整路徑。
internal/test_helpers/pass_all/spawn_redis_server.sh
#!/bin/sh
find "." -type f -name "*.rdb" -exec rm {} +
exec /My/PROJECT/PATH/spawn_redis_server.sh --loglevel nothing $@
然後,你可以使用提供的Makefile在本地執行測試。有多個測試對應於基礎挑戰和擴充套件挑戰。對於基礎挑戰,請執行:
$ make test_base_with_redis
這將執行基礎挑戰中的所有測試。如果你只想檢查到目前為止你完成的步驟,你可以將該規則複製到自定義規則中,並在進展過程中新增步驟,例如:
Makefile
test_base_with_redis_prog: build
CODECRAFTERS_SUBMISSION_DIR=./internal/test_helpers/pass_all \
CODECRAFTERS_TEST_CASES_JSON="[\
{\"slug\":\"jm1\",\"tester_log_prefix\":\"stage-1\",\"title\":\"Stage #1: Bind to a port\"},\
{\"slug\":\"jm2\",\"tester_log_prefix\":\"stage-2\",\"title\":\"Stage #2: Respond to PING\"},\
]" \
dist/main.out
本文介紹
本文使用Mau編寫並使用TeX渲染。封面照片由Tengyart拍攝,可以在Unsplash上找到。它代表了“具有銹跡、綠色油漆和劃痕的豐富金屬紋理”,我發現它既具有藝術吸引力,又與我們將要使用的語言相符。
第一章:初始步驟
我們將逐步進行,並關閉每個艙壁和通風口,直到我們將其逼入絕境。 ——《異形》(1979)
初始步驟在像這樣的應用程式中相當標準。我們將建立一個伺服器,首先監聽特定的TCP埠。然後,我們將修改程式碼以回應單個傳入請求並終止。第三步,伺服器將回應來自同一客戶端的多個傳入請求,最終將服務多個客戶端。
在本章結束時,我們將擁有一個可以使用官方Redis CLI工具執行的應用程式,以服務PING命令。
步驟1:繫結到埠
首先,我們需要建立一個繫結到TCP埠6379的伺服器。Rust標準函式庫提供了可以繫結到通訊端的TcpListener。
src/main.rs
use std::net::TcpListener;
fn main() {
let listener = TcpListener::bind("127.0.0.1:6379").unwrap();
for stream in listener.incoming() {
match stream {
Ok(_stream) => {
println!("accepted new connection");
}
Err(e) => {
println!("error: {}", e);
}
}
}
}
這是伺服器的經典初始步驟。我們硬編碼了伺服器的地址(127.0.0.1)和埠(6379),但未來我們可能希望透過特定的命令列選項使這兩個值可組態。
程式碼解析:
unwrap是一種從Result中提取Ok值的粗略方法。在測試中,它是一種方便的方法,因為如果結果是錯誤,它會引發panic,但在生產程式碼中,它可能不是最佳解決方案。println!是我們如何在標準輸出上列印文字的方法。雖然有更複雜的除錯程式的方法,但列印仍然是瞭解系統中發生了什麼和記錄事件的好方法。- 上述程式碼中隱含的一個概念是
Iterator,在本例中由Incoming實作,它是TcpListener::incoming的傳回型別。
使用Redis CLI進行測試
除了使用CodeCrafters測試之外,你還可以使用Redis CLI與你的伺服器互動。一旦你在系統中安裝了它,你可以先使用以下命令執行你的伺服器:
$ cargo run
或者,如果你正在使用CodeCrafters設定,請執行:
$ spawn_redis_server.sh
然後開啟一個新的終端並執行redis-cli:
$ redis-cli
127.0.0.1:6379>
伺服器應該會列印出“accepted new connection”的訊息。目前,你還不能發出命令,因為伺服器忽略了透過流傳來的資料。
步驟2:回應PING
伺服器必須回應傳入的請求,因此下一步是開始監聽TCP連線並解析傳入的資料。Redis使用一種稱為Redis序列化協定(RESP)的二進位制協定,因此為了理解請求,我們最終必須實作該協定的解析器。
作為第一步,我們可以丟棄傳入的資料並傳送相同的回應,而不管請求是什麼。最簡單的Redis命令是PING,它接收回應+PONG\r\n。這是根據RESP簡單字串編碼的字串PONG,我們稍後會看到。
src/main.rs
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
fn main() {
let listener = TcpListener::bind("127.0.0.1:6379").unwrap();
for stream in listener.incoming() {
match stream {
Ok(mut stream) => {
handle_connection(&mut stream);
}
Err(e) => {
println!("error: {}", e);
}
}
}
}
fn handle_connection(stream: &mut TcpStream) {
let mut buffer = [0; 512];
stream.read(&mut buffer).unwrap();
let response = "+PONG\r\n";
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
在此版本中,我們隔離了傳入連線的管理。每個新的連線,由TcpStream表示,被傳遞給handle_connection函式。在那裡,我們從流中讀取到二進位制緩衝區,然後準備回應並將其寫入流。目前,程式碼仍使用unwrap來管理錯誤情況,但未來值得正確處理錯誤。
程式碼解析:
- 我們使用
handle_connection函式來管理每個新的連線。 - 在該函式中,我們讀取傳入的資料到緩衝區,然後傳送固定的回應
+PONG\r\n。 - 我們使用
write和flush方法將回應寫入流並重新整理它,以確保資料被傳送到客戶端。
初始步驟中的關鍵技術解析
在開發一個類別Redis伺服器的過程中,瞭解如何處理串流(stream)的型別至關重要。以Rust語言為例,當我們檢視TcpListener::incoming的檔案時,會發現其傳回型別為Incoming結構體。該結構體實作了Iterator特性,並且指定了Item = Result<TcpStream, Error>,這引導我們進一步瞭解TcpStream。
Iterator與Item的深入理解
在Rust中,Iterator是一個非常重要的特性,它允許我們對一系列的值進行迭代。Item則是迭代器中每個元素的型別。在TcpListener::incoming的例子中,Item是Result<TcpStream, Error>,意味著每次迭代都可能傳回一個TcpStream例項,或者是一個錯誤。
// 透過Iterator取得TcpStream
for stream in listener.incoming() {
match stream {
Ok(stream) => handle_connection(&mut stream),
Err(e) => println!("連線錯誤:{}", e),
}
}
#### 內容解密:
- `listener.incoming()`傳回一個迭代器,每次迭代嘗試接受一個新的TCP連線。
- `stream`的型別是`Result<TcpStream, Error>`,需要透過模式匹配進行處理。
- 成功時(`Ok(stream)`),將可變的`stream`傳遞給`handle_connection`函式處理。
- 失敗時(`Err(e)`),印出錯誤訊息。
### 測試與TDD(測試驅動開發)
在Rust這種強型別編譯語言中,測試相較於Python等語言更為複雜。雖然可以建立模擬物件(mock),但在執行時替換函式是困難的。因此,嚴格的TDD方法在Rust中難以實作。我們需要更頻繁地應用控制反轉(inversion of control),但仍無法達到像高抽象層級語言那樣的檢查程度。
#### 實務上的測試挑戰
在目前的案例中,若要在高階OOP語言中進行測試,常見的做法是:
* 建立一個只包含`read`方法的`TcpStream`模擬物件。
* 執行`handle_connection`函式。
* 檢查模擬物件的方法是否被正確呼叫。
然而,在Rust中,由於型別系統的限制,這種做法變得更加複雜。我們需要傳遞一個`TcpStream`型別的值給函式,並且很難只為測試重新定義`TcpStream`。
### 程式碼實作與記憶體安全
Rust語言的一大特色是其對記憶體安全的重視。瞭解如何使用參考(references)和可變性(mutability),特別是在傳遞值給函式時,是非常重要的。所有權(Ownership)是Rust中無處不在的概念,理解其原理對於避免編譯錯誤至關重要。
### 處理多個PING命令
為使伺服器能夠回應多個PING命令,需要將`handle_connection`函式中的程式碼放入迴圈中。同時,妥善管理`TcpStream::read`的輸出結果是非常重要的。該函式傳回一個`Result<usize>`,其中包含了接收到的位元組數量。
```rust
fn handle_connection(stream: &mut TcpStream) {
let mut buffer = [0; 512];
loop {
match stream.read(&mut buffer) {
Ok(size) if size != 0 => {
let response = "+PONG\r\n";
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
Ok(_) => {
println!("連線已關閉");
break;
}
Err(e) => {
println!("錯誤:{}", e);
break;
}
}
}
}
#### 內容解密:
- `stream.read(&mut buffer)`是一個阻塞函式,會等待TCP連線上的資料到來。
- 若接收到的位元組數量不為0,則傳送"+PONG\r\n"作為回應。
- 若接收到的位元組數量為0,表示連線已關閉。
- 若發生錯誤,則印出錯誤訊息並中斷迴圈。
### 使用Redis CLI進行測試
現在伺服器已經能夠持續監聽並回應客戶端的請求,可以開啟新的終端機執行`redis-cli`進行互動測試。
### 未來改進方向
目前的伺服器實作雖然能夠透過CodeCrafters挑戰的第三階段,但仍然無法真正處理傳入的請求。未來的改進方向包括:
* 支援更多的Redis命令。
* 改善伺服器的平行處理能力,以支援多個客戶端的同時連線。
總之,本章節介紹了使用Rust開發類別Redis伺服器的初始步驟,包括處理TCP串流、測試、記憶體安全、以及回應多個PING命令等關鍵技術。透過這些技術的實踐,我們能夠建立一個基本可用的伺服器,並為未來的擴充套件奠定堅實的基礎。