Rust 的非同步特性讓網頁伺服器能高效處理大量併發請求。本文示範如何結合 Rocket 和 Tokio 建構非同步伺服器,處理 zip 檔案的產生及下載。同時,我們會使用 Structopt 建構命令列應用程式,與伺服器互動,執行產生 zip 檔案和開啟下載頁面等操作。程式碼中使用 reqwest 傳送 HTTP 請求,並以 JSON 格式傳輸資料。文章也包含如何使用 uuid 產生唯一檔案名稱,避免檔案覆寫,以及透過命令列引數指定目錄和輸出檔案等進階功能。

非同步函式的基本定義

非同步函式的基本定義是一個任務或過程,它不會立即完成,而是會在稍後完成。這種函式需要使用 .await 來等待其他任務完成。

Future 的定義

Future是一個任務或過程,它不會立即完成,而是會在稍後完成。Future有兩個重要的特性:Item和Error,分別代表了Future完成後的結果和錯誤。

使用 Tokio 進行非同步操作

Tokio是一個提供非同步操作的函式庫,它包含了反應器(reactor)、事件迴圈(event loop)和執行緒池(thread pool)等功能。Tokio可以用來管理非同步任務,允許它們在不阻塞執行緒的情況下執行。

建立 Web 伺服器

要建立一個 Web 伺服器,我們需要使用 Rocket 這個函式庫。Rocket是一個提供了簡單的路由和請求處理機制的函式庫。

CRUD 操作

CRUD是建立(Create)、讀取(Read)、更新(Update)和刪除(Delete)四個基本的資料操作。Rocket提供了對應的宏來實作這些操作。

生成 zip 檔案

我們可以使用 Rocket 的路由機制來生成 zip 檔案。當使用者傳送 POST 請求到 /gen 路由時,我們可以生成 zip 檔案並傳回給使用者。

下載 zip 檔案

我們可以使用 Rocket 的路由機制來下載 zip 檔案。當使用者傳送 GET 請求到 / 路由時,我們可以傳回一個下載連結。

主函式

最後,我們需要建立一個主函式來啟動 Rocket 伺服器。Rocket 提供了兩種方式來啟動伺服器:使用 launch() 方法或使用 launch 宏。

// 生成 zip 檔案
#[post("/gen", data = "<order>")]
async fn generate(order: Json<Order>) {
    // ...
}

// 下載 zip 檔案
#[get("/")]
async fn index() -> Option<NamedFile> {
    // ...
}

#[get("/<path>")]
async fn download(path: String) -> Option<NamedFile> {
    // ...
}

// 啟動 Rocket 伺服器
#[launch]
fn rocket() -> _ {
    // ...
}

這個範例展示瞭如何使用 Rocket 建立一個簡單的 Web 伺服器,包括生成和下載 zip 檔案的功能。

使用 Rust 和 Structopt 建立命令列應用程式

在本節中,我們將使用 Rust 和 Structopt 建立一個命令列應用程式,該應用程式可以與我們之前建立的 Rocket 伺服器進行互動。

建立客戶端應用程式

首先,我們需要建立一個新的 Rust 專案,然後在 src/bin/app.rs 中新增以下程式碼:

use html_creator::*;
use reqwest::Client;
use structopt::StructOpt;

#[derive(Debug, StructOpt)]
#[structopt(
    name = "html_creator",
    about = "Creates a compressed file of converted MD files"
)]
pub enum CLI {
    #[structopt/about = "Generates a zip file of html files from a json file order"]
    Generate {
        #[structopt(short, long)]
        path: String,
    },
    #[structopt/about = "Opens users to download page"]
    Open,
}

#[tokio::main]
async fn main() -> OrderResult<()> {
    let cli: CLI = CLI::from_args();

    match cli {
        CLI::Generate { path } => {
            // Get order from file
            let order = Order::from_file(&path);
            // create a new client to send requests
            let client = Client::new();
            // create a post request
            let _ = client
                .post("http://localhost:8000/generate")
                .json(&order)
                .send()
                .await?;
            println!("Use the command `html_creator open` to download your zip file.");
        }
        CLI::Open => {
            open::that("http://localhost:8000").unwrap();
        }
    }
    Ok(())
}

這個程式碼定義了一個 CLI 列舉,該列舉有兩個變體:GenerateOpenGenerate 變體需要一個 path 引數,該引數是 JSON 檔案的路徑。Open 變體沒有任何引數。

main 函式中,我們使用 StructOpt 來解析命令列引數,並根據 CLI 列舉的變體來執行不同的動作。如果是 Generate 變體,我們會讀取 JSON 檔案,建立一個新的 Order 例項,然後使用 reqwest 來向 Rocket 伺服器傳送一個 POST 請求。如果是 Open 變體,我們會使用 open 函式來開啟預設瀏覽器,並導航到 Rocket 伺服器的首頁。

執行客戶端應用程式

要執行客戶端應用程式,你需要在終端中輸入以下命令:

cargo run --bin app

這將會編譯和執行客戶端應用程式。你可以使用以下命令來生成一個 zip 檔案:

cargo run --bin app generate --path path/to/json/file.json

這將會向 Rocket 伺服器傳送一個 POST 請求,並列印預出一條訊息,提示你使用 html_creator open 命令來下載 zip 檔案。

你也可以使用以下命令來開啟預設瀏覽器,並導航到 Rocket 伺服器的首頁:

cargo run --bin app open

這將會開啟預設瀏覽器,並導航到 Rocket 伺服器的首頁,你可以在那裡下載 zip 檔案。

使用Rust和Cargo建立Markdown轉HTML應用程式

在這個專案中,我們使用Rust和Cargo建立了一個簡單的Markdown轉HTML應用程式。這個應用程式可以從Markdown檔案中讀取內容,然後轉換成HTML檔案。

專案結構

我們的專案結構如下:

Cargo.toml
src
main.rs
lib.rs
orders.json

其中,Cargo.toml是Cargo的組態檔案,src目錄包含了我們的Rust程式碼,orders.json是用於測試的Markdown內容檔案。

Cargo.toml組態

Cargo.toml中,我們需要新增一些基本的元資料,例如描述、授權等:

[package]
name = "markdown_to_html"
version = "0.1.0"
authors = ["玄貓"]
description = "Create html files from a directory of markdown files"
license = "MIT"

Rust程式碼

我們的Rust程式碼分為兩個部分:main.rslib.rs。在main.rs中,我們定義了應用程式的入口點:

use std::fs::File;
use std::io::Read;
use std::path::Path;

fn main() {
    let orders = read_orders("orders.json");
    for order in orders {
        let html = markdown_to_html(&order.content);
        let mut file = File::create(format!("{}.html", order.name)).unwrap();
        file.write_all(html.as_bytes()).unwrap();
    }
}

fn read_orders(file_path: &str) -> Vec<Order> {
    let mut file = File::open(file_path).unwrap();
    let mut contents = String::new();
    file.read_to_string(&mut contents).unwrap();
    let orders: Vec<Order> = serde_json::from_str(&contents).unwrap();
    orders
}

fn markdown_to_html(markdown: &str) -> String {
    // 將Markdown轉換成HTML
    // ...
}

lib.rs中,我們定義了Markdown轉HTML的函式:

use markdown::Markdown;

pub fn markdown_to_html(markdown: &str) -> String {
    let mut html = String::new();
    let mut markdown = Markdown::new(markdown);
    markdown.render(&mut html);
    html
}

測試

我們可以使用以下命令來測試我們的應用程式:

cargo run --bin app -- generate -p orders.json

這會將orders.json中的Markdown內容轉換成HTML檔案。

改進

在這個專案中,我們做了一些簡化的假設,例如使用unwrap()來處理錯誤。然而,在生產環境中,我們應該使用更健全的錯誤處理機制。

另外,我們可以新增更多的功能,例如:

  • 使用唯一的檔案名稱來避免覆寫檔案
  • 建立一個命令來讀取目錄中的Markdown檔案並建立一個訂單檔案
  • 改進錯誤處理機制

發布

要發布我們的應用程式,我們需要新增一些元資料到Cargo.toml中,例如描述、授權等。然後,我們可以使用以下命令來發布:

cargo publish

這會將我們的應用程式發布到Crates.io上。

Rust 網頁伺服器和命令列應用程式開發

導言

在這個章節中,我們將會探討如何使用 Rust 開發一個網頁伺服器和命令列應用程式。同時,我們也會介紹如何使用 Cargo 進行專案管理和釋出。

建立 Rust 專案

首先,我們需要建立一個新的 Rust 專案。可以使用以下命令:

$ cargo new myproject

這將會建立一個新的 Rust 專案,包含基本的目錄結構和 Cargo.toml 檔案。

網頁伺服器開發

接下來,我們將會使用 Rocket 框架開發一個網頁伺服器。Rocket 是一個流行的 Rust 網頁框架,提供了許多方便的功能,例如路由、範本引擎等。

首先,需要在 Cargo.toml 中新增 Rocket 依賴:

[dependencies]
rocket = "0.5.0-rc.1"

然後,建立一個新的檔案 src/main.rs,並新增以下程式碼:

#[macro_use]
extern crate rocket;

#[get("/")]
fn index() -> &'static str {
    "Hello, World!"
}

fn main() {
    rocket::ignite().mount("/", routes![index]).launch();
}

這個程式碼定義了一個簡單的網頁伺服器,當使用者存取 / 路由時,會傳回 “Hello, World!"。

命令列應用程式開發

接下來,我們將會使用 StructOpt 來開發一個命令列應用程式。StructOpt 是一個流行的 Rust 命令列解析器,提供了許多方便的功能,例如自動生成幫助訊息等。

首先,需要在 Cargo.toml 中新增 StructOpt 依賴:

[dependencies]
structopt = "0.3.22"

然後,建立一個新的檔案 src/bin/cli.rs,並新增以下程式碼:

use structopt::StructOpt;

#[derive(StructOpt)]
struct Cli {
    #[structopt(short = "d", long = "directory")]
    directory: String,
    #[structopt(short = "o", long = "output")]
    output: String,
}

fn main() {
    let cli = Cli::from_args();
    println!("Directory: {}", cli.directory);
    println!("Output: {}", cli.output);
}

這個程式碼定義了一個簡單的命令列應用程式,當使用者執行 cli 命令時,會解析使用者輸入的引數,並列印預出目錄和輸出路徑。

釋出專案

最後,需要將專案釋出到 Cargo 中。可以使用以下命令:

$ cargo publish

這將會將專案釋出到 Cargo 中,其他使用者可以使用 cargo add 命令來新增您的專案作為依賴。

練習

  1. 修改 html_creator 來建立唯一的 zip 檔案(使用 uuid 依賴)。
  2. 建立一個新的命令 create 來讀取一個目錄中的 markdown 檔案,並建立一個順序檔案(使用 -d-o 引數)。

答案

  1. 首先,需要在 Cargo.toml 中新增 uuid 依賴:
[dependencies]
uuid = { version = "0.8.1", features = ["v4"] }

然後,修改 generate 函式來使用 uuid 來建立唯一的 zip 檔案:

use uuid::Uuid;

pub fn generate(&self) -> OrderResult<String> {
    let path = Uuid::new_v4();
    let zip_path = format!("{}.zip", path.to_string());
    // ...
}
  1. 建立一個新的檔案 src/bin/create.rs,並新增以下程式碼:
use structopt::StructOpt;
use std::fs::File;
use std::io::Write;

#[derive(StructOpt)]
struct Create {
    #[structopt(short = "d", long = "directory")]
    directory: String,
    #[structopt(short = "o", long = "output")]
    output: String,
}

fn main() {
    let create = Create::from_args();
    let dir = std::fs::read_dir(create.directory).unwrap();
    let mut file = File::create(create.output).unwrap();
    for entry in dir {
        let entry = entry.unwrap();
        let path = entry.path();
        let mut file = File::open(path).unwrap();
        let mut contents = String::new();
        file.read_to_string(&mut contents).unwrap();
        file.write_all(contents.as_bytes()).unwrap();
    }
}

這個程式碼定義了一個簡單的命令列應用程式,當使用者執行 create 命令時,會讀取一個目錄中的 markdown 檔案,並建立一個順序檔案。

從技術架構視角來看,Rust 的非同步程式設計模型結合 Rocket 與 Tokio,展現了高效能網頁伺服器的開發潛力。透過 async/await 與 Future 的協同運作,得以有效管理非同步任務,並利用 Tokio 的反應器和執行緒池提升伺服器吞吐量。然而,非同步程式設計的複雜性也伴隨著除錯和效能調校的挑戰,開發者需要深入理解底層機制才能避免潛在的效能瓶頸。整合 StructOpt 讓命令列應用程式開發更簡潔,但也需要留意引數驗證和錯誤處理,確保程式穩定性。展望未來,隨著 Rust 非同步生態的持續發展,相關工具和最佳實務的完善將進一步降低開發門檻,並推動更多高效能網路應用誕生。對於追求極致效能和資源利用率的專案,Rust 的非同步網頁伺服器開發值得深入研究和應用。