Web Server 對比:Hyper vs Rocket
在這篇文章中,我們將比較用於構建 web 應用程序的兩個流行的 Rust 庫。我們將爲每一個庫寫一個例子,並比較它們的人體工程學以及性能。
第一個庫 Hyper 是一個底層的 HTTP 庫,它包含了構建服務器應用程序的底層原語。
第二個庫 Rocket 是功能齊全的,並提供了一種更具有聲明性的方法來構建 web 應用程序。
Demo
我們將建立一個簡單的網站來展示每個庫是如何實現的:
路由
根據給定的 URL 路由不同的響應內容。有些路徑是固定的,在我們的例子中,我們會有一個固定的返回 Hello World 的路由。有些路徑是動態的,可以有參數。在這個例子中,我們將使用 / hello/*name * 來響應 hello name,它將在每個響應中替換 name。
共享狀態
我們希望有一個共享狀態的應用。在這個 Demo 中,我們將使用站點訪問計數器來統計請求的數量。這個數字可以通過訪問 / counter.json 來顯示。在本例中,我們將把計數器存儲在應用程序內存中。但是,如果將其存儲在數據庫中,則共享的將是數據庫 client。
站點還需要很多其他功能,比如處理 HTTP 方法、接收數據、呈現模板和錯誤處理。但在本文和示例中,我們將只比較路由和共享狀態這兩個功能。
Hyper
Hyper 的 readme 描述 Hyper 爲 “一個快速和正確的用 Rust 實現的 HTTP 客戶端和服務器 api”。在本演示中,我們將使用該庫的服務器端。它在 GitHub 上擁有 9.7 萬顆星星和 48M crates 的下載量。它經常被用作一個依賴項,許多其他庫,如 reqwest 和 tonic,都是在它的基礎上構建的。
在本例中,我們將看到僅使用該庫就可以達到何種程度。這個演示使用 Hyper 0.141。下面是網站的完整代碼:
use hyper::server::conn::AddrStream;
use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Request, Response, Server};
use std::convert::Infallible;
use std::sync::{atomic::AtomicUsize, Arc};
#[derive(Clone)]
struct AppContext {
pub counter: Arc<AtomicUsize>,
}
async fn handle(context: AppContext, req: Request<Body>) -> Result<Response<Body>, Infallible> {
// Increment the visit count
let new_count = context
.counter
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
if req.method().as_str() != "GET" {
return Ok(Response::builder().status(406).body(Body::empty()).unwrap());
}
let path = req.uri().path();
let response = if path == "/" {
Response::new(Body::from("Hello World"))
} else if path == "/counter.json" {
let data = format!("{{\"counter\":{}}}", new_count);
Response::builder()
.header("Content-Type", "application/json")
.body(Body::from(data))
.unwrap()
} else if let Some(name) = path.strip_prefix("/hello/") {
Response::new(Body::from(format!("Hello, {}!", name)))
} else {
Response::builder().status(404).body(Body::empty()).unwrap()
};
Ok(response)
}
#[tokio::main]
async fn main() {
let context = AppContext {
counter: Arc::new(AtomicUsize::new(0)),
};
let make_service = make_service_fn(move |_conn: &AddrStream| {
let context = context.clone();
let service = service_fn(move |req| handle(context.clone(), req));
async move { Ok::<_, Infallible>(service) }
});
let server = Server::bind(&"127.0.0.1:3000".parse().unwrap())
.serve(make_service)
.await;
if let Err(e) = server {
eprintln!("server error: {}", e);
}
}
在頂部,我們定義了一個處理所有請求的 handle 函數。
路由是通過 handle 函數中的 if 和 else 鏈完成的。首先,使用 req.uri().path() 提取請求的路徑 (例如索引的 /)。固定的路由很容易使用像 path == "/" 這樣的字符串比較進行分支。對於匹配多個路徑的路由,例如 / hello/ 路由,它使用 str::strip_prefix,如果路徑不以該前綴開頭,返回一個 None;如果路徑以該前綴開頭,返回一個 Some。
"/".strip_prefix("/hello/") == None
"/test".strip_prefix("/hello/") == None
"/hello/jack".strip_prefix("/hello/") == Some("jack")
該函數對於非 GET 的方法請求都提前返回,因爲本例中沒有 POST 路由或其他路由。如果站點接受不同的請求類型,並且必須添加額外的保護,那麼我們可以向 If 語句添加額外的條件。顯然擴展 if 鏈會使代碼變得更加複雜和冗長。
Hyper 從 http crate 裏重新導出 Response,它使用了一個非常簡單的構造器模式來構建 Response。序列化代碼是用 format! 格式手寫的!當然也可以使用 serde crate。
計數器是通過在初始化代碼中創建一個 struct,然後克隆它,併發送給處理程序函數的每個請求。它使用 Arc 而不是 usize。該代碼在處理程序函數的其他操作之前遞增計數器,以便記錄所有請求的訪問。
在運行時性能方面,在三個 30 秒的連接上,Hyper 在上述代碼的索引路由上平均每秒響應 74,563 個請求。這是令人難以置信的快!
Rocket
Rocket 是一個 “使用 Rust 編寫的網頁框架,其重點是易用性、可表達性和速度”。它有 17.4 萬 github star 和 1.7M 的 crates 下載。Rocket 內部使用 Hyper。
在這個演示中,我們使用了 Rocket3 的 0.5.0-rc2 穩定版本。
use rocket::{
fairing::{Fairing, Info, Kind},
get, launch, routes,
serde::{json::Json, Serialize},
Config, Data, Request, State,
};
use std::sync::atomic::AtomicUsize;
#[derive(Serialize, Default)]
#[serde(crate = "rocket::serde")]
struct AppContext {
pub counter: AtomicUsize,
}
#[launch]
fn rocket() -> _ {
let config = Config {
port: 3000,
..Config::debug_default()
};
rocket::custom(&config)
.attach(CounterFairing)
.manage(AppContext::default())
.mount("/", routes![hello1, hello2, counter])
}
struct CounterFairing;
#[rocket::async_trait]
impl Fairing for CounterFairing {
fn info(&self) -> Info {
Info {
name: "Request Counter",
kind: Kind::Request,
}
}
async fn on_request(&self, request: &mut Request<'_>, _: &mut Data<'_>) {
request
.rocket()
.state::<AppContext>()
.unwrap()
.counter
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}
}
#[get("/")]
fn hello1() -> &'static str {
"Hello World"
}
#[get("/hello/<name>")]
fn hello2(name: &str) -> String {
format!("Hello, {}!", name)
}
#[get("/counter.json")]
fn counter(state: &State<AppContext>) -> Json<&AppContext> {
Json(state.inner())
}
在 Rocket 中,我們用一個請求函數來表式每一個請求。get 宏處理路徑的路由,它採用聲明式方法,#[get("/hello/")] 比 let Some(name) = path.strip_prefix("/hello/") 更具有描述性,也更少的代碼。請求函數是用. mount("/", routes![hello1, hello2, counter]) 這種方式註冊的。
應用程序在這裏定義了一個狀態:
#[derive(Serialize, Default)]
#[serde(crate = "rocket::serde")]
struct AppContext {
pub counter: AtomicUsize,
}
Rocket 有一個叫做 “Fairing” 的中間件實現。在這個例子中,它定義了一個 counterfairness,它對每個請求都修改計數器的狀態。
使用與 Hyper 相同的基準測試,Rocket 平均每秒返回 43899 個請求——大約是 Hyper 吞吐量的 60%。
本文翻譯自:
https://www.shuttle.rs/blog/2022/06/01/hyper-vs-rocket
coding 到燈火闌珊 專注於技術分享,包括 Rust、Golang、分佈式架構、雲原生等。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/KOvCMLFf5jB65asS_NAW9A