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

相关推荐
开心工作室_kaic8 分钟前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
一只哒布刘14 分钟前
NFS服务器
运维·服务器
有梦想的刺儿27 分钟前
webWorker基本用法
前端·javascript·vue.js
cy玩具1 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
qq_390161772 小时前
防抖函数--应用场景及示例
前端·javascript
lihuhelihu2 小时前
第3章 CentOS系统管理
linux·运维·服务器·计算机网络·ubuntu·centos·云计算
John.liu_Test2 小时前
js下载excel示例demo
前端·javascript·excel
Yaml42 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事2 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶2 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json