- 原文地址:Writing a Microservice in Rust
- 原文做者:Peter Goldsborough
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:nettee
- 校對者:HearFishle, shixi-li
請容許我在寫這樣一篇用 Rust 寫一個微服務的文章的開頭先談兩句 C++。我成爲 C++ 社區的一個至關活躍的成員已經很長一段時間了。我參加會議並貢獻了演講,跟隨語言的更現代化的特性的發展和傳播,固然也寫了不少代碼。C++ 讓用戶在寫代碼時能對程序的全部方面有很是細粒度的控制,不過代價是陡峭的學習曲線,以及寫出有效的 C++ 代碼所需的大量知識。然而,C++ 也是一個很是古老的語言。它由 Bjarne Stroustrup 在 1985 年構思出來。所以,它即便在現代標準中也帶有不少的歷史包袱。 固然,在 C++ 建立以後,關於語言設計的研究仍在繼續,也致使了一些如 Go、Rust、Crystal 等不少有趣的新語言的誕生。然而,這些新語言中不多有可以既具備比現代 C++ 更有趣的功能,同時仍保證具有和 C++ 一樣的性能和對內存、硬件的控制。Go 想要替代 C++,但正如 Rob Pike 發現的那樣,C++ 程序員對一種性能較差而又提供較少控制的語言不是很感興趣。不過,Rust 卻吸引了不少 C++ 愛好者。Rust 和 C++ 有很多相同的設計目標,好比零成本抽象,以及對內存的精細控制。除此以外,Rust 還添加了不少讓程序更安全、更有表達力,以及讓開發更高效的語言特性。我對 Rust 最感興趣的東西是html
SEGFAULT
了!);const
);閒聊完畢。本文的剩餘部分將引導你建立一個小而完整的微服務 —— 相似於我爲個人博客所寫的 URL 縮短器。我說的微服務指的是一個使用 HTTP,接受請求,訪問數據庫,返回一個響應(可能運送着 HTML),打包在一個 Docker 容器中,並能夠放在雲上的某個地方的這樣一種應用。在這篇文章中,我會構建一個簡單的聊天應用,容許你存儲和檢索消息。我會在過程當中介紹一些相關的包(crate)。你能夠在 GitHub 上找到服務的完整代碼。前端
咱們須要讓咱們的 web 服務作的第一件事就是如何使用 HTTP 協議,也就是咱們的應用(服務器)須要接收並解析 HTTP 請求,並返回 HTTP 響應。雖然有不少相似 Flask 或 Django 的高級框架能將這一切封裝起來,咱們仍是選擇使用稍微低級一點的 hyper 庫來處理 HTTP。這個庫使用網絡庫 tokio 和 futures,讓咱們能建立一個乾淨的異步 web 服務器。此外,咱們還會使用 log 和 env-logger 兩個 crate 來實現日誌功能。android
咱們首先設置好 Cargo.toml
,下載上述的 crate:ios
[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"
複製代碼
而後是實際的代碼。Hyper 中有 Service
的概念。它是一個實現了 Service
trait 的類型,有一個 call
函數,接收一個表示解析過的 HTTP 請求的 hyper::Request
對象做爲參數。對於一個異步服務來講,這個函數必須返回一個 Future
。下面是基本的樣板文件,咱們能夠直接放在 main.rs
中:git
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()))
}
}
複製代碼
注意到咱們還須要爲咱們的服務聲明一些基本的類型。咱們裝箱了 future 類型,由於 futures::future::Future
自己只是一個 trait,不能做爲函數的返回值。在 call()
內部,咱們目前返回一個最簡單的有效值,一個包含空響應的裝箱 future。程序員
要啓動服務器,咱們綁定一個 IP 地址到 hyper::server::Http
實例,並調用它的 run()
方法:github
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();
}
複製代碼
有了上面的代碼,hyper 會在 localhost:8080
開始監聽 HTTP 請求,解析並將其轉發到咱們的 Microservice
類。請注意,每次有新請求到來,都會建立一個新的實例。咱們如今能夠啓動服務器,用 curl 發來一些請求!咱們在終端中啓動服務器:golang
$ 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
複製代碼
而後在另外一個終端中向它發送一些請求:web
$ curl 'localhost:8080'
複製代碼
在第一個終端中,你應該能看到相似下面的輸出sql
$ 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
而且路徑正確,咱們就調用前面提到的函數。注意到咱們能夠優雅地使用組合函數來串聯 future。組合子 and_then
會在 future 正確解析(不包含錯誤)的狀況下,使用 future 中包含的值來調用一個函數。這個調用的函數也必須返回一個新的 future。這容許咱們在多個處理階段之間傳遞值,而不是現場計算出某個值。最終,咱們使用組合子 then
,不管 future 的狀態如何都會執行回調函數。這樣,它會獲得一個 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
這個 crate(你須要使用 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",
)))
}
}
複製代碼
在將表單解析爲一個 hashmap 以後,咱們嘗試從中移除 message
鍵。由於這是一個必填項,因此若是移除失敗,就返回一個錯誤(error)。若是移除成功,咱們接着獲取 username
字段,若是這個字段不存在的話,就使用默認值 "anonymous"
。最後,咱們返回一個包含簡單的 NewMessage
結構體的一個成功的 future。
我如今不會馬上討論 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
這個 crate,你應當將其添加到 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
請求,這些請求發到服務器是要獲取消息。咱們容許請求有兩個查詢參數(query arguments)before
和 after
。兩個參數都是時間戳,用於根據消息的時間戳來約束會獲取哪些消息。兩個參數都是可選的。若是 before
和 after
參數都不存在,咱們將只返回最後的消息。
下面是處理 GET
請求的 match 分支。它的邏輯比前面的代碼略多。
(&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
這個 option 包含一個值,咱們能夠將這個消息傳給 render_page
,它會返回一個構成咱們的響應體的 HTML 頁面,其中在一個簡單的 HTML 列表中顯示消息。若是 option 爲空,query_db
中出現了一個錯誤,咱們會記錄日誌但不會暴露給用戶,因此咱們只是返回狀態碼爲 500 的響應。我將在模板章節介紹 render_page
的實現。
既然咱們的服務中有寫入和讀取的路徑,咱們就須要將它們與數據庫結合起來進行讀寫。Rust 有一個很是好用和流行的對象關係模型(ORM)庫叫作 diesel。這個庫很是有趣和直觀。將它添加到你的 Cargo.toml
中,並啓用 postgres
功能,由於咱們這份教程中要使用 Postgres 數據庫:
diesel = { version = "1.0.0", features = ["postgres"] }
複製代碼
請保證你已經在機器上安裝了 Postgres,而且可使用 psql
登陸(做爲基本的健壯性檢查)。Diesel 還支持 MySQL 等其餘 DBMS,你能夠在學完本教程以後嘗試它們。
讓咱們從爲應用建立數據庫模式開始。咱們將它放入 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、做者的用戶名、消息文本和一個時間戳。上面所說的時間戳的默認值會爲每一個新的條目插入自 epoch 以來的當前秒數。因爲 id
列也是自動遞增的,咱們最終只須要爲每一個新行插入用戶名和消息。
如今咱們須要將此表與 Diesel 集成。爲此,咱們須要經過 cargo install diesel_cli
安裝 Diesel 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!
宏來自於 Diesel。除了模式(schema)以外,Diesel 還要求咱們寫一個模型(model)。這個咱們須要在 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
來肯定 Postgres 數據庫的 URL,不然使用預約義的常量。而後它嘗試建立一個新的數據庫鏈接,若是成功的話則返回。你還須要更新處理 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 中的關鍵字,SQL 中的 WHERE
子句是使用 Diesel 中的 filter
方法實現的。像 >
或 =
這樣的關係操做符則是模型結構體上的方法,如 .gt()
或 .eq()
。
咱們很接近完成了!如今還剩下的就只有編寫咱們以前遺漏的 render_page
。爲此,咱們要使用模板庫。在 web 服務器的上下文中,模板是一種經過動態數據和控制流建立 HTML 頁面的通用概念。其餘語言中流行的模板庫有 JavaScript 的 Handlebars 和 Python 的 Jinja。雖然我在 URL 縮短器 項目中使用了 Rust 上的 Handlebars,可是我不得不說 Rust 的模板庫都不怎麼樣。就像 Rust 中的很多領域同樣,沒有像 Jinja 在 Python 中同樣的「準標準庫」. 這使得從中選擇一個很難,由於你永遠不知道它會不會在將來六個月內被棄用。
雖然如此,咱們的教程中會使用一個叫作 maud 的模板庫。雖然 maud 不是真實世界應用的最具擴展性的選擇,但它也頗有趣和強大,容許咱們直接用 Rust 寫 HTML 模板。maud 還能夠發揮 Rust 宏的力量,若是有的話。也就是說,maud 須要一個 Rust 的每日構建版本,以啓動宏程序(procedural macro)功能。這個功能看起來已經接近穩定了。
首先,在你的 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 容器。這和 Rust 自己沒有任何關係,但在此基礎上了解相關的 Docker 容器是頗有用的。
Rust 開發人員維護了兩個官方的 Docker 鏡像:一個是穩定版,一個是用於每日構建的 Rust。穩定版的 Rust 鏡像就是 rust
,每日構建版的鏡像是 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"] 複製代碼
參考典型的微服務架構,咱們在另外一個 Docker 容器中運行 Postgres 數據庫。以下編寫 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 中還有更多的 web 框架,能夠爲你提供更多的抽象,例如 Rocket。儘管如此,我相信跟隨這個教程,使用 Hyper 稍微接近底層,會帶給你關於如何利用 Rust 寫一個安全且高性能的 web 服務的一些很好的思路。
我寫這篇博文是想分享我在學習 Rust,以及使用個人知識寫一個小型的 URL 縮短器 web 服務 —— 我用這個 web 服務來縮短個人博客的 URL(若是你看一眼瀏覽器的 URL 欄,會發現它很是長)—— 時學到的東西。出於這個緣由,我以爲我如今對 Rust 提供的特性有了深入的認識。也知道了 Rust 的這些特性和現代 C++ 相比,哪些表達能力較強且更安全,而哪些表達能力較弱(但不會更不安全)。
我以爲 Rust 的生態系統可能還須要幾年的時間來穩定,才能讓穩定且維護良好的軟件包完成主要的功能。儘管如此,前途仍是很光明的。Facebook 已經在研究如何使用 Rust 構建託管其代碼庫的新 Mercurial 服務器。愈來愈多的人將 Rust 視爲嵌入式編程的一個有趣選擇。我會密切關注這個語言的發展,這意味着我已經在 Reddit 上訂閱了 r/Rust
。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。