在現代網路應用中,理解底層網路協定和硬體互動至關重要。本文將使用 Rust 語言,一步步建構一個名為 mget 的網路工具,它能夠生成 MAC 位址、傳送 HTTP 請求,並解析 DNS。這個過程涵蓋了網路程式設計的許多關鍵概念,例如 MAC 位址結構、單播和多播模式、以及 TCP/IP 協定堆疊的基礎知識。我們將深入研究 Rust 的新型別模式、列舉和模式匹配等特性,以建構狀態機,處理網路訊息,並最終實作一個功能完整的網路工具。

MAC 位址結構

MAC 位址的結構包括:

  • 第一個 byte 傳輸
  • 當地/通用旗標
  • 單播/多播旗標

在 Rust 語法中,MAC 位址的佈局被指定為 [u8; 6]

根據當地/通用旗標,特定位元的角色會有所不同。

MAC 位址模式

MAC 位址有兩種模式:單播和多播。這兩種形式的傳輸行為是相同的。區別在於當裝置決定是否接受一個框架(frame)時。框架是一個用於描述乙太網路封包的術語。類別比於框架包括封包、包裝器和信封。圖 8.4 顯示了這種區別。

單播位址旨在在兩個直接接觸的點之間傳輸資訊(例如,筆記型電腦和路由器之間)。無線存取點會使事情複雜化,但不會改變基本原理。多播位址可以被多個裝置接受,而單播位址只有單一接收者。單播一詞有些誤導,因為傳送乙太網路封包涉及超過兩個裝置。使用單播位址會改變裝置在接收封包時的行為,但不會改變傳輸的資料。

產生 MAC 位址

當我們開始討論原始 TCP 時,我們將建立一個虛擬硬體裝置。為了說服任何人與我們溝通,我們需要學習如何為虛擬裝置分配 MAC 位址。 macgen 專案可以生成通用管理 MAC 位址。

網路傳輸模式:單播、多播和廣播

在網路傳輸中,資料可以以不同的模式傳送給目的地裝置。這些模式包括單播(Unicast)、多播(Multicast)和廣播(Broadcast)。瞭解這些模式對於設計和實作高效的網路傳輸系統至關重要。

單播(Unicast)

單播是一種一對一的傳輸模式,即資料從一個來源裝置傳送到一個目的地裝置。當一個裝置要傳送資料給另一個特定的裝置時,它會將目的地裝置的MAC地址包含在資料框架(Frame)中。這樣,資料就可以準確地傳送到目的地裝置,而其他裝置會忽略這個框架。

多播(Multicast)

多播是一種一對多的傳輸模式,即資料從一個來源裝置傳送到多個目的地裝置。當一個裝置要傳送資料給多個特定的裝置時,它會將這些目的地裝置的MAC地址包含在資料框架中,並設定多播位元(Multicast Bit)。這樣,所有設定了相同多播地址的裝置都會接受這個框架。

廣播(Broadcast)

廣播是一種一對所有的傳輸模式,即資料從一個來源裝置傳送到網路中的所有裝置。當一個裝置要傳送資料給網路中的所有裝置時,它會將廣播MAC地址(通常是FF:FF:FF:FF:FF:FF)包含在資料框架中。這樣,所有連線到網路的裝置都會接受這個框架。

MAC地址和傳輸模式

MAC地址是一個唯一的識別符號,用於識別網路中的每個裝置。MAC地址由6個位元組組成,通常以十六進製表示。多播MAC地址和單播MAC地址的區別在於最低有效位元(Least Significant Bit, LSB)的設定。當LSB設為1時,MAC地址被認為是多播地址;否則,它被認為是單播地址。

實作網路傳輸模式

在實際應用中,網路傳輸模式的選擇取決於具體的需求和場景。例如,在點對點通訊中,單播模式是最常用的;而在多媒體廣播或群聊應用中,多播模式可能更合適。廣播模式通常用於網路發現或組態等情況。

內容解密:

上述內容介紹了網路傳輸中的三種模式:單播、多播和廣播。每種模式都有其特點和適用場景。在實際應用中,需要根據具體需求選擇合適的傳輸模式,以確保網路系統的效率和可靠性。

圖表翻譯:

下面是一個簡單的Mermaid圖表,示範了單播、多播和廣播的差異:

  graph LR
    A[來源裝置] -->|單播|> B[目的地裝置]
    A -->|多播|> C[多個目的地裝置]
    A -->|廣播|> D[所有連線到的裝置]

這個圖表展示了三種傳輸模式之間的區別,幫助讀者更好地理解這些概念。

MAC 位址產生器

專案設定

首先,我們需要設定專案的版本和編譯版本。這些設定可以在 Cargo.toml 檔案中進行組態。

version = "0.1.0"
edition = "2018"

[dependencies]
rand = "0.7"

MAC 位址結構體

接下來,我們定義一個 MacAddress 結構體來代表 MAC 位址。這個結構體包含一個 [u8; 6] 的陣列,用於儲存 MAC 位址的六個八位元組。

#[derive(Debug)]
struct MacAddress([u8; 6]);

MAC 位址顯示實作

為了方便地顯示 MAC 位址,我們實作了 Display 特徵(trait) для MacAddress 結構體。這使得我們可以使用 {} 格式化-specifier 來顯示 MAC 位址。

impl Display for MacAddress {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let octet = &self.0;
        write!(
            f,
            "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
            octet[0], octet[1], octet[2],
            octet[3], octet[4], octet[5]
        )
    }
}

MAC 位址產生

現在,我們可以使用 rand 函式庫來產生隨機的 MAC 位址。首先,我們需要匯入 rand 函式庫和 RngCore 特徵。

extern crate rand;
use rand::RngCore;

然後,我們可以使用 thread_rng() 函式來取得一個隨機數生成器,並使用 gen::<u8>() 方法來產生隨機的八位元組。

let mut rng = rand::thread_rng();
let mac_address = MacAddress([
    rng.gen::<u8>(),
    rng.gen::<u8>(),
    rng.gen::<u8>(),
    rng.gen::<u8>(),
    rng.gen::<u8>(),
    rng.gen::<u8>(),
]);

顯示 MAC 位址

最後,我們可以使用 println! 宏來顯示產生的 MAC 位址。

println!("{}", mac_address);

這會輸出一個隨機的 MAC 位址,格式為 xx:xx:xx:xx:xx:xx,其中每個 x 代表一個十六進位制數字。

內容解密:

在上述程式碼中,我們使用 rand 函式庫來產生隨機的 MAC 位址。MacAddress 結構體用於儲存 MAC 位址的六個八位元組。Display 特徵的實作使得我們可以使用 {} 格式化-specifier 來顯示 MAC 位址。

圖表翻譯:

  flowchart TD
    A[開始] --> B[產生隨機MAC位址]
    B --> C[顯示MAC位址]
    C --> D[結束]

在這個流程圖中,我們首先產生一個隨機的 MAC 位址,然後顯示它,最後結束程式。

MAC 位址生成器的實作

在網路通訊中,MAC 位址是一個獨一無二的識別符號,用於區分不同的網路裝置。下面,我們將實作一個 MAC 位址生成器,以展示如何使用 Rust 語言建立一個簡單的 MAC 位址生成工具。

MAC 位址結構體

首先,我們定義一個 MacAddress 結構體,用於代表 MAC 位址。這個結構體包含一個 6 個元素的陣列 octets,每個元素都是一個 u8 整數,代表 MAC 位址中的 6 個位元組。

struct MacAddress {
    octets: [u8; 6],
}

MAC 位址生成器

接下來,我們實作 MacAddressnew 方法,用於生成一個隨機的 MAC 位址。這個方法使用 rand 函式庫生成 6 個隨機位元組,並將其儲存在 octets 陣列中。

impl MacAddress {
    fn new() -> MacAddress {
        let mut octets: [u8; 6] = [0; 6];
        rand::thread_rng().fill_bytes(&mut octets);
        octets[0] |= 0b_0000_0011; // 設定最低兩位為 1
        MacAddress { octets }
    }
}

MAC 位址方法

我們還實作了兩個方法:is_localis_unicast。這些方法用於檢查 MAC 位址是否為本地管理位址或單播位址。

impl MacAddress {
    fn is_local(&self) -> bool {
        (self.octets[0] & 0b_0000_0010) == 0b_0000_0010
    }

    fn is_unicast(&self) -> bool {
        (self.octets[0] & 0b_0000_0001) == 0b_0000_0001
    }
}

Crate 中的 metadata

最後,我們可以在 Cargo.toml 檔案中新增 metadata,以描述我們的 crate。

[package]
name = "macgen"
version = "0.1.0"
edition = "2021"

[dependencies]
rand = "0.8.4"

這樣,我們就完成了 MAC 位址生成器的實作。這個工具可以用於生成隨機的 MAC 位址,並提供方法來檢查位址是否為本地管理位址或單播位址。

使用Rust實作狀態機與新型別模式

在Rust中,列舉(enum)是一種強大的工具,常用於實作狀態機。列舉允許您定義一組命名的值,並且可以使用模式匹配來處理不同的狀態。

新型別模式

新型別模式(newtype pattern)是一種設計模式,允許您在不增加額外開銷的情況下,包裝一個基本型別。這種模式常用於建立一個新的型別,該型別具有與基本型別相同的內部結構,但具有不同的語義意義。

例如,以下程式碼展示瞭如何使用新型別模式包裝一個裸陣列:

struct MacAddress([u8; 6]);

impl MacAddress {
    fn new() -> Self {
        MacAddress([0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
    }
}

在這個例子中,MacAddress 結構體包裝了一個長度為 6 的 u8 陣列。這允許您建立一個具有特定語義意義的新型別,而不是使用一個裸陣列。

實作狀態機

列舉是一種實作狀態機的好方法。以下是如何使用列舉實作一個簡單的狀態機:

enum State {
    Init,
    Running,
    Stopped,
}

struct Machine {
    state: State,
}

impl Machine {
    fn new() -> Self {
        Machine { state: State::Init }
    }

    fn start(&mut self) {
        self.state = State::Running;
    }

    fn stop(&mut self) {
        self.state = State::Stopped;
    }
}

在這個例子中,State 列舉定義了三個可能的狀態:InitRunningStoppedMachine 結構體具有一個 state 欄位,該欄位的型別是 State 列舉。

MAC 地址轉換

如果您想要將 MAC 地址轉換為十六進製表示,您可以使用以下程式碼:

fn mac_to_hex(mac: [u8; 6]) -> String {
    mac.iter()
       .map(|b| format!("{:02x}", b))
       .collect::<Vec<String>>()
       .join(":")
}

fn main() {
    let mac = MacAddress([0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
    let hex_mac = mac_to_hex(mac.0);
    println!("{}", hex_mac);
}

這個程式碼定義了一個 mac_to_hex 函式,該函式將 MAC 地址轉換為十六進製表示。然後,在 main 函式中,它建立了一個 MacAddress 例項,並將其轉換為十六進製表示。

內容解密:

上述程式碼展示瞭如何使用 Rust 的列舉和新型別模式實作狀態機和 MAC 地址轉換。列舉允許您定義一組命名的值,並使用模式匹配來處理不同的狀態。新型別模式允許您在不增加額外開銷的情況下,包裝一個基本型別。

圖表翻譯:

以下是 MAC 地址轉換過程的 Mermaid 圖表:

  flowchart TD
    A[MAC 地址] --> B[轉換為十六進位制]
    B --> C[輸出十六進製表示]

這個圖表展示了 MAC 地址轉換過程的流程。首先,MAC 地址被輸入,然後它被轉換為十六進製表示,最後輸出十六進製表示。

實作狀態機以處理網路訊息

在處理網路訊息時,能夠定義狀態機是一個必要的前提。狀態機使得程式碼能夠適應連線性的變化。在 Rust 中,列舉(enum)是一種實作狀態機的有效方法。

狀態機的基本概念

狀態機是指一種可以根據不同的狀態進行不同動作的機制。在網路程式設計中,狀態機常用於處理不同階段的網路連線和資料傳輸。例如,在建立 TCP 連線時,程式需要經歷連線、請求和回應等不同階段。

使用 Rust 的列舉實作狀態機

Rust 的列舉可以用來實作狀態機。列舉是一種可以定義多個變體的型別,每個變體代表了一個特定的狀態。透過使用 match 陳述式,可以根據當前的狀態進行不同的動作。

以下是一個簡單的狀態機實作範例:

enum HttpState {
    Connect,
    Request,
    Response,
}

loop {
    state = match state {
        HttpState::Connect if!socket.is_active() => {
            HttpState::Request
        }
        HttpState::Request if socket.may_send() => {
            socket.send(data);
            HttpState::Response
        }
        HttpState::Response if socket.can_recv() => {
            received = socket.recv();
            HttpState::Response
        }
        HttpState::Response if!socket.may_recv() => {
            break;
        }
        _ => state,
    }
}

在這個範例中,HttpState 列舉定義了三個變體:ConnectRequestResponseloop 迴圈不斷地根據當前的狀態進行不同的動作。

狀態機的優點

使用列舉實作狀態機有幾個優點:

  • 可讀性:列舉使得程式碼更容易閱讀和理解。
  • 型別安全:列舉可以確保狀態機的轉換是型別安全的。
  • 簡潔性:列舉可以使得程式碼更簡潔和易於維護。

結合原始 TCP 包

要處理原始 TCP 包,通常需要有 root 或超級使用者許可權。作業系統提供了相關的 API 來處理原始 TCP 包。在下一節中,我們將更深入地探討如何使用 Rust 來處理原始 TCP 包。

圖表翻譯:

  graph LR
    A[Connect] -->|is_active()|> B[Request]
    B -->|may_send()|> C[Response]
    C -->|can_recv()|> C
    C -->|!may_recv()|> D[Break]

這個圖表展示了狀態機的轉換過程。根據當前的狀態,程式會進行不同的動作。

建立虛擬網路裝置

為了繼續進行這個章節,你需要建立虛擬網路硬體。使用虛擬硬體可以提供更多控制權,以便自由分配IP和MAC地址。同時,這也避免了修改你的硬體設定,這可能會影響它連線到網路的能力。

建立TAP裝置

在Linux系統上,你可以使用以下命令建立一個名為tap-rust的TAP裝置:

sudo ip tuntap add mode tap name tap-rust user $USER

這個命令的作用是:

  • sudo:以root使用者身份執行命令。
  • ip tuntap:告訴ip命令我們正在管理TUN/TAP裝置。
  • add:使用add子命令新增一個新的TAP裝置。
  • mode tap:指定我們想要建立的裝置模式為TAP。
  • name tap-rust:給裝置一個唯一的名稱,tap-rust
  • user $USER:授予非root使用者帳戶對裝置的存取許可權。

驗證裝置建立

當命令執行成功時,ip命令不會輸出任何內容。為了確認我們的tap-rust裝置已經被新增,你可以使用以下命令:

ip tuntap list

這將顯示所有可用的TUN/TAP裝置,包括我們剛剛建立的tap-rust裝置。

內容解密:

上述命令使用了Linux的ip命令來建立和管理TUN/TAP裝置。TUN/TAP裝置是一種虛擬網路裝置,可以讓你模擬真實網路硬體的行為。透過建立這種虛擬裝置,你可以測試和開發網路應用程式,而不需要修改你的物理網路設定。

圖表翻譯:

  flowchart TD
    A[建立TAP裝置] --> B[執行ip tuntap add命令]
    B --> C[指定裝置模式和名稱]
    C --> D[授予非root使用者存取許可權]
    D --> E[驗證裝置建立]

這個流程圖展示了建立TAP裝置的步驟,從執行ip tuntap add命令開始,到驗證裝置建立成功為止。

網路基礎設定與HTTP通訊

為了建立一個基礎的網路環境,我們需要進行一些初始設定。首先,建立一個名為tap-rust的網路裝置,並啟用它。這可以透過以下命令實作:

$ ip tuntap list
tap-rust: tap persist user

接下來,我們需要為這個裝置分配一個IP地址,並設定系統將封包轉發到它。相關命令如下:

$ sudo ip link set tap-rust up
$ sudo ip addr add 192.168.42.100/24 dev tap-rust
$ sudo iptables -t nat -A POSTROUTING -s 192.168.42.0/24 -j MASQUERADE
$ sudo sysctl net.ipv4.ip_forward=1

當你完成了這章的內容後,可以使用以下命令刪除裝置:

$ sudo ip tuntap del mode tap name tap-rust

“Raw” HTTP

現在,我們已經具備了使用HTTP在TCP層級的所有知識。讓我們來實作一個名為mget的專案,它涵蓋了列表8.20-8.23。這個專案相對較大,但理解和建立它將會非常有趣。每個檔案都有不同的角色:

  • main.rs(列表8.20):負責命令列解析和整合功能。
  • ethernet.rs(列表8.21):生成MAC地址並進行MAC地址型別之間的轉換。
  • http.rs(列表8.22):負責與伺服器互動以發出HTTP請求。
  • dns.rs(列表8.23):進行DNS解析,將網域名稱轉換為IP地址。

mget專案結構

以下是mget專案的結構:

ch8-mget
├── Cargo.toml
└── src
    ├── main.rs
    ├── ethernet.rs
    ├── http.rs
    └── dns.rs

你可以透過以下命令下載和執行mget專案:

$ cd rust-in-action/ch8/ch8-mget

專案設定

如果你喜歡手動設定專案,可以按照以下步驟進行:

  1. 執行以下命令:

$ cargo new mget $ cd mget $ cargo install cargo-edit $ cargo add clap@2 $ cargo add url@02 $ cargo add rand@0.7 $ cargo add trust-dns@0.16 –no-default-features $ cargo add smoltcp@0.6 –features=‘proto-igmp proto-ipv4 verbose log’

  2.  確保你的`Cargo.toml`檔案與列表8.19匹配。
3.  在`src`目錄中,列表8.20成為`main.rs`,列表8.21成為`ethernet.rs`,列表8.22成為`http.rs`,列表8.23成為`dns.rs`。

請參考列表8.19-8.23以瞭解具體細節。

## HTTP原始封包的組裝與傳輸
在網路通訊中,HTTP協定是用於傳輸超文字的應用層協定。當我們想要從伺服器取得網頁內容時,需要傳送HTTP請求。下面,我們將實作一個簡單的HTTP GET請求,並使用原始socket進行封包的組裝與傳輸。

### 專案設定
首先,我們需要設定專案的基本資訊,包括名稱、版本號和所需的依賴函式庫。這些資訊可以在`Cargo.toml`檔案中找到。
```toml
[package]
name = "mget"
version = "0.1.0"
edition = "2018"

[dependencies]
clap = "2"
rand = "0.7"
smoltcp = {
    version = "0.6",
    features = ["proto-igmp", "proto-ipv4", "verbose", "log"]
}
trust-dns = {
    version = "0.16",
    default-features = false
}
url = "2"

命令列引數解析

接下來,我們需要解析命令列引數,以便使用者可以指定要存取的網址。這裡,我們使用clap函式庫來解析命令列引數。

use clap::{App, Arg};

fn main() {
    let app = App::new("mget")
       .about("GET a webpage, manually")
       .arg(Arg::with_name("url").required(true));
    //...
}

DNS解析與HTTP請求

在解析命令列引數後,我們需要對指定的網址進行DNS解析,以便取得其對應的IP地址。然後,我們可以使用smoltcp函式庫來建立TCP連線,並傳送HTTP GET請求。

use smoltcp::phy::TapInterface;
use url::Url;

mod dns;
mod ethernet;
mod http;

fn main() {
    //...
    let url = Url::parse("https://example.com").unwrap();
    //...
}

封包組裝與傳輸

最後,我們需要組裝HTTP請求封包,並使用原始socket進行傳輸。這裡,我們需要手動設定TCP和IP頭部,然後將封包傳送到網路中。

use smoltcp::phy::TapInterface;

fn main() {
    //...
    let tap_interface = TapInterface::new("tap0").unwrap();
    //...
}

命令列引數解析與隨機埠選擇

在實作 mget 命令列工具時,正確解析使用者提供的引數至關重要。以下是如何使用 clap 函式庫來定義和解析命令列引數的示例。

從使用者經驗與開發體驗的雙重角度來看,打造一個兼具易用性與彈性的命令列工具至關重要。本文深入探討了 MAC 位址的結構、網路傳輸模式、MAC 位址生成、狀態機的實作以及 HTTP 通訊等核心概念,並逐步引導讀者構建一個名為 mget 的 HTTP 客戶端工具。分析 mget 工具的程式碼結構,可以發現,其巧妙地將網路操作的各個層級抽象成獨立的模組,例如 ethernet.rs 負責 MAC 位址處理、http.rs 負責 HTTP 請求構建、dns.rs 負責網域名稱解析,而 main.rs 則負責整合這些模組並提供使用者介面。這樣的模組化設計不僅提升了程式碼的可讀性和可維護性,也為日後的功能擴充套件奠定了堅實的基礎。然而,目前的 mget 工具仍存在一些限制,例如錯誤處理機制不夠完善、缺乏對 HTTPS 的支援等。展望未來,可以預見的是,隨著 Rust 語言和網路技術的發展,mget 工具的功能將會更加豐富,例如支援 HTTP/2、QUIC 等新興協定,並整合更強大的錯誤處理和日誌記錄功能。對於想要深入理解網路程式設計的開發者而言,持續關注 mget 工具的演進,並積極參與社群貢獻,將有助於提升自身的技術能力。玄貓認為,mget 工具的設計理念和實作方式,體現了 Rust 語言在網路程式設計領域的優勢,值得開發者借鑒和學習。