Rust

tags: rust 學習紀錄

目前計畫

流水帳 DESC

20250608

20250606

20250605

20250505

20250503

20250502

20250427

20250424

20250420

  • blogs 分頁優化
    • 避免資料量大導致效能低落 不使用 offset 的方案
    • 目前架構不太適合非 offset 的方法 思考處理方法中

20250219

20250113

20250102

  • 將專案的 axum 版本更新至 0.8

20250101

  • 除了 blogs 中有用到的圖片 定期刪除

20241224

  • 逐步將資料庫操作分離至對應的 repositories 中

20241221

  • 已經有初步可用的 blogs & 後台管理 的版本
  • 仿 Hackmd 建立筆記的功能 用來方便撰寫 blog
  • 整理需要的功能
    • 前端
      • 前台 展示要展示的筆記
      • 後台 管理寫好的筆記頁面
        • 新增筆記按鈕後進入空白頁面
        • 點擊既有筆記後進入編輯頁面
    • 後端
      • 前台
        • 取得所有可展示筆記
        • 取得特定筆記詳細內容
      • 後台
        • 新增筆記
        • 取得所有筆記
        • 取得特定筆記詳細內容
        • 編輯特定筆記
        • 刪除特定筆記
  • 額外需求
    • 使用 ctrl + v 貼上圖片的功能
  • 參考

20241213

20241212

20241211

20241208

進度

近期 axum 網站想完成的小項目

影片

now

教學影片

axum 17h course

axum 80m

Building Web APIs With Rust and Axum

Rust Crate 大巡游

Rust Programming

Rust 项目实操

抖音商家端 Rust 业务实践@FEDAY2023_陈天壹

SQLx is my favorite PostgreSQL driver to use with Rust

Learning Rust web servers with Axum & SQLx (Twitch live stream)

Mastering Rust Web Services: From Axum to CRUD Operations

替代 socket.io 的新 rust crate

podcast

題目

實用文字記錄

陈天 让Rust成为你的下一门主力语言

Designing a New Rust Class at Stanford: Safety in Systems Programming

Rust 程式設計語言

他人筆記 Rust 基本教學

許多感覺很強的文章

用 rust 實作演算法

鐵人賽文章 記憶體 - stack 與 heap

鐵人賽文章 來玩 Rust 的框架吧! - Rocket - Part I

他人筆記 Rust: Rayon

tokio 概览

RUST中的turbofish语法(一)

RUST中的turbofish语法(二)

docker build 不錯的選擇

  • google 開放的一個安全性 & image 大小適中的選擇
Q : 即使無發行版中有Linux發行版,它對我有什麼好處?它們仍然比普通的Docker鏡像小。
A : 好吧,應用程式依賴於操作系統的事實使得很難保持圖像苗條。讓我們來看看GoogleContainerTools製作的最流行的無發行版圖像(數據截至9年2023月<>日有效):

gcr.io/distroless/static-debian11 - 2.34 MiB - 包括ca證書,時區數據,etc/passwd條目和/tmp目錄。靜態連結的應用程式將從此映像中受益,但是如果您的應用程式具有動態功能並且需要 libc,該怎麼辦?
gcr.io/distroless/base-debian11 - 17.3 MiB - 建立在靜態映射之上,包括glibc,libssl和openssl。該圖像最適合動態連結的程式,但即使在這種情況下,您也可能需要額外的共用庫,這將我們帶到另一個級別(或層,就此而言)。
gcr.io/distroless/cc-debian11 - 19.6 MiB - 建立在基礎映射之上,包括帶有依賴項的libgcc1。解釋型或基於虛擬機的語言(Java,Python,JavaScript)呢?
gcr.io/distroless/java11-debian11 - 210 MiB - 包括一個基本的Linux映射以及帶有依賴項的OpenJDK。
應用程式越動態,它需要的操作系統庫就越多,因此映像大小會膨脹。

Rust Web 框架:Axum 入门一探

鐵人賽 Day03 - 淺談Axum

News

NGINX 局限太多,Cloudflare 最终放弃它并用 Rust 自研了全新替代品 Pingora

筆記

Rust 中資料儲存的方式

Stack 堆疊 (中國叫)

  • 所有在堆疊上的資料都必須是已知固定大小
  • 在編譯時屬於未知或可能變更大小的資料必須儲存在堆積。

Heap 堆積 (中國叫)

  • 堆積就比較沒有組織,當你要將資料放入堆積,你得要求一定大小的空間。
  • 記憶體分配器(memory allocator)會找到一塊夠大的空位,標記為已佔用,然後回傳一個指標(pointer),指著該位置的位址。
  • 這樣的過程稱為在堆積上分配(allocating on the heap),或者有時直接簡稱為分配(allocating)就好(將數值放入堆疊不會被視為是在分配)。
  • 因為指標是固定已知的大小,所以你可以存在堆疊上。
  • 但當你要存取實際資料時,你就得去透過指標取得資料。

兩者在 Rust 的官網說明

  • 將資料推入堆疊會比在堆積上分配還來的,因為分配器不需要去搜尋哪邊才能存入新資料,其位置永遠在堆疊最上方。相對的,堆積就需要比較多步驟,分配器必須先找到一個夠大的空位來儲存資料,然後作下紀錄為下次分配做準備。
  • 堆積上取得資料也比在堆疊上取得來得,因為你需要用追蹤指標才找的到。
  • 現代的處理器如果在記憶體間跳轉越少的話速度就越快。
  • 讓我們繼續用餐廳做比喻,想像伺服器就是在餐廳為數個餐桌點餐。
  • 最有效率的點餐方式就是依照餐桌順序輪流點餐。
  • 如果幫餐桌 A 點了餐之後跑到餐桌 B 點,又跑回到 A 然後又跑到 B 的話,可以想像這是個浪費時間的過程。
  • 同樣的道理,處理器在處理任務時,如果處理的資料相鄰很近(就如同存在堆疊)的話,當然比相鄰很遠(如同存在堆積)來得快。
  • 當你的程式碼呼叫函式時,傳遞給函式的數值(可能包含指向堆積上資料的指標)與函式區域變數會被推入堆疊
  • 當函式結束時,這些數值就會被彈出。
  • 追蹤哪部分的程式碼用到了堆積上的哪些資料、最小化堆積上的重複資料、以及清除堆積上沒在使用的資料確保你不會耗盡空間,這些問題都是所有權系統要處理的。
  • 一旦你理解所有權後,你通常就不再需要經常考慮堆疊與堆積的問題,不過能理解所有權主要就是為了管理堆積有助於解釋為何它要這樣運作。

字串

轉換成其他型別

let guess: u32 = guess.trim() // 去掉左右的空格 & 分隔 & 換行
                    .parse() // 編譯成指定的型別
                    .expect("請輸入一個數字!"); // 錯誤時

取得使用者輸入的字串

use std::io;

io::stdin()
    .read_line(&mut guess) // 把使用者 input 的值寫進變數
    .expect("讀取該行失敗"); // 例外處理

數字

必較

use std::cmp::Ordering;
  • 給 match 用的比較大小,後有三種對應

clipboard

  • 取得 clipboard 中複製的資訊

example

// === Cargo.toml
clipboard-win = "4.5.0"

// === main.rs
use clipboard_win::{formats, get_clipboard};

// 取得 clipboard 複製的資訊
let target: String = get_clipboard(formats::Unicode).expect("To set clipboard");

open

  • 使用預設瀏覽器開啟特定網址

example

// === Cargo.toml
open = "3"

// === main.rs

// 組成 url get 的 string
let url1 = format!("https://aaa.aaa/search/?q={}", target);
let url2 = format!("https://bbb.bbb/search/?q={}&f=_all&s=create_time_DESC&syn=yes", target);
let url3 = format!("https://ccc.ccc/?f_search={}", target);

// 開啟 clipboard 複製的資訊
open::that(url1).unwrap();
open::that(url2).unwrap();
open::that(url3).unwrap();

使用 docker alpine 的問題

  • 執行時發生 "not found" or "no such file"
  • 來源
  • 以下是網路上找到(似乎)可用的範例,未實際測試
################
#### Builder
FROM rust:1.61.0-slim as builder

WORKDIR /usr/src

# Create blank project
RUN USER=root cargo new medium-rust-dockerize

# We want dependencies cached, so copy those first.
COPY Cargo.toml Cargo.lock /usr/src/medium-rust-dockerize/

# Set the working directory
WORKDIR /usr/src/medium-rust-dockerize

# Install target platform (Cross-Compilation) --> Needed for Alpine
RUN rustup target add x86_64-unknown-linux-musl

# This is a dummy build to get the dependencies cached.
RUN cargo build --target x86_64-unknown-linux-musl --release

# Now copy in the rest of the sources
COPY src /usr/src/medium-rust-dockerize/src/

# Touch main.rs to prevent cached release build
RUN touch /usr/src/medium-rust-dockerize/src/main.rs

# This is the actual application build.
RUN cargo build --target x86_64-unknown-linux-musl --release

################
#### Runtime
FROM alpine:3.16.0 AS runtime 

# Copy application binary from builder image
COPY --from=builder /usr/src/medium-rust-dockerize/target/x86_64-unknown-linux-musl/release/medium-rust-dockerize /usr/local/bin

EXPOSE 3030

# Run the application
CMD ["/usr/local/bin/medium-rust-dockerize"]
  • 使用上面的範例修改 thumbor 後,在 build 相關套件的 ring 時出現錯誤
  • 猜測是 ring 不支援該編譯方式

自己測試,可正常編譯 & docker run 的範例

  • 沒使用額外套件
# rust 使用 alpine build 的範例
FROM rust:1.69.0 AS builder

# 移動到 /rust-server 資料夾
WORKDIR /rust-server
# 將目前資料夾初始化成 cargo 專案,專案名稱跟資料夾同名
RUN cargo init

COPY main.rs /rust-server/src/main.rs

# 安裝給 alpine 使用的 architecture
# https://doc.rust-lang.org/cargo/commands/cargo-build.html#compilation-options
RUN rustup target add x86_64-unknown-linux-musl
# 使用此 architecture build
RUN cargo build --target x86_64-unknown-linux-musl --release

# 最後階段
FROM alpine:3.17

WORKDIR /app

# 路徑在 target 下的 architecture 名稱資料夾內,多了一層 architecture 名稱
COPY --from=builder /rust-server/target/x86_64-unknown-linux-musl/release/rust-server .

EXPOSE 8080

# Set the command to run when the container starts
CMD ["/app/rust-server"]

axum 源碼閱讀

websocket

整體概述

這段程式碼是一個範例的 WebSocket 伺服器,它使用了 Rust 語言的 axumtower-http 函式庫。它提供了一個 WebSocket 伺服器,用於與瀏覽器或其他 WebSocket 客戶端建立連接。

程式碼的主要結構如下:

  1. 匯入必要的函式庫和模組。
  2. 使用 axum 函式庫建立一個路由器 app,設定了一些路由。
  3. 透過 axum::Server 綁定並啟動伺服器,監聽指定的 Socket 地址。
  4. 定義了 ws_handler 函數,作為處理 WebSocket 請求的處理器。它接收 WebSocket 的升級請求,並從中提取出一些資訊,如用戶代理(user agent)和連接的 IP 地址。
  5. 定義了 handle_socket 函數,作為處理 WebSocket 連接的狀態機。它處理了與客戶端的握手、收發消息等操作。
  6. 定義了 process_message 函數,用於處理收到的 WebSocket 消息,並將其內容輸出到控制台。

這段程式碼使用了非同步的特性(async/await)和事件驅動的方式處理 WebSocket 連接。它示範了如何在 Rust 中使用 axum 構建 WebSocket 伺服器,以及如何處理 WebSocket 的收發消息和其他事件。

main 函數是程序的入口點,以下是 main 函數的詳細操作:

  1. 初始化日誌記錄器:使用 tracing_subscriber::registry() 創建一個日誌記錄器,並配置日誌過濾器和格式化器。日誌過濾器從環境變量中獲取,如果未設置,則使用默認值。
  2. 設置靜態文件目錄:通過 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets") 構建靜態文件目錄的路徑。其中,env!("CARGO_MANIFEST_DIR") 是 Cargo 生成的項目根目錄路徑。
  3. 創建應用程序路由器:使用 Router::new() 創建一個路由器對象。
  4. 註冊 WebSocket 處理程序:使用 .route("/ws", get(ws_handler)) 註冊一個 WebSocket 處理程序,將路徑 "/ws" 映射到 ws_handler 函數。
  5. 註冊靜態文件服務:使用 .fallback_service(ServeDir::new(assets_dir).append_index_html_on_directories(true)) 註冊一個靜態文件服務,將靜態文件目錄作為回退服務。這將處理所有未匹配的路由,查找對應的靜態文件並返回。
  6. 添加 HTTP 跟踪日誌中間件:使用 .layer(TraceLayer::new_for_http().make_span_with(DefaultMakeSpan::default().include_headers(true))) 添加一個 HTTP 跟踪日誌中間件。這將記錄 HTTP 請求和響應的詳細信息,並包括請求和響應的頭部信息。
  7. 綁定服務器地址並運行:使用 axum::Server::bind(&addr).serve(app.into_make_service_with_connect_info::<SocketAddr>()) 綁定服務器地址,並將應用程序轉換為服務並運行。服務器地址是 SocketAddr 類型的實例,指定了服務器監聽的 IP 地址和端口。

總結來說,main 函數初始化日誌記錄器,設置靜態文件目錄,創建應用程序路由器,並註冊 WebSocket 處理程序和靜態文件服務。然後,添加了一個 HTTP 跟踪日誌中間件。最後,綁定服務器地址並運行服務器。該函數將在程序啟動時執行。

ws_handler 函數是處理 HTTP 請求的處理程序,當 HTTP GET 請求到達 WebSocket 協商的起點時會調用該函數。以下是 ws_handler 函數的詳細操作:

  1. 獲取 WebSocketUpgrade 參數:函數接收 WebSocketUpgrade 參數,該參數表示 WebSocket 的升級過程。
  2. 獲取 user_agentConnectInfo(addr):通過 extract 模組中的 TypedHeaderConnectInfo 提取器,從 HTTP 頭部獲取 UserAgent 和從客戶端連接獲取的 SocketAddr
  3. 檢查 user_agent 的存在:檢查是否獲取到了 user_agent。如果獲取到了,則將其轉換為字符串;如果沒有獲取到,則將其設置為 "Unknown browser"。
  4. 輸出連接信息:輸出連接的相關信息,包括使用者代理(User Agent)和客戶端的 Socket 地址。輸出的格式為 '{user_agent}' at {addr} connected.
  5. 完成升級過程:調用 ws.on_upgrade 函數,將 WebSocket 連接的升級過程完成的回調函數返回。在此回調函數中,可以自定義升級過程的行為,例如傳遞額外的信息。
  6. 返回實現 IntoResponse 的結果:將回調函數返回的結果轉換為實現 IntoResponse trait 的類型,以使其可以作為 HTTP 響應返回。

總結來說,ws_handler 函數負責處理 HTTP 請求,提取相關信息(例如使用者代理和客戶端地址),輸出連接信息,並完成 WebSocket 的升級過程。該函數將在接收到 HTTP GET 請求時被調用,並返回用於升級 WebSocket 連接的回調函數。

handle_socket 函數是一個處理單個 WebSocket 連接的狀態機。以下是該函數的詳細操作:

  1. 發送一個 Ping 消息:首先,它向客戶端發送一個 Ping 消息,以確保客戶端能夠正常回應。如果成功發送 Ping 消息,則輸出 Pinged {who}...,其中 {who} 是客戶端的 Socket 地址;否則輸出 Could not send ping {who}!,並結束處理。
  2. 接收客戶端的消息:接下來,它等待從客戶端接收一個消息。如果成功接收到消息,則進行下一步處理;否則輸出 client {who} abruptly disconnected,並結束處理。
  3. 發送多個消息:使用 for 迴圈,它向客戶端連續發送多個消息。在每次迴圈中,它使用 socket.send 方法發送一個包含文字內容的 Text 消息,消息內容為 "Hi {i} times!",其中 {i} 是迴圈的索引。如果發送消息失敗,則輸出 client {who} abruptly disconnected,並結束處理。在每次迴圈之間,通過 tokio::time::sleep 函數暫停一段時間,以模擬某種等待或事件。
  4. 分離 WebSocket 的發送和接收分支:通過 socket.split 方法將 WebSocket 分離為發送和接收兩個部分,分別獲得 senderreceiver
  5. 發送非請求的消息:使用 sender,它會在獨立的任務(task)中向客戶端發送多個非請求的消息。在每次迴圈中,它使用 sender.send 方法發送一個 Text 消息,消息內容為 "Server message {i} ...",其中 {i} 是迴圈的索引。如果發送消息失敗,則結束發送任務。
  6. 接收客戶端的消息並輸出:使用 receiver,它在另一個獨立的任務中從客戶端接收消息。它使用 receiver.next() 方法獲取下一個消息,並在每次接收到消息時調用 process_message 函數處理該消息。如果 process_message 函數返回 ControlFlow::Break,則結束接收任務。
  7. 等待任一任務完成:使用 tokio::select! 宏,它等待任一任務完成。這意味著它將等待發送任務完成或接收任務完成(其中一個先完成即可)。
  8. 檢查發送任務的狀態:在 tokio::select! 宏後,它檢查發送任務的狀態。如果發送任務成功完成,則輸出 Sender for {who} terminated gracefully.;否則輸出 Sender for {who} abruptly terminated.
  9. 返回適當的 ControlFlow 值:根據發送任務和接收任務的狀態,它返回適當的 ControlFlow 值。如果發送任務和接收任務都正常完成,則返回 ControlFlow::Continue,以指示狀態機繼續運行。否則,返回 ControlFlow::Break,以指示狀態機結束。

總結來說,handle_socket 函數負責處理單個 WebSocket 連接的狀態。它通過發送和接收消息來與客戶端進行通信,並使用非請求的消息和控制流來管理連接的狀態。

process_message 函數是用於處理接收到的 WebSocket 消息的輔助函數。以下是該函數的詳細操作:

  1. 檢查消息的類型:根據接收到的消息的類型進行不同的處理。
  2. 如果是 Text 消息:如果接收到的消息是 Text 消息,則輸出 >>> {who} sent str: {t},其中 {who} 是客戶端的 Socket 地址,{t} 是消息的內容。
  3. 如果是 Binary 消息:如果接收到的消息是 Binary 消息,則輸出 >>> {who} sent {d.len()} bytes: {d},其中 {who} 是客戶端的 Socket 地址,{d.len()} 是消息的字節數,{d} 是消息的內容。
  4. 如果是 Close 消息:如果接收到的消息是 Close 消息,則進行特殊處理。它檢查是否存在 CloseFrame(關閉消息的元數據),如果存在,則輸出 >>> {who} sent close with code {cf.code} and reason {cf.reason}``,其中 {who} 是客戶端的 Socket 地址,{cf.code} 是關閉消息的狀態碼,{cf.reason} 是關閉消息的原因。如果不存在 CloseFrame,則輸出 >>> {who} somehow sent close message without CloseFrame
  5. 如果是 Pong 消息:如果接收到的消息是 Pong 消息,則輸出 >>> {who} sent pong with {v},其中 {who} 是客戶端的 Socket 地址,{v} 是 Pong 消息的內容。
  6. 如果是 Ping 消息:如果接收到的消息是 Ping 消息,則輸出 >>> {who} sent ping with {v},其中 {who} 是客戶端的 Socket 地址,{v} 是 Ping 消息的內容。
  7. 返回 ControlFlow::Continue:函數返回 ControlFlow::Continue,以指示狀態機繼續運行。

總結來說,process_message 函數根據接收到的 WebSocket 消息的類型,對消息進行適當的處理並輸出相關信息。它用於在 handle_socket 函數中顯示接收到的消息的內容和元數據,以及進行相應的處理。

未整理筆記

HashMap

  • 哈希表最核心的特點就是: 巨量的可能輸入和有限的哈希表容量。 這就會引發哈希衝突,也就是兩個或者多個輸入的哈希被映射到了同一個位置,所以我們要能夠處理哈希衝突
  • 兩個主要的解決衝突方式
    • chaining : 鏈地址法
      • 將落在同一 index 的 value 連接起來
      • search 時,找到 index 後一個一個遍歷該 index 下所有 value
    • open addressing : 開放尋址法
      • 衝突發生時依照特定的規則把 value 插入其他空位
  • 源碼

opencc

build 很慢時可試試的方法

[user]
        email = joelai1988@gmail.com
        name = kawagami
[core]
        editor = vim
[url "git@github.com:"]
        insteadOf = https://github.com/
[url "https://github.com/rust-lang/crates.io-index"]
        insteadOf = https://github.com/rust-lang/crates.io-index

docker build 相關

在 Multi-stage builds 中想使用 apline 或是 scratch 做為最後基底的兩個方法

  • 有些 crate 像是 ring 會導致 build 失敗
  • 此時我目前還沒找到解法,只有換回其他能 build 通過的 final image 基底,像是 ubuntu 或是 debian slim 版本
  • 在 build 的時候使用 rust:alpine 版本
    • 可以直接用一般的流程
    • COPY from=builder /app/target/release/APPNAME /final/APPNAME
    • 就好
    • 以下為簡單範例
FROM rust:alpine as builder

WORKDIR /app

COPY Cargo.toml .
COPY /src ./src

RUN cargo build --release

# FROM alpine as final
FROM scratch as final

WORKDIR /final

COPY --from=builder /app/target/release/PROJECTNAME /final/PROJECTNAME

CMD ["/final/PROJECTNAME"]
  • 在 build 的時候使用 rust:1.75 之類的一般版本

# 在 build 前安裝此環境
RUN rustup target add x86_64-unknown-linux-musl
# 使用這個編譯
RUN cargo build --target x86_64-unknown-linux-musl

......

# 再到這位置取得編譯後的檔案
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/APPNAME /final/APPNAME

  • 以下為簡單範例
FROM rust:1.75-slim-bookworm as builder

WORKDIR /app

COPY Cargo.toml .
COPY /src ./src

RUN rustup target add x86_64-unknown-linux-musl
RUN cargo build --release --target x86_64-unknown-linux-musl

# FROM alpine as final
FROM scratch as final

WORKDIR /final

COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/PROJECTNAME /final/PROJECTNAME

CMD ["/final/PROJECTNAME"]

演講影片 axum 的設計概念

等待閱讀清單