Rust搭建Web服务器

概述

Rust 的优势之一是在 crates.io 网站上发布的大量免费的可用包。

cargo 命令可以让你的代码轻松使用 crates.io 上的包:它将下载包的正确版本,然后会构建包,并根据用户的要求更新包。一个 Rust包,无论是库还是可执行文件,都叫作 crate(发音为 /kreɪt/,意思是"板条箱") 。

Cargo 和 crates.io 的名字都来源于这个术语。

需求

使用 actix-web(Web 框架crate)、serde(序列化 crate)以及它们所依赖的各种其他 crate来组装出一个简单的 Web 服务器。该网站会提示用户输入两个数值并计算它们的最大公约数。

创建项目

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

bash 复制代码
$ cargo new actix-gcd
Created binary (application) `actix-gcd` package
$ cd actix-gcd

配置依赖

然后,编辑新项目的 Cargo.toml 文件以列出所要使用的包,其内容应该是这样的:

toml 复制代码
[package]
name = "actix-gcd"
version = "0.1.0"
edition = "2021"
# 请到"The Cargo Book"查看更多的键及其定义
[dependencies]
actix-web = "1.0.8"
serde = { version = "1.0", features = ["derive"] }

Cargo.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页面

在第一次迭代中,我们将实现此 Web 服务器的一个简单版本:它只会给出让用户输入要计算的数值的页面。

actix-gcd/src/main.rs 的内容如下所示:

rust 复制代码
use actix_web::{web, App, HttpResponse, HttpServer};

fn main() {
	let server = HttpServer::new(|| {
		App::new().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。

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

bash 复制代码
cargo run

此刻,在浏览器中访问给定的 URL 就会看到页面。

但很遗憾,单击"Compute GCD"除了将浏览器导航到一个空白页面外,没有做任何事。为了继续解决这个问题,可以往 App 中添加另一个路由,以处理来自表单的 POST 请求。

处理表单数据

现在终于用到我们曾在 Cargo.toml 文件中列出的 serde crate了:它提供了一个便捷工具来协助处理表单数据。首先,将以下 use指令添加到 src/main.rs 的顶部:

bash 复制代码
use serde::Deserialize;

Rust 程序员通常会将所有的 use 声明集中放在文件的顶部,但这并非绝对必要:Rust 允许这些声明以任意顺序出现,只要它们出现在适当的嵌套级别即可。

接下来,定义一个 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 值并以结构化的格式序列化它们。

有了这个定义,就可以很容易地编写处理函数了:

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.n, form.m, gcd(form.n, form.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 函数替换成以下这个版本:

rust 复制代码
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 函数,它位于 actixgcd/src/main.rs 文件中。有了它,你就可以中断运行中的服务器,重新构建并启动程序了:

bash 复制代码
cargo run

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

实战案例1:配置国内源

字节跳动新的 Rust 镜像源以及安装rust

修改配置文件:

bash 复制代码
vi ~/.cargo/config

完整代码如下:

bash 复制代码
[source.crates-io]
replace-with = 'rsproxy'

[source.rsproxy]
registry = "https://rsproxy.cn/crates.io-index"

[registries.rsproxy]
index = "https://rsproxy.cn/crates.io-index"

[net]
git-fetch-with-cli = true

配置Rustup Mirror,修改配置文件:

bash 复制代码
vim ~/.bashrc

在末尾追加:

bash 复制代码
export RUSTUP_DIST_SERVER="https://rsproxy.cn"
export RUSTUP_UPDATE_ROOT="https://rsproxy.cn/rustup"

如果需要安装Rust,完成上面2步,然后执行:

bash 复制代码
curl --proto '=https' --tlsv1.2 -sSf https://rsproxy.cn/rustup-init.sh | sh  

实战案例2:显示网页内容

创建项目:

bash 复制代码
cargo new hello

引入依赖,修改:

bash 复制代码
cd hello
vim Cargo.toml

完整代码如下:

toml 复制代码
[package]
name = "hello"
version = "0.1.0"
edition = "2021"

[dependencies]
actix-web = "4"

编写代码,修改:

bash 复制代码
vim src/main.rs

完整代码如下:

rust 复制代码
use actix_web::{get, web, App, HttpServer, Responder};

#[get("/hello/{name}")]
async fn greet(name: web::Path<String>) -> impl Responder {
    format!("Hello {name}")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let server = HttpServer::new(|| {
        App::new().service(greet)
    });
    println!("Serving on http://localhost:3000");
    server.bind("0.0.0.0:3000")?
        .run()
        .await
}

运行程序:

bash 复制代码
(base) zhangdapeng@zhangdapeng:~/code/rust/hello$ cargo run
warning: `/home/zhangdapeng/.cargo/config` is deprecated in favor of `config.toml`
note: if you need to support cargo 1.38 or earlier, you can symlink `config` to `config.toml`
warning: `/home/zhangdapeng/.cargo/config` is deprecated in favor of `config.toml`
note: if you need to support cargo 1.38 or earlier, you can symlink `config` to `config.toml`
   Compiling hello v0.1.0 (/home/zhangdapeng/code/rust/hello)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.45s
     Running `target/debug/hello`
Serving on http://localhost:3000

浏览器访问:http://192.168.77.129:3000/hello/zhangdapeng

相关推荐
ekskef_sef38 分钟前
32岁前端干了8年,是继续做前端开发,还是转其它工作
前端
群联云防护小杜1 小时前
如何给负载均衡平台做好安全防御
运维·服务器·网络·网络协议·安全·负载均衡
sunshine6411 小时前
【CSS】实现tag选中对钩样式
前端·css·css3
奈何不吃鱼1 小时前
【Linux】ubuntu依赖安装的各种问题汇总
linux·运维·服务器
爱码小白1 小时前
网络编程(王铭东老师)笔记
服务器·网络·笔记
蜜獾云1 小时前
linux firewalld 命令详解
linux·运维·服务器·网络·windows·网络安全·firewalld
真滴book理喻1 小时前
Vue(四)
前端·javascript·vue.js
蜜獾云1 小时前
npm淘宝镜像
前端·npm·node.js
dz88i81 小时前
修改npm镜像源
前端·npm·node.js
Jiaberrr2 小时前
解锁 GitBook 的奥秘:从入门到精通之旅
前端·gitbook