返回文章列表

Rust Web 伺服器與Mandelbrot集合計算

本文介紹如何使用 Rust 的 Iron 框架構建 Web 伺服器,並實作處理不同 HTTP 路徑的路由功能。文章詳細說明瞭如何解析表單資料、計算最大公約數,以及如何使用 Rust 的平行程式設計特性。此外,文章還探討了 Mandelbrot 集合的計算方法,包含複數運算、迭代過程和逃逸時間演算法,並示範如何使用

Web 開發 Rust

Rust 的 Iron 框架提供簡潔的 API 建立 Web 伺服器。透過 Router 型別,可以輕鬆地將不同路徑的請求對映到對應的處理函式。本文示範瞭如何使用 Router 處理 GET 和 POST 請求,並解析表單資料進行最大公約數計算。同時,也介紹了 Rust 的錯誤處理機制,例如使用 Result 型別和 match 表示式處理表單解析和型別轉換可能發生的錯誤。此外,文章還探討了 Mandelbrot 集合的計算方法,從簡單的迭代公式到複數迭代,逐步解釋了其數學原理。並使用 Rust 語言實作了 escape_time 函式,用於判斷一個複數是否屬於 Mandelbrot 集合,其中包含了複數運算、迭代過程和逃逸時間演算法等關鍵技術細節。最後,文章還示範瞭如何使用 Rust 的 num crate 進行複數計算,以及如何解析命令列引數和處理錯誤,例如使用 Option 型別來表示可能存在或不存在的值。

簡易的Web伺服器實作

在前面的章節中,我們已經成功建立了一個基本的Web伺服器,但點選「Compute GCD」按鈕並沒有任何反應。本章節將會介紹如何使用Router型別來將不同的處理函式與不同的路徑關聯起來。

使用Router處理不同路徑

首先,我們需要在iron-gcd/src/main.rs檔案中加入以下宣告:

extern crate router;
use router::Router;

接下來,我們可以修改main函式如下:

fn main() {
    let mut router = Router::new();
    router.get("/", get_form, "root");
    router.post("/gcd", post_gcd, "gcd");
    println!("Serving on http://localhost:3000...");
    Iron::new(router).http("localhost:3000").unwrap();
}

在上述程式碼中,我們建立了一個Router例項,並為兩個特定的路徑建立了處理函式。然後,我們將這個Router例項傳遞給Iron::new,以建立一個根據URL路徑決定呼叫哪個處理函式的Web伺服器。

內容解密:

  1. let mut router = Router::new();:建立一個新的Router例項。
  2. router.get("/", get_form, "root");:將get_form函式與根路徑("/")關聯起來。
  3. router.post("/gcd", post_gcd, "gcd");:將post_gcd函式與"/gcd"路徑關聯起來,並指定請求方法為POST。
  4. Iron::new(router).http("localhost:3000").unwrap();:將Router例項傳遞給Iron::new,並啟動Web伺服器監聽在本地的3000埠。

實作post_gcd函式

現在,我們可以開始實作post_gcd函式:

extern crate urlencoded;
use std::str::FromStr;
use urlencoded::UrlEncodedBody;

fn post_gcd(request: &mut Request) -> IronResult<Response> {
    let mut response = Response::new();
    let form_data = match request.get_ref::<UrlEncodedBody>() {
        Err(e) => {
            response.set_mut(status::BadRequest);
            response.set_mut(format!("Error parsing form data: {:?}\n", e));
            return Ok(response);
        }
        Ok(map) => map
    };

    // ...(後續程式碼)
}

內容解密:

  1. let form_data = match request.get_ref::<UrlEncodedBody>() { ... }:嘗試從請求中解析表單資料。如果解析失敗,則傳回錯誤回應。
  2. request.get_ref::<UrlEncodedBody>():使用UrlEncodedBody解析請求中的表單資料。
  3. Err(e):如果解析失敗,則設定回應狀態為BadRequest,並傳回錯誤訊息。
  4. Ok(map):如果解析成功,則將解析結果儲存在form_data變數中。

後續的程式碼將會繼續處理表單資料,並計算最大公約數(GCD)。

完整post_gcd函式實作

以下是完整的post_gcd函式實作:

fn post_gcd(request: &mut Request) -> IronResult<Response> {
    let mut response = Response::new();
    let form_data = match request.get_ref::<UrlEncodedBody>() {
        Err(e) => {
            response.set_mut(status::BadRequest);
            response.set_mut(format!("Error parsing form data: {:?}\n", e));
            return Ok(response);
        }
        Ok(map) => map
    };

    let unparsed_numbers = match form_data.get("n") {
        None => {
            response.set_mut(status::BadRequest);
            response.set_mut(format!("form data has no 'n' parameter\n"));
            return Ok(response);
        }
        Some(nums) => nums
    };

    let mut numbers = Vec::new();
    for unparsed in unparsed_numbers {
        match u64::from_str(&unparsed) {
            Err(_) => {
                response.set_mut(status::BadRequest);
                response.set_mut(
                    format!("Value for 'n' parameter not a number: {:?}\n",
                            unparsed));
                return Ok(response);
            }
            Ok(n) => { numbers.push(n); }
        }
    }

    let mut d = numbers[0];
    for m in &numbers[1..] {
        d = gcd(d, *m);
    }

    response.set_mut(status::Ok);
    response.set_mut(mime!(Text/Html; Charset=Utf8));
    response.set_mut(
        format!("The greatest common divisor of the numbers {:?} is <b>{}</b>\n",
                numbers, d));
    Ok(response)
}

內容解密:

  1. let unparsed_numbers = match form_data.get("n") { ... }:從表單資料中取得名為"n"的引數值。如果不存在,則傳回錯誤回應。
  2. let mut numbers = Vec::new();:建立一個空的向量,用於儲存解析後的數字。
  3. for unparsed in unparsed_numbers { ... }:遍歷未解析的數字,並嘗試將其轉換為u64型別。如果轉換失敗,則傳回錯誤回應。
  4. let mut d = numbers[0];:初始化最大公約數為第一個數字。
  5. for m in &numbers[1..] { ... }:遍歷剩餘的數字,並計算最大公約數。
  6. response.set_mut(format!("The greatest common divisor of the numbers {:?} is <b>{}</b>\n", numbers, d));:設定回應內容,顯示最大公約數的結果。

Rust 程式語言特性與應用

Rust 是一種系統程式語言,以其記憶體安全和平行程式設計的能力而聞名。本篇文章將探討 Rust 的一些關鍵特性,包括 Result 型別、match 表示式、平行程式設計等。

Result 型別與錯誤處理

在 Rust 中,Result 是一種列舉型別,用於表示可能成功或失敗的操作結果。它有兩個變體:Ok(value) 表示成功,Err(error) 表示失敗。以下是一個使用 Result 的例子:

let result: Result<i32, &str> = Ok(10);
match result {
    Ok(value) => println!("成功:{}", value),
    Err(error) => println!("失敗:{}", error),
}

內容解密:

  • Result 型別用於處理可能失敗的操作,例如檔案讀取或網路請求。
  • match 表示式用於檢查 Result 的變體,並根據結果執行不同的程式碼分支。
  • Ok(value) 分支處理成功情況,Err(error) 分支處理失敗情況。

match 表示式

match 表示式是 Rust 中用於模式匹配的強大工具。它允許根據值的不同變體執行不同的程式碼。以下是一個簡單的例子:

let num = 2;
match num {
    1 => println!("一"),
    2 => println!("二"),
    _ => println!("其他"),
}

內容解密:

  • match 表示式根據 num 的值執行不同的分支。
  • _ 萬用字元用於匹配任何未被明確列出的值。

平行程式設計

Rust 的所有權系統和借用檢查器確保了平行程式設計的安全性。以下是一個簡單的平行程式範例,使用 Rust 的標準函式庫進行執行緒管理:

use std::thread;

fn main() {
    let handle = thread::spawn(|| {
        println!("Hello from a thread!");
    });
    handle.join().unwrap();
}

內容解密:

  • thread::spawn 用於建立一個新的執行緒。
  • handle.join().unwrap() 用於等待執行緒完成執行。

Mandelbrot 集計算

Mandelbrot 集是一種著名的分形,可以透過迭代一個簡單的函式來計算。以下是一個簡單的 Mandelbrot 集計算範例:

fn mandelbrot(c: (f64, f64), max_iter: usize) -> usize {
    let mut z = (0.0, 0.0);
    let mut iter = 0;
    while iter < max_iter && z.0 * z.0 + z.1 * z.1 < 4.0 {
        z = (z.0 * z.0 - z.1 * z.1 + c.0, 2.0 * z.0 * z.1 + c.1);
        iter += 1;
    }
    iter
}

fn main() {
    let max_iter = 100;
    for i in 0..100 {
        for j in 0..100 {
            let c = (i as f64 / 50.0 - 2.0, j as f64 / 50.0 - 1.0);
            let iter = mandelbrot(c, max_iter);
            println!("{} {}", i, iter);
        }
    }
}

內容解密:

  • mandelbrot 函式計算給定複數 c 的 Mandelbrot 集迭代次數。
  • main 函式演示瞭如何呼叫 mandelbrot 函式並列印結果。

Mandelbrot 集的數學原理與 Rust 實作

Mandelbrot 集是一種複雜的數學結構,其定義根據一個簡單的迭代公式。在本章中,我們將探討 Mandelbrot 集的數學原理,並使用 Rust 語言實作相關的計算。

簡單的迭代公式

首先考慮一個簡單的迭代公式:$x = x^2$。這個公式描述了一個數值的平方運算。根據初始值的不同,這個迭代過程會產生不同的結果。

內容解密:

  • 當初始值 $x$ 小於 1 時,$x^2$ 會變得更小,因此 $x$ 會趨近於零。
  • 當初始值 $x$ 等於 1 時,$x^2$ 仍然等於 1,因此 $x$ 保持不變。
  • 當初始值 $x$ 大於 1 時,$x^2$ 會變得更大,因此 $x$ 會趨近於無窮大。
  • 當初始值 $x$ 為負數時,$x^2$ 會變成正數,然後遵循上述規則。
@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultTextAlignment center
skinparam rectangleBackgroundColor #F5F5F5
skinparam rectangleBorderColor #333333
skinparam arrowColor #333333

title 內容解密:

rectangle "小於1" as node1
rectangle "等於1" as node2
rectangle "大於1" as node3
rectangle "負數" as node4

node1 --> node2
node2 --> node3
node3 --> node4

@enduml

此圖示說明瞭不同初始值對迭代結果的影響。

加法迭代公式

接下來考慮一個稍微複雜的迭代公式:$x = x^2 + c$,其中 $c$ 是一個常數。這個公式在每次迭代中將 $c$ 加到 $x^2$ 上。

內容解密:

  • 當 $c$ 大於 0.25 或小於 -2.0 時,$x$ 會趨近於無窮大。
  • 否則,$x$ 會保持在零附近。

複數迭代公式

現在將迭代公式擴充套件到複數域,使用複數 $z$ 和 $c$。這裡使用 num crate 提供的 Complex 型別來表示複數。

extern crate num;
use num::Complex;

#[allow(dead_code)]
fn complex_square_add_loop(c: Complex<f64>) {
    let mut z = Complex { re: 0.0, im: 0.0 };
    loop {
        z = z * z + c;
    }
}

內容解密:

  • Complex { re: 0.0, im: 0.0 } 表示複數零。
  • z * z + c 是複數的平方加法運算。

Mandelbrot 集的定義

Mandelbrot 集是由所有使得 $z$ 不趨近於無窮大的複數 $c$ 組成的集合。

escape_time 函式

最終,我們實作了一個 escape_time 函式,用於判斷一個複數 $c$ 是否屬於 Mandelbrot 集。

fn escape_time(c: Complex<f64>, limit: u32) -> Option<u32> {
    let mut z = Complex { re: 0.0, im: 0.0 };
    for i in 0..limit {
        z = z*z + c;
        if z.norm_sqr() > 4.0 {
            return Some(i);
        }
    }
    None
}

內容解密:

  • escape_time 函式接受一個複數 c 和一個迭代次數限制 limit
  • 如果 c 不屬於 Mandelbrot 集,則傳回 Some(i),其中 i 是迭代次數。
  • 如果 c 可能屬於 Mandelbrot 集,則傳回 None

Option 型別

Rust 的標準函式庫定義了一個 Option 型別,用於表示一個可能存在或不存在的值。

enum Option<T> {
    None,
    Some(T),
}

內容解密:

  • Option<T> 可以表示任何型別 T 的值。
  • Some(v) 表示存在一個值 v
  • None 表示不存在值。

Mandelbrot 集合的計算與解析

在計算 Mandelbrot 集合的過程中,escape_time 函式扮演著至關重要的角色。它傳回一個 Option<u32> 型別的值,用於指示複數 c 是否屬於 Mandelbrot 集合。如果 c 不屬於該集合,函式會傳回 Some(i),其中 i 代表迭代次數;而當 c 屬於該集合時,函式則傳回 None

迭代過程與距離計算

for 迴圈中,迭代從 0 開始,直到達到指定的 limit 為止。與之前的範例類別似,這裡的 for 迴圈用於遍歷整數範圍。

for i in 0..limit {
    // ...
}

在判斷複數 z 是否超出半徑為 2 的圓形區域時,程式採用了 z.norm_sqr() 方法來計算 z 與原點之間的距離平方。透過比較距離平方與 4.0,可以避免進行平方根運算,從而提升計算效率。

檔案註解與檔案生成

在 Rust 程式碼中,使用 /// 符號來標記檔案註解(documentation comments)。這些註解能夠被 rustdoc 工具解析,進而生成線上檔案。Rust 標準函式庫的檔案便是以這種形式撰寫的。

解析命令列引數

為了控制輸出影像的解析度以及顯示的 Mandelbrot 集合區域,程式需要解析命令列引數。以下是一個用於解析座標對的函式:

use std::str::FromStr;

/// 解析字串 `s` 為座標對,例如 "400x600" 或 "1.0,0.5"。
fn parse_pair<T: FromStr>(s: &str, separator: char) -> Option<(T, T)> {
    match s.find(separator) {
        None => None,
        Some(index) => match (T::from_str(&s[..index]), T::from_str(&s[index + 1..])) {
            (Ok(l), Ok(r)) => Some((l, r)),
            _ => None,
        },
    }
}

#[test]
fn test_parse_pair() {
    assert_eq!(parse_pair::<i32>("", ','), None);
    assert_eq!(parse_pair::<i32>("10,", ','), None);
    assert_eq!(parse_pair::<i32>(",10", ','), None);
    assert_eq!(parse_pair::<i32>("10,20", ','), Some((10, 20)));
    assert_eq!(parse_pair::<i32>("10,20xy", ','), None);
    assert_eq!(parse_pair::<f64>("0.5x", 'x'), None);
    assert_eq!(parse_pair::<f64>("0.5x1.5", 'x'), Some((0.5, 1.5)));
}

內容解密:

  1. parse_pair 函式定義:此函式為泛型函式,能夠解析字串 s 為一對由指定分隔符號 separator 分隔的值。
  2. 泛型與 trait 約束<T: FromStr> 表示 T 必須實作 FromStr trait,確保能夠將字串轉換為 T 型別的值。
  3. find 方法與匹配:使用 s.find(separator) 尋找分隔符號的位置。若未找到,則傳回 None;否則,嘗試將分隔符號前後的字串解析為 T 型別的值。
  4. 錯誤處理:若解析失敗,則傳回 None;成功則傳回包含解析結果的 Some((l, r))
  5. 測試案例:透過多個測試案例驗證 parse_pair 函式在不同輸入下的行為。

解析複數

利用 parse_pair 函式,可以輕鬆實作解析複數的函式:

/// 解析以逗號分隔的一對浮點數為複數。
fn parse_complex(s: &str) -> Option<Complex<f64>> {
    match parse_pair(s, ',') {
        Some((re, im)) => Some(Complex { re, im }),
        None => None,
    }
}

#[test]
fn test_parse_complex() {
    assert_eq!(parse_complex("1.25,-0.0625"), Some(Complex { re: 1.25, im: -0.0625 }));
    assert_eq!(parse_complex(",-0.0625"), None);
}

內容解密:

  1. parse_complex 函式:呼叫 parse_pair 解析字串為一對浮點數,若成功則構建 Complex<f64> 值。
  2. 簡化結構初始化:Rust 允許使用同名變數初始化結構欄位,因此可以直接寫成 Complex { re, im }
  3. 錯誤傳遞:若 parse_pair 傳回 None,則直接傳遞給呼叫者。