用 Rust 编写 HTTP 服务器
在本章中,您将深入研究使用 Rust 进行 TCP 和 HTTP 通信。
这些协议通常是通过用于构建 Web 应用程序的更高级别的库和框架为开发人员抽象出来的。 那么,为什么讨论低层协议很重要? 这是一个公平的问题。

学习使用 TCP 和 HTTP 非常重要,因为它们构成了 Internet 上大多数通信的基础。 流行的应用程序通信协议和技术(例如 REST、gRPC 和 Websocket)使用 HTTP 和 TCP 进行传输。 在 Rust 中设计和构建基本的 TCP 和 HTTP 服务器可以让您有信心设计、开发更高级别的应用程序后端服务并对其进行故障排除。
但是,如果您渴望开始使用示例应用程序,则可以转到第 3 章,然后在适合您的时间再返回到本章。
在本章中,您将学习以下内容:
编写TCP客户端和服务器。
构建一个库以在 TCP 原始字节流和 HTTP 消息之间进行转换。
构建一个可以提供静态网页(又名 Web 服务器)以及 json 数据(又名 Web 服务)的 HTTP 服务器。 使用标准 HTTP 客户端(例如 cURL(命令行)工具和 Web 浏览器)测试服务器。
通过本练习,您将了解如何使用 Rust 数据类型和特征来建模现实世界的网络协议,并增强您的 Rust 基础知识。
本章分为两节。 在第一部分中,您将使用 Rust 开发一个可以通过 TCP/IP 进行通信的基本网络服务器。 在第二部分中,您将构建一个 Web 服务器,用于响应网页和 json 数据的 GET 请求。 您只需使用 Rust 标准库(无需外部板条箱)即可实现这一切。 您要构建的 HTTP 服务器并不是功能齐全或可用于生产的。 但这将服务于我们既定的目的。
让我们开始吧。
我们谈到现代应用程序被构建为一组独立的组件和服务,一些属于前端,一些属于后端,一些属于分布式软件基础设施。
每当我们有单独的组件时,就会出现这些组件如何相互通信的问题。 客户端(网络浏览器或移动应用程序)如何与后端服务通信? 后端服务如何与数据库等软件基础设施通信? 这就是网络模型发挥作用的地方。
网络模型描述了消息发送者与其接收者之间如何进行通信。 它解决了诸如应以什么格式发送和接收消息、应如何将消息分解为物理数据传输的字节、如果数据包未到达目的地应如何处理错误等问题。 OSI 模型 是最流行的网络模型,是根据全面的七层框架定义的。 但出于互联网通信的目的,称为 TCP/IP 模型的简化四层模型通常足以描述发出请求的客户端与处理该请求的服务器之间如何通过互联网进行通信。 TCP/IP 模型在此处描述 (https://www.w3.org/People/Frystyk/thesis/TcpIp.html)。
TCP/IP 模型是一组简化的互联网通信标准和协议。 它分为四个抽象层:网络访问层、互联网层、传输层和应用层,每个层都可以使用有线协议的灵活性。 该模型以其构建的两个主要协议命名:传输控制协议 (TCP) 和互联网协议 (IP)。 如图 2.1 所示。 主要需要注意的是,这四层相辅相成,确保消息从发送进程成功发送到接收进程。
图 2.1。 TCP/IP网络模型
现在我们将了解这四层中每一层在通信中的作用。
应用层是最高的抽象层。 该层可以理解消息的语义。 例如,Web浏览器和Web服务器使用HTTP进行通信,或者电子邮件客户端和电子邮件服务器使用SMTP(简单邮件传输协议)进行通信。 还有其他此类协议,例如 DNS(域名服务)和 FTP(文件传输协议)。 所有这些都被称为应用层协议,因为它们处理特定的用户应用程序——例如网页浏览、电子邮件或文件传输。在本书中,我们将主要关注应用层的HTTP协议。
传输层提供可靠的端到端通信。 应用层处理具有特定语义的消息(例如发送 GET 请求以获取货运详细信息),而传输协议则处理发送和接收原始字节。 (注意:所有应用层协议消息最终都会转换为原始字节以供传输层传输)。 TCP 和 UDP 是该层使用的两个主要协议,QUIC(快速 UDP 互联网连接)也是最近加入的协议。 TCP 是一种面向连接的协议,允许对数据进行分区传输并在接收端以可靠的方式重新组装。 UDP 是一种无连接协议,与 TCP 不同,它不提供传送保证。 因此,UDP 速度更快,适合某些类别的应用程序,例如 DNS 查找、语音或视频应用程序。在本书中,我们将重点关注传输层的 TCP 协议。
网络层使用 IP 地址和路由器来定位信息包并将其路由到网络上的主机。 虽然 TCP 层专注于在由 IP 地址和端口号标识的两个服务器之间发送和接收原始字节,但网络层担心将数据包从源发送到目的地的最佳路径是什么。 我们不需要直接使用网络层,因为 Rust 的标准库提供了使用 TCP 和套接字的接口,并处理网络层通信的内部结构。
网络访问层是 TCP/IP 网络模型的最低层。 它负责通过主机之间的物理链路(例如使用网卡)传输数据。就我们的目的而言,使用什么物理介质进行网络通信并不重要。
现在我们已经了解了 TCP/IP 网络模型,我们将学习如何使用 TCP/IP 协议在 Rust 中发送和接收消息。
2.1 用 Rust 编写 TCP 服务器在本节中,您将学习如何相当轻松地在 Rust 中执行基本的 TCP/IP 网络通信。 让我们首先了解如何使用 Rust 标准库中的 TCP/IP 结构。
2.1.1 设计TCP/IP通信流程Rust 标准库通过 std::net 模块提供网络原语,其文档可以在以下位置找到:https://doc.rust-lang.org/std/net/。 该模块支持基本的 TCP 和 UDP 通信。 有两种特定的数据结构:TcpListener 和 TcpStream,它们具有实现我们的场景所需的大量方法。
让我们看看如何使用这两种数据结构。
TcpListener 用于创建绑定到特定端口的 TCP 套接字服务器。 客户端可以向指定套接字地址(机器的 IP 地址和端口号的组合)的套接字服务器发送消息。 一台机器上可能运行多个 TCP 套接字服务器。 当网卡上有传入网络连接时,操作系统使用端口号将消息路由到正确的 TCP 套接字服务器。
这里显示了创建套接字服务器的示例代码。
use std::net::TcpListener;let listener = TcpListener::bind("127.0.0.1:3000")
绑定到端口后,套接字服务器应开始侦听下一个传入连接。 这是实现的,如下所示:
listener.accept()
要连续(循环)侦听传入连接,请使用以下方法:
listener.incoming()
Listener.incoming() 方法返回在此侦听器上接收到的连接的迭代器。 每个连接代表一个 TcpStream 类型的字节流。 可以在此 TcpStream 对象上发送或接收数据。 请注意,对 TcpStream 的读取和写入是以原始字节完成的。 接下来显示代码片段。(注意:为简单起见,排除了错误处理)
for stream in listener.incoming() { //Read from stream into a bytes buffer stream.read(&mut [0;1024]); // construct a message and write to stream let message = "Hello".as_bytes(); stream.write(message)}
注意
为了从流中读取数据,我们构建了一个字节缓冲区(在 Rust 中称为字节切片)。
为了写入流,我们构造了一个字符串切片并使用 as_bytes() 方法将其转换为字节切片
到目前为止,我们已经了解了 TCP 套接字服务器的服务器端。 在客户端,可以与 TCP 套接字服务器建立连接,如下所示:
let stream = TcpStream.connect("172.217.167.142:80")
回顾一下,连接管理函数可从 std::net 模块的 TcpListener 结构中获得。 要在连接上读取和写入,请使用 TcpStream 结构。
现在让我们应用这些知识来编写一个有效的 TCP 客户端和服务器。
2.1.2 编写TCP服务器和客户端我们首先设置一个项目结构。 图 2.2 显示了名为 scene1 的工作区,其中包含四个项目 - tcpclient、tcpserver、http 和 httpserver。
对于 Rust 项目,工作区是一个包含其他项目的容器项目。 工作区结构的好处是它使我们能够将多个项目作为一个单元进行管理。 它还有助于将所有相关项目无缝存储在单个 git 存储库中。 我们将创建一个名为 scene1 的工作区项目。 在此工作区下,我们将使用 Rust 项目构建和依赖项工具 Cargo 创建四个新的 Rust 项目。 这四个项目分别是tcpclient、tcpserver、http和httpserver。
图 2.2。 cargo工作空间项目
此处列出了用于创建工作区和关联项目的命令。
开始一个新的cargo项目:
cargo new scenario1 && cd scenario1
scenario 1 目录也可以称为工作空间根目录。
在scenario1目录下,创建以下四个新的Rust项目:
cargo new tcpservercargo new tcpclientcargo new httpservercargo new --lib http
tcpserver 将是 TCP 服务器代码的二进制项目tcpclient 将是 TCP 客户端代码的二进制项目httpserver 将是 HTTP 服务器代码的二进制项目http 将是 http 协议功能的库项目
现在项目已创建,我们必须将 scene1 项目声明为工作区并指定其与四个子项目的关系。 添加以下内容:
清单 2.1。 scenario1/Cargo.toml
[workspace]members = [ "tcpserver","tcpclient", "http", "httpserver",]
我们现在将分两次迭代编写 TCP 服务器和客户端的代码:
在第一次迭代中,我们将编写 TCP 服务器和客户端来对从客户端到服务器建立的连接进行健全性检查。
在第二次迭代中,我们将从客户端发送文本到服务器,并让服务器回显它。
关于遵循代码的一般注意事项本章(以及整本书)中显示的许多代码片段都有内联编号的代码注释来描述代码。 如果您将代码(从本书的任何章节)复制并粘贴到代码编辑器中,请确保删除代码注释编号(否则程序将无法编译)。 另外,粘贴的代码有时可能会错位,因此可能需要手动验证将粘贴的代码与本章中的代码片段进行比较,以防出现编译错误。
迭代1
进入tcpserver文件夹,修改src/main.rs如下:
清单 2.2。 TCP 服务器的第一次迭代 (tcpserver/src/main.rs)
use std::net::TcpListener;fn main() { let connection_listener = TcpListener::bind("127.0.0.1:3000").unwrap(); #1 println!("Running on port 3000"); for stream in connection_listener.incoming() { #2 let _stream = stream.unwrap(); #3 println!("Connection established"); }}
从工作区的根文件夹(scenario 1),运行:
cargo run -p tcpserver #1
服务器将启动,并且消息 Running on port 3000 将打印到终端。 现在,我们有一个正在运行的 TCP 服务器正在侦听 localhost 上的端口 3000。
接下来我们来编写一个 TCP 客户端来与 TCP 服务器建立连接。
清单 2.3。 tcpclient/src/main.rs
use std::net::TcpStream;fn main() { let _stream = TcpStream::connect("localhost:3000").unwrap(); #1}
在新终端中,从工作区的根文件夹中运行:
cargo run -p tcpclient
您将看到消息“连接已建立”打印到运行 TCP 服务器的终端,如下所示:
Running on port 3000Connection established
现在我们有一个在端口 3000 上运行的 TCP 服务器,以及一个可以与其建立连接的 TCP 客户端。
我们现在可以尝试从客户端发送消息并让服务器回显该消息。
迭代 2:修改tcpserver/src/main.rs文件如下:
清单 2.4。 完成TCP服务器
use std::io::{Read, Write}; #1use std::net::TcpListener;fn main() { let connection_listener = TcpListener::bind("127.0.0.1:3000").unwrap(); println!("Running on port 3000"); for stream in connection_listener.incoming() { let mut stream = stream.unwrap(); #2 println!("Connection established"); let mut buffer = [0; 1024]; stream.read(&mut buffer).unwrap(); #3 stream.write(&mut buffer).unwrap(); #4 }}
在所示的代码中,我们正在向客户端回显,无论我们从客户端收到什么。 从工作区根目录使用 Cargo run -p tcpserver 运行 TCP 服务器。
读和写特征Rust 中的特征定义了共享行为。 它们与其他语言的界面类似,但也有一些差异。 Rust 标准库(std)定义了由 std 中的数据类型实现的几个特征。 这些特征也可以通过用户定义的数据类型(例如结构和枚举)来实现。
读和写是 Rust 标准库中定义的两个这样的特征。
读取特征允许从源读取字节。 实现 Read 特征的源示例包括 File、Stdin(标准输入)和 TcpStream。 Read 特征的实现者需要实现一种方法 - read()。 这允许我们使用相同的 read() 方法从 File、Stdin、TcpStream 或实现 Read 特征的任何其他类型中读取。
类似地,Write 特征表示面向字节接收器的对象。 Write 特征的实现者实现了两个方法 - write() 和flush()。 实现 Write 特征的类型示例包括 File、Stderr、Stdout 和 TcpStream。 这个特性允许我们使用 write() 方法写入文件、标准输出、标准错误或 TcpStream。
下一步是修改 TCP 客户端以将消息发送到服务器,然后打印从服务器接收回的内容。 修改文件tcpclient/src/main.rs如下:
清单 2.5。 完成TCP客户端
use std::io::{Read, Write};use std::net::TcpStream;use std::str;fn main() { let mut stream = TcpStream::connect("localhost:3000").unwrap(); stream.write("Hello".as_bytes()).unwrap(); #1 let mut buffer = [0; 5]; stream.read(&mut buffer).unwrap(); #2 println!( "Got response from server:{:?}", #3 str::from_utf8(&buffer).unwrap() );}
从工作区根目录使用 Cargo run -p tcpclient 运行 TCP 客户端。 确保 TCP 服务器也在另一个终端窗口中运行。
您将看到以下消息打印到 TCP 客户端的终端窗口:
Got response from server:"Hello"
恭喜。 您已经编写了可以相互通信的 TCP 服务器和 TCP 客户端。
结果类型和 unwrap() 方法在 Rust 中,函数或方法无法返回 Result<T,E> 类型是惯用的。 这意味着如果成功,Result 类型会包装另一个数据类型 T;如果失败,则包装一个 Error 类型,然后将其返回给调用函数。 调用函数依次检查 Result 类型并将其解包以接收类型 T 或类型 Error 的值以进行进一步处理。
在到目前为止的示例中,我们在多个地方使用了 unwrap() 方法,以通过标准库方法检索嵌入到 Result 对象中的值。 如果操作成功,unwrap() 方法返回 T 类型的值,如果发生错误,则发生恐慌。 在现实应用程序中,这不是正确的方法,因为 Rust 中的 Result 类型用于可恢复的故障,而恐慌用于不可恢复的故障。 然而,我们使用它是因为使用 unwrap() 简化了我们的代码以用于学习目的。 我们将在后面的章节中介绍正确的错误处理。
在本节中,我们学习了如何在 Rust 中进行 TCP 通信。 您还注意到 TCP 是一个低级协议,仅处理字节流。 它对所交换的消息和数据的语义没有任何理解。 对于编写 Web 应用程序,语义消息比原始字节流更容易处理。 因此,我们需要使用更高级别的应用程序协议,例如 HTTP,而不是 TCP。 这就是我们将在下一节中讨论的内容。
2.2 用 Rust 编写 HTTP 服务器在本节中,我们将用 Rust 构建一个可以与 HTTP 消息通信的 Web 服务器。
但 Rust 没有内置对 HTTP 的支持。 没有我们可以使用的 std::http 模块。 尽管有第三方 HTTP 包可用,但我们将从头开始编写一个。 通过这个,我们将学习如何应用 Rust 来开发现代 Web 应用程序所依赖的底层库和服务器。
让我们首先想象一下我们要构建的 Web 服务器的功能。 客户端与Web服务器各模块之间的通信流程如图2.3所示。
图 2.3 Web服务器消息流
我们的 Web 服务器将有四个组件 - 服务器、路由器、处理程序和 HTTP 库。 每个组件都有特定的用途,符合单一职责原则 (SRP)。 服务器侦听传入的 TCP 字节流。 HTTP 库解释字节流并将其转换为 HTTP 请求(消息)。 路由器接受 HTTP 请求并确定要调用哪个处理程序。 处理程序处理 HTTP 请求并构造 HTTP 响应。 HTTP 响应消息使用 HTTP 库转换回字节流,然后发送回客户端。
图 2.4 显示了 HTTP 客户端-服务器通信的另一个视图,这次描述了 HTTP 消息如何流经 TCP/IP 协议栈。 TCP/IP 通信在客户端和服务器端的操作系统级别进行处理,Web 应用程序开发人员仅使用 HTTP 消息。
图 2.4. HTTP 协议栈通信
让我们按以下顺序构建代码:
构建 HTTP 库
为项目编写main()函数
编写服务器模块
编写路由器模块
编写处理程序模块
为了方便起见,图 2.5 显示了代码设计的摘要,显示了 http 库和 httpserver 项目的关键模块、结构和方法。
图 2.5。 Web服务器设计概述
我们将为该图中所示的模块、结构和方法编写代码。 以下是图中每个组件的作用的简短摘要:
http:包含 HttpRequest 和 HttpResponse 类型的库。 它实现了 HTTP 请求和响应之间的转换逻辑,以及相应的 Rust 数据结构。
httpserver:主 Web 服务器,包含 main() 函数、套接字服务器、处理程序和路由器,并管理它们之间的协调。 它既充当 Web 服务器(提供 html)又充当 Web 服务(提供 json)。
我们开始吧?
2.2.1 解析HTTP请求报文在本节中,我们将构建一个 HTTP 库。 该库将包含执行以下操作的数据结构和方法:
解释传入的字节流并将其转换为 HTTP 请求消息
构造 HTTP 响应消息并将其转换为字节流以便通过网络传输
我们现在准备编写一些代码。
回想一下,我们已经在 scene1 工作区下创建了一个名为 http 的库。
HTTP 库的代码将放置在 http/src 文件夹下。
在http/src/lib.rs中添加以下代码:
pub mod httprequest;
这告诉编译器我们正在 http 库中创建一个名为 httprequest 的新的可公开访问的模块。
另外,从此文件中删除预先生成的测试脚本(通过 Cargo 工具)。 稍后我们将编写测试用例。
在http/src下创建两个新文件httprequest.rs和httpresponse.rs,分别包含处理HTTP请求和响应的功能。
我们将从设计 Rust 数据结构来保存 HTTP 请求开始。 当 TCP 连接上有传入字节流时,我们将对其进行解析并将其转换为强类型 Rust 数据结构以进行进一步处理。 然后,我们的 HTTP 服务器程序可以使用这些 Rust 数据结构,而不是 TCP 流。
表 1 显示了表示传入 HTTP 请求所需的 Rust 数据结构摘要:
表 2.1。 显示我们将要构建的数据结构列表的表格。
数据结构名称
Rust 数据类型
描述
HttpRequest
struct
代表一个HTTP请求
Method
enum
指定 HTTP 方法的允许值(动作)
Version
enum
指定 HTTP 版本允许的值
我们将在这些数据结构上实现一些特征,以传递一些行为。 表 2 显示了我们将在三种数据结构上实现的特征的描述。
表 2.2。 显示 HTTP 请求的数据结构实现的特征列表的表。
Rust trait 实现
描述
From<&str>
此特征可以将传入的字符串切片转换为 HttpRequest 数据结构
Debug
用于打印调试消息
PartialEq
用于比较值作为解析和自动化测试脚本的一部分
现在让我们将此设计转换为代码。 我们将编写数据结构和方法。
方法我们将在这里编写方法枚举和特征实现的代码。
将以下代码添加到http/src/httprequest.rs。
此处显示了方法枚举的代码。 我们使用枚举数据结构,因为我们希望在实现中仅允许 HTTP 方法的预定义值。 在此版本的实现中,我们仅支持两种 HTTP 方法 - GET 和 POST 请求。 我们还将添加第三种类型 - 未初始化,在运行程序中的数据结构初始化期间使用。
将以下代码添加到http/src/httprequest.rs:
#[derive(Debug, PartialEq)]pub enum Method { Get, Post, Uninitialized,}
Method 的特征实现如下所示(将添加到 httprequest.rs):
impl From<&str> for Method { fn from(s: &str) -> Method { match s { "GET" => Method::Get, "POST" => Method::Post, _ => Method::Uninitialized, } }}
在 From 特征中实现 from 方法使我们能够从 HTTP 请求行读取方法字符串,并将其转换为 Method::Get 或 Method::Post 变体。 为了了解实现此特征的好处并测试此方法是否有效,让我们编写一些测试代码。 将以下内容添加到 http/src/httprequest.rs:
#[cfg(test)]mod tests { use super::; #[test] fn test_method_into() { let m: Method = "GET".into(); assert_eq!(m, Method::Get); }}
从工作区根目录运行以下命令:
cargo test -p http
您将注意到一条与此类似的消息,表明测试已通过。
running 1 testtest httprequest::tests::test_method_into ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
仅使用 .into() 语法将字符串“GET”转换为 Method::Get 变体,这是实现 From 特征的好处。 它可以生成干净、可读的代码。
现在让我们看一下 Version 枚举的代码。
版本Version 枚举的定义如下所示。 尽管我们仅使用 HTTP/1.1 作为示例,但我们将支持两个 HTTP 版本以供说明。 还有第三种类型 - 未初始化,用作默认初始值。
将以下代码添加到http/src/httprequest.rs:
#[derive(Debug, PartialEq)]pub enum Version { V1_1, V2_0, Uninitialized,}
Version 的特征实现与 Method enum 的特征实现类似(将添加到 httprequest.rs)。
impl From<&str> for Version { fn from(s: &str) -> Version { match s { "HTTP/1.1" => Version::V1_1, _ => Version::Uninitialized, } }}
在 From 特征中实现 from 方法使我们能够从传入的 HTTP 请求中读取 HTTP 协议版本,并将其转换为 Version 变体。
我们来测试一下这个方法是否有效。 将以下内容添加到先前添加的 mod 测试块内的 http/src/httprequest.rs 中(在 test_method_into() 函数之后),并使用 Cargo test -p http 从工作空间根运行测试:
#[test] fn test_version_into() { let m: Version = "HTTP/1.1".into(); assert_eq!(m, Version::V1_1); }
您将在终端上看到以下消息:
running 2 teststest httprequest::tests::test_method_into ... oktest httprequest::tests::test_version_into ... oktest result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
现在这两个测试都通过了。 仅使用 .into() 语法将字符串“HTTP/1.1”转换为 Version::V1_1 变体,这是实现 From 特征的好处。
Http请求这代表完整的 HTTP 请求。 此处的代码显示了该结构。 将此代码添加到文件 http/src/httprequest.rs 的开头。
清单 2.6。 HTTP请求的结构
use std::collections::HashMap;#[derive(Debug, PartialEq)]pub enum Resource { Path(String),}#[derive(Debug)]pub struct HttpRequest { pub method: Method, pub version: Version, pub resource: Resource, pub headers: HashMap<String, String>, pub msg_body: String,}
HttpRequest 结构的 From<&str> 特征实现是我们练习的核心。 这使我们能够将传入的请求转换为便于进一步处理的 Rust HTTP 请求数据结构。
图 2.6 显示了典型 HTTP 请求的结构。
图2.6 HTTP请求的结构
该图显示了一个示例 HTTP 请求,其中包含一个请求行、一组一个或多个标头行,后跟一个空行,然后是一个可选的消息正文。 我们必须解析所有这些行并将它们转换为我们的 HTTPRequest 类型。 这将是 from() 函数的工作,作为 From<&str> 特征实现的一部分。
这里列出了 From<&str> 特征实现的核心逻辑:
读取传入 HTTP 请求中的每一行。 每行由 CRLF (\r\n) 分隔。
按如下方式评估每一行:
如果该行是请求行(我们正在查找关键字 HTTP 来检查它是否是请求行,因为所有请求行都包含 HTTP 关键字和版本号),请从该行中提取方法、路径和 HTTP 版本。
如果该行是标头行(由分隔符“:”标识),则提取标头项的键和值并将它们添加到请求标头列表中。 请注意,HTTP 请求中可以有多个标头行。 为了简单起见,我们假设键和值必须由可打印的 ASCII 字符组成(即以 10 为基数,值在 33 到 126 之间的字符,冒号除外)。
如果一行为空 (\n\r),则将其视为分隔行。 在这种情况下无需采取任何措施
如果消息正文存在,则扫描并将其存储为字符串。
将以下代码添加到http/src/httprequest.rs。
让我们以较小的块来看一下代码。 首先,这是代码的框架。 暂时不要输入它,这只是为了显示代码的结构。
impl From<String> for HttpRequest { fn from(req: String) -> Self {}}fn process_req_line(s: &str) -> (Method, Resource, Version) {}fn process_header_line(s: &str) -> (String, String) {}
我们有一个 from() 方法,我们应该为 From 特征实现它。 还有另外两个支持函数,分别用于解析请求行和标题行。
我们首先看一下 from() 方法。 将以下内容添加到 httprequest.rs。
清单 2.7 解析传入的 HTTP 请求:from() 方法
impl From<String> for HttpRequest { fn from(req: String) -> Self { let mut parsed_method = Method::Uninitialized; let mut parsed_version = Version::V1_1; let mut parsed_resource = Resource::Path("".to_string()); let mut parsed_headers = HashMap::new(); let mut parsed_msg_body = ""; // Read each line in the incoming HTTP request for line in req.lines() { // If the line read is request line, call function process_req_line() if line.contains("HTTP") { let (method, resource, version) = process_req_line(line); parsed_method = method; parsed_version = version; parsed_resource = resource; // If the line read is header line, call function process_header_line() } else if line.contains(":") { let (key, value) = process_header_line(line); parsed_headers.insert(key, value); // If it is blank line, do nothing } else if line.len() == 0 { // If none of these, treat it as message body } else { parsed_msg_body = line; } } // Parse the incoming HTTP request into HttpRequest struct HttpRequest { method: parsed_method, version: parsed_version, resource: parsed_resource, headers: parsed_headers, msg_body: parsed_msg_body.to_string(), } }}
根据前面描述的逻辑,我们尝试检测传入 HTTP 请求中的各种类型的行,然后使用解析的值构造一个 HTTPRequest 结构体。 接下来我们将看看这两种支持方法。
下面是处理传入请求的请求行的代码。 将其添加到 httprequest.rs,位于 impl From<String> for HttpRequest {} 块之后。
清单 2.8 解析传入的 HTTP 请求:process_req_line() 函数
fn process_req_line(s: &str) -> (Method, Resource, Version) { // Parse the request line into individual chunks split by whitespaces. let mut words = s.split_whitespace(); // Extract the HTTP method from first part of the request line let method = words.next().unwrap(); // Extract the resource (URI/URL) from second part of the request line let resource = words.next().unwrap(); // Extract the HTTP version from third part of the request line let version = words.next().unwrap(); ( method.into(), Resource::Path(resource.to_string()), version.into(), )}
这是解析标题行的代码。 在 process_req_line() 函数之后将其添加到 httprequest.rs 中。
清单 2.9 解析传入的 HTTP 请求:process_header_line() 函数
fn process_header_line(s: &str) -> (String, String) { // Parse the header line into words split by separator (':') let mut header_items = s.split(":"); let mut key = String::from(""); let mut value = String::from(""); // Extract the key part of the header if let Some(k) = header_items.next() { key = k.to_string(); } // Extract the value part of the header if let Some(v) = header_items.next() { value = v.to_string() } (key, value)}
这就完成了 HTTPRequest 结构的 From 特征实现的代码。
让我们在 http/src/httprequest.rs 中的 mod 测试(测试模块)内为 HTTP 请求解析逻辑编写一个单元测试。 回想一下,我们已经在测试模块中编写了测试函数 test_method_into() 和 test_version_into()。 此时,测试模块在 httprequest.rs 文件中应如下所示:
#[cfg(test)]mod tests { use super::; #[test] fn test_method_into() { let m: Method = "GET".into(); assert_eq!(m, Method::Get); } #[test] fn test_version_into() { let m: Version = "HTTP/1.1".into(); assert_eq!(m, Version::V1_1); }}
现在,在 test_version_into() 函数之后,将另一个测试函数添加到文件中的同一测试模块中。
清单 2.10。 用于解析 HTTP 请求的测试脚本
#[test] fn test_read_http() { let s: String = String::from("GET /greeting HTTP/1.1\r\nHost:localhost:3000\r\nUser-Agent: curl/7.64.1\r\nAccept:/\r\n\r\n"); #1 let mut headers_expected = HashMap::new(); #2 headers_expected.insert("Host".into(), " localhost".into()); headers_expected.insert("Accept".into(), " /".into()); headers_expected.insert("User-Agent".into(), " curl/7.64.1".into()); let req: HttpRequest = s.into(); #3 assert_eq!(Method::Get, req.method); #4 assert_eq!(Version::V1_1, req.version); #5 assert_eq!(Resource::Path("/greeting".to_string()), req.resource); #6 assert_eq!(headers_expected, req.headers); #7 }
从工作区根文件夹中使用 Cargo test -p http 运行测试。
您应该看到以下消息,表明所有三个测试均已通过:
running 3 teststest httprequest::tests::test_method_into ... oktest httprequest::tests::test_version_into ... oktest httprequest::tests::test_read_http ... oktest result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
我们已经完成了HTTP请求处理的代码。 该库能够解析传入的 HTTP GET 或 POST 消息,并将其转换为 Rust 数据结构。
现在让我们编写处理 HTTP 响应的代码。
2.2.2 构造HTTP响应消息让我们定义一个 HTTPResponse 结构体,它将代表我们程序中的 HTTP 响应消息。 我们还将编写一个方法来将此结构转换(序列化)为 HTTP 客户端(例如 Web 浏览器)可以理解的格式正确的 HTTP 消息。
我们首先回顾一下 HTTP 响应消息的结构。 这将帮助我们定义我们的结构。
图 2.7 显示了典型 HTTP 响应的结构。
图 2.7。 HTTP响应的结构
首先创建一个文件 http/src/httpresponse.rs(如果之前没有创建)。 将 httpresponse 添加到 http/lib.rs 的模块导出部分,如下所示:
pub mod httprequest;pub mod httpresponse;
将以下代码添加到http/src/httpresponse.rs。
清单 2.11。 HTTP响应的结构
use std::collections::HashMap;use std::io::{Result, Write};#[derive(Debug, PartialEq, Clone)]pub struct HttpResponse<'a> { version: &'a str, status_code: &'a str, status_text: &'a str, headers: Option<HashMap<&'a str, &'a str>>, body: Option<String>,}
HttpResponse 结构体包含协议版本、状态代码、状态描述、可选标头列表和可选主体。 请注意,对所有引用类型的成员字段都使用了生命周期注释 'a。
Rust 的生命周期在 Rust 中,每个引用都有一个生命周期,即引用有效的范围。 Rust 中的生命周期是一项重要功能,旨在防止在手动管理内存的语言(例如 C/C++)中常见的悬空指针和释放后使用错误。 Rust 编译器推断(如果未指定)或使用(如果指定)引用的生命周期注释来验证引用不会比它所指向的基础值的生命周期长。
另请注意对特征 Debug、PartialEq 和 Clone 使用 #[derive] 注释。 这些称为可派生特征,因为我们要求编译器为 HttpResponse 结构派生这些特征的实现。 通过实现这些特征,我们的结构体获得了出于调试目的而打印出来的能力,可以将其成员值与其他值进行比较,并克隆自身。
我们将为 HttpResponse 结构实现的方法列表如下所示:
默认特征实现:我们之前使用#[derive]注释自动派生了一些特征。 我们现在将手动实现默认特征。 这让我们可以为结构成员指定默认值。
方法 new():此方法创建一个新结构,其成员具有默认值。
方法 send_response():此方法将 Http 结构的内容序列化为用于在线传输的有效 HTTP 响应消息,并通过 TCP 连接发送原始字节。
Getter 方法:我们还将为版本、status_code、status_text、headers 和 body 实现一组 getter 方法,它们是 struct HttpResponse 的成员字段。
From 特征实现:最后,我们将实现 From 特征,帮助我们将 HttpResponse 结构转换为表示有效 HTTP 响应消息的 String 类型。
让我们在 http/src/httpresponse.rs 下添加所有这些的代码。
默认特征实现我们将从 HttpResponse 结构的默认特征实现开始。
清单 2.12。 HTTP 响应的默认特征实现
impl<'a> Default for HttpResponse<'a> { fn default() -> Self { Self { version: "HTTP/1.1".into(), status_code: "200".into(), status_text: "OK".into(), headers: None, body: None, } }}
实现 Default 特征允许我们执行以下操作来创建具有默认值的新结构:
let mut response: HttpResponse<'a> = HttpResponse::default();
new()方法的实现
new() 方法接受一些参数,设置其他参数的默认值并返回 HttpResponse 结构。 在 HttpResponse 结构体的 impl 块下添加以下代码。 由于该结构对其成员之一具有引用类型,因此 impl 块声明还必须指定生命周期参数(此处显示为“a”)。
清单 2.13。 HttpResponse 的 new() 方法 (httpresponse.rs)
impl<'a> HttpResponse<'a> { pub fn new( status_code: &'a str, headers: Option<HashMap<&'a str, &'a str>>, body: Option<String>, ) -> HttpResponse<'a> { let mut response: HttpResponse<'a> = HttpResponse::default(); if status_code != "200" { response.status_code = status_code.into(); }; response.headers = match &headers { Some(_h) => headers, None => { let mut h = HashMap::new(); h.insert("Content-Type", "text/html"); Some(h) } }; response.status_text = match response.status_code { "200" => "OK".into(), "400" => "Bad Request".into(), "404" => "Not Found".into(), "500" => "Internal Server Error".into(), _ => "Not Found".into(), }; response.body = body; response }}
new() 方法首先使用默认参数构造一个结构体。 然后评估作为参数传递的值并将其合并到结构中。
send_response() 方法send_response() 方法用于将 HttpResponse 结构转换为字符串,并通过 TCP 连接传输。 可以将其添加到 impl 块中,位于 httpresponse.rs 中的 new() 方法之后。
impl<'a> HttpResponse<'a> { // new() method not shown here pub fn send_response(&self, write_stream: &mut impl Write) -> Result<()> { let res = self.clone(); let response_string: String = String::from(res); let _ = write!(write_stream, "{}", response_string); Ok(()) }}
此方法接受 TCP 流(实现 Write 特征)作为输入,并将格式良好的 HTTP 响应消息写入该流。
HTTP 响应结构的 Getter 方法让我们为结构体的每个成员编写 getter 方法。 我们需要这些来在 httpresponse.rs 中构建 HTML 响应消息。
清单 2.14。 HttpResponse 的 Getter 方法
impl<'a> HttpResponse<'a> { fn version(&self) -> &str { self.version } fn status_code(&self) -> &str { self.status_code } fn status_text(&self) -> &str { self.status_text } fn headers(&self) -> String { let map: HashMap<&str, &str> = self.headers.clone().unwrap(); let mut header_string: String = "".into(); for (k, v) in map.iter() { header_string = format!("{}{}:{}\r\n", header_string, k, v); } header_string } pub fn body(&self) -> &str { match &self.body { Some(b) => b.as_str(), None => "", } }}
getter 方法允许我们将数据成员转换为字符串类型。
Form trait最后,让我们在 httpresponse.rs 中实现用于将 HTTPResponse 结构转换(序列化)为 HTTP 响应消息字符串的方法。
清单 2.15。 将 Rust 结构序列化为 HTTP 响应消息的代码
impl<'a> From<HttpResponse<'a>> for String { fn from(res: HttpResponse) -> String { let res1 = res.clone(); format!( "{} {} {}\r\n{}Content-Length: {}\r\n\r\n{}", &res1.version(), &res1.status_code(), &res1.status_text(), &res1.headers(), &res.body.unwrap().len(), &res1.body() ) }}
请注意格式字符串中 \r\n 的使用。 这用于插入新行字符。 回想一下,HTTP 响应消息由以下序列组成:状态行、标头、空行和可选的消息正文。
让我们编写一些单元测试。 如图所示创建一个测试模块块,并将每个测试添加到该块中。 暂时不要输入它,这只是为了显示测试代码的结构。
#[cfg(test)]mod tests { use super::; // Add unit tests here. Each test needs to have a #[test] annotation}
我们将首先检查状态代码为 200(成功)的消息的 HTTP 响应结构的构造。
将以下内容添加到 httpresponse.rs 文件末尾。
清单 2.16。 HTTP 成功 (200) 消息的测试脚本
#[cfg(test)]mod tests { use super::;#[test] fn test_response_struct_creation_200() { let response_actual = HttpResponse::new( "200", None, Some("Item was shipped on 21st Dec 2020".into()), ); let response_expected = HttpResponse { version: "HTTP/1.1", status_code: "200", status_text: "OK", headers: { let mut h = HashMap::new(); h.insert("Content-Type", "text/html"); Some(h) }, body: Some("Item was shipped on 21st Dec 2020".into()), }; assert_eq!(response_actual, response_expected); }}
我们将测试 404(未找到页面)HTTP 消息。 在 mod test {} 块中的测试函数 test_response_struct_creation_200() 之后添加以下测试用例:
清单 2.17。 404消息的测试脚本
#[test] fn test_response_struct_creation_404() { let response_actual = HttpResponse::new( "404", None, Some("Item was shipped on 21st Dec 2020".into()), ); let response_expected = HttpResponse { version: "HTTP/1.1", status_code: "404", status_text: "Not Found", headers: { let mut h = HashMap::new(); h.insert("Content-Type", "text/html"); Some(h) }, body: Some("Item was shipped on 21st Dec 2020".into()), }; assert_eq!(response_actual, response_expected); }
最后,我们将检查HTTP响应结构体是否以正确的格式被正确地序列化为在线HTTP响应消息。在测试函数test_response_struct_creation_404()之后,在mod tests {}块中添加以下测试。
清单2.18.用于检查格式正确的HTTP响应消息的测试脚本
#[test] fn test_http_response_creation() { let response_expected = HttpResponse { version: "HTTP/1.1", status_code: "404", status_text: "Not Found", headers: { let mut h = HashMap::new(); h.insert("Content-Type", "text/html"); Some(h) }, body: Some("Item was shipped on 21st Dec 2020".into()), }; let http_string: String = response_expected.into(); let response_actual = "HTTP/1.1 404 Not Found\r\nContent-Type:text/html\r\nContent-Length: 33\r\n\r\n Item was shipped on 21st Dec 2020"; assert_eq!(http_string, response_actual); }
我们现在就做测试。从工作区根目录运行以下命令:
cargo test -p http
您应该看到以下消息,显示http模块中已通过6个测试。注意,这包括对HTTP请求和HTTP响应模块的测试。
running 6 teststest httprequest::tests::test_method_into ... oktest httprequest::tests::test_version_into ... oktest httpresponse::tests::test_http_response_creation ... oktest httpresponse::tests::test_response_struct_creation_200 ... oktest httprequest::tests::test_read_http ... oktest httpresponse::tests::test_response_struct_creation_404 ... oktest result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
如果测试失败,检查代码中是否有任何错别字或未对齐(如果您复制粘贴了它)。特别是重新检查下面的字符串字面量(它相当长,容易出错):
"HTTP/1.1 404 Not Found\r\nContent-Type:text/html\r\nContent-Length:33\r\n\r\nItem was shipped on 21st Dec 2020";
如果你仍然在执行测试时遇到问题,请参考git repo。
这就完成了http库的代码。让我们回顾一下http服务器的设计,再次如图2.8所示
图2.8. Web服务器消息流
我们编写了http库。让我们编写main()函数、服务器、路由器和处理程序。从这里开始,我们将不得不从http项目切换到httpserver项目目录,以编写代码。
为了引用httpserver项目中的http库,在后者的Cargo.toml中添加以下内容。
[dependencies]http = {path = "../http"}
2.2.3编写main()函数和服务器模块
让我们采用自上而下的方法。我们main.rs
清单2.19. main()函数
mod handler;mod server;mod router;use server::Server;fn main() { // Start a server let server = Server::new("localhost:3000"); //Run the server server.run();}
主函数导入了三个模块-处理器、服务器和路由器。
接下来,在httpserver/src下创建三个文件:handler.rs、server.rs和router.rs。
服务器模块让我们在httpserver/src/server.rs中编写服务器模块的代码。
清单2.20.服务器模块
use super::router::Router;use http::httprequest::HttpRequest;use std::io::prelude::;use std::net::TcpListener;use std::str;pub struct Server<'a> { socket_addr: &'a str,}impl<'a> Server<'a> { pub fn new(socket_addr: &'a str) -> Self { Server { socket_addr } } pub fn run(&self) { // Start a server listening on socket address let connection_listener = TcpListener::bind(self.socket_addr).unwrap(); println!("Running on {}", self.socket_addr); // Listen to incoming connections in a loop for stream in connection_listener.incoming() { let mut stream = stream.unwrap(); println!("Connection established"); let mut read_buffer = [0; 90]; stream.read(&mut read_buffer).unwrap(); // Convert HTTP request to Rust data structure let req: HttpRequest = String::from_utf8(read_buffer.to_vec()).unwrap().into(); // Route request to appropriate handler Router::route(req, &mut stream); } }}
服务器模块有两种方法:
new() 接受一个套接字地址(主机和端口),并返回一个 Server 实例。 run() 方法执行以下操作:
绑定在套接字上,
监听传入的连接,
在有效连接上读取字节流,
将流转换为 HttpRequest 结构实例
将请求传递给 Router 进行进一步处理
2.2.4 编写router和handler模块路由器模块检查传入的 HTTP 请求并确定将请求路由到的正确处理程序以进行处理。 将以下代码添加到httpserver/src/router.rs。
清单 2.21。 路由器模块
use super::handler::{Handler, PageNotFoundHandler, StaticPageHandler, WebServiceHandler};use http::{httprequest, httprequest::HttpRequest, httpresponse::HttpResponse};use std::io::prelude::;pub struct Router;impl Router { pub fn route(req: HttpRequest, stream: &mut impl Write) -> () { match req.method { // If GET request httprequest::Method::Get => match &req.resource { httprequest::Resource::Path(s) => { // Parse the URI let route: Vec<&str> = s.split("/").collect(); match route[1] { // if the route begins with /api, invoke Web service "api" => { let resp: HttpResponse = WebServiceHandler::handle(&req); let _ = resp.send_response(stream); } // Else, invoke static page handler _ => { let resp: HttpResponse = StaticPageHandler::handle(&req); let _ = resp.send_response(stream); } } } }, // If method is not GET request, return 404 page _ => { let resp: HttpResponse = PageNotFoundHandler::handle(&req); let _ = resp.send_response(stream); } } }}
Router 检查传入方法是否为 GET 请求。 如果是这样,它将按以下顺序执行检查:
如果 GET 请求路由以 /api 开头,则会将请求路由到 WebServiceHandler
如果 GET 请求针对任何其他资源,则假定该请求针对静态页面并将请求路由到 StaticPageHandler
如果不是GET请求,则返回404错误页面
接下来我们看一下Handler模块。
处理程序对于处理程序模块,让我们添加几个外部 crate 来处理 json 序列化和反序列化 - serde 和 serde_json。 httpserver 项目的 Cargo.toml 文件如下所示:
[dependencies]http = {path = "../http"}serde = {version = "1.0.117",features = ["derive"]}serde_json = "1.0.59"
将以下代码添加到httpserver/src/handler.rs。
让我们从模块导入开始:
use http::{httprequest::HttpRequest, httpresponse::HttpResponse};use serde::{Deserialize, Serialize};use std::collections::HashMap;use std::env;use std::fs;
让我们定义一个名为Handler的trait,如下所示:
清单2.22. Trait Handler定义
pub trait Handler { fn handle(req: &HttpRequest) -> HttpResponse; fn load_file(file_name: &str) -> Option<String> { let default_path = format!("{}/public", env!("CARGO_MANIFEST_DIR")); let public_path = env::var("PUBLIC_PATH").unwrap_or(default_path); let full_path = format!("{}/{}", public_path, file_name); let contents = fs::read_to_string(full_path); contents.ok() }}
请注意,trait Handler 包含两个方法:
handle():必须为任何其他用户数据类型实现此方法才能实现该特征。
load_file() :此方法是从httpserver根文件夹中的公共目录加载文件(非json)。 该实现已作为特征定义的一部分提供。
我们现在将定义以下数据结构:
StaticPageHandler - 提供静态网页服务,
WebServiceHandler - 提供 json 数据
PageNotFoundHandler - 提供 404 页面服务
OrderStatus - 用于加载从 json 文件读取的数据的结构
将以下代码添加到httpserver/src/handler.rs。
清单 2.23。 处理程序的数据结构
#[derive(Serialize, Deserialize)]pub struct OrderStatus { order_id: i32, order_date: String, order_status: String,}pub struct StaticPageHandler;pub struct PageNotFoundHandler;pub struct WebServiceHandler;
让我们为三个处理程序结构实现 Handler 特征。 让我们从 PageNotFoundHandler 开始。
impl Handler for PageNotFoundHandler { fn handle(_req: &HttpRequest) -> HttpResponse { HttpResponse::new("404", None, Self::load_file("404.html")) }}
如果调用 PageNotFoundHandler 结构体上的 handle 方法,它将返回一个新的 HttpResponse 结构体实例,其状态码为:404,并且正文包含从文件 404.html 加载的一些 html。
这是 StaticPageHandler 的代码。
清单 2.24。 提供静态网页的处理程序
impl Handler for StaticPageHandler { fn handle(req: &HttpRequest) -> HttpResponse { // Get the path of static page resource being requested let http::httprequest::Resource::Path(s) = &req.resource; // Parse the URI let route: Vec<&str> = s.split("/").collect(); match route[1] { "" => HttpResponse::new("200", None, Self::load_file("index.html")), "health" => HttpResponse::new("200", None, Self::load_file("health.html")), path => match Self::load_file(path) { Some(contents) => { let mut map: HashMap<&str, &str> = HashMap::new(); if path.ends_with(".css") { map.insert("Content-Type", "text/css"); } else if path.ends_with(".js") { map.insert("Content-Type", "text/javascript"); } else { map.insert("Content-Type", "text/html"); } HttpResponse::new("200", Some(map), Some(contents)) } None => HttpResponse::new("404", None, Self::load_file("404.html")), }, } }}
如果在StaticPageHandler上调用handle()方法,则会进行以下处理:
如果传入请求是针对 localhost:3000/,则加载文件index.html 中的内容并构造一个新的 HttpResponse 结构
如果传入请求针对 localhost:3000/health,则加载文件 health.html 中的内容,并构造一个新的 HttpResponse 结构
如果传入请求针对任何其他文件,该方法会尝试在 httpserver/public 文件夹中查找并加载该文件。 如果未找到文件,则会发回 404 错误页面。 如果找到该文件,则会加载内容并将其嵌入到 HttpResponse 结构中。 请注意,HTTP 响应消息中的 Content-Type 标头是根据文件类型设置的。
让我们看一下代码的最后一部分——WebServiceHandler。
清单 2.25。 提供 json 数据的处理程序
impl WebServiceHandler { fn load_json() -> Vec<OrderStatus> { #1 let default_path = format!("{}/data", env!("CARGO_MANIFEST_DIR")); let data_path = env::var("DATA_PATH").unwrap_or(default_path); let full_path = format!("{}/{}", data_path, "orders.json"); let json_contents = fs::read_to_string(full_path); let orders: Vec<OrderStatus> = serde_json::from_str(json_contents.unwrap().as_str()).unwrap(); orders }}// Implement the Handler traitimpl Handler for WebServiceHandler { fn handle(req: &HttpRequest) -> HttpResponse { let http::httprequest::Resource::Path(s) = &req.resource; // Parse the URI let route: Vec<&str> = s.split("/").collect(); // if route if /api/shipping/orders, return json match route[2] { "shipping" if route.len() > 2 && route[3] == "orders" => { let body = Some(serde_json::to_string(&Self::load_json()).unwrap()); let mut headers: HashMap<&str, &str> = HashMap::new(); headers.insert("Content-Type", "application/json"); HttpResponse::new("200", Some(headers), body) } _ => HttpResponse::new("404", None, Self::load_file("404.html")), } }}
如果在WebServiceHandler结构体上调用handle()方法,则会进行以下处理:
如果 GET 请求针对 localhost:3000/api/shipping/orders,则会加载包含订单的 json 文件,并将其序列化为 json,作为响应正文的一部分返回。
如果是其他路由,则返回404错误页面。
我们已经完成了代码。 我们现在必须创建 html 和 json 文件,以便测试 Web 服务器。
2.2.5 测试Web服务器在本节中,我们将首先创建测试网页和 json 数据。 然后,我们将针对各种场景测试 Web 服务器并分析结果。
在httpserver根文件夹下创建两个子文件夹data和public。 在 public 文件夹下,创建四个文件 - index.html、health.html、404.html、styles.css。 在数据文件夹下,创建以下文件 -orders.json。
此处显示指示性内容。 您可以根据自己的喜好更改它们。
httpserver/public/index.html
清单 2.26。 索引网页
<!DOCTYPE html><html lang="en"> <head> <meta charset="utf-8" /> <link rel="stylesheet" href="styles.css"> <title>Index!</title> </head> <body> <h1>Hello, welcome to home page</h1> <p>This is the index page for the web site</p> </body></html>
httpserver/public/styles.css
h1 { color: red; margin-left: 25px;}
httpserver/public/health.html
清单 2.27。 健康测试页<!DOCTYPE html><html lang="en"> <head> <meta charset="utf-8" /> <title>Health!</title> </head> <body> <h1>Hello welcome to health page!</h1> <p>This site is perfectly fine</p> </body></html>
httpserver/public/404.html
<!DOCTYPE html> <html lang="en"><head><meta charset="utf-8" /> <title>Not Found!</title> </head> <body> <h1>404 Error</h1> <p>Sorry the requested page does not exist</p> </body></html>
httpserver/data/orders.json
清单 2.28。 订单的 Json 数据文件
[ { "order_id": 1, "order_date": "21 Jan 2020", "order_status": "Delivered" }, { "order_id": 2, "order_date": "2 Feb 2020", "order_status": "Pending" }]
我们现在已经准备好运行服务器了。
从工作区根目录运行 Web 服务器,如下所示:
cargo run -p httpserver
然后从浏览器窗口或使用curl工具测试以下URL:
localhost:3000/localhost:3000/healthlocalhost:3000/api/shipping/orderslocalhost:3000/invalid-path
您会注意到,如果您在浏览器上调用这些命令,对于第一个 URL,您应该看到红色字体的标题。 转到 Chrome 浏览器(或其他浏览器上的等效开发工具)中的网络选项卡并查看浏览器下载的文件。 您将看到,除了 index.html 文件之外,浏览器还会自动下载 styles.css,从而将样式应用于索引页面。 如果进一步检查,您可以看到 CSS 文件的 Content-Type 为 text/css,HTML 文件的 Content-Type 为 text/html,从我们的 Web 服务器发送到浏览器。
同样,如果您检查为 /api/shipping/orders 路径发送的响应内容类型,您将看到浏览器收到的 application/json 作为响应标头的一部分。
关于构建 Web 服务器的部分到此结束。
在本节中,我们编写了一个 HTTP 服务器和一个可以提供静态页面以及 json 数据的 http 消息库。 虽然前一个功能与术语 Web 服务器相关,但后者是我们开始看到 Web 服务功能的地方。 我们的 httpserver 项目既充当静态 Web 服务器,又充当提供 json 数据的 Web 服务。 当然,常规 Web 服务将提供更多方法,而不仅仅是 GET 请求。 但本练习的目的是演示 Rust 从头开始构建此类 Web 服务器和 Web 服务的能力,无需使用任何 Web 框架或外部 http 库。
我希望您喜欢按照代码进行操作,并获得正常运行的服务器。 如果您有任何困难,可以参考第 2 章的代码库。
这结束了本章的两个核心目标,即构建 TCP 服务器/客户端和构建 HTTP 服务器。
本章的完整代码可以在 https://git.manning.com/agileauthor/eshwarla/-/tree/master/code 找到。
2.3 总结TCP/IP 模型是一组简化的 Internet 通信标准和协议。 它分为四个抽象层:网络访问层、互联网层、传输层和应用层。 TCP 是传输层协议,其他应用程序级协议(例如 HTTP)通过它进行操作。 我们构建了一个使用 TCP 协议交换数据的服务器和客户端。
TCP 也是一种面向流的协议,其中数据作为连续的字节流进行交换。
我们使用 Rust 标准库构建了一个基本的 TCP 服务器和客户端。 TCP 不理解 HTTP 等消息的语义。 我们的 TCP 客户端和服务器只是交换字节流,而不了解所传输数据的语义。
HTTP 是应用层协议,是大多数 Web 服务的基础。 HTTP 在大多数情况下使用 TCP 作为传输协议。
我们构建了一个 HTTP 库来解析传入的 HTTP 请求并构建 HTTP 响应。 HTTP 请求和响应是使用 Rust 结构和枚举进行建模的。
我们构建了一个 HTTP 服务器,它提供两种类型的内容:静态网页(带有样式表等相关文件)和 json 数据。
我们的 Web 服务器可以接受请求并向标准 HTTP 客户端(例如浏览器和curl 工具)发送响应。
我们通过实现几个特征向自定义结构添加了额外的行为。 其中一些是使用 Rust 注释自动派生的,另一些是手动编码的。 我们还使用生命周期注释来指定结构内引用的生命周期。
您现在已经掌握了基础知识,可以了解如何使用 Rust 来开发低级 HTTP 库和 Web 服务器,以及 Web 服务的开端。 在下一章中,我们将直接使用用 Rust 编写的生产就绪 Web 框架来开发 Web 服务。