我們在2019年的最后兩天,參加了Prodigy Education舉辦的黑客馬拉松,許多團隊聚在一起努力將他們的想法變成現(xiàn)實砰粹。
我們之中有的人只是單純?yōu)榱撕猛妫械氖窍雽W(xué)一些新的知識牛隅,還有些人可能是想證明一些概念或想法。
我在過去幾周總是被動的獲取Rust相關(guān)信息或使用Rust的代碼,因此我認(rèn)為hackathon是一次學(xué)習(xí)Rust的絕佳時機。
hackathon的時間緊迫性使我更加快速的去學(xué)習(xí)种玛,同時也會去解決現(xiàn)實世界的一些問題。
為什么是Rust
在我職業(yè)生涯的前10年中瓤檐,有8年都在使用C和C++赂韵。
從好的方面來講,我喜歡像C++這樣可以提供靜態(tài)類型的語言距帅,因為它能在編譯期就能夠早早的發(fā)現(xiàn)錯誤右锨。
我個人對于C++的一些看法是:
- 工程師很容易搬起石頭砸自己的腳
- 作為一門編程語言括堤,它已經(jīng)非常臃腫且復(fù)雜
- 缺乏良好的碌秸、標(biāo)準(zhǔn)的廣泛適用的包管理系統(tǒng)
自從我改做Web應(yīng)用以來绍移,一直是做Python和JavaScript開發(fā),使用像Django讥电、Flask和Express這樣的框架蹂窖。
到目前為止,我在Python和JavaScript中的開發(fā)經(jīng)驗是恩敌,它們可以提供良好的程序迭代和交付速度瞬测,但有時會占用大量的CPU和內(nèi)存,即使服務(wù)是相對空閑的纠炮。
我經(jīng)常發(fā)現(xiàn)自己寫好的C++程序月趟,會缺失一些安全性、速度和精簡性恢口。
我想要尋找一種像Rust這樣精簡的孝宗、裸機編程語言來開發(fā)web應(yīng)用。
沒有運行時耕肩,沒有垃圾回收因妇。直接加載二進(jìn)制代碼,交給內(nèi)核執(zhí)行猿诸。
目標(biāo)
我的目標(biāo)是完成一個后端由Rust編寫婚被,前端是JavaScript+React完成的類似于S3作為圖床的應(yīng)用程序,用戶可以做以下事情:
- 瀏覽圖床中所有的圖片(分頁可選)
- 上傳圖片
- 上傳圖片時可以給圖片增加標(biāo)簽
- 通過名稱進(jìn)行查詢或過濾
所有有趣的hackathon項目都有一個名字梳虽,所以我決定將這個項目命名為:
RustIC -> Rust + Image Contents
我認(rèn)為如果我做到了以下這些事情址芯,那么這次hackathon之行對我個人來說就是成功的:
- 對Rust有一個基本的理解,包括它的類型系統(tǒng)和內(nèi)存模型
- 探索S3的對于文件和任意標(biāo)簽的預(yù)簽名鏈接功能
- 寫出一個可以驗證的功能正常的應(yīng)用
由于我的主要目標(biāo)是開發(fā)功能窜觉,同時兼顧學(xué)習(xí)是复。很多代碼是我一邊學(xué)一邊寫的,所以代碼組織和效率可能并不是最理想的竖螃,因為這些屬于次要目標(biāo)淑廊。
Rust的原則
在我開始之前,我?guī)е闷嫘娜チ私饬艘獙W(xué)習(xí)的語言的設(shè)計師在創(chuàng)建這門語言時內(nèi)心的原則是什么特咆。我找到了一個簡化版本和一個詳細(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.”
從后端開始
Google搜索“Rust web framework“,排在最前面的是Rocket菜职。我進(jìn)入這個網(wǎng)站青抛,發(fā)現(xiàn)文檔的示例都一目了然。
有一點需要注意的是Rocket需要Rust的nightly版本酬核,不過在hackathon上這都是小問題蜜另。
GitHub的代碼庫中有著非常豐富的例子适室。完美!
我使用Cargo創(chuàng)建了一個新的項目举瑰,在TOML文件中加入了Rocket依賴捣辆,然后跟著Rocket的入門指南,寫了第一段代碼:
#[get("/")]
fn index() -> &'static str {
"Hello, world!"
}
fn main() {
rocket::ignite().mount("/", routes![index]).launch();
}
對于熟悉Django此迅、Flask汽畴、Express等框架等同學(xué)來說,這段代碼讀起來非常容易耸序。作為一名Rocket用戶忍些,你可以使用宏作為裝飾器來將路由映射到對應(yīng)的處理函數(shù)上。
在編譯時坎怪,宏將被擴展坐昙。這對開發(fā)者是完全透明的。如果你想看擴展后的代碼芋忿,可以使用cargo-expand炸客。
以下是我在構(gòu)建Rust應(yīng)用程序時的一些有趣的或者有挑戰(zhàn)性的亮點:
指定路由響應(yīng)
我想要以JSON的數(shù)據(jù)格式返回S3中所有的文件列表。
你可以看到路由關(guān)聯(lián)的處理函數(shù)的代碼決定了響應(yīng)類型戈钢。
設(shè)置響應(yīng)結(jié)構(gòu)非常容易痹仙,如果你想要返回JSON格式的數(shù)據(jù),并且每個字段都有自己的結(jié)構(gòu)和類型殉了,那對應(yīng)的就是Rust的struct
开仰。
所以你應(yīng)該先定義一個結(jié)構(gòu)體struct(S)
來接受響應(yīng),并且需要進(jìn)行標(biāo)注:
#[derive(Serialize)]
struct(s)被標(biāo)記了#[derive(Serialize)]
薪铜,因此可以通過`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)我意識到我的前端很有可能使用POST方法上傳格式為multipart/form-data
的表單數(shù)據(jù)時,我就開始深入研究如何使用Rocket來構(gòu)建程序了隔箍。
不幸的是谓娃,Rocket0.4版本不支持multipart,看起來在0.5版本會支持蜒滩。
這意味著我需要使用multipart crate并集成到Rocket中滨达。最終代碼可以正常運行,但是如果Rocket支持multipart將會使代碼更加簡潔俯艰。
#[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
路由寫好了以后捡遍,我就開始用curl或Postman來進(jìn)行測試了,現(xiàn)在已經(jīng)是時候開始把前端集成進(jìn)來了竹握。我需要適當(dāng)設(shè)置響應(yīng)頭以避免跨域問題画株。
Rocket依舊沒有支持這個特性。
然后我在GitHub代碼庫中找到了一些解決方案:
// 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();
}
過了一會,我發(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(())
}
運行起來
我們只需要一個簡單的cargo run
命令就可以讓程序運行起來
我機器上的活動監(jiān)視器告訴我這個程序正在運行中,并且只消耗了2.7MB內(nèi)存良拼。
而且這還只是沒有經(jīng)過優(yōu)化的調(diào)試版本。項目使用- release
標(biāo)簽打包的話充边,運行時只需要1.6MB內(nèi)存庸推。
基于Rust的后端服務(wù)器,我們請求/contents
這個路由會得到如下響應(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
}
]
}
前端的工作相對簡單一些浇冰,我們使用的是:
- React
- React Bootstrap
- react-grid-gallery
- react-tags-input
用戶可以在我們的頁面瀏覽圖片贬媒,也可以通過文件名或標(biāo)簽來進(jìn)行檢索或過濾。
用戶還可以通過拖拽來上傳文件肘习,并且可以在提交上傳之前打上標(biāo)簽际乘。
我喜歡使用Rust構(gòu)建應(yīng)用程序的原因
- Cargo對于依賴和應(yīng)用管理的程度簡直令人驚嘆
- 編譯器對于我們處理編譯錯誤幫助非常大,有位博主在博客中描述了他是如何按照編譯器大指導(dǎo)來寫代碼的漂佩。我的經(jīng)驗也比較類似脖含。
- 我需要的每一項功能都有crate,這讓我感到非常驚喜
- 在線的Rust Playground投蝉,讓我可以運行小的代碼片段养葵。
- Rust語言服務(wù)器,已經(jīng)很好的集成到了Visual Studio Code瘩缆,它能夠提供實時錯誤檢查关拒、格式設(shè)置、符號查找等庸娱。這讓我可以在幾個小時內(nèi)不編譯就能取得不錯的進(jìn)展着绊。
不便、驚喜和麻煩
盡管Rust的文檔很棒熟尉,但我不得不依賴一些crates的文檔和例子归露。有些crates有很棒的集成測試,提供了一些關(guān)于如何使用的提示斤儿。當(dāng)然了靶擦,Stack Overflow和Reddit也給我提供了很多幫助。
另外還要注意的是:
- 理解所有權(quán)雇毫、生命周期和所有權(quán)借用會使學(xué)習(xí)難度陡增玄捕,特別是在為期兩天的黑客馬拉松中努力提供功能時。我將它們與C++做比較并且弄清楚棚放,但有時還是會感到困惑枚粘。
- 在所有的事情中,
Strings
攔住了我?guī)追昼娖牵貏e是String
和&str
的區(qū)別更是令人困惑——直到我花了些時間來理解所有權(quán)馍迄、生命周期和所有權(quán)借用才搞清楚這些福也。
其他的一些觀察
- Rust中沒有真正意義上的null類型,通常情況下攀圈,空值需要用
Option
類型的None
來表示 - 模式匹配非常棒暴凑,這是我在Scala中最喜歡的一個特性,在Rust中也一樣赘来。這種代碼看起來表現(xiàn)力很強现喳,并且允許編譯器標(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)))
}
}
- 說起安全和不安全模式犬辰,你仍然可以進(jìn)行更底層的編程嗦篱,比如說在不安全的模式下可以和C語言代碼通過接口交互。盡管Rust中有很多正確性檢查幌缝,但你仍然可以在不安全模塊中做一些騷操作灸促,例如解引用。讀代碼的人也可以從不安全模塊中獲取到很多信息涵卵。
- 通過
Box
在堆中分配內(nèi)存空間浴栽,而不是new
和delete
。剛開始感覺比較奇怪轿偎,但是也很容易理解吃度。標(biāo)準(zhǔn)庫中還定義了其他的一些智能指針,如果你需要使用引用數(shù)量或者弱引用時就可以直接使用贴硫。 - Rust中的異常也很有趣椿每,因為它沒有異常。你可以選擇使用
Result<T, E>
表示可以恢復(fù)的錯誤英遭,也可以用panic!
宏表示不可恢復(fù)的錯誤间护。
// 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();
手冊中是這樣描述的:
在多數(shù)情況下,Rust需要你盡可能了解錯誤挖诸,并且在編譯之前對其做出相應(yīng)的處理汁尺。這個需求使你的程序更加健壯,保證你在發(fā)布之前就可以發(fā)現(xiàn)并處理其中的錯誤多律。
要點和教訓(xùn)
- John Carmack曾經(jīng)將編寫Rust的經(jīng)歷描述為“非常有益”痴突。我同意這種感受,這次hackathon給我的感覺就像是打開了一扇新世界的大門并且發(fā)現(xiàn)了很多新鮮事物狼荞,這些收獲絕不僅僅是停留在代碼層面的辽装。
- 事后看來,我應(yīng)該更加嚴(yán)謹(jǐn)?shù)倪x擇網(wǎng)絡(luò)框架的相味。再多想一下的話拾积,我可能會走出一條不同的道路。我下次可能會選擇iron、actix-web, 或者是 tiny-http拓巧。
- 我只學(xué)到了Rust的皮毛斯碌,16個小時是不可能完全成為一名Rustacean的,即使我對這門語言充滿了好奇心肛度,也做了一些深入的了解傻唾。我對Rust的未來感到興奮,我認(rèn)為它為構(gòu)建應(yīng)用程序帶來了很多規(guī)范承耿,它是一種表現(xiàn)力非常豐富的語言冠骄,并且能為我們提供與C++性能相當(dāng)?shù)倪\行速度和內(nèi)存性能呢。
資源
原文鏈接
https://medium.com/better-programming/learning-to-use-rust-over-a-16-hour-hackathon-5f0ac2f604df