前言:
现在我们对“执行当前web请求期间生成了未经处理的异常信息”大约比较讲究,姐妹们都需要了解一些“执行当前web请求期间生成了未经处理的异常信息”的相关知识。那么小编也在网上搜集了一些对于“执行当前web请求期间生成了未经处理的异常信息””的相关知识,希望大家能喜欢,小伙伴们快快来学习一下吧!到目前为止,我们已经以灵活、可扩展和可重用的方式构建了待办事项模块。 然而,就网络编程而言,这只能让我们到目前为止。 我们希望我们的待办事项模块能够快速到达多人,而用户无需在自己的计算机上安装 Rust。 我们可以使用网络框架来做到这一点。 Rust 有很多东西可以提供。 最初,我们将在 Actix Web 框架中构建主服务器。
为了实现这一目标,我们将以模块化方式构建服务器的视图; 我们可以毫不费力地将待办事项模块插入到我们的 Web 应用程序中。 必须注意的是,Actix Web 框架使用异步函数定义视图。 因此,我们还将介绍异步编程,以更好地了解 Actix Web 框架的工作原理。
在本章中,我们将讨论以下主题:
Actix Web 框架简介
启动基本 Actix Web 服务器
了解闭包
了解异步编程
通过 Web 编程了解 async 和 wait
使用 Actix Web 框架管理视图
技术要求
当我们转向使用 Rust 构建 Web 应用程序时,我们将不得不开始依赖第三方软件包来为我们完成一些繁重的工作。 Rust 通过一个名为 Cargo 的包管理器来管理依赖项。 要使用 Cargo,我们必须通过以下 URL 在我们的计算机上安装 Rust:。
Actix Web 框架简介
在撰写本文时,Actix Web 是最流行的 Rust Web 框架,从 GitHub 页面上的活动可以看出。 您可能会想跳入另一个看起来更符合人体工程学的框架,例如 Rocket,或者更快、更轻量级的框架,例如 Hyper。 我们将在本书后面的各个不同章节中介绍这些框架; 然而,我们必须记住,我们首先要尝试了解 Rust 的 Web 编程。
考虑到我们是 Rust 和 Web 编程的新手,Actix Web 是一个很好的开始。 这并不是太低级的,我们只是试图让服务器来处理一系列视图、数据库连接和身份验证。 它也很流行、稳定,并且有大量文档。 当尝试超越本书并开发自己的 Web 应用程序时,这将有助于获得愉快的编程体验。 建议您先熟悉 Actix Web,然后再转向其他 Web 框架。 这并不是说 Actix Web 是最好的并且所有其他框架都很糟糕; 这只是为了促进顺利的学习和发展体验。 考虑到这一点,我们现在可以继续第一部分,在那里我们设置一个基本的 Web 服务器。
启动基本 Actix Web 服务器
使用 Cargo 进行构建非常简单。 我们需要做的就是导航到要构建项目的目录并运行以下命令:
cargo new web_app
前面的命令构建了一个基本的 Cargo Rust 项目。 当我们探索这个应用程序时,我们得到以下结构:
└── web_app ├── Cargo.toml └── src └── main.rs
现在,我们可以使用以下代码在 Cargo.toml 文件中定义 Actix Web 依赖项:
[dependencies]actix-web = "4.0.1"
根据前面的代码,我们现在可以继续构建 Web 应用程序。 现在,我们将使用以下代码将其全部放入 src/main.rs 文件中:
use actix_web::{web, App, HttpServer, Responder, HttpRequest};async fn greet(req: HttpRequest) -> impl Responder { let name = req.match_info().get("name").unwrap_or("World"); format!("Hello {}!", name)}#[actix_web::main]async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() .route("/", web::get().to(greet)) .route("/{name}", web::get().to(greet)) .route("/say/hello", web::get().to(|| async { "Hello Again!" })) }) .bind("127.0.0.1:8080")? .run() .await}
在前面的代码中,我们可以看到我们从 actix_web 箱中导入了所需的结构和特征。 我们可以看到我们使用了几种不同的方式来定义视图。 我们通过构建函数来定义视图。 这需要一个 HttpRequest 结构。 然后它从请求中获取名称,然后返回一个可以从 actix_web 箱中实现 Responder 特征的变量。 Responder 特征将我们的类型转换为 HTTP 响应。 我们使用 .route("/", web::get().to(greet)) 命令将为应用程序服务器创建的greet函数指定为路由视图。 我们还可以看到,我们可以使用 .route("/{name}", web::get().to(greet)) 命令将 URL 中的名称传递到greet 函数。 最后,我们将一个闭包传递到最终路由中。 根据我们的配置,让我们运行以下命令:
cargo run
我们将得到以下打印输出:
Finished dev [unoptimized + debuginfo] target(s) in 0.21s Running `target/debug/web_app`
我们可以在前面的输出中看到,现在没有日志记录。 这是预期的,我们稍后将配置日志记录。 现在我们的服务器正在运行,对于以下每个 URL 输入,我们应该期望浏览器中出现相应的输出:
Input: : Hello World!Input: : Hello maxwell!Input: : Hello Again!
在 src/main.rs 文件中的前面的代码中,我们可以看到有一些我们以前没有遇到过的新语法。 我们用 #[actix_web::main] 宏装饰了 main 函数。 这标志着我们的异步主函数作为 Actix Web 系统的入口点。 这样,我们可以看到我们的函数是异步的,并且我们正在使用闭包来构建我们的服务器。 我们将在接下来的几节中介绍这两个概念。 在下一节中,我们将研究闭包以真正了解正在发生的情况。
了解闭包
闭包本质上是函数,但它们也是匿名的,这意味着它们没有名称。 这意味着闭包可以传递到函数和结构中。 然而,在我们深入研究传递闭包之前,让我们通过使用以下代码在空白 Rust 程序中定义基本闭包(如果您愿意,可以使用 Rust 游乐场)来探索闭包:
fn main() { let test_closure = |string_input| { println!("{}", string_input); }; test_closure("test");}
运行前面的代码将为我们提供以下打印输出:
test
在前面的输出中,我们可以看到我们的闭包的行为就像一个函数。 但是,我们不使用大括号来定义输入,而是使用管道。
您可能已经注意到,在前面的闭包中,我们没有定义 string_input 参数的数据类型; 但是,代码仍然运行。 这与需要定义参数数据类型的函数不同。 这是因为函数是向用户公开的显式接口的一部分。 如果代码可以访问函数,则可以在代码中的任何位置调用该函数。 另一方面,闭包的生命周期很短,并且仅与它们所在的作用域相关。因此,编译器可以根据作用域中闭包的使用推断出传递到闭包中的类型。 因为我们在调用闭包时传入了 &str,所以编译器知道 string_input 类型是 &str。 虽然这很方便,但我们需要知道闭包不是通用的。 这意味着闭包具有具体类型。 例如,定义闭包后,让我们尝试运行以下代码:
test_closure("test"); test_closure(23);
我们会得到以下错误:
7 | test_closure(23); | ^^ expected `&str`, found integer
发生错误是因为对闭包的第一次调用告诉编译器我们需要 &str,因此第二次调用会中断编译过程。
范围不仅仅影响闭包。 闭包遵循与变量相同的作用域规则。 例如,假设我们要尝试运行以下代码:
fn main() { { let test_closure = |string_input| { println!("{}", string_input); }; } test_closure("test");}
它会拒绝编译,因为当我们尝试调用闭包时,它不在调用范围内。 考虑到这一点,您可以正确地假设其他范围规则适用于闭包。 例如,如果我们尝试运行以下代码,您认为会发生什么?
fn main() { let another_str = "case"; let test_closure = |string_input| { println!("{} {}", string_input, another_str); }; test_closure("test");}
如果您认为我们会得到以下输出,那么您是对的:
test case
与函数不同,闭包可以访问自己作用域内的变量。 因此,为了尝试以我们可以理解的简单方式描述闭包,它们有点像我们调用来执行计算的作用域中的动态变量。
我们可以通过利用 move 来获取闭包中使用的外部变量的所有权,如以下代码所示:
let test_closure = move |string_input| { println!("{} {}", string_input, another_str);};
因为这里定义的闭包中使用了 move,所以在声明 test_closure 后就不能使用 another_str 变量,因为 test_closure 取得了 another_str 的所有权。
我们还可以将闭包传递给函数; 但是,必须注意的是,我们也可以将函数传递给其他函数。 我们可以通过以下代码实现将函数传递给其他函数:
fn add_doubles(closure: fn(i32) -> i32, one: i32, two: i32) -> i32 { return closure(one) + closure(two)}fn main() { let closure = |int_input| { return int_input * 2 }; let outcome = add_doubles(closure, 2, 3); println!("{}", outcome);}
在前面的代码中,我们可以看到我们定义了一个闭包,它将传入和返回的整数加倍。 然后,我们使用 fn(i32)-> i32 的表示法将其传递到 add_doubles 函数中,这称为函数指针。 当涉及到闭包时,我们可以实现以下特征之一:
Fn:不可变地借用变量
FnMut:可变借用变量
FnOnce:获取变量的所有权,因此只能调用一次
我们可以使用以下代码将一个实现了上述特征之一的闭包传递到我们的 add_doubles 函数中:
fn add_doubles(closure: Box<dyn Fn(i32) -> i32>, one: i32, two: i32) -> i32 { return closure(one) + closure(two)}fn main() { let one = 2; let closure = move |int_input| { return int_input * one }; let outcome = add_doubles(Box::new(closure), 2, 3); println!("{}", outcome);}
在这里,我们可以看到闭包函数参数具有 Box<dyn Fn(i32) -> i32> 签名。 这意味着 add_doubles 函数接受已实现接受 i32 并返回 i32 的 Fn 特征的闭包。 Box 结构是一个智能指针,我们将闭包放在堆上,因为我们在编译时不知道闭包的大小。 您还可以看到我们在定义闭包时使用了 move。 这是因为我们使用的是闭包外部的一个变量。 一个变量可能存在的时间不够长; 因此,闭包拥有它的所有权,因为我们在定义闭包时使用了 move。
考虑到我们已经介绍过的关于闭包的内容,我们可以使用以下代码再次查看服务器应用程序中的 main 函数:
#[actix_web::main]async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() .route("/", web::get().to(greet)) .route("/{name}", web::get().to(greet)) .route("/say/hello", web::get().to( || async { "Hello Again!" })) }) .bind("127.0.0.1:8080")? .run() .await}
在前面的代码中,我们可以看到我们在使用 HttpServer::new 函数构建 HttpServer 后正在运行它。 知道我们现在所知道的,我们可以看到我们已经传入了一个返回 App 结构的闭包。 根据我们对闭包的了解,我们可以对使用此代码执行的操作更加自信。 如果它返回 App 结构,我们基本上可以在闭包中做我们喜欢的事情。 考虑到这一点,我们可以通过以下代码获得有关该过程的更多信息:
#[actix_web::main]async fn main() -> std::io::Result<()> { HttpServer::new(|| { println!("http server factory is firing"); App::new() .route("/", web::get().to(greet)) .route("/{name}", web::get().to(greet)) .route("/say/hello", web::get().to( || async { "Hello Again!" })) }) .bind("127.0.0.1:8080")? .workers(3) .run() .await}
在前面的代码中,我们可以看到我们添加了一条 print 语句来告诉我们闭包正在触发。 我们还添加了另一个称为worker的功能。 这意味着我们可以定义使用多少worker来创建我们的服务器。 我们还打印出服务器工厂正在关闭中。 运行前面的代码会得到以下打印输出:
Finished dev [unoptimized + debuginfo] target(s) in 2.45s Running `target/debug/web_app`http server factory is firinghttp server factory is firinghttp server factory is firing
前面的结果告诉我们,闭包被触发了 3 次。 改变工人数量向我们表明,这与解雇关闭的次数之间有直接关系。 如果省略了workers函数,则根据系统拥有的核心数量来触发闭包。 我们将在下一节中探讨这些工作人员如何融入服务器进程。
现在我们了解了 App 结构构建的细微差别,是时候看看程序结构的主要变化,即异步编程。
了解异步编程
直到本章为止,我们一直在按顺序编写代码。 这对于标准脚本来说已经足够了。 然而,在 Web 开发中,异步编程很重要,因为对服务器有多个请求,并且 API 调用会引入空闲时间。 在其他一些语言中,例如Python,我们可以构建Web服务器而无需触及任何异步概念。 虽然这些 Web 框架中使用了异步概念,但实现是在幕后定义的。 Rust 框架 Rocket 也是如此。 然而,正如我们所见,它是直接在 Actix Web 中实现的。
在使用异步代码时,我们必须理解两个主要概念:
进程:进程是正在执行的程序。 它有自己的内存堆栈、变量寄存器和代码。
线程:线程是由调度程序独立管理的轻量级进程。 但是,它确实与其他线程和主程序共享数据、代码和堆。 但是,线程不共享堆栈。
下面的经典图表演示了这一点:
现在我们已经了解了线程是什么以及它们与我们的代码在高层次上的关系,我们可以使用一个玩具示例来了解如何在代码中使用线程并直接查看这些线程的效果。 一个典型的例子是构建一个仅休眠、阻塞时间的基本函数。 这可以模拟耗时的功能,例如网络请求。 我们可以使用以下代码顺序运行它:
use std::{thread, time};fn do_something(number: i8) -> i8 { println!("number {} is running", number); let two_seconds = time::Duration::new(2, 0); thread::sleep(two_seconds); return 2}fn main() { let now = time::Instant::now(); let one: i8 = do_something(1); let two: i8 = do_something(2); let three: i8 = do_something(3); println!("time elapsed {:?}", now.elapsed()); println!("result {}", one + two + three);}
运行前面的代码将为我们提供以下打印输出:
number 1 is runningnumber 2 is runningnumber 3 is runningtime elapsed 6.0109845sresult 6
在前面的输出中,我们可以看到耗时的函数按照我们期望的顺序运行。 运行整个程序只需要 6 秒多一点的时间,这是有道理的,因为我们正在运行三个昂贵的函数,每个函数休眠 2 秒。 我们的昂贵函数还返回值 2。当我们将所有三个昂贵函数的结果加在一起时,我们将得到值 6 的结果,这就是我们所拥有的。 我们通过同时启动三个线程并等待它们完成后再继续,将整个程序的速度加快到大约 2 秒。 在继续之前等待线程完成称为连接。 因此,在开始分拆线程之前,我们必须使用以下代码导入连接处理程序:
use std::thread::JoinHandle;
现在,我们可以使用以下代码在主函数中启动线程:
let now = time::Instant::now();let thread_one: JoinHandle<i8> = thread::spawn( || do_something(1));let thread_two: JoinHandle<i8> = thread::spawn( || do_something(2));let thread_three: JoinHandle<i8> = thread::spawn( || do_something(3));let result_one = thread_one.join();let result_two = thread_two.join();let result_three = thread_three.join();println!("time elapsed {:?}", now.elapsed());println!("result {}", result_one.unwrap() + result_two.unwrap() + result_three.unwrap());
运行前面的代码会得到以下打印输出:
number 1 is runningnumber 3 is runningnumber 2 is runningtime elapsed 2.002991041sresult 6
我们可以看到,整个过程仅花费了 2 秒多一点的时间。 这是因为所有三个线程都同时运行。 另请注意,线程三在线程二之前触发。 如果您得到的序列为 1、2 和 3,请不要担心。线程以不确定的顺序完成。 调度是确定性的; 然而,在幕后发生了数以千计的事件,需要 CPU 执行某些操作。 因此,每个线程获得的确切时间片永远不会相同。 这些微小的变化加起来。 因此,我们无法保证线程将按确定的顺序完成。
回顾一下我们如何分离线程,我们可以看到我们将一个闭包传递到我们的线程中。 如果我们尝试通过线程传递 do_something 函数,我们会收到一个错误,抱怨编译器期望 FnOnce<()> 闭包,但却发现了 i8。 这是因为标准闭包实现了 FnOnce<()> 公共特征,而我们的 do_something 函数只是返回 i8。 当实现 FnOnce<()> 时,闭包只能被调用一次。 这意味着当我们创建一个线程时,我们可以保证闭包只能被调用一次,然后当它返回时,线程就结束了。 由于我们的 do_something 函数是闭包的最后一行,因此返回 i8。 然而,必须注意的是,仅仅因为实现了 FnOnce<()> 特征,并不意味着我们不能多次调用它。 仅当上下文需要时才会调用此特征。 这意味着如果我们要在线程上下文之外调用闭包,我们可以多次调用它。
另请注意,我们直接解开结果。 根据我们所知,我们可以推断出 JoinHandle 结构体上的 join 函数返回 Result,我们也知道它可以是 Err 或 Ok。 我们知道直接解开结果是可以的,因为我们只是在sleep然后返回一个整数。 我们还打印出了结果,确实是整数。 然而,我们的错误并不是您所期望的。 我们得到的完整结果类型是 Result<i8, Box<dyn Any + Send>>。 我们已经知道 Box 是什么; 然而,dyn Any + Send 似乎是新的。 dyn 是一个关键字,我们用它来指示正在使用的特征类型。
Any 和 Send 是必须实现的两个特征。 Any 特征用于动态类型,这意味着数据类型可以是任何类型。 Send 特征意味着从一个线程移动到另一个线程是安全的。 Send 特征还意味着从一个线程复制到另一个线程是安全的。 因此,我们发送的内容已经实现了 Copy 特征,因为我们发送的内容可以在线程之间发送。 现在我们明白了这一点,我们可以通过仅匹配 Result 结果来处理线程的结果,然后将错误向下转换为字符串以使用以下代码获取错误消息:
match thread_result { Ok(result) => { println!("the result for {} is {}", result, name); } Err(result) => { if let Some(string) = result.downcast_ref::<String>() { println!("the error for {} is: {}", name, string); } else { println!("there error for {} does not have a message", name); } }}
前面的代码使我们能够优雅地管理线程的结果。 现在,没有什么可以阻止您记录线程的失败或根据先前线程的结果启动新线程。 由此,我们可以看出 Result 结构体有多么强大。 我们可以对线程做更多的事情,例如给它们命名或通过通道在它们之间传递数据。 然而,本书的重点是Web编程,而不是高级并发设计模式和概念。 然而,本章末尾提供了有关该主题的进一步阅读。
我们现在了解了如何在 Rust 中启动线程、它们返回什么以及如何处理它们。 有了这些信息,我们就可以继续下一节了解 async 和 wait 语法,因为这将在我们的 Actix Web 服务器中使用。
了解async和await
async 和await 语法管理上一节中介绍的相同概念; 然而,存在一些细微差别。 我们不是简单地生成线程,而是创建 future,然后在需要时操纵它们。
在计算机科学中,未来是未经处理的计算。 这是结果尚不可用的地方,但是当我们调用或等待时,未来将填充计算结果。 描述这一点的另一种方式是,未来是表达尚未准备好的价值的一种方式。 因此,未来并不完全是一个线程。 事实上,线程可以利用 future 来最大限度地发挥其潜力。 例如,假设我们有多个网络连接。 我们可以为每个网络连接拥有一个单独的线程。 这比顺序处理所有连接要好,因为慢速网络连接会阻止其他更快的连接被处理,直到它本身被处理,从而导致整体处理时间变慢。 然而,为每个网络连接旋转线程并不是免费的。 相反,我们可以为每个网络连接拥有一个未来。 当未来准备好时,这些网络连接可以由线程池中的线程处理。 因此,我们可以明白为什么在Web编程中使用Future,因为有很多并发连接。
future也可以称为承诺、延迟或延期。 为了探索 futures,我们将创建一个新的 Cargo 项目并利用 Cargo.toml 文件中创建的 futures:
[dependencies]futures = "0.3.21"
安装了前面的crate后,我们可以使用以下代码在 main.rs 中导入我们需要的内容:
use futures::executor::block_on;use std::{thread, time};
我们可以仅使用异步语法来定义 future。 block_on 函数将阻塞程序,直到我们定义的 future 被执行。 我们现在可以使用以下代码定义 do_something 函数:
async fn do_something(number: i8) -> i8 { println!("number {} is running", number); let two_seconds = time::Duration::new(2, 0); thread::sleep(two_seconds); return 2}
do_something 函数本质上执行代码所说的操作,即打印出它是什么数字,休眠 2 秒,然后返回一个整数。 然而,如果我们直接调用它,我们就得不到i8。 相反,直接调用 do_something 函数将为我们提供 Future<Output = i8>。 我们可以使用以下代码在主函数中运行 future 并为其计时:
fn main() { let now = time::Instant::now(); let future_one = do_something(1); let outcome = block_on(future_one); println!("time elapsed {:?}", now.elapsed()); println!("Here is the outcome: {}", outcome);}
运行前面的代码将为我们提供以下打印输出:
number 1 is runningtime elapsed 2.00018789sHere is the outcome: 2
这是我们所期望的。 但是,让我们看看如果在使用以下代码调用 block_on 函数之前输入额外的睡眠函数会发生什么:
fn main() { let now = time::Instant::now(); let future_one = do_something(1); let two_seconds = time::Duration::new(2, 0); thread::sleep(two_seconds); let outcome = block_on(future_one); println!("time elapsed {:?}", now.elapsed()); println!("Here is the outcome: {}", outcome);}
我们将得到以下打印输出:
number 1 is runningtime elapsed 4.000269667sHere is the outcome: 2
因此,我们可以看到,在我们使用 block_on 函数应用执行器之前,我们的 future 不会执行。
这可能有点费力,因为我们可能只想要一个可以稍后在同一函数中执行的 future。 我们可以使用 async/await 语法来做到这一点。 例如,我们可以调用 do_something 函数并阻塞代码,直到使用 main 函数内的等待语法完成为止,代码如下:
let future_two = async { return do_something(2).await};let future_two = block_on(future_two);println!("Here is the outcome: {:?}", future_two);
异步块的作用是返回一个 future。 在这个块中,我们调用 do_something 函数来阻塞异步块,直到 do_something 函数通过使用await 表达式得到解析。 然后我们将 block_on 函数应用于 future_two 的future。
看看我们前面的代码块,这可能看起来有点多余,因为只需两行调用 do_something 函数并将其传递给 block_on 函数的代码即可完成。 在这种情况下,它是多余的,但它可以让我们在如何调用future方面有更多的灵活性。 例如,我们可以使用以下代码调用 do_something 函数两次并将它们添加在一起作为返回:
let future_three = async { let outcome_one = do_something(2).await; let outcome_two = do_something(3).await; return outcome_one + outcome_two};let future_outcome = block_on(future_three);println!("Here is the outcome: {:?}", future_outcome);
将前面的代码添加到我们的 main 函数中将为我们提供以下打印输出:
number 2 is runningnumber 3 is runningHere is the outcome: 4
虽然前面的输出是我们期望的结果,但我们知道这些 future 将按顺序运行,并且该代码块的总时间将略高于 4 秒。 也许我们可以通过使用 join 来加快速度。 我们已经看到连接通过同时运行线程来加速线程。 这确实有道理,它也将有助于加快我们的未来发展。 首先,我们必须使用以下代码导入连接宏:
use futures::join
现在,我们可以使用 join 来实现 future,并使用以下代码来计时实施:
let future_four = async { let outcome_one = do_something(2); let outcome_two = do_something(3); let results = join!(outcome_one, outcome_two); return results.0 + results.1};let now = time::Instant::now();let result = block_on(future_four);println!("time elapsed {:?}", now.elapsed());println!("here is the result: {:?}", result);
在前面的代码中,我们可以看到 join 宏返回结果的元组,并且我们解压缩该元组以给出相同的结果。 然而,如果我们运行代码,我们可以看到,虽然我们得到了我们想要的结果,但我们未来的执行速度并没有加快,仍然停留在 4 秒以上。 这是因为 future 没有使用异步任务运行。 我们将不得不使用异步任务来加快 future 的执行速度。 我们可以通过执行以下步骤来实现这一目标:
创造所需的future。
将它们放入向量中。
循环遍历向量,为向量中的每个未来分派任务。
连接异步任务并对向量求和。
这可以通过下图直观地映射出来:
Figure 3.2 – The steps to running multiple futures at once
要同时加入所有 future,我们必须使用另一个 crate,通过 async_std crate 创建我们自己的异步连接函数。 我们使用以下代码在 Cargo.toml 文件中定义此箱子:
async-std = "1.11.0"
现在我们有了 async_std ,我们可以导入执行图 3.2 中列出的方法所需的内容,方法是使用以下代码在 main.rs 文件顶部导入我们需要的内容:
use std::vec::Vec;use async_std;use futures::future::join_all;
在主函数中,我们现在可以使用以下代码定义我们的未来:
let async_outcome = async { // 1. let mut futures_vec = Vec::new(); let future_four = do_something(4); let future_five = do_something(5); // 2. futures_vec.push(future_four); futures_vec.push(future_five); // 3. let handles = futures_vec.into_iter().map( async_std::task::spawn).collect::<Vec<_>>(); // 4. let results = join_all(handles).await; return results.into_iter().sum::<i8>();};
在这里,我们可以看到我们定义了future (1),然后将它们添加到vec (2) 中。 然后,我们使用 into_iter 函数循环遍历向量中的 future。 然后,我们使用 async_std::task::spawn 在每个 future 上生成一个线程。 这与 std::task::spawn 类似。 那么,为什么要为这些额外的头痛烦恼呢? 我们可以循环遍历向量并为每个任务生成一个线程。 这里的区别在于 async_std::task::spawn 函数在同一线程中分拆异步任务。 因此,我们在同一个线程中同时运行两个 future! 然后,我们连接所有句柄,等待这些任务完成,然后返回所有这些线程的总和。 现在我们已经定义了 async_outcome future,我们可以使用以下代码运行它并对其计时:
let now = time::Instant::now();let result = block_on(async_outcome);println!("time elapsed for join vec {:?}", now.elapsed());println!("Here is the result: {:?}", result);
运行我们的附加代码将给出以下附加打印输出:
number 4 is runningnumber 5 is runningtime elapsed for join vec 2.007713458sHere is the result: 4
它正在工作! 我们成功地让两个异步任务在同一个线程中同时运行,导致两个 future 在 2 秒多一点的时间内执行完毕!
正如我们所看到的,在 Rust 中生成线程和异步任务非常简单。 但是,我们必须注意,将变量传递给线程和异步任务则不然。 Rust 的借用机制确保了内存安全。 将数据传递到线程时,我们必须执行额外的步骤。 进一步讨论线程之间共享数据背后的一般概念不利于我们的 Web 项目。 但是,我们可以简要说明哪些类型允许我们共享数据:
std::sync::Arc:此类型使线程能够引用外部数据:
use std::sync::Arc;use std::thread;let names = Arc::new(vec!["dave", "chloe", "simon"]);let reference_data = Arc::clone(&names); let new_thread = thread::spawn(move || { println!("{}", reference_data[1]);});std::sync::Mutex:这种类型使线程能够改变外部数据:
use std::sync::Mutex;use std::thread;let count = Mutex::new(0); let new_thread = thread::spawn(move || { count.lock().unwrap() += 1;});
在线程内部,我们取消引用锁的结果,解开它,并改变它。 必须注意的是,只有持有锁后才能访问共享状态。
我们现在已经介绍了足够多的异步编程,可以回到我们的 Web 编程了。 并发性是一个可以用整本书来涵盖的主题,进一步阅读部分引用了其中一个主题。 现在,我们必须回到 Web 开发中探索 Rust,看看我们对 Rust 异步编程的了解如何影响我们对 Actix Web 服务器的理解。
通过 Web 编程探索异步和等待
了解了我们对异步编程的了解后,我们现在可以从不同的角度来看待 Web 应用程序中的 main 函数,如下所示:
#[actix_web::main]async fn main() -> std::io::Result<()> { HttpServer::new( || { App::new() .route("/", web::get().to(greet)) .route("/{name}", web::get().to(greet)) .route("/say/hello", web::get().to(|| async { "Hello Again!" })) }) .bind("127.0.0.1:8080")? .workers(3) .run() .await}
我们知道我们的greet函数是一个异步函数,因此是一个future。 我们还可以看到,传递到 /say/hello 视图的闭包也使用了异步语法。 我们还可以看到 HttpServer::new 函数在 async fn main() 中使用了await 语法。 因此,我们可以推断我们的 HttpServer::new 函数是一个执行器。 但是,如果我们删除 #[actix_web::main] 宏,我们会收到以下错误:
`main` function is not allowed to be `async`
这是因为我们的主函数(即我们的入口点)将返回一个 future,而不是运行我们的程序。 #[actix_web::main] 是一个运行时实现,使所有内容都可以在当前线程上运行。 #[actix_web::main] 宏标记要由 Actix 系统执行的异步函数(在本例中为主函数)。
笔记
冒着陷入困境的风险,Actix crate 基于 actor 模型运行并发计算。 这就是actor进行计算的地方。 参与者可以相互发送和接收消息。 Actor 可以改变自己的状态,但它们只能通过消息影响其他 Actor,这消除了基于锁的同步的需要(我们介绍的互斥体是基于锁的)。 进一步探索这个模型并不能帮助我们开发基本的网络应用程序。 然而,Actix crate 确实有关于使用 Actix 编码并发系统的良好文档,网址为 。
我们在这里已经介绍了很多内容。 如果您觉得自己没有保留所有内容,请不要感到压力。 我们简要介绍了有关异步编程的一系列主题。 我们不需要彻底理解它就可以开始构建基于 Actix Web 框架的应用程序。
您可能还会觉得我们所涵盖的内容过多。 例如,我们可以启动一个服务器,并在需要时使用异步语法,仅在不真正知道发生了什么的情况下打出视图。 不了解正在发生的事情,但知道在哪里放置异步不会让我们在构建玩具应用程序时放慢速度。 然而,当涉及到调试和设计应用程序时,这种短暂的游览是非常宝贵的。 为了证实这一点,我们可以看一个实际的例子。 我们可以看看这个智能 Stack Overflow 解决方案,用于在一个文件中运行多个服务器:- Different-ports。
Stack Overflow 解决方案中的代码基本上涉及在一个运行时运行两台服务器。 首先,他们使用以下代码定义视图:
use actix_web::{web, App, HttpServer, Responder};use futures::future;async fn utils_one() -> impl Responder { "Utils one reached\n"}async fn health() -> impl Responder { "All good\n"}
一旦定义了视图,就在 main 函数中定义两个服务器:
#[actix_rt::main]async fn main() -> std::io::Result<()> { let s1 = HttpServer::new(move || { App::new().service(web::scope("/utils").route( "/one", web::get().to(utils_one))) }) .bind("0.0.0.0:3006")? .run(); let s2 = HttpServer::new(move || { App::new().service(web::resource( "/health").route(web::get().to(health))) }) .bind("0.0.0.0:8080")? .run(); future::try_join(s1, s2).await?; Ok(())}
我没有在这段代码中添加任何符号,但它不应该吓到你。 我们可以自信地推断出 s1 和 s2 是 run 函数返回的 future。 然后我们将这两个 future 连接在一起并等待它们完成。 我们的代码和 Stack Overflow 解决方案中的代码也有细微的差别。 我们的解决方案利用了await? 然后返回 Ok 并包含以下代码片段:
future::try_join(s1, s2).await?; Ok(())}
这是因为一个? 运算符本质上是尝试匹配。 join(s1,s2).await? 大致展开为以下代码:
match join(s1, s2).await { Ok(v) => v, Err(e) => return Err(e.into()),}
而 join(s1, s2).await.unwrap() 大致扩展为以下代码:
match join(s1, s2).await { Ok(v) => v, Err(e) => panic!("unwrap resulted in {}", e),}
因为? 运算符,提供解决方案的人必须在末尾插入 Ok,因为主函数返回 Result,而这通过实现 ? 被取消。
因此,在疯狂的解决方案中,Stack Overflow 已经证明了涵盖异步编程的重要性。 我们可以查看代码并弄清楚正在发生什么以及 Stack Overflow 上的海报如何实现他们所做的事情。 这也意味着我们可以自己发挥创造力。 没有什么可以阻止我们创建三个服务器并在主函数中运行它们。 这就是 Rust 真正闪耀的地方。 花时间学习 Rust 让我们能够安全地潜入低级领域,并对我们所做的事情进行更细粒度的控制。 您会发现在使用 Rust 进行编程的任何领域都是如此。
在尝试构建应用程序之前,我们还应该研究一个概念,这就是运行时。 我们知道,我们必须有 Actix Web 宏才能使 main 函数成为future。 如果我们查看 Tokio crate,我们可以发现它是 Rust 编程语言的异步运行时,提供了编写网络应用程序所需的构建块。 Tokio 的运作很复杂; 但是,如果我们查看有关加速运行时的 Tokio 文档,我们可以添加如下图所示:
在上图中,我们可以看到有任务排队并且处理器正在处理它们。 我们之前处理了我们的任务,所以这看起来应该很熟悉。 考虑到这一点,知道我们可以使用 Tokio 而不是 Actix Web 宏来运行我们的服务器可能不会太令人震惊。 为此,我们使用以下代码在 Cargo.toml 文件中定义 Tokio 依赖项:
tokio = { version = "1.17.0", features = ["full"] }
通过前面的代码,我们现在可以使用以下代码在 main.rs 文件中切换宏:
#[tokio::main]async fn main() -> std::io::Result<()> { HttpServer::new( || { App::new() .route("/", web::get().to(greet)) .route("/{name}", web::get().to(greet)) }) .bind("127.0.0.1:8080")? .bind("127.0.0.1:8081")? .workers(3) .run() .await}
运行前面的代码将为我们提供与运行服务器相同的结果。 使用 Tokio 而不是 Actix 运行时宏时可能会出现一些不一致的情况。 虽然这是一个有趣的结果,它展示了我们如何自信地配置我们的服务器,但在本书的其余部分中,当涉及到在 Actix 中开发待办事项应用程序时,我们将使用 Actix 运行时宏。 我们将在第 14 章“探索 Tokio 框架”中重新审视 Tokio。
我们现在已经介绍了足够的服务器配置以及服务器如何处理请求以提高工作效率。 我们现在可以继续定义我们的视图以及如何在下一节中处理它们。
使用 Actix Web 框架管理视图
到目前为止,我们已经在 main.rs 文件中定义了所有视图。 这对于小型项目来说很好; 然而,随着我们项目的发展,这将无法很好地扩展。 找到正确的观点可能很困难,更新它们可能会导致错误。 它还使得从 Web 应用程序中删除模块或将其插入到 Web 应用程序中变得更加困难。 此外,如果我们将所有视图都定义在一个页面上,那么如果更大的团队正在开发该应用程序,这可能会导致大量合并冲突,因为如果他们正在更改以下内容的定义,他们都将想要更改同一个文件。 因此,最好保留模块中包含的一组视图的逻辑。 我们可以通过构建一个处理身份验证的模块来探索这一点。 在本章中,我们不会围绕身份验证构建逻辑,但在探索如何管理视图模块的结构时,这是一个很好的简单示例。 在我们编写任何代码之前,我们的 Web 应用程序应该具有以下文件布局:
├── main.rs└── views ├── auth │ ├── login.rs │ ├── logout.rs │ └── mod.rs ├── mod.rs
每个文件内的代码可以描述如下:
main.rs:定义服务器的入口点
views/auth/login.rs:定义登录视图的代码
views/auth/logout.rs:定义注销视图的代码
views/auth/mod.rs:定义 auth 视图的工厂
views/mod.rs:定义整个应用程序所有视图的工厂
首先,让我们从一个基本的 Web 服务器开始我们的入口点,main.rs 文件中没有额外的内容,代码如下:
use actix_web::{App, HttpServer};mod views;#[actix_web::main]async fn main() -> std::io::Result<()> { HttpServer::new(|| { let app = App::new(); return app }) .bind("127.0.0.1:8000")? .run() .await}
前面的代码很简单,应该不会有什么意外。 稍后我们将更改代码,然后可以继续定义视图。 对于本章,我们只想返回一个字符串来说明视图是什么。 我们会知道我们的应用程序结构有效。 我们可以使用以下代码在views/auth/login.rs文件中定义基本的登录视图:
pub async fn login() -> String { format!("Login view")}
现在,views/auth/logout.rs 文件中的注销视图采用以下形式也就不足为奇了:
pub async fn logout() -> String { format!("Logout view")}
现在我们的视图已经定义好了,我们需要做的就是在 mod.rs 文件中定义工厂,以使我们的服务器能够为它们提供服务。 我们的工厂提供应用程序的数据流,采用以下形式:
我们可以在图 3.4 中看到,链接工厂给我们带来了很大的灵活性。 如果我们想从应用程序中删除所有身份验证视图,我们只需删除主视图工厂中的一行代码即可做到这一点。 我们还可以重用我们的模块。 例如,如果我们要在多个服务器上使用 auth 模块,我们只需为 auth 视图模块创建一个 git 子模块,然后在其他服务器上使用它。 我们可以使用以下代码在views/auth/mod.rs文件中构建我们的auth模块工厂视图:
mod login;mod logout;use actix_web::web::{ServiceConfig, get, scope};pub fn auth_views_factory(app: &mut ServiceConfig) { app.service(scope("v1/auth").route("login", get().to(login::login)).route("logout", get().to(logout::logout)) );}
在前面的代码中,我们可以看到我们传入了 ServiceConfig 结构体的可变引用。 这使我们能够在不同的字段中定义服务器上的视图等内容。 该结构的文档指出,它允许更大的应用程序将配置拆分为不同的文件。 然后我们将服务应用于 ServiceConfig 结构。 该服务使我们能够定义一个视图块,所有视图块都使用范围中定义的前缀进行填充。 我们还声明,目前我们正在使用 get 方法,以便在浏览器中轻松访问它。 现在,我们可以使用以下代码将 auth 视图工厂插入到views/mod.rs 文件中的主视图工厂中:
mod auth;use auth::auth_views_factory;use actix_web::web::ServiceConfig;pub fn views_factory(app: &mut ServiceConfig) { auth_views_factory(app);}
在前面的代码中,我们只需一行代码就可以分割整个视图模块。 我们还可以根据需要链接模块。 例如,如果我们想在 auth 视图模块中包含子模块,我们可以,并且我们只需将这些 auth 子模块的工厂提供给 auth 工厂即可。 我们还可以在一个工厂中定义多个服务。 我们的 main.rs 文件与添加的配置函数几乎保持不变,如以下代码所示:
use actix_web::{App, HttpServer};mod views;#[actix_web::main]async fn main() -> std::io::Result<()> { HttpServer::new(|| { let app = App::new().configure(views::views_factory); return app }) .bind("127.0.0.1:8000")? .run() .await}
当我们在 App 结构上调用配置函数时,我们将视图工厂传递给配置函数,该函数将把配置结构传递给我们的工厂函数。 由于configure函数返回Self,即App结构,我们可以在闭包结束时返回结果。 我们现在可以运行我们的服务器,结果如下:
我们可以看到带有预期前缀的应用程序可以正常工作! 至此,我们已经涵盖了自信处理 HTTP 请求的所有基础知识。
概括
在本章中,我们介绍了线程、Future 和异步函数的基础知识。 因此,我们能够在野外查看多服务器解决方案并自信地了解正在发生的事情。 这样,我们就在上一章中学到的概念的基础上构建了定义视图的模块。 此外,我们还链接了工厂,使我们的视图能够动态构建并添加到服务器中。 通过这种链式工厂机制,我们可以在构建服务器时将整个视图模块插入或移出配置。
我们还构建了一个定义路径的实用程序结构,标准化了一组视图的 URL 定义。 在以后的章节中,我们将使用这种方法来构建身份验证、JSON 序列化和前端模块。 根据我们所介绍的内容,我们将能够在下一章中构建以各种不同方式从用户提取和返回数据的视图。 有了这种模块化的理解,我们就拥有了坚实的基础,使我们能够在 Rust 中构建真实世界的 Web 项目,其中逻辑是隔离的并且可以配置,并且可以以可管理的方式添加代码。
在下一章中,我们将致力于处理请求和响应。 我们将学习如何将参数、主体、标头和表单传递给视图并通过返回 JSON 来处理它们。 我们将把这些新方法与我们在上一章中构建的待办事项模块一起使用,以便通过服务器视图与待办事项进行交互。
问题
HttpServer::new 函数中传递了什么参数以及该参数返回什么?
闭包与函数有何不同?
进程和线程有什么区别?
异步函数和普通函数有什么区别?
await和join有什么区别?
工厂链有什么好处?
答案
一个闭包被传递到 HttpServer::new 函数中。 HttpServer::new 函数必须返回 App 结构,以便在 HttpServer::new 函数触发后,bind 和 run 函数可以对它们进行操作。
闭包可以与其范围之外的变量交互。
进程是使用自己的内存堆栈、寄存器和变量执行的程序,而线程是独立管理但与其他线程和主程序共享数据的轻量级进程。
普通函数一被调用就会执行,而异步函数是一个 Promise,必须使用阻塞函数来执行。
wait 阻塞程序以等待 future 被执行; 但是,join 函数可以同时运行多个线程或 future。 wait 也可以在 join 函数上执行。
工厂链使我们能够灵活地构建和编排各个模块。 模块内工厂关注的是模块如何构建,模块外工厂关注的是不同模块如何编排。