XIKEW.COM - 实用教程 - 一个简单的Web服务器 - 实用教程,Rust, web, http, 服务器 - 学习Rust还是比较推荐有一些基础的编程基础并且有良好的自学能力。所以本教程并不累赘讲述Rust的基础知识,通过实例开发的记录来记录相关的知识。

一个简单的Web服务器
RUST 编程 学习 教程 9/23/2024 4:51:00 PM 阅读:2

学习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 错误通常表示在读取连接时,连接被对端异常关闭。

  1. 客户端过早地关闭了连接,而服务器在尝试读取时发现连接已不存在。
  2. 网络不稳定或存在故障,导致连接意外中断。
  3. 客户端发送的数据不符合预期,服务器在处理时出现错误导致连接被关闭。 :::

所以再将输出的内容返回之前,我们应该要将客户端发送的数据成功读取才行。这样的设计有效的确保了,交互的有效性,只有在服务端接受完整发功过去的信息后,才会正常的接受服务端返回的数据。

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:

多线程

目前这样的服务器是没有使用意义的,如果我们返回的数据内容比较多,就会产生几个问题

  1. 并发问题:同时访问网站的用户服务器只能一个一个的接待,一旦服务器处理的时间变长,用户就会产生等待。这不是我们熟知的服务器逻辑
  2. 性能问题: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