Rust Study Guide - Веб-программирование на Rust

Rust
использовать 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)
}
Добавить поддержку базы данных

РжавчинаdieselORM в настоящее время является лучшим 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: