跳过框架:使用 Hyper 构建简单的 Rust API

2025-05-25

跳过框架:使用 Hyper 构建简单的 Rust API

简介

在本文中,我将介绍如何使用hyper HTTP 库创建一个小型 Web API。该应用是todo-mvp的一个实现,正如 David Wickes 在他的文章中介绍的那样:

编辑:我已经为 Rust 2021 重新实现了这一点。你或许应该阅读那篇文章

该项目的规定之一todo-mvp是,每个实现都应避免使用“框架”,而应仅使用库。“框架”是一个模糊的术语,而且并不总是容易界定,因此我遵循了一条经验法则:如果 crate 文档将自己称为框架,则它不适合使用。这大大缩小了可用的工具范围,但事实证明,hyper这足以构建一个这样的应用程序,而不会带来太多额外的复杂性。

Hyper 是一个底层 HTTP 实现。它提供客户端和服务器类型,并公开了其所基于的底层Tokio异步运行时。我们还会引入一些其他的 crate,但仍然不具备完整的功能框架。

设置

您需要获取稳定的 Rust 工具链。如果需要,请参阅rustup。安装完成后,启动一个新的可执行项目:

$ cargo new simple-todo
$ cd simple-todo
$ cargo run
   Compiling simple-todo v0.1.0 (/home/ben/code/simple-todo)
    Finished dev [unoptimized + debuginfo] target(s) in 1.30s
     Running `target/debug/simple-todo`
Hello, world!
Enter fullscreen mode Exit fullscreen mode

用你喜欢的编辑器打开新simple-todo目录。在深入代码之前,我们先定义一下依赖项。Cargo.toml如下所示:

[package]
name = "simple-todo"
version = "0.1.0"
authors = ["You <you@yourcoolsite.com>"]
edition = "2018"

[dependencies]
futures = "0.1"
hyper = "0.12"
lazy_static = "1.3"
log = "0.4"
pretty_env_logger = "0.3"
serde = "1.0"
serde_derive = "1.0"
tera = "0.11"

[dependencies.uuid]
features = ["serde", "v4"]
version = "0.7"
Enter fullscreen mode Exit fullscreen mode

除了 之外hyper,我们还使用了几个额外的辅助 crate。简而言之,futures提供零成本异步编程原语,lazy_static允许我们定义static需要运行时初始化的 s(例如Vec::new()),logpretty_env_logger提供日志记录,serdeserde_derive用于序列化,tera从类似 Jinja 的模板文件执行 HTML 模板,并且Uuid提供 uuid!这些 crate 提供了我们的基本构建块。

这是一个小程序,将完全在 中定义main.rs。打开该文件,println!从模板中删除该语句cargo new,然后启动日志记录:

入口点

fn main() {
    pretty_env_logger::init();
}
Enter fullscreen mode Exit fullscreen mode

请注意,在 Rust 2018 中,extern crate除非需要导入宏,否则我们可以省略声明。

在设置服务器之前,我们需要一个要绑定的地址。在本演示中,我们直接硬编码即可。在 init 函数的正下方添加以下代码:

let addr = "127.0.0.1:3000".parse().unwrap();
Enter fullscreen mode Exit fullscreen mode

parse()方法将返回一个std::net::SocketAddr

接下来,我们需要在文件顶部引入一些导入内容:

use futures::{future, Future, Stream};
use hyper::{
    client::HttpConnector, rt, service::service_fn, Body, Client, Request,
    Response, Server
};
Enter fullscreen mode Exit fullscreen mode

现在我们可以完成了main()

    rt::run(future::lazy(move || {
        // create a Client for all Services
        let client = Client::new();

        // define a service containing the router function
        let new_service = move || {
            // Move a clone of Client into the service_fn
            let client = client.clone();
            service_fn(move |req| router(req, &client))
        };

        // Define the server - this is what the future_lazy() we're building will resolve to
        let server = Server::bind(&addr)
            .serve(new_service)
            .map_err(|e| eprintln!("Server error: {}", e));

        println!("Listening on http://{}", addr);
        server
    }));
Enter fullscreen mode Exit fullscreen mode

这不会完全进行类型检查 - 为了使其编译,您可以为router我们在service_fn调用中引用的函数添加上面的以下存根:

fn router(req: Request<Body>, _client: &Client<HttpConnector>) -> Box<Future<Item = Response<Body>, Error = Box<dyn std::error::Error + Send + Sync>> + Send> {
    unimplemented!()
}
Enter fullscreen mode Exit fullscreen mode

这部分内容比较丰富,我们来分解一下。整个代码都在一个调用中rt:run()。这里rt代表运行时,指的是默认的 Tokio 运行时。我们的程序会立即启动并进入这个异步环境。

在内部,我们调用future::lazy,它接受一个闭包并返回一个Future解析该闭包的函数。其余定义都在这个闭包中,包含几个步骤。我们构建了一个 hyper Client,它能够发出 HTTP 请求。

下一步是创建一个Service。这是一个 trait,代表一个异步函数,用于从请求到响应——这正是我们的 Web 服务器需要处理的!我们不需要手动实现这个 trait,而是自己定义这个函数(在本例中是router()),然后使用service_fn辅助函数将其转换为Service。接下来,我们需要做的就是创建Server本身,绑定到我们提供的地址,并让它提供这项服务。

差不多就是这样了。现在我们的工作就是定义响应,不管有没有框架,这都是你的工作!

路由器

不过,首先看一下这个router()签名。很恶心吧?在导入语句下面添加几个类型别名:

type GenericError = Box<dyn std::error::Error + Send + Sync>;
type ResponseFuture = Box<Future<Item = Response<Body>, Error = GenericError> + Send>;


fn router(req: Request<Body>, _client: &Client<HttpConnector>) -> ResponseFuture {
    unimplemented!()
}
Enter fullscreen mode Exit fullscreen mode

每当我们想要将响应返回给连接时,它都必须以Responsewrapped up in a Futurewrapped up in a 的形式返回Box——这样更容易输入绝对是个好主意!现在我们可以开始定义路由了。在开始之前,请先将BodyMethod和添加StatusCode到导入列表中hyper

我们可以利用 Rust 模式匹配来正确调度响应:

match (req.method(), req.uri().path()) {
    (&Method::GET, "/") | (&Method::GET, "index.html") => unimplemented!(),
    _ => four_oh_four(),
    }
Enter fullscreen mode Exit fullscreen mode

我们同时匹配方法和路径——对“/”的 POST 请求不会匹配此分支。我们可以在此处添加任意数量的匹配分支,任何没有对应分支的传入请求都将获得响应four_oh_four()

static NOTFOUND: &[u8] = b"Oops! Not Found";

fn four_oh_four() -> ResponseFuture {
    let body = Body::from(NOTFOUND);
    Box::new(future::ok(
        Response::builder()
            .status(StatusCode::NOT_FOUND)
            .body(body)
            .unwrap(),
    ))
}
Enter fullscreen mode Exit fullscreen mode

正如预期的那样,这个函数返回一个ResponseFuture。对于 404 页面,我们只需使用这个静态值作为主体即可。future::ok返回一个立即解析的 Future,我们使用构建器模式来构建一个Response。为了最大程度地保证正确性,我们hyper设置了枚举StatusCode

HTML

为了构建索引页,我们将使用tera,它提供了类似 Jinja2 的 HTML 模板。我们需要一个宏,并将其设置为静态,因此需要一些声明:

#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate tera;

// ...

use tera::{Context, Tera};
Enter fullscreen mode Exit fullscreen mode

todo-mvp项目要求每个实现都使用相同的模板。这篇文章与 Jinja2 或 HTML 无关,因此我建议你从这里下载并保存到simple-todo/templates/index.html。你还需要保存todo.csssimple-todo/src/resource/todo.css

Tera 使用起来非常简单。添加以下代码片段:

lazy_static! {
    pub static ref TERA: Tera = compile_templates!("templates/**/*");
}
Enter fullscreen mode Exit fullscreen mode

瞧,模板就完成了。现在我们可以这样写index()

fn index() -> ResponseFuture {
    let mut ctx = Context::new();
    let body = Body::from(TERA.render("index.html", &ctx).unwrap().to_string());
    Box::new(future::ok(Response::new(body)))
}
Enter fullscreen mode Exit fullscreen mode

要将数据注入 Tera 模板,请将其放入 中tera::Context,并将模板路径和此上下文传递给render()。然后,我们只需将结果字符串包装在 中即可ResponseFuture!别忘了更新 中的匹配分支,router()以调用此函数而不是unimplemented!()

状态

但是有一个问题——我们实际上并没有在上下文中放入任何数据!如果你运行这个程序,它会在加载这个模板时崩溃,并抱怨todostodosLen上下文中找不到 和 。这是一个非常合理的抱怨,它们不在那里。

在像这样的异步应用程序中跟踪状态可能是一个复杂的问题,但这就是 Rust。我们必须std::sync尝试一下!具体来说,我们将使用Arc和的组合RwLock来安全地跨线程存储待办事项,而无需真正思考它。

首先,导入附加内容:

#[macro_use]
extern crate serde_derive;

// ...

use std::sync::{Arc, RwLock};
use uuid::Uuid;
Enter fullscreen mode Exit fullscreen mode

现在,Todo 类型:

#[derive(Debug, Serialize)]
pub struct Todo {
    done: bool,
    name: String,
    id: Uuid,
}

impl Todo {
    fn new(name: &str) -> Self {
        Self {
            done: false,
            name: String::from(name),
            id: Uuid::new_v4(),
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

new_v4()方法将为任何新的 随机生成一个唯一标识符Todo。同时为所有待办事项列表添加一个新的类型别名:

type Todos = Arc<RwLock<Vec<Todo>>>;
Enter fullscreen mode Exit fullscreen mode

现在我们可以在lazy_static!块中实例化它:

lazy_static! {
    pub static ref TERA: Tera = compile_templates!("templates/**/*");
    pub static ref TODOS: Todos = Arc::new(RwLock::new(Vec::new()));
}
Enter fullscreen mode Exit fullscreen mode

我们需要一些辅助函数来操作列表:

fn add_todo(t: Todo) {
    let todos = Arc::clone(&TODOS);
    let mut lock = todos.write().unwrap();
    lock.push(t);
}

fn remove_todo(id: Uuid) {
    let todos = Arc::clone(&TODOS);
    let mut lock = todos.write().unwrap();
    // find the index
    let mut idx = lock.len();
    for (i, todo) in lock.iter().enumerate() {
        if todo.id == id {
            idx = i;
        }
    }
    // remove that element if found
    if idx < lock.len() {
        lock.remove(idx);
    }
}

fn toggle_todo(id: Uuid) {
    let todos = Arc::clone(&TODOS);
    let mut lock = todos.write().unwrap();
    for todo in &mut *lock {
        if todo.id == id {
            todo.done = !todo.done;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

当你调用 时Arc::clone(),它会创建一个指向相同数据的新指针,并增加其引用计数。然后,我们获取底层 的写锁RwLock,之后就可以安全地操作Vec内部内容了。使用这些辅助函数,我们的路由处理程序可以以保证线程安全的方式安全地操作状态。最后,我们可以index()在定义 之后立即构建上下文ctx

let todos = Arc::clone(&TODOS);
let lock = todos.read().unwrap();
ctx.insert("todos", &*lock);
ctx.insert("todosLen", &(*lock).len());
Enter fullscreen mode Exit fullscreen mode

处理程序

现在运行应用程序并指向您的浏览器localhost:3000应该显示给定的 HTML(无样式表)。

应用程序的其余部分很简单。我们只需要填写其余的处理程序。例如,要加载缺少的样式表,你需要一个新的 match 分支:

(&Method::GET, "/static/todo.css") => stylesheet(),
Enter fullscreen mode Exit fullscreen mode

以及构建响应的函数:

fn stylesheet() -> ResponseFuture {
    let body = Body::from(include_str!("resource/todo.css"));
    Box::new(future::ok(
        Response::builder()
            .status(StatusCode::OK)
            .header(header::CONTENT_TYPE, "text/css")
            .body(body)
            .unwrap(),
    ))
}
Enter fullscreen mode Exit fullscreen mode

没什么奇怪的!每个待办事项列表操作也都有一个端点:

(&Method::POST, "/done") => toggle_todo_handler(req),
(&Method::POST, "/not-done") => toggle_todo_handler(req),
(&Method::POST, "/delete") => remove_todo_handler(req),
(&Method::POST, "/") => add_todo_handler(req),
Enter fullscreen mode Exit fullscreen mode

这些处理程序都采用相同的格式:

fn add_todo_handler(req: Request<Body>) -> ResponseFuture {
    Box::new(
        req.into_body()
            .concat2() // concatenate all the chunks in the body
            .from_err() // like try! for Result, but for Futures
            .and_then(|whole_body| {
                let str_body = String::from_utf8(whole_body.to_vec()).unwrap();
                let words: Vec<&str> = str_body.split('=').collect();
                add_todo(Todo::new(words[1]));
                redirect_home()
            }),
    )
}
Enter fullscreen mode Exit fullscreen mode

这有点复杂。我们需要读取请求,然后对其进行处理。在本例中,存储在 中的请求主体str_body类似于item=TodoName。有更强大的解决方案,但我只是拆分了 ,=并在结果上调用add_todo函数将其添加到列表中。然后我们重定向到主页,每次返回主页时index()都会调用 ,它会根据当前应用程序状态重建 HTML!toggleremove处理程序几乎相同,只是调用了相应的函数。

重定向也并不奇怪:

fn redirect_home() -> ResponseFuture {
    Box::new(future::ok(
        Response::builder()
            .status(StatusCode::SEE_OTHER)
            .header(header::LOCATION, "/")
            .body(Body::from(""))
            .unwrap(),
    ))
}
Enter fullscreen mode Exit fullscreen mode

这看起来就像你在任何工具包中写的那样。该应用还包含一些 SVG:

 (&Method::GET, path_str) => image(path_str),
Enter fullscreen mode Exit fullscreen mode
fn image(path_str: &str) -> ResponseFuture {
    let path_buf = PathBuf::from(path_str);
    let file_name = path_buf.file_name().unwrap().to_str().unwrap();
    let ext = path_buf.extension().unwrap().to_str().unwrap();

    match ext {
        "svg" => {
            // build the response
            let body = {
                let xml = match file_name {
                    "check.svg" => include_str!("resource/check.svg"),
                    "plus.svg" => include_str!("resource/plus.svg"),
                    "trashcan.svg" => include_str!("resource/trashcan.svg"),
                    "x.svg" => include_str!("resource/x.svg"),
                    _ => "",
                };
                Body::from(xml)
            };
            Box::new(future::ok(
                Response::builder()
                    .status(StatusCode::OK)
                    .header(header::CONTENT_TYPE, "image/svg+xml")
                    .body(body)
                    .unwrap(),
            ))
        }
        _ => four_oh_four(),
    }
}
Enter fullscreen mode Exit fullscreen mode

这就是全部内容。要添加更多路由,只需添加一个新的匹配分支,router()并编写一个返回 的函数即可ResponseFuture。这是一个可靠、高性能的基础,并且可以轻松以各种方式扩展,因为您不受任何特定预定模式的约束。总而言之,hyper对于简单的用例而言,使用简单的框架而不是更高级的框架编写服务器并不会降低其人机工程学,并且可以大大减少应用程序的开销。我目前最喜欢的框架是,actix-web但它的二进制文件太大,冷编译所需的时间也太长,这几乎是荒谬的。当最终目标足够简单时,为什么不使用简单的工具呢?

完整的实现可以在这里找到。

文章来源:https://dev.to/decidously/skip-the-framework-build-a-simple-rust-api-with-hyper-4jf5
PREV
再见,Electron。你好,Tauri!
NEXT
适应 C++ 吞下药丸