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