【译】教你用16个小时从0构建一个Rust应用
我们在2019年的最后两天,参加了Prodigy Education举办的黑客马拉松,许多团队聚在一起努力将他们的想法变成现实。
我们之中有的人只是单纯为了好玩,有的是想学一些新的知识,还有些人可能是想证明一些概念或想法。
我在过去几周总是被动的获取Rust相关信息或使用Rust的代码,因此我认为hackathon是一次学习Rust的绝佳时机。
hackathon的时间紧迫性使我更加快速的去学习,同时也会去解决现实世界的一些问题。
为什么是Rust
在我职业生涯的前10年中,有8年都在使用C和C++。
从好的方面来讲,我喜欢像C++这样可以提供静态类型的语言,因为它能在编译期就能够早早的发现错误。
我个人对于C++的一些看法是:
- 工程师很容易搬起石头砸自己的脚
- 作为一门编程语言,它已经非常臃肿且复杂
- 缺乏良好的、标准的广泛适用的包管理系统
自从我改做Web应用以来,一直是做Python和JavaScript开发,使用像Django、Flask和Express这样的框架。
到目前为止,我在Python和JavaScript中的开发经验是,它们可以提供良好的程序迭代和交付速度,但有时会占用大量的CPU和内存,即使服务是相对空闲的。
我经常发现自己写好的C++程序,会缺失一些安全性、速度和精简性。
我想要寻找一种像Rust这样精简的、裸机编程语言来开发web应用。
没有运行时,没有垃圾回收。直接加载二进制代码,交给内核执行。
目标
我的目标是完成一个后端由Rust编写,前端是JavaScript+React完成的类似于S3作为图床的应用程序,用户可以做以下事情:
- 浏览图床中所有的图片(分页可选)
- 上传图片
- 上传图片时可以给图片增加标签
- 通过名称进行查询或过滤
所有有趣的hackathon项目都有一个名字,所以我决定将这个项目命名为:
RustIC -> Rust + Image Contents
我认为如果我做到了以下这些事情,那么这次hackathon之行对我个人来说就是成功的:
- 对Rust有一个基本的理解,包括它的类型系统和内存模型
- 探索S3的对于文件和任意标签的预签名链接功能
- 写出一个可以验证的功能正常的应用
由于我的主要目标是开发功能,同时兼顾学习。很多代码是我一边学一边写的,所以代码组织和效率可能并不是最理想的,因为这些属于次要目标。
Rust的原则
在我开始之前,我带着好奇心去了解了要学习的语言的设计师在创建这门语言时内心的原则是什么。我找到了一个简化版本和一个详细版本。
与我在许多博客上读到的内容相反,Rust是有可能发生内存泄露(循环引用)和之行不安全的操作(unsafe代码块中)的,详细描述在上面的FAQ中。
“We [the language creators] do not intend [for Rust] to be 100% static, 100% safe, 100% reflective.”
从后端开始
Google搜索“Rust web framework“,排在最前面的是Rocket。我进入这个网站,发现文档的示例都一目了然。
有一点需要注意的是Rocket需要Rust的nightly版本,不过在hackathon上这都是小问题。
GitHub的代码库中有着非常丰富的例子。完美!
我使用Cargo创建了一个新的项目,在TOML文件中加入了Rocket依赖,然后跟着Rocket的入门指南,写了第一段代码:
#[get("/")]
fn index() -> &'static str {"Hello, world!"
}fn main() {rocket::ignite().mount("/", routes![index]).launch();
}
对于熟悉Django、Flask、Express等框架等同学来说,这段代码读起来非常容易。作为一名Rocket用户,你可以使用宏作为装饰器来将路由映射到对应的处理函数上。
在编译时,宏将被扩展。这对开发者是完全透明的。如果你想看扩展后的代码,可以使用cargo-expand。
以下是我在构建Rust应用程序时的一些有趣的或者有挑战性的亮点:
指定路由响应
我想要以JSON的数据格式返回S3中所有的文件列表。
你可以看到路由关联的处理函数的代码决定了响应类型。
设置响应结构非常容易,如果你想要返回JSON格式的数据,并且每个字段都有自己的结构和类型,那对应的就是Rust的struct
。
所以你应该先定义一个结构体struct(S)
来接受响应,并且需要进行标注:
#[derive(Serialize)]
struct(s)被标记了#[derive(Serialize)]
,因此可以通过rocket_contrib::json::Json将它转换成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 objectis_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
}
处理分段上传
当我意识到我的前端很有可能使用POST方法上传格式为multipart/form-data
的表单数据时,我就开始深入研究如何使用Rocket来构建程序了。
不幸的是,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 necessaryif !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.rslet 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来进行测试了,现在已经是时候开始把前端集成进来了。我需要适当设置响应头以避免跨域问题。
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();
}
过了一会,我发现了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
命令就可以让程序运行起来
我机器上的活动监视器告诉我这个程序正在运行中,并且只消耗了2.7MB内存。
而且这还只是没有经过优化的调试版本。项目使用- release
标签打包的话,运行时只需要1.6MB内存。
基于Rust的后端服务器,我们请求/contents
这个路由会得到如下响应:
{"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
用户可以在我们的页面浏览图片,也可以通过文件名或标签来进行检索或过滤。
用户还可以通过拖拽来上传文件,并且可以在提交上传之前打上标签。
我喜欢使用Rust构建应用程序的原因
- Cargo对于依赖和应用管理的程度简直令人惊叹
- 编译器对于我们处理编译错误帮助非常大,有位博主在博客中描述了他是如何按照编译器大指导来写代码的。我的经验也比较类似。
- 我需要的每一项功能都有crate,这让我感到非常惊喜
- 在线的Rust Playground,让我可以运行小的代码片段。
- Rust语言服务器,已经很好的集成到了Visual Studio Code,它能够提供实时错误检查、格式设置、符号查找等。这让我可以在几个小时内不编译就能取得不错的进展。
不便、惊喜和麻烦
尽管Rust的文档很棒,但我不得不依赖一些crates的文档和例子。有些crates有很棒的集成测试,提供了一些关于如何使用的提示。当然了,Stack Overflow和Reddit也给我提供了很多帮助。
另外还要注意的是:
- 理解所有权、生命周期和所有权借用会使学习难度陡增,特别是在为期两天的黑客马拉松中努力提供功能时。我将它们与C++做比较并且弄清楚,但有时还是会感到困惑。
- 在所有的事情中,
Strings
拦住了我几分钟,特别是String
和&str
的区别更是令人困惑——直到我花了些时间来理解所有权、生命周期和所有权借用才搞清楚这些。
其他的一些观察
- Rust中没有真正意义上的null类型,通常情况下,空值需要用
Option
类型的None
来表示 - 模式匹配非常棒,这是我在Scala中最喜欢的一个特性,在Rust中也一样。这种代码看起来表现力很强,并且允许编译器标记未处理的情况。
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)))}
}
- 说起安全和不安全模式,你仍然可以进行更底层的编程,比如说在不安全的模式下可以和C语言代码通过接口交互。尽管Rust中有很多正确性检查,但你仍然可以在不安全模块中做一些骚操作,例如解引用。读代码的人也可以从不安全模块中获取到很多信息。
- 通过
Box
在堆中分配内存空间,而不是new
和delete
。刚开始感觉比较奇怪,但是也很容易理解。标准库中还定义了其他的一些智能指针,如果你需要使用引用数量或者弱引用时就可以直接使用。 - Rust中的异常也很有趣,因为它没有异常。你可以选择使用
Result<T, E>
表示可以恢复的错误,也可以用panic!
宏表示不可恢复的错误。
// 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();
手册中是这样描述的:
在多数情况下,Rust需要你尽可能了解错误,并且在编译之前对其做出相应的处理。这个需求使你的程序更加健壮,保证你在发布之前就可以发现并处理其中的错误。
要点和教训
- John Carmack曾经将编写Rust的经历描述为“非常有益”。我同意这种感受,这次hackathon给我的感觉就像是打开了一扇新世界的大门并且发现了很多新鲜事物,这些收获绝不仅仅是停留在代码层面的。
- 事后看来,我应该更加严谨的选择网络框架的。再多想一下的话,我可能会走出一条不同的道路。我下次可能会选择iron、actix-web, 或者是 tiny-http。
- 我只学到了Rust的皮毛,16个小时是不可能完全成为一名Rustacean的,即使我对这门语言充满了好奇心,也做了一些深入的了解。我对Rust的未来感到兴奋,我认为它为构建应用程序带来了很多规范,它是一种表现力非常丰富的语言,并且能为我们提供与C++性能相当的运行速度和内存性能呢。
资源
RustIC后端代码
RustIC前端代码
Rusoto:一个Rust的AWS SDK
原文链接
https://medium.com/better-programming/learning-to-use-rust-over-a-16-hour-hackathon-5f0ac2f604df
【译】教你用16个小时从0构建一个Rust应用相关推荐
- iOS 教你使用MP、AVPlayer、AVPlayerVC构建一个完整的视频播放器
1.前言 标题必须要浮夸!要感觉像是一个大新闻.长者如是说. 其实是前几天去面试的时候,被要求说必须做过视频播放相关项目.有点闹心之余,就花了点时间在家写了一个简单播放器,基本实现了主流播放器的大致功 ...
- python实现目标检测_由浅入深:教你如何构建一个行人目标检测模型(Python)
摘要: 零基础也能构建一个目标检测(对象检测)模型?小编教你使用Image AI库从头开始构建一个行人检测模型. 当我们看到一张图片时,我们的大脑会立即识别图片中所包含的物体.而对于机器来说,识别这些 ...
- 清明出游,在高速上堵了16个小时。
清明节3天假期结束了,你们都干什么了?我出去转了一下. 但是这个清明节,我太累了.因为清明节3天假期,我有两天都在高速上..堵着.... 清明节,听说江西婺源的油菜花不错,就带着家人想着从杭州开车过去 ...
- 每天工作16个小时,如果辞职,会不会显得自己没责任心呢?
什么样的工作能每天工作16个小时?那估计上班时间都是闭着眼睛在做事了吧?就算是996工资,也不可能工作满16个小时的.这种工作不辞职还等什么?难道你工资月薪两三万以上吗?如果你工资非常高,那当我没说, ...
- 刘强东母校演讲:每天工作16个小时,不断学习才能进步
今日(10月28日),京东集团董事局主席兼CEO刘强东,携妻子章泽天一起回到了家乡江苏宿迁,作为校友代表参加了母校宿迁中学的九十周年校庆,并发表了演讲. 创建于1927年的宿迁中学是江苏省重点高中,1 ...
- 16 张图教你如何从 0 到 1 构建一个稳定、高性能的 Redis 集群!
作者 | Magic Kaito 责编 | 张文 来源 | 水滴与银弹(ID:waterdrop_bullet) 这篇文章我想和你聊一聊 Redis 的架构演化之路. 现如今 Redis 变得越来 ...
- 搭建表白墙 公众号_韶大表白墙 第十季 第75期 | 篮球共青杯决赛体教1班穿黑色衣服的0号...
在表白墙给你写一句话 成了我每天睡前的习惯 2020.10.30 | 天气晴 晚安 ❤表白 1表白图委会主席团的何季香师姐,你好温柔,我好喜欢你,喜欢你这件事我想了九九八十一遍都不嫌烦 有缘可以留下 ...
- 实践操作:六步教你如何用开源框架Tensorflow对象检测API构建一个玩具检测器
TensorFlow对象检测API是一个建立在TensorFlow之上的开源框架,可以轻松构建,训练和部署对象检测模型. 到目前为止,API的性能给我留下了深刻的印象.在这篇文章中,我将API的对象设 ...
- 学校老师绝对不会教的方法,让你的孩子拥有一个开挂般的人生!
比勤奋更重要的,是孩子的思维能力. 从上幼儿园开始,很多父母很喜欢给孩子报各种兴趣班,比如钢琴班.英语班.乐高班.报各种各样的课程,就是希望孩子具有18般武艺,赢在起跑线上. 其实除了外在的能力,不显 ...
最新文章
- Spring AOP 面向切面编程相关注解
- VMware虚拟网络相关知识
- 白话Elasticsearch41-深入聚合数据分析之案例实战__过滤+聚合:统计价格大于2000的电视平均价格
- vue-cli-plugin-element的使用
- GPU 共享内存bank冲突(shared memory bank conflicts)
- java中md5怎么实现的_java中md5算法的实现
- python字典的值可以是字典吗_python字典的值可以是字典吗
- 小米自然语言处理工程师招聘条件与自己的对应整理
- 不同公式等号对齐_数学公式编辑器“制作amp;套用公式模板”和“文本对齐问题”的完美解决方案...
- Docker结合Consul实现的服务发现(一)
- [网络安全提高篇] 一一三.Powershell恶意代码检测 (1)论文总结及抽象语法树(AST)提取
- 大叔配萝莉/正太的电影一般都不会差。。。
- 中南大学计算机学院复试差额录取比例,中南大学在职研究生复试差额比例大概是多少...
- Android 仿微信发送坐标,Android最新版高德地圖poi檢索仿微信發送位置
- 目前主流的移动广告联盟有哪些呢?
- 经典重写alert方法
- (3)数仓建设-数据仓库设计方案
- 网站制作教程:新手如何用PageAdmin建站系统做网站
- 硬件工程师成长之路(5)——板子调试
- A Lightened CNN for Deep Face Representation读后感