在这篇文章中,我们将比较用于构建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/<user>路由,它使用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<AtomicUsize>而不是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/<name>")]比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