学习Rust还是比较推荐有一些基础的编程基础并且有良好的自学能力。所以本教程并不累赘讲述Rust的基础知识,通过实例开发的记录来记录相关的知识。 关键字: Rust, web, http, 服务器 [[toc]]
单线程 Web 服务器
use std::net::TcpListener;
fn main() {
let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
println!("端口监听成功!");
}
}
通过这个例子可以快速的建立一个 Tcp 的端口监听,接下来就可以通过访问 http://127.0.0.1:8080 触发打印事件。
页面输出内容
目前通过浏览器访问的页面并不能成功打开,因为 Response 还没有做任何处理
for stream in listener.incoming() {
let mut stream = stream.unwrap();
println!("端口监听成功!");
let response = "HTTP/1.1 200 OK\r\n\r\nHello World!";
// 写入返回的数据
stream.write_all(response.as_bytes()).unwrap();
// 立即刷新返回数据
stream.flush().unwrap();
}
::: tip
stream.write_all 需要 mut 的权限
:::
现在我们通过浏览器打开,试试!不出意外的话,虽然程序正常运作,但浏览器还是访问错误的!
我们使用Api请求工具发现 read ECONNRESET
字样的报错。
::: tip
read ECONNRESET 错误通常表示在读取连接时,连接被对端异常关闭。
- 客户端过早地关闭了连接,而服务器在尝试读取时发现连接已不存在。
- 网络不稳定或存在故障,导致连接意外中断。
- 客户端发送的数据不符合预期,服务器在处理时出现错误导致连接被关闭。 :::
所以再将输出的内容返回之前,我们应该要将客户端发送的数据成功读取才行。这样的设计有效的确保了,交互的有效性,只有在服务端接受完整发功过去的信息后,才会正常的接受服务端返回的数据。
fn main() {
let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
for stream in listener.incoming() {
let mut stream = stream.unwrap();
println!("端口监听成功!");
// 暂时无法确认客户端发送的数据量,姑且认为小于1024吧
let mut buffer = [0; 1024];
stream.read(&mut buffer).unwrap();
let response = "HTTP/1.1 200 OK\r\n\r\nHello World!";
stream.write_all(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
}
终于成功在浏览器中返回了 Hello World! :smile:
多线程
目前这样的服务器是没有使用意义的,如果我们返回的数据内容比较多,就会产生几个问题
- 并发问题:同时访问网站的用户服务器只能一个一个的接待,一旦服务器处理的时间变长,用户就会产生等待。这不是我们熟知的服务器逻辑
- 性能问题:stream.read 每次读取数据时,它直接与底层的输入流(如文件、网络连接等)进行交互。这意味着每次调用都可能触发一个相对昂贵的系统调用(例如从文件读取数据或从网络接收数据)。简单来说,它不适合投入生产!
thread::spawn
thread::spawn 是 Rust 标准库中用于创建新线程的函数。它接受一个闭包作为参数,这个闭包中的代码会在新创建的线程中执行。
利用这个知识点我们可以用来优化上面的单线程服务器代码。 我们创建一个方法 handle_client
fn handle_client(mut stream: TcpStream) {
let mut buffer = [0; 1024];
stream.read(&mut buffer).unwrap();
let response = format!("HTTP/1.1 200 OK\r\n\r\nHello World!");
stream.write_all(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
fn main() {
let listener: TcpListener = TcpListener::bind("127.0.0.1:8080").unwrap();
println!("服务启动成功[8080]");
for stream in listener.incoming() {
let stream = stream.unwrap();
thread::spawn(|| {
handle_client(stream);
});
}
}
很好,我们解决了并发问题! ::: warning 事实上这个方案依旧存在一个风险,就是当访问量巨大时服务器会因为资源耗尽而宕机!这个我们放后面解决! :::
BufReader
BufReader 内部维护了一个缓冲区。当您从 BufReader 读取数据时,它首先检查缓冲区中是否已经有可用的数据。如果有,直接从缓冲区中返回数据,避免了立即进行系统调用。只有当缓冲区为空时,BufReader 才会执行系统调用一次性读取较大块的数据填充缓冲区。这样,通过减少系统调用的次数,提高了读取数据的效率。
fn handle_client(mut stream: TcpStream) {
let buf_reader = BufReader::new(&mut stream);
let _: Vec<_> = buf_reader
.lines()
.map(|result| result.unwrap())
.take_while(|line| !line.is_empty())
.collect();
let response = format!("HTTP/1.1 200 OK\r\n\r\nHello World!");
stream.write_all(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
::: tip take_while 是一个迭代器适配器方法。 对于 take_while(predicate) ,它会从迭代器中依次取出元素,只要取出的元素满足给定的谓词 predicate 条件就继续取出。一旦遇到一个不满足条件的元素,迭代就停止,后续的元素都不会再被处理。
尝试注释 take_while 令它不生效,我们就会发现效率低了很多! :::
这样一个最简单的小型多并发服务器就完成了!
Build 发布应用
Windows
因为当前在 Windows 环境开发,所以发布应用非常简单
cargo build --release
Linux
::: tip 通常我们不推荐在 Windows 发布 Linux 应用,因为配置上很麻烦:satisfied: 推荐在 Windows 上部署一个 WSL虚拟环境 或 Docker 环境 :::
::: danger 以下方法可能会遇到正常打包的情况,因为开发环境引用的三方库可能不兼容目标环境 :::
先了解下目标系统的情况
uname -m
如当前的环境是 aarch64.Linux
rustup target add aarch64-unknown-linux-gnu
::: tip rustup target add 命令用于添加特定的目标架构,以便您能够为该架构编译 Rust 代码。 ::: ::: tip 您可以在 Rust 官方支持的平台列表 中找到更多可用的目标架构及其对应的名称。 :::
下载安装完成后就可以发布了
cargo build --release --target aarch64-unknown-linux-gnu