program code for Hands-On Microservices with Rust译 使用Hyper Crate开发微服务(第二章)

本章将简要介绍如何使用Rust和hyper crate创建微服务。 我们将了解HTTP协议的基础知识和路由原理。 我们还将使用一种简单的方法描述使用Rust完全编写的最小REST服务。

在本章中,我们将介绍以下主题:

  • 使用hyper
  • 处理HTTP请求
  • 使用正则表达式进行路由
  • 从环境中获取参数

技术要求

因为我们在本章中开始编写代码,所以您需要使用某些软件来编译和运行示例:

  • 我建议你使用rustup工具,这将使你的Rust实例保持最新。 如果您没有此工具,可以从https://rustup.rs/获取。
    安装完成后,运行rustup update命令更新当前安装。
  • Rust编译器,至少版本1.31。
  • 我们将用于编译代码的超级包需要OpenSSL(https://www.openssl.org/))库。
    最流行的操作系统已经包含OpenSSL软件包,您可以按照软件包管理器的手册进行安装。
  • 您可以从GitHub获取本章中显示的示例,网址为https://github.com/PacktPublishing/Hands-On-Microservices-with-Rust/tree/master/Chapter02.

绑定一个小型服务器

在本节中,我们将从头开始创建一个Tiny Server。 我们将从必要的依赖开始,声明一个main函数,然后尝试构建并运行它。

添加必要的依赖项

首先,我们需要创建一个新文件夹,我们将添加必要的依赖项来创建我们的第一个微服务。 使用货物制作一个名为hyper-microservice:的新项目:

> cargo new hyper-microservice

打开创建的文件夹并将依赖项添加到Cargo.toml文件中:

[dependencies]
hyper = "0.12"

单一依赖是hyper create 。 这个create的最新版本是异步的,位于futures create的顶部。 它还使用tokio crate进行运行时,其中包括调度程序,reactor和异步套接字。 一些必要类型的tokio crate在hyper :: rt模块中重新导出。 hyper的主要目的是使用HTTP协议进行操作,这意味着crate可以支持将来的其他运行时。

服务器的主要功能

让我们从main函数开始,逐个添加必要的依赖项,详细查看为什么需要每个依赖项。 最小的HTTP服务器需要以下内容:

  • 要绑定的地址
  • 用于处理传入请求的服务器实例
  • 任何请求的默认处理程序
  • 服务器实例将运行的reactor(运行时)

服务器的地址

我们需要的第一件事是地址。套接字地址由IP地址和端口号组成。我们将在本书中使用IPv4,因为它得到了广泛的支持。在第6章,反应性微服务 - 增加容量和性能,我们将讨论微服务的扩展和相互通信,我将展示一些使用IPv6的例子。

标准Rust库包含一个IpAddr类型来表示IP地址。我们将使用SocketAddr结构,它包含端口号的IpAddr和u16。我们可以从([u8; 4],u16)类型的元组构造SocketAddr。将以下代码添加到我们的main函数中:

let addr =([127,0,0,1],8080).into();

我们在这里使用了impl 来自<(I,u16)>的SocketAddr特征的实现,这反过来又使用了impl From <[u8; 4]>对于IpAddr。这让我们可以使用.into()方法调用来构造一个
来自元组的套接字地址。同样,我们可以使用构造函数创建新的SocketAddr实例。在生产应用程序中,我们将从外部字符串(命令行参数或环境变量)解析套接字地址,如果没有设置变体,我们将从具有默认值的元组创建SocketAddr。

服务器实例

现在我们可以创建一个服务器实例并绑定到这个地址:

let builder = Server::bind(&addr);

上一行创建了一个带有绑定构造函数的hyper :: server :: Server实例,该绑定构造函数实际上返回的是Builder,而不是Server实例。 Server结构实现Future trait。 它与Result具有类似的作用,但描述了一个不可立即获得的值。 您将在第5章“了解期货板条箱的异步操作”中了解有关期货箱的未来和其他特征的更多信息。

设置请求处理程序

Builder结构提供了调整所创建服务器参数的方法。例如,hyper的服务器支持HTTP1和HTTP2。您可以使用构建器值来选择一个协议或两者。在以下示例中,我们使用构建器附加服务以使用serve方法处理传入的HTTP请求:

let server = builder.serve(|| {    service_fn_ok(|_| {        Response::new(Body::from("Almost microservice..."))    })});

在这里,我们使用构建器实例来附加生成Service实例的函数。该函数实现了hyper :: service :: NewService特性。然后生成的项必须实现hyper :: service :: Service特征。超级箱中的服务是接收请求并给出响应的函数。我们还没有实现这个特性
例;相反,我们将使用service_fn_ok函数,该函数将具有合适类型的函数转换为服务
处理程序。

有两个相应的结构:hyper :: Request和hyper :: Response。在前面的代码中,我们忽略了一个请求参数,并为每个请求构造了相同的响应。响应包含一组静态文本。

将服务器实例添加到运行时

由于我们现在有一个处理程序,我们可以启动服务器。运行时期望Future实例具有Future 类型,但Server结构实现具有hyper :: Error错误类型的Future。我们可以使用此错误来通知用户有关问题,但在我们的示例中,我们只会删除任何错误。您可能还记得,drop函数需要任何类型的单个参数,并返回一个单位为空的类型。 Future trait使用map_err方法。它使用一个函数更改错误类型,该函数需要原始错误类型并返回一个新错误类型。使用以下命令从服务器中删除错误:

let server = server.map_err(drop);

我们现在拥有所需的一切,并可以使用特定的运行时启动服务器。使用hyper :: rt :: run函数启动服务器:

hyper::rt::run(server);

不要编译它,因为我们没有导入类型。将其添加到源文件的头部:

use hyper::{Body, Response, Server};
use hyper::rt::Future;
use hyper::service::service_fn_ok;

我们需要导入我们正在使用的不同超类型:服务器,响应和正文。在最后一行中,我们使用了service_fn_ok函数。未来的进口需要特别关注;这是期货箱的重新出口特征,它在高箱中随处可见。在下一章中,我们将详细研究这一特性。

建立和运行

您现在可以使用以下命令编译代码并启动服务器:

cargo run

使用浏览器连接到服务器。 在浏览器的地址栏中输入http:// localhost:8080 /,浏览器将连接到您的服务器,并显示一个页面,其中包含您在上一代码中输入的文本:
program code for Hands-On Microservices with Rust译 使用Hyper Crate开发微服务(第二章)_第1张图片

改变重启

当您正在开发Web服务器时,可以立即访问已编译和正在运行的应用程序。 无论何时更改代码,都必须手动重启货物运行。 我建议您在货物上安装和使用cargo-watch子命令。 这将监视对项目文件所做的更改,并重新启动您选择的其他命令。

要安装货物表,请执行以下步骤:

  1. 在控制台中键入以下命令:
cargo install cargo-watch
  1. 使用带命令的run命令:
cargo watch -x "run"

您可以在引号之间向run命令添加额外的参数,或者在 - 字符后添加额外的参数。

处理传入的请求

我们已经创建了一个服务器,但它能够响应实际请求之前不是很有用。 在本节中,我们将为请求添加处理程序并使用REST的原则。

添加服务功能

在上一节中,我们实现了基于service_fn_ok函数的简单服务,这些函数期望服务函数不会抛出任何错误。还有service_fn函数,可用于创建可返回错误的处理程序。这些更适合异步Future结果。正如我们之前看到的,Future trait有两种相关的类型:一种用于成功结果,一种用于错误。 service_fn函数希望使用IntoFuture特征将结果转换为future。您可以在下一章中阅读有关期货箱及其类型的更多信息。让我们将以前的服务函数更改为返回Future实例的函数:

let server = builder.serve(|| service_fn(microservice_handler));

然后添加这个未实现的服务功能:

fn microservice_handler(req: Request)    -> impl Future, Error=Error>{    unimplemented!();}

与前一个类似,此函数需要一个Request,但它不会返回一个简单的Response实例。相反,它返回未来的结果。由于Future是一个特征(没有大小),我们不能从函数返回一个未实现的实体,我们必须将它包装在一个Box中。然而,在这种情况下,我们使用了一种全新的方法,这是一种非常重要的特征。这允许我们按值返回特征的实现,而不是通过引用。我们的未来可以解析为hyper :: Response 项或hyper :: Error错误类型。如果您从头开始创建项目并且未使用本书附带的代码示例,则应导入必要的类型:

use futures::{future, Future};
use hyper::{Body, Error, Method, Request, Response, Server, StatusCode};
use hyper::service::service_fn;

我们还从期货箱中输入了Future特征。 确保您在Cargo.toml文件中使用edition =“2018”,或者在main.rs中导入包装箱:

extern crate futures;
extern crate hyper;

我们首先将类型导入代码,但我们仍然需要在Cargo.toml文件中导入包。 在Cargo.toml的依赖项列表中添加这些包:

[dependencies]
futures = "0.1"
hyper = "0.12"

我更喜欢订购从泛型到更具体的依赖。 或者,您可以使用字母顺序。

实现服务功能

我们的服务功能将支持两种请求:

  • 使用索引页面响应GET对/ path的请求
  • 具有NOT_FOUND响应的其他请求

要检测相应的方法和路径,我们可以使用Request对象的方法。请参阅以下代码:

fn microservice_handler(req: Request)    -> impl Future, Error=Error>{       
								 match (req.method(), req.uri().path()) {        
								 		    (&Method::GET, "/") => {   
								 		     			future::ok(Response::new(INDEX.into()))
								 		     			}, 
								 		     _ => {               
								 		       			 let response = Response::builder()
								 		       			 .status(StatusCode::NOT_FOUND)
								 		       			 .body(Body::empty()).unwrap(); 
								 		       			  future::ok(response)            },       }}

我使用匹配表达式来检测从req.method()函数返回的相应方法,以及req.uri()。path()方法的链调用返回的Request的URI路径。

method()函数返回对Method实例的引用。 Method是一个包含所有支持的HTTP方法的枚举。 Rust不使用返回方法字符串的其他流行语言,而是使用有限枚举中的一组严格方法。这有助于在编译期间检测拼写错误。

还会返回使用future :: ok函数创建的Future实例。此函数会立即使用相应类型的项将结果解析为成功结果。这很有用
静态值;我们不需要等待创建它们。

未来对象是一个长时间操作,不会立即返回结果。运行时将轮询未来,直到返回结果。在数据库上执行异步请求很有用。我们将在第7章“可靠地与数据库集成”中执行此操作。

我们也可以返回流而不是整个结果。期货箱包含这些案件的Stream特征。我们将在第5章“使用Futures Crate了解异步操作”中进一步研究。

在我们的匹配表达式中,我们使用Method :: GET和“/”路径来检测索引页面的请求。在这种情况下,我们将返回一个Response,它构造一个新函数和一个HTML字符串作为参数。

如果没有找到与_模式匹配的页面,我们将返回一个带有来自StateCode枚举的NOT_FOUND状态代码的响应。它包含HTTP协议的所有状态代码。我们使用body方法来构造响应,并使用空Body作为该函数的参数。为了检查我们之前没有使用它,我们使用unwrap来解压缩Result中的Response。

索引页面

我们需要的最后一件事是索引页面。 在请求时返回有关微服务的一些信息被认为是好的形式,但出于安全原因,您可能会隐藏它。

我们的索引页面是一个包含HTML内容的简单字符串:

const INDEX: &'static str = r#"                Rust Microservice                   

Rust Microservice

"#;

这是一个无法修改的常量值。 Рay注意字符串的开头,r#“,如果之前没有使用它。这是Rust中的一种多行字符串,必须以”#结尾。

现在您可以编译代码并使用浏览器查看页面。 我打开了开发者工具来显示请求的状态代码:
program code for Hands-On Microservices with Rust译 使用Hyper Crate开发微服务(第二章)_第2张图片

如果您尝试获取不存在的资源,您将获得404状态代码,我们使用StatusCode :: NOT_FOUND常量设置:
program code for Hands-On Microservices with Rust译 使用Hyper Crate开发微服务(第二章)_第3张图片

实现REST原则

如果每个人都要从头开始创建与微服务交互的规则,那么我们就会有过多的私人互通标准。 REST不是一套严格的规则,但它是一种旨在简化微服务交互的架构风格。 它提供了一组建议的HTTP方法来创建,读取,更新和删除数据; 并执行操作。 我们将为我们的服务添加方法,使其符合REST原则。

添加共享状态

如果必须从单独的线程更改共享数据,那么您可能已经听说共享数据是一件坏事并且可能导致瓶颈。但是,如果我们想要共享通道的地址或者我们不需要频繁访问它,共享数据会很有用。在本节中,我们需要一个用户数据库。在下面的示例中,我将向您展示如何向生成器函数添加共享状态。可以出于多种原因使用此方法,例如保持与数据库的连接。

用户数据库显然会保存有关用户的数据。让我们添加一些类型来处理这个:

type UserId = u64;
struct UserData;

UserId表示用户的唯一标识符。 UserData表示存储的数据,但我们在此示例中使用空结构进行序列化和解析流。

我们的数据库如下:

type UserDb = Arc>>;

Arc是一个原子引用计数器,它为单个数据实例提供多个引用(在我们的例子中,这是数据板上的Mutex)。原子实体可以安全地与多个线程一起使用。它使用本机原子操作来禁止克隆引用。这是因为两个或多个线程可能损坏引用计数器并且可能导致分段错误,如果计数器大于代码中的引用,则会导致数据丢失或内存泄漏。

Mutex是一个互斥包装器,用于控制对可变数据的访问。 Mutex是一个原子标志,它检查只有一个线程可以访问数据,而其他线程必须等到锁定互斥锁的线程释放它。

您已经考虑到,如果您在一个线程中有一个锁定的互斥锁并且该线程发生混乱,则互斥锁实例会中毒,如果您尝试将其从另一个线程锁定,则会出现错误。

如果异步服务器可以在单个线程中工作,您可能想知道我们为什么要检查这些类型。有两个原因。首先,您可能需要在多个线程中运行服务器以进行扩展。其次,所有提供交互功能的类型,例如Sender对象(来自标准库,期货箱或其他任何地方)或数据库连接,通常都包含在这些类型中,以使它们与多线程环境兼容。知道幕后发生的事情会很有用。

您可能熟悉标准库类型,但Slab可能看起来有点不同。这种类型可以被认为是Web服务器开发中的银弹。大多数池使用此设备。 Slab是一个分配器,可以存储和删除由有序数字标识的任何值。它还可以重用已删除项目的插槽。它类似于Vec类型,如果删除元素,它将不会调整大小,但会自动重用可用空间。对于服务器,保持连接或请求很有用,例如在JSON-RPC协议实现中。

在这种情况下,我们使用Slab为用户分配新ID并与用户保持数据。我们使用Arc和Mutex对来保护我们的数据争用数据库,因为可以在不同的线程中处理不同的响应,这两个线程都可以尝试访问数据库。实际上,Rust不会让你在没有这些包装器的情况下编译代码。

我们必须添加一个额外的依赖项,因为Slab类型在外部板条箱中可用。使用Cargo.toml添加:

[dependencies]
slab = "0.4"
futures = "0.1"
hyper = "0.12"

在main.rs文件中导入这些必要的类型:

use std::fmt;use std::sync::{Arc, Mutex};
use slab::Slab;
use futures::{future, Future};
use hyper::{Body, Error, Method, Request, Response, Server, StatusCode};
use hyper::service::service_fn;

让我们在下一节中编写一个处理程序和一个main函数。

从服务功能访问共享状态

要访问共享状态,您需要提供对共享数据的引用。这很简单,因为我们已经用Arc包装了我们的状态,它为我们提供了一个clone()函数来复制对共享对象的引用。

由于我们的服务功能需要额外的参数,我们必须重写定义并调用我们的microservice_handler函数。现在它有一个额外的参数,它是对共享状态的引用:

fn microservice_handler(req: Request, user_db: &UserDb)    -> impl Future, Error=Error>

我们还必须将这个预期的引用发送到main函数:

fn main() {     let addr = ([127, 0, 0, 1], 8080).into();     let builder = Server::bind(&addr);     let user_db = Arc::new(Mutex::new(Slab::new()));     let server = builder.serve(move || {         let user_db = user_db.clone();         service_fn(move |req| microservice_handler(req, &user_db))     });     let server = server.map_err(drop);     hyper::rt::run(server); }

如您所见,我们创建了一个Slab并用Mutex和Arc包装它。之后,我们将名为user_db的对象移动到使用move关键字的服务器构建器的serve函数调用中。当引用进入闭包时,我们可以将它发送到microservice_handler。这是一个由发送到service_fn调用的闭包调用的处理函数。我们必须克隆引用以将其移动到嵌套闭包,因为可以多次调用该闭包。但是,我们不应该完全移动对象,因为发送到serve函数的闭包可以被多次调用,因此运行时可能稍后需要该对象。
换句话说,两个闭包都可以被多次调用。 service_fn的闭包将在与运行时相同的线程中调用,我们可以使用它内部值的引用。

解析微服务中的路径

Web开发中的一项常见任务是使用与持久存储一起使用的函数。这些函数通常称为创建,读取,更新和删除(CRUD)函数。它们是最常见的数据操作。

我们可以为我们的服务实现CRUD集,但首先我们必须确定我们想要使用的实体。想象一下,我们需要三种类型的实体:用户,文章和评论。在这种情况下,我建议您分离微服务,因为用户微服务负责身份,文章微服务负责内容,评论微服务处理内容。但是,如果您可以将这些实体重用于多个上下文,您将获得更多好处。

在我们实现所有处理程序之前,我们需要一个帮助函数,它使用相应的HTTP状态代码创建空响应:

fn response_with_code(status_code: StatusCode) -> Response {   

 Response::builder()        
 .status(status_code)       
  .body(Body::empty())      
    .unwrap()
}

此函数执行一些简单的操作 - 它需要状态代码,创建新的响应构建器,设置该状态,并添加空体。我们现在可以添加一个新的请求处理程序来检查三种路径变体:

  • 索引页(路径/)
  • 带有用户数据的操作(前缀/用户/)
  • 其他路径

我们可以使用匹配表达式来完成所有这些情况。将以下代码添加到microservices_handler函数:

let response = {
        match (req.method(), req.uri().path()) {
            (&Method::GET, "/") => {
                Response::new(INDEX.into())
            },
            (method, path) if path.start
            s_with(USER_PATH) => {                unimplemented!();            },            _ => {                response_with_code(StatusCode::NOT_FOUND)            },        }    };    future::ok(response)

如您所见,我们在第二个分支中使用if表达式来检测路径以/ user /前缀开头。此前缀实际存储在USER_PATH常量中:

const USER_PATH: &str = "/user/";

与前面的示例不同,在这种情况下,我们将使用全新的response_with_code函数来返回NOT_FOUND HTTP响应。我们还为响应变量分配响应,并使用它来创建Future :: ok函数的Future实例。

实现REST方法

我们的微服务已经可以区分不同的路径。 剩下的就是实现对用户数据的请求处理。 所有传入的请求都必须在其路径中包含/ user /前缀。

提取用户的标识符

要修改特定用户,我们需要他们的标识符。 REST指定您需要从路径获取ID,因为REST将数据实体映射到URL。

我们可以使用已经拥有的路径尾部来提取用户的标识符。这就是我们使用字符串的starts_with方法而不是检查与USER_PATH与路径尾部的强相等性的原因。

我们之前声明了UserId类型,它等于u64无符号数。使用(方法,路径)模式将此代码添加到先前声明的匹配表达式的第二个分支,以从路径中提取用户的标识符:

let user_id = path.trim_left_matches(USER_PATH)        .parse::()        .ok()        .map(|x| x as usize);

str :: trim_left_matches方法如果匹配参数中提供的字符串,则删除字符串的一部分。之后,我们使用str :: parse方法,该方法尝试将字符串(剩余的尾部)转换为实现标准库的FromStr特征的类型。 UserId已经实现了这一点,因为它等于u64类型,可以从字符串中解析。

解析方法返回Result。我们将它转​​换为带有Result :: ok函数的Option实例。我们不会尝试使用ID处理错误。 None值表示缺少值或错误值。

我们还可以使用返回的Option实例的映射将值转换为usize类型。这是因为Slab使用usize作为ID,但usize类型的实际大小取决于平台架构,它可以是不同的。它可以是u32或u64,具体取决于您可以使用的最大内存地址。

为什么我们不能为UserId使用usize,因为它实现了FromStr特性?这是因为客户端需要与HTTP服务器相同的行为,而不依赖于体系结构平台。在HTTP请求中使用不可预测的大小参数是不好的做法。

有时,选择一种类型来识别数据可能很困难。我们使用map将u64值转换为usize。但是,对于usize等于u32的体系结构,这不起作用,因为UserId可能大于内存限制。在微服务很小的情况下它是安全的,但对于你将在生产中使用的微服务来说,这是一个重要的要点。通常,这个问题很容易解决,因为您可以使用数据库的ID类型。

获取对共享数据的访问权限

在此用户处理程序中,我们需要访问用户的数据库。因为数据库是一个用Mutex实例包装的Slab实例,所以我们必须锁定互斥锁才能拥有对slab的独占访问权限。有一个Mutex :: lock函数返回Result 。 MutexGuard是一个范围锁,意味着它将代码块或范围保留在其中,它实现了Deref和DerefMut特征,以提供对保护对象下数据的透明访问。报告处理程序中的所有错误是一种很好的做法。您可以记录错误并将500(内部错误)HTTP代码返回给客户端。为了简单起见,我们将使用unwrap方法并期望互斥锁正确锁定:

let mut users = user_db.lock().unwrap();

在这里,我们在生成请求期间锁定了互斥锁。在这种情况下,我们立即创建完整的响应,这是正常的。如果结果延迟或我们使用流时,我们不应该一直锁定互斥锁。这将为所有请求创建瓶颈,因为如果所有请求都依赖于单个共享对象,则服务器无法并行处理请求。对于没有立即获得结果的情况,您可以克隆对互斥锁的引用,并在需要访问数据的短时间内将其锁定。

REST方法

我们想要涵盖所有基本的CRUD操作。 使用REST的原则,有适合这些操作的合适HTTP方法 - POST,GET,PUT和DELETE。 我们可以使用match表达式来检测相应的HTTP方法:

match (method, user_id) {    // Put other branches here    _ => {        response_with_code(StatusCode::METHOD_NOT_ALLOWED)    },}

这里,我们使用了一个带有两个值的元组 - 一个方法和一个用户标识符,它由Option 类型表示。 如果客户端请求不受支持的方法,则会有一个默认分支返回METHOD_NOT_ALLOWED消息(405 HTTP状态代码)。

让我们讨论每个操作的匹配表达式的每个分支

POST - 创建数据

服务器刚刚启动时,它不包含任何数据。 为了支持数据创建,我们使用没有用户ID的POST方法。 将以下分支添加到match(method,user_id)表达式:

(&Method::POST, None) => {    let id = users.insert(UserData);    Response::new(id.to_string().into())}

此代码将UserData实例添加到用户数据库,并在具有OK状态的响应中发送用户的关联ID(HTTP状态代码为200)。 默认情况下,此代码由Response :: new函数设置。

在这种情况下,UserData是一个空结构。 但是,在实际应用中,它必须包含实际数据。 我们使用一个空结构来避免序列化,但是您可以在第4章使用Serde Crate进行数据序列化和反序列化时,根据serde crate阅读有关序列化和反序列化的更多信息。

如果客户端使用POST请求设置ID,该怎么办? 您可以通过两种方式解释此案例 - 忽略它或尝试使用提供的ID。 在我们的示例中,我们将通知客户端请求错误。 添加以下分支来处理这种情况:

(&Method::POST, Some(_)) => {    response_with_code(StatusCode::BAD_REQUEST)}

此代码返回带有BAD_REQUEST状态代码的响应(400 HTTP状态代码)。

GET - 读取数据

创建数据时,我们需要能够读取它。 对于这种情况,我们可以使用HTTP GET方法。 将以下分支添加到代码中:

(&Method::GET, Some(id)) => {     if let Some(data) = users.get(id) {         Response::new(data.to_string().into())     } else {         response_with_code(StatusCode::NOT_FOUND)     } }

此代码使用用户数据库尝试按路径中提供的ID查找用户。 如果找到了用户,我们将把它的数据转换为String并转换为Body以与Response一起发送。 如果未找到用户,则处理程序分支将使用NOT_FOUND状态代码(经典404错误)进行响应。

要使UserData可转换为String,我们必须为该类型实现ToString特征。 但是,实现Display trait通常更有用,因为将为实现Display特征的每个类型自动派生ToString。 在main.rs源文件中的某处添加此代码:

impl fmt::Display for UserData {    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {        f.write_str("{}")    }}

在此实现中,我们返回一个带有空JSON对象“{}”的字符串。 真正的微服务必须使用serde特性进行此类转换。

PUT - 更新数据

保存数据后,我们可能希望提供修改数据的功能。 这是PUT方法的任务。 使用此方法处理对数据的更改:

(&Method::PUT, Some(id)) => {    if let Some(user) = users.get_mut(id) {        *user = UserData;        response_with_code(StatusCode::OK)    } else {        response_with_code(StatusCode::NOT_FOUND)    }},

此代码尝试使用get_mut方法在用户数据库中查找用户实例。 这将返回一个包含Some选项的可变引用,如果找不到相应的值,则返回None选项。 我们可以使用解除引用运算符*来替换存储中的数据。

如果找到并替换了用户的数据,则分支返回OK状态代码。 如果没有用户具有请求的ID,则分支返回NOT_FOUND。

DELETE - 删除数据

当我们不再需要数据时,我们可以删除它。这是DELETE方法的目的。在分支中使用它如下:

此代码检查Slab是否包含数据并使用remove方法将其删除。我们不立即使用remove方法,因为这要求数据预先存储在存储中,因此如果数据不存在则会发生混乱。

通常,Web服务实际上不会删除数据,而只是将其标记为已删除。这是一个合理的做法,因为它允许您以后浏览数据并提高服务或公司的效率。但是,这是一种冒险的做法。用户应该能够完全删除他们的数据,因为敏感数据可能代表威胁。新法律,例如GDPR法律(https://en.wikipedia.org/wiki/General_D ta_Protection_Regulation),保护用户拥有其数据的权利,并规定了数据保护的某些要求。违反此类法律可能会导致罚款。处理敏感数据时,请务必记住这一点。

路由高级请求

在前面的示例中,我们使用模式匹配来检测请求的目标。 这不是一种灵活的技术,因为路径通常包含必须考虑的额外字符。 例如,/ user / 1 / path包含尾部斜杠/,它不能在我们的微服务的先前版本中使用用户ID进行解析。 有一个灵活的工具来解决这个问题:正则表达式。

使用正则表达式定义路径

正则表达式是表示要在字符串中搜索的模式的字符序列。 正则表达式使您能够创建使用正式声明将文本拆分为多个部分的小解析器。 Rust有一个名为regex的箱子,正则表达式搭配的流行缩写。 您可以在此处了解有关此箱子的更多信息:https://crates.io/crates/regex。

添加必要的依赖项

要在我们的服务器中使用正则表达式,我们需要两个包:regex和lazy_static。第一个提供了一个Regex类型来创建和匹配正则表达式与字符串。第二个有助于将Regex实例存储在静态上下文中。我们可以为静态变量赋值常量,因为它们是在程序加载到内存时创建的。要使用复杂表达式,我们必须添加初始化代码并使用它来执行表达式,将结果分配给静态变量。 lazy_static包含一个lazy_static!宏为我们自动完成这项工作。此宏创建静态变量,执行表达式,并将评估值分配给该变量。我们还可以使用局部变量而不是静态变量为本地上下文中的每个请求创建正则表达式对象。但是,这会占用运行时开销,因此最好事先创建并重用它。

将两个依赖项添加到Cargo.toml文件:

[dependencies]
slab = "0.4"
futures = "0.1"
hyper = "0.12"
lazy_static = "1.0"
regex = "1.0"

除了上一个示例中main.rs源文件中的导入外,还添加了两个导入:

use lazy_static::lazy_static;
use regex::Regex;

我们将使用lazy_static宏和Regex类型来构造正则表达式。

写正则表达式

正则表达式包含一种特殊语言,用于编写模式以从字符串中提取数据。 我们的示例需要三种模式:

  • 索引页面的路径
  • 用户管理的路径
  • 用户列表的路径(示例服务器的新功能)

有一个Regex :: new函数可以创建正则表达式。 删除以前的USER_PATH常量,并在惰性静态块中添加三个新的正则表达式常量:

lazy_static! {   
 static ref INDEX_PATH: Regex = Regex::new("^/(index\\.html?)?$").unwrap(); 
    static ref USER_PATH: Regex = Regex::new("^/user/((?P\\d+?)/?)?$").unwrap();  
      static ref USERS_PATH: Regex = Regex::new("^/users/?$").unwrap();}

如您所见,正则表达式看起来很复杂。 为了更好地理解它们,让我们分析它们。

索引页面的路径

INDEX_PATH表达式与以下路径匹配:

  • /
  • 将/index.htm
  • /index.html

适合这些路径的表达式是“^ /(index \。html?)?$”。

^符号表示必须有一个字符串开头,而$符号表示必须有一个字符串结尾。当我们在两侧放置这些符号时,我们会阻止路径中的所有前缀和后缀,并期望完全匹配。 ()括号表示必须有一个组。组中的表达式被视为不可分割的单元。

的?符号表示前一个字符是可选的。我们将它放在l字符后面,以允许路径中的文件同时具有.htm和.html扩展名。正如您稍后将看到的,我们没有要读取的索引文件。我们将它用作根路径处理程序的别名。问号也用于具有文件名的整个组以适合空的根路径/。

点符号(。)适合任何字符,但我们需要一个真正的点符号。要将点作为符号处理,我们必须在它之前添加反斜杠(\)。但是,单个反斜杠将被解释为转义符的开始,因此我们必须使用反斜杠对(\)来使反斜杠成为普通符号。

所有其他字符都按原样处理,包括/符号。

用户管理的路径

USER_PATH表达式可以适合以下路径:

  • /user /
  • / user / ,其中表示数字组
  • / user / /,与前一个相同,但带有反斜杠

这些情况可以使用“^ / user /((?P \ d +?)/?)?$”正则表达式来处理。这个表达式有点复杂。它包括两组(一组是嵌套的)和一些其他奇怪的字符。让我们仔细看看。

?P 是一个分组属性,用于设置捕获组的名称。括号中的每个组都可以通过regex :: Captures对象访问。可以通过名称访问命名组。

\ d是一个匹配任何数字的特殊表达式。要指定我们有一个或多个数字,我们应该添加+符号,它告诉我们它可能有多少重复。也可以添加*符号,它告诉我们重复零次或多次,但我们在正则表达式中没有使用它。

有两组。第一个嵌套了名称user_id。它必须包含仅被解析为UserId类型的数字。第二个是包含可选尾部斜杠的封闭组。整个组是可选的,这意味着表达式可以包含没有任何标识符的/ user / path。

用户列表的路径

USERS_PATH是一个新模式,我们在前面的例子中没有。 我们将使用它来返回服务器上的完整用户列表。 此模式仅适合路径的两个变体:

  • / users /(带斜杠)
  • / users(没有斜杠)

处理这些情况的正则表达式非常简单:“^ / users /? ” 。 我 们 已 经 看 到 了 这 种 模 式 中 的 所 有 符 号 。 它 期 望 一 个 字 符 串 以 符 号 和 斜 杠 符 号 开 头 。 之 后 , 它 期 望 用 户 在 尾 部 / ? 处 有 可 选 的 斜 杠 。 最 后 , 它 期 望 带 有 ”。 我们已经看到了这种模式中的所有符号。 它期望一个字符串以^符号和斜杠符号开头。 之后,它期望用户在尾部/?处有可选的斜杠。 最后,它期望带有 /符号的字符串结束。

匹配表达式

我们必须重新组织microservice_handler的代码,因为我们不能在匹配表达式中使用正则表达式。我们必须在开始时使用路径提取方法,因为我们需要大多数响应:

let response = {    let method = req.method();    let path = req.uri().path();    let mut users = user_db.lock().unwrap();    // Put regular expressions here};futures::ok()

我们要检查的第一件事是索引页面请求。添加以下代码:

if INDEX_PATH.is_match(path) {    if method == &Method::GET {        Response::new(INDEX.into())    } else {       response_with_code(StatusCode::METHOD_NOT_ALLOWED)    }

这使用INDEX_PATH正则表达式来检查请求的路径是否与使用Regex :: is_match方法的索引页请求匹配,该方法返回bool值。在这里,我们正在检查请求的方法,因此只允许GET。

然后,我们将使用用户列表请求的替代条件继续if子句:

} else if USERS_PATH.is_match(path) {    if method == &Method::GET {        let list = users.iter()            .map(|(id, _)| id.to_string())            .collect::>()            .join(",");        Response::new(list.into())    } else {        response_with_code(StatusCode::METHOD_NOT_ALLOWED)    }

此代码使用USERS_PATH模式检查客户端是否请求了用户列表。这是一条新的路径。在此之后,我们迭代数据库中的所有用户并将他们的ID连接在一个字符串中。

以下代码用于处理REST请求:

} else if let Some(cap) = USER_PATH.captures(path) {    let user_id = cap.name("user_id").and_then(|m| {        m.as_str()            .parse::()            .ok()            .map(|x| x as usize)    });    // Put match expression with (method, user_id) tuple

此代码使用USER_PATH和Regex :: capture方法。它返回一个Captures对象,其中包含所有捕获组的值。如果模式与方法不匹配,则返回None值。如果模式匹配,我们得到一个存储在cap变量中的对象。 Captures结构具有name方法以按名称获取捕获的值。我们使用user_id作为组的名称。该组可以是可选的,name方法返回一个Option。我们使用Option的and_then方法将其替换为已解析的UserId。最后,user_id变量采用Option 值,方式与我们的微服务的先前版本相同。为了避免重复,我跳过了请求与(method,user_id)元组相同的块 - 只需从本章上一节的示例中复制此部分。

最后一部分是一个默认处理程序,它返回一个带有NOT_FOUND状态代码的响应:

} else {    response_with_code(StatusCode::NOT_FOUND)}

该服务现已完成,因此可以编译和运行。在第13章“测试和调试Rust微服务”中,您将了解如何调试微服务。但是,现在,您可以使用curl命令发送一些POST请求并在浏览器中检查结果。在shell中键入以下命令以添加三个用户,并删除ID为1的第二个用户:

如果您在浏览器中获取用户列表,则应显示以下内容:

$ curl -X POST http://localhost:8080/user/0
$ curl -X POST http://localhost:8080/user/1
$ curl -X POST http://localhost:8080/user/2
$ curl -X DELETE http://localhost:8080/user/1
$ curl http://localhost:8080/users0,2

正如您所看到的,我们使用/ users请求而没有带curl的尾部斜杠,而/ users /使用浏览器中的尾部斜杠。这个结果意味着正则表达式和请求路由都可以工作。

program code for Hands-On Microservices with Rust译 使用Hyper Crate开发微服务(第二章)_第4张图片

摘要

在本章中,我们使用超级板条箱创建了一个微服务。我们从最小的例子开始,只响应Rust Microservice消息。然后,我们创建了一个具有两个不同路径的微服务 - 第一个是索引页面请求,第二个是NOT_FOUND响应。

一旦我们学会了基础知识,我们就开始使用匹配表达式来使微服务REST兼容。我们还添加了处理用户数据的功能,包括四个基本操作 - 创建,读取,更新和删除。为了扩展本章最后一个示例中的路由功能,我们基于正则表达式实现了路由。正则表达式是紧凑模式,用于检查和提取文本中的数据。

在本章中,我们遇到了各种板条箱 - 超级,期货,板坯,正则表达式和lazy_static。我们将在下一章详细讨论这些内容。

由于我们已经学会了在下一章中创建最小的HTTP微服务,我们将学习如何使其可配置以及如何将日志记录附加到它,因为微服务在远程服务器上工作,我们需要能够配置它而无需重新编译并且能够看到日志中的微服务发生的所有问题。

你可能感兴趣的:(program,code,for,Hands-On,Micr)