использовать HTTP
Первое, что вам нужно сделать, это определить, как использовать HTTP в ваших веб-сервисах Rust. Это означает, что наш сервер приложений должен иметь возможность анализировать HTTP-запрос и возвращать ответ. Другие языки, такие как python, Flash и Django, можно использовать напрямую.Для Rust можно использовать относительно низкоуровневую структуру Hyper для обработки HTTP-запросов.Hyper основан на tokio и future, который может легко создать асинхронный сервер . , и используйте log и env_log crate для поддержки журналов.
Сначала создайте проект и добавьте зависимости.
[package]
name = "microservice_rs"
version = "0.1.0"
authors = ["you <your@email>"]
[dependencies]
env_logger = "0.5.3"
futures = "0.1.17"
hyper = "0.11.13"
log = "0.4.1"
Теперь напишите код для обработки http-запроса. гипер имеетService
концепция, реализацияService
черта имеетcall
метод, принятьRequest
Объект, который обрабатывает HTTP-запросы. Поскольку Hyper является асинхронным, он должен возвращатьFuture
, код показан ниже:
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!("Microserivce received a request: {:?}", request);
Box::new(futures::future::ok(Response::new()))
}
}
Обратите внимание, что некоторые типы также определены в Microservice, и future возвращает тип Box, потому чтоfutures::fugure::Future
является чертой, мы не можем узнать ее размер.
Напишите код для запуска сервера ниже.
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();
}
бегать:
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'
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_LOG="microservice=debug"
Параметр env_log, который может управлять уровнем лога.
Обработка POST-запросов
Напишем логику обработки 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))),
}
}
мы проходимmatch
для обработки различных запросов. В этом примере метод запроса — Get или Post. В настоящее время единственным допустимым путем является/
. Если метод запроса&Post
и путь/
, то будут вызываться некоторые функции,parse_form
и т. д. для обработки.and_then
Комбайнер обработает каждый метод, а возвращенный результат будет передан следующей функции для продолжения обработки и, наконец,then
Вернуть результат.
Ниже приведена простая функция записи, используемая в приведенном выше примере.
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 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
Тип (то есть тело сообщения) и анализ тела сообщения для получения имени пользователя и сообщения.
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",
)))
}
}
Сначала проанализируйте тело сообщения, разберите его на хэш-карту, а затем получите информацию в теле сообщения через хэш-карту.
Функция write_to_db пока не будет представлена.Функция этой функции заключается в записи информации в базу данных, которая будет представлена в следующей главе.
Теперь написатьmake_post_response
функция.
#[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()),
}
}
Используйте match для проверки успешности обработки. В случае успеха верните информацию о времени. В случае неудачи верните информацию об ошибке. Здесь используется Serde_json, не забудьте добавить его в Cargo.toml.
Пишите ниже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)
}
Обработка GET-запросов
Далее мы обрабатываем запрос GET и получаем сообщение, отправляя запрос GET на сервер. Запрос 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>
, позвонивparse_query
Для обработки параметров URL TimeRange определяется следующим образом:
struct TimeRange {
before: Option<i64>,
after: Option<i64>,
}
query_db
Роль состоит в том, чтобы получить информацию о сообщении из базы данных. Давайте реализуем функцию parse_query
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(error) = *result {
return Err(format!("Error parsing 'after': {}", error));
}
}
Ok(TimeRange{
before: before.map(|b| b.unwrap()),
after: after.map(|b| b.unwrap()),
})
}
Напишите следующее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)
}
Добавить поддержку базы данных
Ржавчинаdiesel
ORM в настоящее время является лучшим ORM в Rust, поэтому мы будем использовать его напрямую.diesel
Для выполнения операций с базой данных наш выбор базы данныхpostgresql
. Добавьте следующий код в Cargo.toml
diesel = {version = "1.0.0", features = ["postgres"]}
Установить одновременноdiesel_cli
cargo install diesel_cli
Во-первых, для оператора нам нужно создать таблицу базы данных, положить ее в schemas/message.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)
)
Затем мы используем дизель для создания схемы
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,
}
}
table! — это макрос, определенный Diesel, который используется для представления соответствия полей данных, которые хранятся в файле schema.rs, и в то же время необходимо создать файл src/models.rs.
#[derive(Queryable, Serialize, Debug)]
pub struct Message {
pub id: i32,
pub username: String,
pub message: String,
pub timestamp: i64,
}
Структура модели — это сообщение, используемое приведенным выше кодом. Теперь добавьте несколько необходимых модулей в main
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate diesel;
mod schema;
mod models;
Реализовать метод 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 предоставляет очень интуитивно понятный и типобезопасный API.
- Укажите таблицу, в которую необходимо вставить
- Укажите данные для вставки
- Укажите данные, которые вы хотите вернуть
- Вызовите get_result, чтобы выполнить sql и получить результат
Точно так же для NewMessage его также необходимо определить в src/models.rs.
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,
}
Теперь нам нужно изменить метод вызова, который должен получить ссылку на БД внутри
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 connection to database {}", error.description());
None
}
}
}
fn call(&self, request: Request) -> Self::Response {
let db_connection = match connect_to_db() {
Some(connection) => connection,
None => {
return Box::new(futures::future::ok(
Response::new().with_status(StatusCode::IntervalServerError),
));
}
};
}
Затем вам нужно изменить соответствие, которое обрабатывает запросы Post и Get.
(&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, "/") => {
(&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)
}
Реализовать метод 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 u64))
.filter(messages::timestamp.gt(after as u64))
.load::<Message>(db_connection)
}
(Some(before), _) => {
message::table
.filter(messages::timestamp.lt(before as u64))
.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 query Db: {}", error);
None
}
}
}
визуализировать html
Мы используемmaudКак движок рендеринга для html. Добавить в Cargo.toml
maud = "0.17.2"
Затем объявите в main.rs
#![feature(proc_macro)]
extern crate maud;
Давайте реализуем метод render_html
fn render_html(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()
}
запустить весь проект
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 '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>
Упаковка с докером
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: