我們?cè)?019年的最后兩天,參加了Prodigy Education舉辦的黑客馬拉松,許多團(tuán)隊(duì)聚在一起努力將他們的想法變成現(xiàn)實(shí)。
我們之中有的人只是單純?yōu)榱撕猛?,有的是想學(xué)一些新的知識(shí),還有些人可能是想證明一些概念或想法。
我在過(guò)去幾周總是被動(dòng)的獲取Rust相關(guān)信息或使用Rust的代碼,因此我認(rèn)為hackathon是一次學(xué)習(xí)Rust的絕佳時(shí)機(jī)。
hackathon的時(shí)間緊迫性使我更加快速的去學(xué)習(xí),同時(shí)也會(huì)去解決現(xiàn)實(shí)世界的一些問(wèn)題。
為什么是Rust

在我職業(yè)生涯的前10年中,有8年都在使用C和C++。
從好的方面來(lái)講,我喜歡像C++這樣可以提供靜態(tài)類型的語(yǔ)言,因?yàn)樗茉诰幾g期就能夠早早的發(fā)現(xiàn)錯(cuò)誤。
我個(gè)人對(duì)于C++的一些看法是:
- 工程師很容易搬起石頭砸自己的腳
- 作為一門編程語(yǔ)言,它已經(jīng)非常臃腫且復(fù)雜
- 缺乏良好的、標(biāo)準(zhǔn)的廣泛適用的包管理系統(tǒng)
自從我改做Web應(yīng)用以來(lái),一直是做Python和JavaScript開(kāi)發(fā),使用像Django、Flask和Express這樣的框架。
到目前為止,我在Python和JavaScript中的開(kāi)發(fā)經(jīng)驗(yàn)是,它們可以提供良好的程序迭代和交付速度,但有時(shí)會(huì)占用大量的CPU和內(nèi)存,即使服務(wù)是相對(duì)空閑的。
我經(jīng)常發(fā)現(xiàn)自己寫(xiě)好的C++程序,會(huì)缺失一些安全性、速度和精簡(jiǎn)性。
我想要尋找一種像Rust這樣精簡(jiǎn)的、裸機(jī)編程語(yǔ)言來(lái)開(kāi)發(fā)web應(yīng)用。
沒(méi)有運(yùn)行時(shí),沒(méi)有垃圾回收。直接加載二進(jìn)制代碼,交給內(nèi)核執(zhí)行。
目標(biāo)
我的目標(biāo)是完成一個(gè)后端由Rust編寫(xiě),前端是JavaScript+React完成的類似于S3作為圖床的應(yīng)用程序,用戶可以做以下事情:
- 瀏覽圖床中所有的圖片(分頁(yè)可選)
- 上傳圖片
- 上傳圖片時(shí)可以給圖片增加標(biāo)簽
- 通過(guò)名稱進(jìn)行查詢或過(guò)濾
所有有趣的hackathon項(xiàng)目都有一個(gè)名字,所以我決定將這個(gè)項(xiàng)目命名為:
RustIC -> Rust + Image Contents

我認(rèn)為如果我做到了以下這些事情,那么這次hackathon之行對(duì)我個(gè)人來(lái)說(shuō)就是成功的:
- 對(duì)Rust有一個(gè)基本的理解,包括它的類型系統(tǒng)和內(nèi)存模型
- 探索S3的對(duì)于文件和任意標(biāo)簽的預(yù)簽名鏈接功能
- 寫(xiě)出一個(gè)可以驗(yàn)證的功能正常的應(yīng)用
由于我的主要目標(biāo)是開(kāi)發(fā)功能,同時(shí)兼顧學(xué)習(xí)。很多代碼是我一邊學(xué)一邊寫(xiě)的,所以代碼組織和效率可能并不是最理想的,因?yàn)檫@些屬于次要目標(biāo)。
Rust的原則
在我開(kāi)始之前,我?guī)е闷嫘娜チ私饬艘獙W(xué)習(xí)的語(yǔ)言的設(shè)計(jì)師在創(chuàng)建這門語(yǔ)言時(shí)內(nèi)心的原則是什么。我找到了一個(gè)簡(jiǎn)化版本和一個(gè)詳細(xì)版本。
與我在許多博客上讀到的內(nèi)容相反,Rust是有可能發(fā)生內(nèi)存泄露(循環(huán)引用)和之行不安全的操作(unsafe代碼塊中)的,詳細(xì)描述在上面的FAQ中。
“We [the language creators] do not intend [for Rust] to be 100% static, 100% safe, 100% reflective.”

從后端開(kāi)始
Google搜索“Rust web framework“,排在最前面的是Rocket。我進(jìn)入這個(gè)網(wǎng)站,發(fā)現(xiàn)文檔的示例都一目了然。
有一點(diǎn)需要注意的是Rocket需要Rust的nightly版本,不過(guò)在hackathon上這都是小問(wèn)題。
GitHub的代碼庫(kù)中有著非常豐富的例子。完美!
我使用Cargo創(chuàng)建了一個(gè)新的項(xiàng)目,在TOML文件中加入了Rocket依賴,然后跟著Rocket的入門指南,寫(xiě)了第一段代碼:
#[get("/")]
fn index() -> &'static str {
"Hello, world!"
}
fn main() {
rocket::ignite().mount("/", routes![index]).launch();
}
對(duì)于熟悉Django、Flask、Express等框架等同學(xué)來(lái)說(shuō),這段代碼讀起來(lái)非常容易。作為一名Rocket用戶,你可以使用宏作為裝飾器來(lái)將路由映射到對(duì)應(yīng)的處理函數(shù)上。
在編譯時(shí),宏將被擴(kuò)展。這對(duì)開(kāi)發(fā)者是完全透明的。如果你想看擴(kuò)展后的代碼,可以使用cargo-expand。
以下是我在構(gòu)建Rust應(yīng)用程序時(shí)的一些有趣的或者有挑戰(zhàn)性的亮點(diǎn):
指定路由響應(yīng)
我想要以JSON的數(shù)據(jù)格式返回S3中所有的文件列表。
你可以看到路由關(guān)聯(lián)的處理函數(shù)的代碼決定了響應(yīng)類型。
設(shè)置響應(yīng)結(jié)構(gòu)非常容易,如果你想要返回JSON格式的數(shù)據(jù),并且每個(gè)字段都有自己的結(jié)構(gòu)和類型,那對(duì)應(yīng)的就是Rust的struct。
所以你應(yīng)該先定義一個(gè)結(jié)構(gòu)體struct(S)來(lái)接受響應(yīng),并且需要進(jìn)行標(biāo)注:
#[derive(Serialize)]
struct(s)被標(biāo)記了#[derive(Serialize)],因此可以通過(guò)`rocket_contrib::json::Json將它轉(zhuǎn)換成JSON。
#[derive(Serialize)]
struct BucketContents {
data: Vec<S3Object>,
}
#[derive(Serialize)]
struct S3Object {
file_name: String,
presigned_url: String,
tags: String,
e_tag: String, // AWS generated MD5 checksum hash for object
is_filtered: bool,
}
#[get("/contents?<filter>")]
fn get_bucket_contents(
filter: Option<&RawStr>
) -> Result<Json<BucketContents>, Custom<String>> {
// Returns either Ok(Json(BucketContents)) or,
// a Custom error with a reason
}
處理分段上傳
當(dāng)我意識(shí)到我的前端很有可能使用POST方法上傳格式為multipart/form-data的表單數(shù)據(jù)時(shí),我就開(kāi)始深入研究如何使用Rocket來(lái)構(gòu)建程序了。
不幸的是,Rocket0.4版本不支持multipart,看起來(lái)在0.5版本會(huì)支持。
這意味著我需要使用multipart crate并集成到Rocket中。最終代碼可以正常運(yùn)行,但是如果Rocket支持multipart將會(huì)使代碼更加簡(jiǎn)潔。
#[post("/upload", data = "<data>")]
// signature requires the request to have a `Content-Type`. The preferred way to handle the incoming
// data would have been to use the FromForm trait as described here: https://rocket.rs/v0.4/guide/requests/#forms
// Unfortunately, file uploads are not supported through that mechanism since a file upload is performed as a
// multipart upload, and Rocket does not currently (As of v0.4) support this.
// https://github.com/SergioBenitez/Rocket/issues/106
fn upload_file(cont_type: &ContentType, data: Data) -> Result<Custom<String>, Custom<String>> {
// this and the next check can be implemented as a request guard but it seems like just
// more boilerplate than necessary
if !cont_type.is_form_data() {
return Err(Custom(
Status::BadRequest,
"Content-Type not multipart/form-data".into()
));
}
let (_, boundary) = cont_type.params()
.find(|&(k, _)| k == "boundary")
.ok_or_else(
|| Custom(
Status::BadRequest,
"`Content-Type: multipart/form-data` boundary param not provided".into()
)
)?;
// The hot mess that ensues is some weird combination of the two links that follow
// and a LOT of hackery to move data between closures.
// https://github.com/SergioBenitez/Rocket/issues/106
// https://github.com/abonander/multipart/blob/master/examples/rocket.rs
let mut d = Vec::new();
data.stream_to(&mut d).expect("Unable to read");
let mut mp = Multipart::with_body(Cursor::new(d), boundary);
let mut file_name = String::new();
let mut categories_string = String::new();
let mut raw_file_data = Vec::new();
mp.foreach_entry(|mut entry| {
if *entry.headers.name == *"fileName" {
let file_name_vec = entry.data.fill_buf().unwrap().to_owned();
file_name = from_utf8(&file_name_vec).unwrap().to_string()
} else if *entry.headers.name == *"tags" {
let tags_vec = entry.data.fill_buf().unwrap().to_owned();
categories_string = from_utf8(&tags_vec).unwrap().to_string();
} else if *entry.headers.name == *"file" {
raw_file_data = entry.data.fill_buf().unwrap().to_owned()
}
}).expect("Unable to iterate");
let s3_file_manager = s3_interface::S3FileManager::new(None, None, None, None);
s3_file_manager.put_file_in_bucket(file_name.clone(), raw_file_data);
let tag_name_val_pairs = vec![("tags".to_string(), categories_string)];
s3_file_manager.put_tags_on_file(file_name, tag_name_val_pairs);
return Ok(
Custom(Status::Ok, "Image Uploaded".to_string())
);
}
配置CORS
路由寫(xiě)好了以后,我就開(kāi)始用curl或Postman來(lái)進(jìn)行測(cè)試了,現(xiàn)在已經(jīng)是時(shí)候開(kāi)始把前端集成進(jìn)來(lái)了。我需要適當(dāng)設(shè)置響應(yīng)頭以避免跨域問(wèn)題。
Rocket依舊沒(méi)有支持這個(gè)特性。
然后我在GitHub代碼庫(kù)中找到了一些解決方案:
// CORS Solution below comes from: https://github.com/SergioBenitez/Rocket/issues/25
extern crate rocket;
use std::io::Cursor;
use rocket::fairing::{Fairing, Info, Kind};
use rocket::{Request, Response};
use rocket::http::{Header, ContentType, Method};
struct CORS();
impl Fairing for CORS {
fn info(&self) -> Info {
Info {
name: "Add CORS headers to requests",
kind: Kind::Response
}
}
fn on_response(&self, request: &Request, response: &mut Response) {
if request.method() == Method::Options ||
response.content_type() == Some(ContentType::JSON) ||
response.content_type() == Some(ContentType::Plain) {
response.set_header(Header::new("Access-Control-Allow-Origin", "http://localhost:3000"));
response.set_header(Header::new("Access-Control-Allow-Methods", "POST, GET, OPTIONS"));
response.set_header(Header::new("Access-Control-Allow-Headers", "Content-Type"));
response.set_header(Header::new("Access-Control-Allow-Credentials", "true"));
}
if request.method() == Method::Options {
response.set_header(ContentType::Plain);
response.set_sized_body(Cursor::new(""));
}
}
}
fn main() {
rocket::ignite().attach(
CORS()
).mount(
"/",
routes![get_bucket_contents, upload_file]
).launch();
}
過(guò)了一會(huì),我發(fā)現(xiàn)了rocket_cors,它幫助我大幅縮減了代碼量。
fn main() -> Result<(), Error> {
let allowed_origins = AllowedOrigins::some_exact(&["http://localhost:3000"]);
let cors = rocket_cors::CorsOptions {
allowed_origins,
allowed_methods: vec![Method::Get, Method::Post].into_iter().map(From::from).collect(),
allowed_headers: AllowedHeaders::some(&["Content-Type", "Authorization", "Accept"]),
allow_credentials: true,
..Default::default()
}
.to_cors()?;
rocket::ignite().attach(cors)
.mount("/", routes![get_bucket_contents, upload_file])
.launch();
Ok(())
}
運(yùn)行起來(lái)
我們只需要一個(gè)簡(jiǎn)單的cargo run命令就可以讓程序運(yùn)行起來(lái)

我機(jī)器上的活動(dòng)監(jiān)視器告訴我這個(gè)程序正在運(yùn)行中,并且只消耗了2.7MB內(nèi)存。
而且這還只是沒(méi)有經(jīng)過(guò)優(yōu)化的調(diào)試版本。項(xiàng)目使用- release標(biāo)簽打包的話,運(yùn)行時(shí)只需要1.6MB內(nèi)存。

基于Rust的后端服務(wù)器,我們請(qǐng)求/contents這個(gè)路由會(huì)得到如下響應(yīng):
{
"data": [
{
"file_name": "Duck.gif",
"presigned_url": "https://s3.amazonaws.com/rustic-images/Duck.gif?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIARDWJNDW3U8329UDNJ%2F20200107%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20200107T050353Z&X-Amz-Expires=1800&X-Amz-Signature=1369c003b2f54510882bf9982ab56d024d6c9d2655a4d86f8907313c7499b56d&X-Amz-SignedHeaders=host",
"tags": "animal",
"e_tag": "\"93c570cadd6b8b2f85b47c2f14fd82a1\"",
"is_filtered": false
},
{
"file_name": "GIZMO.png",
"presigned_url": "https://s3.amazonaws.com/rustic-images/GIZMO.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIARDWJNDW3U8329UDNJ%2F20200107%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20200107T050353Z&X-Amz-Expires=1800&X-Amz-Signature=040e76c2df5a9a54ed4fbc8490378cf732b32bae78f628448536fc610018c0c3&X-Amz-SignedHeaders=host",
"tags": "robots",
"e_tag": "\"2cde221a0c7a72c0a7a60cffce29a0bc\"",
"is_filtered": false
},
{
"file_name": "GreenSmile.gif",
"presigned_url": "https://s3.amazonaws.com/rustic-images/GreenSmile.gif?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIARDWJNDW3U8329UDNJ%2F20200107%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20200107T050354Z&X-Amz-Expires=1800&X-Amz-Signature=d115b107de530ce15b3590abdbab355c2a9481a81131f88bf4ad2a59ca11bbac&X-Amz-SignedHeaders=host",
"tags": "smile-face",
"e_tag": "\"86854a599540f50bdc5e837d30ca34f9\"",
"is_filtered": false
}
]
}
前端的工作相對(duì)簡(jiǎn)單一些,我們使用的是:
- React
- React Bootstrap
- react-grid-gallery
- react-tags-input
用戶可以在我們的頁(yè)面瀏覽圖片,也可以通過(guò)文件名或標(biāo)簽來(lái)進(jìn)行檢索或過(guò)濾。

用戶還可以通過(guò)拖拽來(lái)上傳文件,并且可以在提交上傳之前打上標(biāo)簽。

我喜歡使用Rust構(gòu)建應(yīng)用程序的原因
- Cargo對(duì)于依賴和應(yīng)用管理的程度簡(jiǎn)直令人驚嘆
- 編譯器對(duì)于我們處理編譯錯(cuò)誤幫助非常大,有位博主在博客中描述了他是如何按照編譯器大指導(dǎo)來(lái)寫(xiě)代碼的。我的經(jīng)驗(yàn)也比較類似。
- 我需要的每一項(xiàng)功能都有crate,這讓我感到非常驚喜

- 在線的Rust Playground,讓我可以運(yùn)行小的代碼片段。
- Rust語(yǔ)言服務(wù)器,已經(jīng)很好的集成到了Visual Studio Code,它能夠提供實(shí)時(shí)錯(cuò)誤檢查、格式設(shè)置、符號(hào)查找等。這讓我可以在幾個(gè)小時(shí)內(nèi)不編譯就能取得不錯(cuò)的進(jìn)展。
不便、驚喜和麻煩
盡管Rust的文檔很棒,但我不得不依賴一些crates的文檔和例子。有些crates有很棒的集成測(cè)試,提供了一些關(guān)于如何使用的提示。當(dāng)然了,Stack Overflow和Reddit也給我提供了很多幫助。

另外還要注意的是:
- 理解所有權(quán)、生命周期和所有權(quán)借用會(huì)使學(xué)習(xí)難度陡增,特別是在為期兩天的黑客馬拉松中努力提供功能時(shí)。我將它們與C++做比較并且弄清楚,但有時(shí)還是會(huì)感到困惑。
- 在所有的事情中,
Strings攔住了我?guī)追昼?,特別是String和&str的區(qū)別更是令人困惑——直到我花了些時(shí)間來(lái)理解所有權(quán)、生命周期和所有權(quán)借用才搞清楚這些。
其他的一些觀察
- Rust中沒(méi)有真正意義上的null類型,通常情況下,空值需要用
Option類型的None來(lái)表示 - 模式匹配非常棒,這是我在Scala中最喜歡的一個(gè)特性,在Rust中也一樣。這種代碼看起來(lái)表現(xiàn)力很強(qiáng),并且允許編譯器標(biāo)記未處理的情況。
match bucket_contents {
Err(why) => match why {
S3ObjectError::FileWithNoName => Err(Custom(
Status::InternalServerError,
"Encountered bucket objects with no name".into()
)),
S3ObjectError::MultipleTagsWithSameName => Err(Custom(
Status::InternalServerError,
"Encountered a file with a more than one tag named 'tags'".into()
))
},
Ok(s3_objects) => {
let visible_s3_objects: Vec<S3Object> = s3_objects.into_iter()
.filter(|obj| !obj.is_hidden())
.collect();
Ok(Json(BucketContents::new(visible_s3_objects)))
}
}
- 說(shuō)起安全和不安全模式,你仍然可以進(jìn)行更底層的編程,比如說(shuō)在不安全的模式下可以和C語(yǔ)言代碼通過(guò)接口交互。盡管Rust中有很多正確性檢查,但你仍然可以在不安全模塊中做一些騷操作,例如解引用。讀代碼的人也可以從不安全模塊中獲取到很多信息。
- 通過(guò)
Box在堆中分配內(nèi)存空間,而不是new和delete。剛開(kāi)始感覺(jué)比較奇怪,但是也很容易理解。標(biāo)準(zhǔn)庫(kù)中還定義了其他的一些智能指針,如果你需要使用引用數(shù)量或者弱引用時(shí)就可以直接使用。 - Rust中的異常也很有趣,因?yàn)樗鼪](méi)有異常。你可以選擇使用
Result<T, E>表示可以恢復(fù)的錯(cuò)誤,也可以用panic!宏表示不可恢復(fù)的錯(cuò)誤。
// This code:
// 1. Takes a vector of objects representing S3 contents
// 2. Uses filter to remove entries we don't care about
// 3. Uses map to transform each object into another type, but terminates iteration
// . if the lambda passed to map returns an Err.
// 4. If all iterations produced an Ok(S3Object) result, these are collected into a Vec<S3Object>
let bucket_contents: Result<Vec<S3Object>, S3ObjectError> = bucket_list
.into_iter()
.filter(|bucket_obj| bucket_obj.size.unwrap_or(0) != 0) // Eliminate folders
.map(|bucket_obj| {
if let None = bucket_obj.key {
return Err(S3ObjectError::FileWithNoName);
}
let file_name = bucket_obj.key.unwrap();
let e_tag = bucket_obj.e_tag.unwrap_or(String::new());
let tag_req_output = s3_file_manager.get_tags_on_file(file_name.clone());
let tags_with_categories: Vec<Tag> = tag_req_output.into_iter()
.filter(|tag| tag.key == "tags")
.collect();
if tags_with_categories.len() > 1 {
return Err(S3ObjectError::MultipleTagsWithSameName);
}
let tag_value = if tags_with_categories.len() == 0 {
"".to_string()
} else {
tags_with_categories[0].value.clone()
};
let presigned_url = s3_file_manager.get_presigned_url_for_file(
file_name.clone()
);
Ok(S3Object::new(
file_name,
e_tag,
tag_value,
presigned_url,
false,
))
})
.collect();
手冊(cè)中是這樣描述的:
在多數(shù)情況下,Rust需要你盡可能了解錯(cuò)誤,并且在編譯之前對(duì)其做出相應(yīng)的處理。這個(gè)需求使你的程序更加健壯,保證你在發(fā)布之前就可以發(fā)現(xiàn)并處理其中的錯(cuò)誤。
要點(diǎn)和教訓(xùn)
- John Carmack曾經(jīng)將編寫(xiě)Rust的經(jīng)歷描述為“非常有益”。我同意這種感受,這次hackathon給我的感覺(jué)就像是打開(kāi)了一扇新世界的大門并且發(fā)現(xiàn)了很多新鮮事物,這些收獲絕不僅僅是停留在代碼層面的。
- 事后看來(lái),我應(yīng)該更加嚴(yán)謹(jǐn)?shù)倪x擇網(wǎng)絡(luò)框架的。再多想一下的話,我可能會(huì)走出一條不同的道路。我下次可能會(huì)選擇iron、actix-web, 或者是 tiny-http。
- 我只學(xué)到了Rust的皮毛,16個(gè)小時(shí)是不可能完全成為一名Rustacean的,即使我對(duì)這門語(yǔ)言充滿了好奇心,也做了一些深入的了解。我對(duì)Rust的未來(lái)感到興奮,我認(rèn)為它為構(gòu)建應(yīng)用程序帶來(lái)了很多規(guī)范,它是一種表現(xiàn)力非常豐富的語(yǔ)言,并且能為我們提供與C++性能相當(dāng)?shù)倪\(yùn)行速度和內(nèi)存性能呢。
資源
原文鏈接
https://medium.com/better-programming/learning-to-use-rust-over-a-16-hour-hackathon-5f0ac2f604df