在現代網路應用中,理解底層網路協定和硬體互動至關重要。本文將使用 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 位址生成器
接下來,我們實作 MacAddress
的 new
方法,用於生成一個隨機的 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_local
和 is_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
列舉定義了三個可能的狀態:Init
、Running
和 Stopped
。Machine
結構體具有一個 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
列舉定義了三個變體:Connect
、Request
和 Response
。loop
迴圈不斷地根據當前的狀態進行不同的動作。
狀態機的優點
使用列舉實作狀態機有幾個優點:
- 可讀性:列舉使得程式碼更容易閱讀和理解。
- 型別安全:列舉可以確保狀態機的轉換是型別安全的。
- 簡潔性:列舉可以使得程式碼更簡潔和易於維護。
結合原始 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
專案設定
如果你喜歡手動設定專案,可以按照以下步驟進行:
- 執行以下命令:
$ 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 語言在網路程式設計領域的優勢,值得開發者借鑒和學習。