Мы приняли участие в хакатоне, организованном Prodigy Education в последние два дня 2019 года, где собрались вместе многие команды и усердно работали, чтобы воплотить свои идеи в жизнь.
Некоторые из нас просто хотят развлечься, некоторые хотят узнать что-то новое, а некоторые из нас могут просто захотеть доказать какую-то концепцию или идею.
Последние несколько недель я пассивно собирал информацию о Rust или работал с кодом на Rust, поэтому я подумал, что хакатоны — отличное время для изучения Rust.
Нехватка времени на хакатоне позволяет мне учиться быстрее и одновременно решать реальные проблемы.
Почему ржавчина
В первые 10 лет своей карьеры я использовал C и C++ в течение 8 лет.
С другой стороны, мне нравятся такие языки, как C++, которые обеспечивают статическую типизацию, потому что они обнаруживают ошибки на ранней стадии компиляции.
Вот некоторые из моих личных мнений о C++:
- Инженеры могут запросто выстрелить себе в ногу
- Как язык программирования он уже очень раздутый и сложный
- Отсутствие хорошей, стандартной, широко применимой системы управления пакетами
С тех пор, как я переключился на веб-приложения, я занимаюсь разработкой на Python и JavaScript, используя такие фреймворки, как Django, Flask и Express.
Мой опыт разработки на Python и JavaScript до сих пор показывает, что они могут обеспечить хорошую итерацию программы и скорость доставки, но иногда могут занимать много ресурсов ЦП и памяти, даже когда служба относительно простаивает.
Я часто ловлю себя на том, что пишу программы на C++, которым не хватает безопасности, скорости и простоты.
Я ищу минимальный язык программирования на чистом железе, такой как Rust, для разработки веб-приложений.
Нет времени выполнения, нет сборки мусора. Загрузите двоичный код напрямую и передайте его ядру для выполнения.
Цель
Моя цель — завершить приложение, в котором бэкенд написан на Rust, а фронтенд — JavaScript+React, по аналогии с S3 в качестве графа.Пользователи могут сделать следующее:
- Просмотрите все изображения в картинной кроватке (нумерация страниц не обязательна)
- загрузить изображение
- Вы можете добавлять теги к изображениям при загрузке изображений
- Запрос или фильтр по имени
У всех интересных проектов хакатона есть название, поэтому я решил назвать этот:
RustIC -> Rust + Image Contents
Я считаю этот хакатон успешным лично для меня, если я сделаю следующее:
- Иметь базовое представление о Rust, включая его систему типов и модель памяти.
- Изучите возможности предварительно подписанных ссылок S3 для файлов и произвольных тегов
- Напишите проверяемое функциональное приложение
Так как моя основная цель — развивать фичи с учетом обучения. Много кода я написал, пока учился, поэтому организация и эффективность кода могут быть не оптимальными, поскольку это второстепенные цели.
Принципы ржавчины
Прежде чем я начал, мне было любопытно посмотреть, что имели в виду разработчики языка для изучения, когда создавали язык. я нашел одинупрощенная версияс однимПодробная версия.
Вопреки тому, что я читал во многих блогах, в Rust есть вероятность утечек памяти (циклические ссылки) и небезопасных операций (в небезопасных блоках), как подробно описано в FAQ выше.
«Мы [создатели языка] не собираемся [для Rust] быть на 100% статичным, на 100% безопасным, на 100% отражающим».
Начните с бэкенда
Поиск в Google по запросу "Rust web framework" далRocket. Я зашел на этот сайт и обнаружил, что все примеры документации понятны с первого взгляда.
Следует отметить, что для Rocket требуется ночная версия Rust, но это незначительная проблема на хакатоне.
ГитхабБиблиотека кодаЕсть очень богатые примеры. Идеально!
я используюCargoСоздал новый проект, добавил зависимости Rocket в файл TOML и следовал указаниям Rocket.Руководство по началу работы, написал первый фрагмент кода:
#[get("/")]
fn index() -> &'static str {
"Hello, world!"
}
fn main() {
rocket::ignite().mount("/", routes![index]).launch();
}
Для студентов, знакомых с такими фреймворками, как Django, Flask, Express и т. д., этот код очень легко читается. Как пользователь Rocket, вы можете использовать макросы в качестве декораторов для сопоставления маршрутов с соответствующими обработчиками.
Во время компиляции макрос будет развернут. Это полностью прозрачно для разработчика. Если вы хотите увидеть расширенный код, вы можете использоватьcargo-expand.
Вот некоторые интересные или сложные моменты моей работы по созданию приложений на Rust:
Укажите ответ маршрута
Я хочу вернуть список всех файлов в S3 в формате данных JSON.
Вы можете видеть, что код обработчика, связанного с маршрутом, определяет тип ответа.
Настроить структуру ответа очень легко, если вы хотите вернуть данные в формате JSON, и каждое поле имеет свою структуру и тип, он соответствует Rust'овскомуstruct
.
Итак, вы должны сначала определить структуруstruct(S)
чтобы принять ответ и должен быть аннотирован:
#[derive(Serialize)]
структуры отмечены#[derive(Serialize)]
, поэтому его можно преобразовать в JSON с помощью `rocket_contrib::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 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
}
Обработка многокомпонентных загрузок
Когда я понял, что есть большая вероятность, что мой интерфейс использует метод POST для загрузки формата какmultipart/form-data
form data, я начал копаться в том, как использовать Rocket для создания программ.
К сожалению, Rocket 0.4 не поддерживает multipart, похоже будет в 0.5.
Это означает, что мне нужно использоватьmultipartящик и интегрировать в 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, и теперь пришло время заняться интеграцией фронтенда. Мне нужно правильно установить заголовки ответов, чтобы избежать междоменных проблем.
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,7 МБ памяти.
И это просто не оптимизированные отладочные сборки. Использование проекта- release
Если тег упакован, во время выполнения требуется только 1,6 МБ памяти.
Бэкенд-сервер на основе 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 и управления приложениями просто потрясающий
- Компилятор очень помогает нам бороться с ошибками компиляции, блогер вблогОписывает, как он писал код в соответствии с большими рекомендациями компилятора. Мой опыт очень похож.
- Я был приятно удивлен, что для каждой функции, которая мне нужна, есть ящик.
- онлайнRust Playground, что позволяет мне запускать небольшие фрагменты кода.
- Языковой сервер Rust, хорошо интегрированный в Visual Studio Code, обеспечивает проверку ошибок в реальном времени, форматирование, поиск символов и многое другое. Это позволило мне добиться приличного прогресса за несколько часов без компиляции.
Неудобства, неожиданности и неприятности
Хотя документация Rust великолепна, мне пришлось полагаться на документацию и примеры некоторых ящиков. В некоторых ящиках есть отличные интеграционные тесты, которые дают некоторые подсказки о том, как их использовать. Конечно, мне очень помогли Stack Overflow и Reddit.
Также обратите внимание, что:
- Понимание прав собственности, жизненного цикла и заимствования прав собственности может усложнить обучение, особенно при попытке реализовать функциональность во время двухдневного хакатона. Я сравниваю их с C++ и разбираюсь, но иногда все равно путаюсь.
- Во всем,
Strings
остановил меня на несколько минут, особенноString
а также&str
Разница еще более запутанная - я не знал, пока я не потратил некоторое время на понимание владения, жизни и заимствования права собственности.
некоторые другие наблюдения
- В Rust нет настоящего нулевого типа, обычно нулевые значения нужно использовать с
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 требует, чтобы вы как можно лучше понимали ошибки и соответствующим образом обрабатывали их перед компиляцией. Это требование делает вашу программу более надежной, гарантируя, что вы сможете отловить и обработать ошибки в ней до того, как она будет выпущена.
Очки и уроки
- Джон Кармак однажды назвал опыт написания Rust «очень полезным». Я согласен с этим чувством, этот хакатон ощущается как открытие двери в новый мир и открытие множества новых вещей, и эти достижения не только на уровне кода.
- Оглядываясь назад, я должен был быть более строгим в выборе веб-фреймворка. Если подумать, я мог бы пойти другим путем. Я мог бы выбрать в следующий разiron,actix-web, илиtiny-http.
- Я только поверхностно познакомился с Rust, за 16 часов полностью стать рустацем невозможно, хотя мне любопытен язык, и я получил кое-какие глубокие знания. Я воодушевлен будущим Rust, я думаю, что он дает много спецификаций для создания приложений, это очень выразительный язык, и он дает нам скорость работы и производительность памяти, сравнимую с производительностью C++.