rust搭建搭建 Web 服务器

178 阅读7分钟

Rust 的优势之一是在 crates.io 网站上发布的大量免费的可用包。 cargo 命令可以让你的代码轻松使用 crates.io 上的包:它将下载 包的正确版本,然后会构建包,并根据用户的要求更新包。一个 Rust 包,无论是库还是可执行文件,都叫作 crate(发音为 /kreɪt/,意思 是“板条箱”) 。Cargo 和 crates.io 的名字都来源于这个术语。

为了展示这种工作过程,我们将使用 actix-web(Web 框架 crate)、serde(序列化 crate)以及它们所依赖的各种其他 crate 来组装出一个简单的 Web 服务器。

创建web项目

首先,让 Cargo 创建一个新包,命名为 actix-gcd:

cargo new actix-gcd

actix-gcd中的文件然后,编辑新项目的 Cargo.toml 文件以列出所要使用的包,其内容 应该是这样的Cargo.toml注意这里有个坑,就是必须使用管理员权限打开cmd并且,定位到指定的文件进行编辑,不然会报错这是没有使用管理员权限打开cmdCargo.toml 中 [dependencies] 部分的每一行都给出了 crates.io 上的 crate 名称,以及我们想要使用的那个 crate 的版 本。在本例中,我们需要 1.0.8 版的 actix-web crate 和 1.0 版的 serde crate。crates.io 上这些 crate 的版本很可能比此处 展示的版本新,但通过指明在测试此代码时所使用的特定版本,可以 确保即使发布了新版本的包,这些代码仍然能继续编译。 crate 可能具备某些可选特性:一部分接口或实现不是所有用户都需 要的,但将其包含在那个 crate 中仍然有意义。例如,serde crate 就提供了一种非常简洁的方式来处理来自 Web 表单的数据,但根据 serde 的文档,只有选择了此 crate 的 derive 特性时它才可用, 因此我们在 Cargo.toml 文件中请求了它。 请注意,只需指定要直接用到的那些 crate 即可,cargo 会负责把 它们自身依赖的所有其他 crate 带进来。 在第一次迭代中,我们将实现此 Web 服务器的一个简单版本:它只会 给出让用户输入要计算的数值的页面。actix-gcd/src/main.rs 的内 容如下所示找到文件进行代码编写

use actix_web::{web, App, HttpResponse, HttpServer};
fn main() {
 let server = HttpServer::new(|| {
 App::new()
3
3
 .route("/", web::get().to(get_index))
 });
 println!("Serving on http://localhost:3000...");
 server
 .bind("127.0.0.1:3000").expect("error binding server to
address")
 .run().expect("error running server");
}
fn get_index() -> HttpResponse {
 HttpResponse::Ok()
 .content_type("text/html")
 .body(
 r#"
 <title>GCD Calculator</title>
 <form action="/gcd" method="post">
 <input type="text" name="n"/>
 <input type="text" name="m"/>
 <button type="submit">Compute GCD</button>
 </form>
 "#,
 )
}

use 声明可以让来自 actix-web crate 的定义用起来更容易些。当我们写下 use actix_web::...} 时,花括号中列出的每个名称 都可以直接用在代码中,而不必每次都拼出全名,比如actix_web::HttpResponse 可以简写为 HttpResponse。(稍后还会提及 serde crate。) main 函数很简单:它调用 HttpServer::new 创建了一个响应单个路径 "/" 请求的服务器,打印了一条信息以提醒我们该如何连接 它,然后监听本机的 TCP 端口 3000。 我们传给 HttpServer::new 的参数是 Rust 闭包表达式| {App::new() ...。闭包是一个可以像函数一样被调用的值。这个 闭包没有参数,如果有参数,那么可以将参数名放在两条竖线 || 之间。{ ... } 是闭包的主体。当我们启动服务器时,Actix 会启动一个线程池来处理传入的请求。每个线程都会调用这个闭包来获取 App值的新副本,以告诉此线程该如何路由这些请求并处理它们闭包会调用 App::new 来创建一个新的空白 App,然后调用它的route 方法为路径 "/" 添加一个路由。提供给该路由的处理程序web::get().to(get_index) 会通过调用函数 get_index 来处理 HTTP 的 GET 请求。route 方法的返回值就是调用它的那个App,不过其现在已经有了新的路由。由于闭包主体的末尾没有分号,因此此 App 就是闭包的返回值,可供 HttpServer 线程使用。get_index 函数会构建一个 HttpResponse 值,该值表示对 HTTP GET / 请求的响应。HttpResponse::Ok() 表示 HTTP 200 OK 状态,意味着请求成功。我们会调用它的 content_type 方法和body 方法来填入该响应的细节,每次调用都会返回在前一次基础上修改过的 HttpResponse。最后会以 body 的返回值作为get_index 的返回值。 由于响应文本包含很多双引号,因此我们使用 Rust 的“原始字符串”语法来编写它:首先是字母 r、0 到多个井号(#)标记、一个双引号,然后是字符串本体,并以另一个双引号结尾,后跟相同数量的# 标记。任何字符都可以出现在原始字符串中而不被转义,包括双引号。事实上,Rust 根本不认识像 " 这样的转义序列。我们总是可以在引号周围使用比文本内容中出现过的 # 更多的 # 标记,以确保字符串能在期望的地方结束。编写完 main.rs 后,可以使用 cargo run 命令来执行为运行它而要做的一切工作:获取所需的 crate、编译它们、构建我们自己的程序、将所有内容链接在一起,最后启动 main.rs。此刻,在浏览器中访问给定的 URL但很遗憾,单击“Compute GCD”除了将浏览器导航到一个空白页面外,没有做任何事。为了继续解决这个问题,可以往 App 中添加另一 个路由,以处理来自表单的 POST 请求。 现在终于用到我们曾在 Cargo.toml 文件中列出的 serde crate了:它提供了一个便捷工具来协助处理表单数据。首先,将以下 use 指令添加到 src/main.rs 的顶部:

use serde::Deserialize;

我们通常会将所有的 use 声明集中放在文件的顶部,但这并非绝对必要:Rust 允许这些声明以任意顺序出现,只要它们出现在适当的嵌套级别即可。接下来,定义一个 Rust 结构体类型,用以表示期望从表单中获得的值:

#[derive(Deserialize)]
struct GcdParameters {
 n: u64,
 m: u64,
}

上述代码定义了一个名为 GcdParameters 的新类型,它有两个字段(n 和 m),每个字段都是一个 u64,这是我们的 gcd 函数想要的 参数类型。 此 struct 定义上面的注解是一个属性,就像之前用来标记测试函数 的 #[test] 属性一样。在类型定义之上放置一个 #[derive(Deserialize)] 属性会要求 serde crate 在程序编译时检查此类型并自动生成代码,以便从 HTML 表单 POST 提交过来的格式化数据中解析出此类型的值。事实上,该属性足以让你从几乎任何种类的结构化数据(JSON、YAML、TOML 或许多其他文本格式和二进制格式中的任何一种)中解析 GcdParameters 的值。serde crate 还提供了一个 Serialize 属性,该属性会生成代码来执行相 反的操作,获取 Rust 值并以结构化的格式序列化它们。有了这个定义,就可以很容易地编写处理函数了:

fn post_gcd(form: web::Form<GcdParameters>) -> HttpResponse {
 if form.n == 0 || form.m == 0 {
 return HttpResponse::BadRequest()
 .content_type("text/html")
 .body("Computing the GCD with zero is boring.");
 }
 let response =
 format!("The greatest common divisor of the numbers {} and
{} \
 is <b>{}</b>\n",
 form.nform.m, gcd(form.nform.m));
 HttpResponse::Ok()
 .content_type("text/html")
 .body(response)
}

对于用作 Actix 请求处理程序的函数,其参数必须全都是 Actix 知道该如何从 HTTP 请求中提取出来的类型。post_gcd 函数接受一个 参数 form,其类型为 web::Form。当且仅当 T 可以从 HTML 表单提交过来的数据反序列化时,Actix 才能知道该 如何从 HTTP 请求中提取任意类型为 web::Form 的值。由于我们已经将 #[derive(Deserialize)] 属性放在了GcdParameters 类型定义上,Actix 可以从表单数据中反序列化它,因此请求处理程序可以要求以 web::Form值作为参数。这些类型和函数之间的关系都是在编译期指定的。如果使用了 Actix 不知道该如何处理的参数类型来编写处理函数,那么Rust 编译器会直接向你报错。 来看看 post_gcd 内部,如果任何一个参数为 0,则该函数会先行返回 HTTP 400 BAD REQUEST 错误,因为如果它们为 0,我们的 gcd 函数将崩溃。同时,post_gcd 会使用 format! 宏来为此请求构造出响应体。format! 与 println! 很像,但它不会将文本写入标准 输出,而是会将其作为字符串返回。一旦获得响应文本,post_gcd就会将其包装在 HTTP 200 OK 响应中,设置其内容类型,并将它返回给请求者。还必须将 post_gcd 注册为表单处理程序。为此,可以将 main 函 数替换成以下这个版本:

fn main() {
 let server = HttpServer::new(|| {
 App::new()
 .route("/", web::get().to(get_index))
 .route("/gcd", web::post().to(post_gcd))
 });
 println!("Serving on http://localhost:3000...");
 server
 .bind("127.0.0.1:3000").expect("error binding server to
address")
 .run().expect("error running server");
}

这里唯一的变化是添加了另一个 route 调用,确立web::post().to(post_gcd) 作为路径 "/gcd" 的处理程序。最后剩下的部分是我们之前编写的 gcd 函数,它位于 actix-gcd/src/main.rs 文件中。有了它,你就可以中断运行中的服务器,重新构建并启动程序了:

cargo run

这一次,访问 http://localhost:3000,输入一些数值,然后单击“Compute GCD”按钮,应该会看到一些实质性结果