- Оригинальный адрес:Writing a Microservice in Rust
- Оригинальный автор:Peter Goldsborough
- Перевод с:Программа перевода самородков
- Постоянная ссылка на эту статью:GitHub.com/rare earth/gold-no…
- Переводчик:nettee
- Корректор:HearFishle, shixi-li
Разрешите мне написать этоНаписание микросервиса на RustВ начале статьи я расскажу о двух предложениях о C++. Я был довольно активным членом сообщества C++ в течение достаточно долгого времени. Я прихожу на встречу ивнес речь, следил за развитием и распространением более современных возможностей языка, ну и конечно было написано много кода. C++ дает пользователю очень детальный контроль над всеми аспектами программы при написании кода, но за счет крутой кривой обучения и затрат на написание эффективного кода C++.много знаний. Однако C++ также является очень старым языком. Он был задуман Бьерном Страуструпом в 1985 году. Таким образом, он несет в себе большой исторический багаж даже по современным меркам. Конечно, после создания C++ исследования по проектированию языков продолжались, что привело к некоторымGo,Rust,CrystalИ так много интересных новых языков рождается. Однако немногие из этих новых языков обладают более интересными функциями, чем современный C++,в то же времяГарантированно имеет ту же производительность и контроль над памятью и оборудованием, что и C++. Go хочет заменить C++, но посколькуКак обнаружил Роб Пайк, программисты на C++ не очень заинтересованы в языке, который менее производительен и предлагает меньше контроля. Однако Rust привлек многих энтузиастов C++. Rust и C++ имеют много общих целей проектирования, таких какАбстракция с нулевой стоимостью, и детальный контроль над памятью. Кроме того, Rust добавляет множество языковых функций, которые делают программы более безопасными, выразительными и продуктивными. Что меня больше всего интересует в Rust, так это
-
одолжить чек, что значительно повышает безопасность памяти (больше не
SEGFAULT! ); - Неизменяемость по умолчанию (
const); - Интуитивный синтаксический сахар, такой как сопоставление с образцом;
- Встроенных (арифметических) неявных преобразований между типами нет.
Чат окончен. Оставшаяся часть этой статьи поможет вам создать небольшой, но полныйМикросервисы- похоже на то, что я написал для своего блогаСокращатель URL. я сказалМикросервисыОтносится к приложению, которое использует HTTP, принимает запросы, обращается к базе данных, возвращает ответ (возможно, доставляющий HTML), упаковано в контейнер Docker и может быть размещено где-нибудь в облаке. В этой статье я создам простое приложение для чата, которое позволит вам хранить и получать сообщения. По пути я представлю несколько связанных ящиков. Ты сможешьна GitHubНайдите полный код службы.
использовать HTTP
Первое, что нам нужно сделать, чтобы наш веб-сервис делал то, какИспользуйте HTTP-протокол, то есть нашему приложению (серверу) необходимо получать и анализировать HTTP-запросы и возвращать HTTP-ответы. Хотя есть много подобныхFlaskилиDjangoФреймворк высокого уровня может инкапсулировать все это, но мы по-прежнему предпочитаем использовать более низкий уровень.hyperбиблиотека для обработки HTTP. Эта библиотека использует сетевую библиотекуtokioиfutures, что позволяет нам создать чистый асинхронный веб-сервер. Кроме того, мы будем использоватьlogиenv-loggerДва крейта для реализации функции логирования.
Сначала мы устанавливаемCargo.toml, загрузите приведенный выше ящик:
[package]
name = "microservice_rs"
version = "0.1.0"
authors = ["you <you@email>"]
[dependencies]
env_logger = "0.5.3"
futures = "0.1.17"
hyper = "0.11.13"
log = "0.4.1"
Тогда есть фактический код. Гипер имеетServiceКонцепция чего-либо. это реализацияServiceтип признака, который имеетcallФункция, которая получает проанализированный HTTP-запросhyper::Requestобъект в качестве параметра. Для асинхронной службы эта функция должна возвращатьFuture. Ниже приведен базовый шаблон, который мы можем поместить непосредственно вmain.rsсередина:
extern crate hyper;
extern crate futures;
#[macro_use]
extern crate log;
extern crate env_logger;
use hyper::server::{Request, Response, Service};
use futures::future::Future;
struct Microservice;
impl Service for Microservice {
type Request = Request;
type Response = Response;
type Error = hyper::Error;
type Future = Box<Future<Item = Self::Response, Error = Self::Error>>;
fn call(&self, request: Request) -> Self::Future {
info!("Microservice received a request: {:?}", request);
Box::new(futures::future::ok(Response::new()))
}
}
Обратите внимание, что нам также необходимо объявить некоторые базовые типы для нашего сервиса. Мы упаковали будущий тип, потому чтоfutures::future::Futureсам по себе является просто чертой и не может использоваться в качестве возвращаемого значения функции. существуетcall()Внутри мы в настоящее время возвращаем самое простое допустимое значение, коробочное будущее, содержащее пустой ответ.
Для запуска сервера мы привязываем IP-адрес кhyper::server::Httpэкземпляр и вызовите егоrun()метод:
fn main() {
env_logger::init();
let address = "127.0.0.1:8080".parse().unwrap();
let server = hyper::server::Http::new()
.bind(&address, || Ok(Microservice {}))
.unwrap();
info!("Running microservice at {}", address);
server.run().unwrap();
}
С приведенным выше кодом гипер будет вlocalhost:8080Начните прослушивать HTTP-запросы, анализировать их и пересылать на нашMicroserviceсвоего рода. Обратите внимание, что каждый раз, когда приходит новый запрос, создается новый экземпляр. Теперь мы можем запустить сервер и отправить несколько запросов с помощью curl! Запускаем сервер в терминале:
$ RUST_LOG="microservice=debug" cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/microservice`
INFO 2018-01-21T23:35:05Z: microservice: Running microservice at 127.0.0.1:8080
Затем отправьте ему несколько запросов в другом терминале:
$ curl 'localhost:8080'
В первом терминале вы должны увидеть вывод, подобный следующему
$ RUST_LOG="microservice=debug" cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/microservice`
Running microservice at 127.0.0.1:8080
INFO 2018-01-21T23:35:05Z: microservice: Running microservice at 127.0.0.1:8080
INFO 2018-01-21T23:35:06Z: microservice: Microservice received a request: Request { method: Get, uri: "/", version: Http11, remote_addr: Some(V4(127.0.0.1:61667)), headers: {"Host": "localhost:8080", "User-Agent": "curl/7.54.0", "Accept": "*/*"} }
Да здравствует! У нас есть базовый сервер, написанный на Rust. Обратите внимание, что в приведенной выше команде я будуRUST_LOG="microservice=debug"добавленcargo runсередина. так какenv_loggerбудет искать эту конкретную переменную среды, и таким образом мы контролируем ее поведение. Эта переменная среды ("microservice=debug") указывает корневой модуль журнала, который мы хотим запустить, а вторая часть (=следующий раздел) указывает минимальный видимый уровень журнала. По умолчанию толькоerror!будет записано.
Теперь давайте заставим наш сервер что-то делать. Поскольку мы создаем чат-приложение, мы хотим обработать два типа запросов:POSTзапрос (с данными формы, содержащими имя пользователя и сообщение) иGETзапрос (с необязательным отфильтрованным по времениbeforeиafterпараметр).
перениматьPOSTпросить
Давайте начнем с этой части записи данных. Наш акцепт отправляется на корневой путь нашего сервиса ("/")изPOSTзапрос и ожидать, что запрошенные данные формы будут содержатьusernameиmessageполе. Затем эта информация передается в функцию и записывается в базу данных. Наконец, мы возвращаем ответ.
сначала перепишиcall()метод:
fn call(&self, request: Request) -> Self::Future {
match (request.method(), request.path()) {
(&Post, "/") => {
let future = request
.body()
.concat2()
.and_then(parse_form)
.and_then(write_to_db)
.then(make_post_response);
Box::new(future)
}
_ => Box::new(futures::future::ok(
Response::new().with_status(StatusCode::NotFound),
)),
}
}
Мы различаем разные запросы, сопоставляя их метод и путь. В нашем случае запрашиваемый метод будетPostилиGet. Единственным допустимым путем к нашему сервису является корневой путь"/". если метод&PostИ путь правильный, вызываем упомянутую ранее функцию. Обратите внимание, что мы можем элегантно использовать функции композиции для объединения фьючерсов. Комбинаторand_thenБудет вызывать функцию со значением, содержащимся в будущем, если будущее разрешается правильно (без ошибок). Эта вызываемая функция также должна возвращать новое будущее. Это позволяет нам передавать значения между несколькими этапами обработки вместона местеРассчитать значение. Наконец, мы используем комбинаторыthen, функция обратного вызова выполняется независимо от состояния будущего. Таким образом, он получитResult, а не значение.
Вот содержание функции, использованной выше:
struct NewMessage {
username: String,
message: String,
}
fn parse_form(form_chunk: Chunk) -> FutureResult<NewMessage, hyper::Error> {
futures::future::ok(NewMessage {
username: String::new(),
message: String::new(),
})
}
fn write_to_db(entry: NewMessage) -> FutureResult<i64, hyper::Error> {
futures::future::ok(0)
}
fn make_post_response(
result: Result<i64, hyper::Error>,
) -> FutureResult<hyper::Response, hyper::Error> {
futures::future::ok(Response::new().with_status(StatusCode::NotFound))
}
нашuseЗаявление также немного изменилось:
use hyper::{Chunk, StatusCode};
use hyper::Method::{Get, Post};
use hyper::server::{Request, Response, Service};
use futures::Stream;
use futures::future::{Future, FutureResult};
давайте посмотримparse_form. он получаетChunk(тело сообщения), из которого анализируются имя пользователя и сообщение, а ошибки обрабатываются соответствующим образом. Для разбора формы используемurlЭтот ящик (чтобы его скачать, вам нужно использовать Cargo):
use std::collections::HashMap;
use std::io;
fn parse_form(form_chunk: Chunk) -> FutureResult<NewMessage, hyper::Error> {
let mut form = url::form_urlencoded::parse(form_chunk.as_ref())
.into_owned()
.collect::<HashMap<String, String>>();
if let Some(message) = form.remove("message") {
let username = form.remove("username").unwrap_or(String::from("anonymous"));
futures::future::ok(NewMessage {
username: username,
message: message,
})
} else {
futures::future::err(hyper::Error::from(io::Error::new(
io::ErrorKind::InvalidInput,
"Missing field 'message",
)))
}
}
После разбора формы в хэшмап пытаемся удалить из нееmessageключ. Поскольку это обязательное поле, в случае неудачного удаления возвращается ошибка. Если удаление прошло успешно, то получаемusernameПоле, если это поле не существует, используйте значение по умолчанию"anonymous". Наконец, мы возвращаем простойNewMessageУспешное будущее структур.
Я не буду обсуждать это прямо сейчасwrite_to_dbфункция. Само взаимодействие с базой данных очень сложное, поэтому я буду использовать следующую главу, чтобы представить эту функцию и соответствующую функцию для чтения сообщений из базы данных. Однако, отмечаяwrite_to_dbвозвращение к успехуi64Значение типа, представляющее собой временную метку, когда новое сообщение было зафиксировано в базе данных.
Давайте сначала посмотрим, как мы можем вернуть ответ на любой запрос к микросервису:
#[macro_use]
extern crate serde_json;
fn make_post_response(
result: Result<i64, hyper::Error>,
) -> FutureResult<hyper::Response, hyper::Error> {
match result {
Ok(timestamp) => {
let payload = json!({"timestamp": timestamp}).to_string();
let response = Response::new()
.with_header(ContentLength(payload.len() as u64))
.with_header(ContentType::json())
.with_body(payload);
debug!("{:?}", response);
futures::future::ok(response)
}
Err(error) => make_error_response(error.description()),
}
}
мы вresultсопоставьте и посмотрите, сможем ли мы успешно записать в базу данных. В случае успеха мы создаем полезную нагрузку JSON, которая формирует тело ответа, которое мы возвращаем. Для этого я использовалserde_jsonэтот ящик, вы должны добавить его вCargo.tomlсередина. При построении структуры ответа нам нужно установить правильные заголовки HTTP. В данном примере это означает, чтоContent-LengthВ поле заголовка устанавливается длина тела ответа,Content-TypeПоле заголовка настроено наapplication/json.
Я переработал код, чтобы сделать функцию, формирующую тело ответа в случае ошибки, отдельной функцией.make_error_response, потому что мы будем использовать его позже:
fn make_error_response(error_message: &str) -> FutureResult<hyper::Response, hyper::Error> {
let payload = json!({"error": error_message}).to_string();
let response = Response::new()
.with_status(StatusCode::InternalServerError)
.with_header(ContentLength(payload.len() as u64))
.with_header(ContentType::json())
.with_body(payload);
debug!("{:?}", response);
futures::future::ok(response)
}
Конструкция ответа очень похожа на предыдущую функцию, за исключением того, что на этот раз мы должны установить HTTP-статус ответа наStatusCode::InternalServerError(статус 500). Статус по умолчанию — OK (200), поэтому нам не нужно было устанавливать статус раньше.
перениматьGETпросить
Далее мы обращаемся кGETЗапросы, которые отправляются на сервер для получения сообщений. Мы разрешаем запросам иметь два аргумента запросаbeforeиafter. Оба параметра являются временными метками и используются для ограничения того, какие сообщения будут извлекаться на основе временных меток сообщений. Оба параметра являются необязательными. еслиbeforeиafterНи один из параметров не присутствует, мы просто вернем последнее сообщение.
Ниже представлена обработкаGETВетвь соответствия запроса. В нем немного больше логики, чем в предыдущем коде.
(&Get, "/") => {
let time_range = match request.query() {
Some(query) => parse_query(query),
None => Ok(TimeRange {
before: None,
after: None,
}),
};
let response = match time_range {
Ok(time_range) => make_get_response(query_db(time_range)),
Err(error) => make_error_response(&error),
};
Box::new(response)
}
позвонивrequest.query(), мы получаемOption<&str>, потому что URI может вообще не иметь строки запроса. Если запрос существует, мы вызываемparse_query, который анализирует параметры запроса, возвращаяTimeRangeструктура. его определение
struct TimeRange {
before: Option<i64>,
after: Option<i64>,
}
так какbeforeиafterВсе параметры являются необязательными, мы будемTimeRangeОба поля структуры имеют значениеOption. Кроме того, временная метка может быть недействительной (например, не числом), поэтому мы должны обрабатывать случай, когда синтаксический анализ ее значения не удается. при этих обстоятельствах,parse_queryвернет сообщение об ошибке, которое мы можем перенаправить на то, что мы написали ранееmake_error_responseфункция. Если синтаксический анализ прошел успешно, мы можем продолжить вызовquery_db(получить новости для нас) иmake_get_response(создать подходящийResponseобъект и возвращается клиенту).
Чтобы разобрать строку запроса, мы снова используем предыдущийurl::form_urlencodedфункция, потому что ее синтаксис по-прежнемуkey=value&key=value. Затем мы пытаемся получитьbeforeиafterДва значения и преобразовать их в целочисленный тип (т.е. тип временной метки):
fn parse_query(query: &str) -> Result<TimeRange, String> {
let args = url::form_urlencoded::parse(&query.as_bytes())
.into_owned()
.collect::<HashMap<String, String>>();
let before = args.get("before").map(|value| value.parse::<i64>());
if let Some(ref result) = before {
if let Err(ref error) = *result {
return Err(format!("Error parsing 'before': {}", error));
}
}
let after = args.get("after").map(|value| value.parse::<i64>());
if let Some(ref result) = after {
if let Err(ref error) = *result {
return Err(format!("Error parsing 'after': {}", error));
}
}
Ok(TimeRange {
before: before.map(|b| b.unwrap()),
after: after.map(|a| a.unwrap()),
})
}
К сожалению, код здесь немного громоздкий и повторяющийся, но его трудно сделать лучше, не добавляя сложности. По существу, мы пытаемся получить из формыbeforeиafterдва поля. Если поле существует, попробуйте проанализировать его какi64. Я хотел бы иметь возможность объединить несколькоif letутверждение, поэтому мы можем написать:
if let Some(ref result) = before && let Err(ref error) = *result {
return Err(format!("Error parsing 'before': {}", error));
}
Однако на данный момент это невозможно в Rust (оборачивая методы в кортежи, вif letоператор для записи нескольких значений, но эти значения не могут зависеть друг от друга, как здесь).
пропустить сейчасquery_dbесли,make_get_responseВыглядит очень просто:
fn make_get_response(
messages: Option<Vec<Message>>,
) -> FutureResult<hyper::Response, hyper::Error> {
let response = match messages {
Some(messages) => {
let body = render_page(messages);
Response::new()
.with_header(ContentLength(body.len() as u64))
.with_body(body)
}
None => Response::new().with_status(StatusCode::InternalServerError),
};
debug!("{:?}", response);
futures::future::ok(response)
}
еслиmessagesЭта опция содержит значение, мы можем передать это сообщениеrender_page, который возвращает HTML-страницу, составляющую тело нашего ответа, отображая сообщение в виде простого HTML-списка. Если опция пуста,query_dbВ , есть ошибка, которую мы зарегистрируем, но не сообщим пользователю, поэтому мы просто вернем ответ с кодом состояния 500. Я расскажу в главе шаблонаrender_pageреализация.
подключиться к базе данных
Теперь, когда у нас есть пути записи и чтения в нашем сервисе, нам нужно объединить их с базой данных для чтения и записи. В Rust есть очень полезная и популярная библиотека объектно-реляционной модели (ORM) под названиемdiesel. Эта библиотека очень забавная и интуитивно понятная. добавь его в свойCargo.tomlи включитьpostgresфункция, потому что мы собираемся использовать ее в этом урокеPostgresбаза данных:
diesel = { version = "1.0.0", features = ["postgres"] }
Убедитесь, что на вашем компьютере установлен Postgres и вы можете использоватьpsqlЛогин (как базовая проверка работоспособности). Diesel также поддерживает другие СУБД, такие как MySQL, вы можете попробовать их после этого руководства.
Начнем с создания схемы базы данных для приложения. мы вставляем этоschemas/messages.sqlсередина:
CREATE TABLE messages (
id SERIAL PRIMARY KEY,
username VARCHAR(128) NOT NULL,
message TEXT NOT NULL,
timestamp BIGINT NOT NULL DEFAULT EXTRACT('epoch' FROM CURRENT_TIMESTAMP)
)
В каждой строке таблицы хранится сообщение, включая монотонно возрастающий идентификатор, имя пользователя автора, текст сообщения и метку времени. Значение по умолчанию метки времени, упомянутой выше, будет вставлять текущее количество секунд с начала эпохи для каждой новой записи. так какidСтолбцы также автоматически увеличиваются, и в итоге нам просто нужно вставить имя пользователя и сообщение для каждой новой строки.
Теперь нам нужно интегрировать эту таблицу с Diesel. Для этого нам нужно пройтиcargo install diesel_cliУстановите дизельный CLI. Затем вы можете запустить следующую команду:
$ export DATABASE_URL=postgres://<user>:<password>@localhost
$ diesel print-schema | tee src/schema.rs
table! {
messages (id) {
id -> Int4,
username -> Varchar,
message -> Text,
timestamp -> Int8,
}
}
в<user>:<password>это имя пользователя и пароль вашей базы данных. Если ваша база данных не имеет пароля, вам нужно ввести только имя пользователя. Последняя команда выводит представление базы данных, написанное на Rust, которое мы можем сохранить вsrc/schema.rsсередина.table!Макрос от Дизеля. Кромемодель(схема), Дизель также попросил нас написатьМодель(модель). Это нам нужноsrc/models.rsНапишите сами:
#[derive(Queryable, Serialize, Debug)]
pub struct Message {
pub id: i32,
pub username: String,
pub message: String,
pub timestamp: i64,
}
Эта модель представляет собой структуру Rust, с которой мы взаимодействуем в нашем коде. Для этого нам нужно добавить несколько объявлений в основной модуль:
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate diesel;
mod schema;
mod models;
На данный момент мы готовы добавить функции, которые мы пропустили ранее.write_to_dbиquery_db.
записать в базу данных
мы начинаем сwrite_to_dbНачинать. Эта функция просто записывает запись в базу данных и возвращает метку времени ее создания:
use diesel::prelude::*;
use diesel::pg::PgConnection;
fn write_to_db(
new_message: NewMessage,
db_connection: &PgConnection,
) -> FutureResult<i64, hyper::Error> {
use schema::messages;
let timestamp = diesel::insert_into(messages::table)
.values(&new_message)
.returning(messages::timestamp)
.get_result(db_connection);
match timestamp {
Ok(timestamp) => futures::future::ok(timestamp),
Err(error) => {
error!("Error writing to database: {}", error.description());
futures::future::err(hyper::Error::from(
io::Error::new(io::ErrorKind::Other, "service error"),
))
}
}
}
Это так просто! Diesel предоставляет очень интуитивно понятный и типобезопасный интерфейс запросов, который мы используем для:
- Указываем таблицу, в которую мы хотим вставить,
- указываем значение, которое хотим вставить (подробнее об этом чуть позже),
- указать значение, которое мы хотим вернуть (если есть), и
- перечислить
get_result, который фактически выполнит запрос.
Это возвращает намQueryResult<i64>объект, мы можем сопоставить его, обрабатывая ошибки по мере необходимости. Две вещи выше, которые должны вас удивить: (1) мы можем напрямую конвертироватьNewMessageСтруктура переходит в Diesel, и (2) мы используем волшебную, ранее не существовавшуюdb_connectionпараметр. Давайте разгадаем эти две загадки! Для (1) код, который я дал вам выше, на самом деле не скомпилируется. Чтобы код скомпилировался, нам нужноNewMessageструктура переехала вsrc/models.rs, просто поставьMessageниже структуры. Код выглядит следующим образом:
use schema::messages;
#[derive(Queryable, Serialize, Debug)]
pub struct Message {
pub id: i32,
pub username: String,
pub message: String,
pub timestamp: i64,
}
#[derive(Insertable, Debug)]
#[table_name = "messages"]
pub struct NewMessage {
pub username: String,
pub message: String,
}
Таким образом, Diesel может напрямую связать поля нашей структуры со столбцами в базе данных. Как лаконично! Обратите внимание, что для этого таблица в базе данных должна называтьсяmessages,какtable_nameпоказаны свойства.
Для второй загадки нам нужно немного изменить код, чтобы представить концепцию соединений с базой данных. существуетService::call(), поместите следующее вверху:
fn call(&self, request: Request) -> Self::Future {
let db_connection = match connect_to_db() {
Some(connection) => connection,
None => {
return Box::new(futures::future::ok(
Response::new().with_status(StatusCode::InternalServerError),
))
}
};
вconnect_to_dbОпределяется следующим образом
use std::env;
const DEFAULT_DATABASE_URL: &'static str = "postgresql://postgres@localhost:5432";
fn connect_to_db() -> Option<PgConnection> {
let database_url = env::var("DATABASE_URL").unwrap_or(String::from(DEFAULT_DATABASE_URL));
match PgConnection::establish(&database_url) {
Ok(connection) => Some(connection),
Err(error) => {
error!("Error connecting to database: {}", error.description());
None
}
}
}
Эта функция ищет переменные окруженияDATABASE_URLчтобы определить URL-адрес базы данных Postgres, в противном случае используйте предопределенную константу. Затем он пытается создать новое соединение с базой данных и возвращается в случае успеха. Также необходимо обновить обработкуGETиPOSTкод:
(&Post, "/") => {
let future = request
.body()
.concat2()
.and_then(parse_form)
.and_then(move |new_message| write_to_db(new_message, &db_connection))
.then(make_post_response);
Box::new(future)
}
(&Get, "/") => {
let time_range = match request.query() {
Some(query) => parse_query(query),
None => Ok(TimeRange {
before: None,
after: None,
}),
};
let response = match time_range {
Ok(time_range) => make_get_response(query_db(time_range, &db_connection)),
Err(error) => make_error_response(&error),
};
Box::new(response)
}
С помощью этой схемы мы создаем новое соединение с базой данных каждый раз, когда приходит запрос. В зависимости от вашей конфигурации эта схема может подойти. Тем не менее, вы также можете рассмотреть возможность использованияr2d2установить одинпул соединенийчтобы держать определенное количество подключений открытыми и давать вам подключение, когда вам это нужно.
база данных запросов
Теперь мы можем записывать новые сообщения в базу данных — и это здорово. Далее мы выясним, как считывать их обратно, запрашивая базу данных соответствующим образом. давайте сделаем этоquery_db:
fn query_db(time_range: TimeRange, db_connection: &PgConnection) -> Option<Vec<Message>> {
use schema::messages;
let TimeRange { before, after } = time_range;
let query_result = match (before, after) {
(Some(before), Some(after)) => {
messages::table
.filter(messages::timestamp.lt(before as i64))
.filter(messages::timestamp.gt(after as i64))
.load::<Message>(db_connection)
}
(Some(before), _) => {
messages::table
.filter(messages::timestamp.lt(before as i64))
.load::<Message>(db_connection)
}
(_, Some(after)) => {
messages::table
.filter(messages::timestamp.gt(after as i64))
.load::<Message>(db_connection)
}
_ => messages::table.load::<Message>(db_connection),
};
match query_result {
Ok(result) => Some(result),
Err(error) => {
error!("Error querying DB: {}", error);
None
}
}
}
К сожалению, этот код немного сложен. Это потому чтоbeforeиafterобаOption, а Diesel в настоящее время не поддерживает простой способ постепенного создания запросов. Поэтому мы можем только исчерпывающеbeforeилиafterдаSomeилиNone, а затем решите выполнить ноль, один или два фильтра. Тем не менее, сам запрос очень прост и интуитивно понятен. так какwhereэто ключевое слово в Rust, в SQLWHEREпункт используется в Дизелеfilterметод реализован. рисунок>или=Такие реляционные операторы являются методами структуры модели, такими как.gt()или.eq().
Отображение HTML-шаблонов
Мы очень близки к этому! Осталось только написать то, что мы пропустили ранееrender_page. Для этого мы будем использоватьшаблонбиблиотека. В контексте веб-сервера шаблон — это общая концепция создания HTML-страниц с динамическими данными и потоком управления. Популярные библиотеки шаблонов на других языках — это библиотеки JavaScript.Handlebarsи питонаJinja. Хотя яСокращатель URLиспользуется в проектеРули на Rust, но я должен сказать, что библиотеки шаблонов Rustне очень хорошо. Как и во многих других областях Rust, здесь нет «квазистандартной библиотеки», как Jinja в Python, что усложняет выбор одной из них, потому что никогда не знаешь, станет ли она устаревшей в ближайшие шесть месяцев.
Тем не менее, в нашем уроке мы будем использоватьmaudбиблиотека шаблонов. Хотя мод не самый расширяемый вариант для реальных приложений, он также интересен и мощен, позволяя нам писать HTML-шаблоны непосредственно на Rust. Мод также может использовать возможности макросов Rust, если они доступны. Тем не менее, Мод нуждается в ежедневной сборке Rust, чтобы включить процедурные макросы. эта функцияКажется, он близок к стабильному.
Во-первых, в Cargo.toml добавьтеmaud:
[dependencies]
maud = "0.17.2"
Затем добавьте следующее объявление в вашmain.rsВерх:
#![feature(proc_macro)]
extern crate maud;
Теперь вы можете написатьrender_pageв настоящее время:
fn render_page(messages: Vec<Message>) -> String {
(html! {
head {
title "microservice"
style "body { font-family: monospace }"
}
body {
ul {
@for message in &messages {
li {
(message.username) " (" (message.timestamp) "): " (message.message)
}
}
}
}
}).into_string()
}
какого черта? Это действительно удивительно. Подумайте об этом и сделайте глубокий вдох. Это использование макросов Rust для написания HTML-страниц. Какого черта.
Оно делает! Наши микросервисы были написаны, и оченьмикро. Запустим:
$ DATABASE_URL="postgresql://goldsborough@localhost" RUST_LOG="microservice=debug" cargo run
Compiling microservice v0.1.0 (file:///Users/goldsborough/Documents/Rust/microservice)
Finished dev [unoptimized + debuginfo] target(s) in 12.30 secs
Running `target/debug/microservice`
INFO 2018-01-22T01:22:16Z: microservice: Running microservice at 127.0.0.1:8080
Затем в другом терминале:
$ curl -X POST -d 'username=peter&message=hi' 'localhost:8080'
{"timestamp":1516584255}
$ curl -X POST -d 'username=mike&message=hi2' 'localhost:8080'
{"timestamp":1516584282}
Вы должны сразу увидеть журнал отладки:
...
DEBUG 2018-01-22T01:24:14Z: microservice: Request { method: Post, uri: "/", version: Http11, remote_addr: Some(V4(127.0.0.1:64869)), headers: {"Host": "localhost:8080", "User-Agent": "curl/7.54.0", "Accept": "*/*", "Content-Length": "25", "Content-Type": "application/x-www-form-urlencoded"} }
DEBUG 2018-01-22T01:24:14Z: microservice: Response { status: Ok, version: Http11, headers: {"Content-Length": "24", "Content-Type": "application/json"} }
...
Теперь мы используемGETчтобы получить новости:
$ curl 'localhost:8080'
<head><title>microservice</title><style>body { font-family: monospace }</style></head><body><ul><li>peter (1516584255): hi</li><li>mike (1516584282): hi2</li></ul></body>
или вы открываете его в своем браузереhttp://localhost:8080:
Вы также можете попробовать добавить URL-адрес запроса?after=<timestamp>&before=<timestamp>и убедитесь, что вы получаете сообщения только в указанный период времени.
Упаковка с помощью Docker
Я кратко расскажу об упаковке этого приложения в виде контейнера Docker. Это не имеет ничего общего с самим Rust, но помимо этого полезно знать о контейнерах Docker.
Разработчики Rust поддерживают два официальных образа Docker: один для стабильной версии и один для ежедневных сборок Rust. Стабильный образ Rustrust, изображение для ежедневной сборкиrust-lang/rust:nightly. Расширить наш контейнер на основе одного из этих образов очень просто. Мы хотим создавать образы на основе ежедневных сборок.DockerfileСодержимое должно выглядеть так:
FROM rustlang/rust:nightly
MAINTAINER <your@email>
WORKDIR /var/www/microservice/
COPY . .
RUN rustc --version
RUN cargo install
CMD ["microservice"]
Ссылаясь на типичную микросервисную архитектуру, мы запускаем базу данных Postgres в другом контейнере Docker. Пишите следующим образомDockerfile-db:
FROM postgres
MAINTAINER <your@email>
# Create the table on start-up
ADD schemas/messages.sql /docker-entrypoint-initdb.d/
затем используйтеdocker-compose.yamlСоедините их вместе:
version: '2'
services:
server:
build:
context: .
dockerfile: docker/Dockerfile
networks:
- network
ports:
- "8080:80"
environment:
DATABASE_URL: postgresql://postgres:secret@db:5432
RUST_BACKTRACE: 1
RUST_LOG: microservice=debug
db:
build:
context: .
dockerfile: docker/Dockerfile-db
restart: always
networks:
- network
environment:
POSTGRES_PASSWORD: secret
networks:
network:
Этот файл немного сложный, но после его написания все остальное просто. Заметил, что я поместил оба файла Dockerfile вdocker/Под содержанием. Теперь просто бегиdocker-compose up:
$ docker-compose up
Recreating microservice_db_1 ...
Recreating microservice_server_1 ... done
Attaching to microservice_db_1, microservice_server_1
server_1 | INFO 2018-01-22T01:38:57Z: microservice: Running microservice at 127.0.0.1:8080
db_1 | 2018-01-22 01:38:57.886 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432
db_1 | 2018-01-22 01:38:57.886 UTC [1] LOG: listening on IPv6 address "::", port 5432
db_1 | 2018-01-22 01:38:57.891 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
db_1 | 2018-01-22 01:38:57.917 UTC [20] LOG: database system was shut down at 2018-01-22 00:10:07 UTC
db_1 | 2018-01-22 01:38:57.939 UTC [1] LOG: database system is ready to accept connections
Конечно, вывод может отличаться при первом запуске. Но в любом случае наша работа закончена. Вы можете загрузить этот код в репозиторий GitHub и поставить (бесплатно)AWSилиGoogle CloudНапример, вы можете получить доступ к своему сервису извне. Вау!
Эпилог
Приведенный выше фрагмент состоит примерно из 270 строк, что более чем достаточно для создания всего нашего микросервиса на Rust. Наш код, вероятно, не слишком мал по сравнению с эквивалентным кодом, например, во Flask. Однако в Rust есть гораздо больше веб-фреймворков, которые могут дать вам больше абстракций, например.Rocket. Тем не менее, я считаю, что следуя этому руководству, используяHyperНемного приблизившись к основанию, вы получите несколько хороших идей о том, как написать безопасный и производительный веб-сервис на Rust.
Я написал этот пост в блоге, чтобы поделиться тем, что я изучаю Rust и использую свои знания для написания небольшогоВеб-служба сокращения URL-адресов- кое-что, что я узнал, используя этот веб-сервис для сокращения URL-адреса моего блога (если вы посмотрите на строку URL-адреса вашего браузера, она очень длинная) -. По этой причине я чувствую, что теперь у меня есть четкое представление о том, что может предложить Rust. Вы также знаете, какие из этих возможностей Rust более выразительны и безопасны, чем современный C++, а какие менее выразительны (но не менее безопасны).
Я чувствую, что экосистеме Rust может потребоваться несколько лет, чтобы стабилизироваться, прежде чем стабильный и хорошо поддерживаемый пакет заработает основную функциональность. Тем не менее будущее выглядит светлым. Facebook уже работает над тем, как использовать Rust для создания нового программного обеспечения, в котором размещается его кодовая база.Меркуриал сервер. Rust все чаще рассматривается как интересный вариант для встроенного программирования. Я буду внимательно следить за развитием языка, а это значит, что я подписался на Reddit.r/Rust.
Если вы обнаружите ошибки в переводе или в других областях, требующих доработки, добро пожаловать наПрограмма перевода самородковВы также можете получить соответствующие бонусные баллы за доработку перевода и PR. начало статьиПостоянная ссылка на эту статьюЭто ссылка MarkDown этой статьи на GitHub.
Программа перевода самородковэто сообщество, которое переводит высококачественные технические статьи из ИнтернетаНаггетсДелитесь статьями на английском языке на . Охват контентаAndroid,iOS,внешний интерфейс,задняя часть,блокчейн,продукт,дизайн,искусственный интеллектЕсли вы хотите видеть более качественные переводы, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.