Rust 同步方式訪問 REST API 的完整指南
在 Rust 中不使用異步機制訪問 REST API 是完全可行的,特別適合簡單應用、腳本或不需要高并發的場景。以下是完整的同步實現方案:
📦 依賴選擇
推薦庫:
[dependencies]
reqwest = { version = "0.11", features = ["blocking", "json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
🔧 基礎 GET 請求
1. 簡單文本響應
use reqwest::blocking::get;fn main() -> Result<(), Box<dyn std::error::Error>> {let url = "https://jsonplaceholder.typicode.com/posts/1";let response = get(url)?;if response.status().is_success() {let body = response.text()?;println!("Response body: {}", body);} else {println!("Request failed with status: {}", response.status());}Ok(())
}
2. 解析 JSON 響應
use serde::Deserialize;#[derive(Debug, Deserialize)]
struct Post {userId: u32,id: u32,title: String,body: String,
}fn main() -> Result<(), Box<dyn std::error::Error>> {let url = "https://jsonplaceholder.typicode.com/posts/1";let response = get(url)?;if response.status().is_success() {let post: Post = response.json()?;println!("Post title: {}", post.title);println!("Full post: {:#?}", post);} else {println!("Request failed with status: {}", response.status());}Ok(())
}
📤 POST 請求示例
1. 發送 JSON 數據
use reqwest::blocking::Client;
use serde::Serialize;#[derive(Debug, Serialize)]
struct NewPost {title: String,body: String,userId: u32,
}fn main() -> Result<(), Box<dyn std::error::Error>> {let client = Client::new();let new_post = NewPost {title: "My New Post".to_string(),body: "This is the body of my new post".to_string(),userId: 1,};let response = client.post("https://jsonplaceholder.typicode.com/posts").json(&new_post).send()?;println!("Status: {}", response.status());println!("Response: {}", response.text()?);Ok(())
}
2. 發送表單數據
use reqwest::blocking::Client;fn main() -> Result<(), Box<dyn std::error::Error>> {let client = Client::new();let params = [("username", "john_doe"), ("password", "secret123")];let response = client.post("https://example.com/login").form(¶ms).send()?;println!("Login response: {}", response.text()?);Ok(())
}
🔐 高級功能
1. 添加請求頭
use reqwest::blocking::Client;
use reqwest::header;fn main() -> Result<(), Box<dyn std::error::Error>> {let client = Client::new();let mut headers = header::HeaderMap::new();headers.insert(header::AUTHORIZATION, "Bearer token123".parse()?);headers.insert(header::USER_AGENT, "MySyncRustClient/1.0".parse()?);let response = client.get("https://api.example.com/protected").headers(headers).send()?;println!("Protected resource: {}", response.text()?);Ok(())
}
2. 超時設置
use reqwest::blocking::Client;
use std::time::Duration;fn main() -> Result<(), Box<dyn std::error::Error>> {let client = Client::builder().timeout(Duration::from_secs(5)).build()?;let response = client.get("https://api.example.com/slow-endpoint").send()?;println!("Response: {}", response.text()?);Ok(())
}
3. 處理分頁
use reqwest::blocking::Client;
use serde::Deserialize;#[derive(Debug, Deserialize)]
struct Page {items: Vec<Item>,next_page: Option<u32>,
}#[derive(Debug, Deserialize)]
struct Item {id: u32,name: String,
}fn fetch_all_items() -> Result<Vec<Item>, Box<dyn std::error::Error>> {let client = Client::new();let mut all_items = Vec::new();let mut page_num = 1;loop {let url = format!("https://api.example.com/items?page={}", page_num);let response = client.get(&url).send()?;if !response.status().is_success() {return Err(format!("Request failed: {}", response.status()).into());}let page: Page = response.json()?;all_items.extend(page.items);match page.next_page {Some(next) => page_num = next,None => break,}}Ok(all_items)
}
🛡? 錯誤處理最佳實踐
1. 自定義錯誤類型
use thiserror::Error;#[derive(Error, Debug)]
enum ApiError {#[error("HTTP request failed: {0}")]HttpError(#[from] reqwest::Error),#[error("API returned error: {0}")]ApiError(String),#[error("Invalid response format")]ParseError,
}fn fetch_data() -> Result<String, ApiError> {let response = reqwest::blocking::get("https://api.example.com/data")?;if response.status().is_success() {response.text().map_err(|_| ApiError::ParseError)} else {let status = response.status();let body = response.text().unwrap_or_default();Err(ApiError::ApiError(format!("{}: {}", status, body)))}
}
2. 重試機制
use reqwest::blocking::Client;
use std::thread;
use std::time::Duration;fn fetch_with_retry(url: &str, max_retries: u32) -> Result<String, Box<dyn std::error::Error>> {let client = Client::new();let mut retries = 0;loop {match client.get(url).send() {Ok(response) if response.status().is_success() => {return response.text().map_err(|e| e.into());}Ok(response) => {eprintln!("Request failed: {}", response.status());}Err(e) => {eprintln!("Request error: {}", e);}}retries += 1;if retries >= max_retries {return Err("Max retries exceeded".into());}// 指數退避let delay = 2u64.pow(retries);eprintln!("Retrying in {} seconds...", delay);thread::sleep(Duration::from_secs(delay));}
}
📊 性能優化
1. 復用 HTTP 客戶端
use reqwest::blocking::Client;fn main() -> Result<(), Box<dyn std::error::Error>> {// 創建一次,多次復用let client = Client::new();let response1 = client.get("https://api.example.com/resource1").send()?;// 處理響應1...let response2 = client.get("https://api.example.com/resource2").send()?;// 處理響應2...Ok(())
}
2. 多線程處理(有限并發)
use reqwest::blocking::Client;
use std::thread;fn fetch_urls(urls: &[&str]) -> Vec<String> {let client = Client::new();let handles: Vec<_> = urls.iter().map(|url| {let url = (*url).to_string();thread::spawn(move || {match client.get(&url).send() {Ok(resp) => resp.text().unwrap_or_else(|_| "Error reading response".into()),Err(_) => "Request failed".into(),}})}).collect();handles.into_iter().map(|h| h.join().unwrap()).collect()
}fn main() {let urls = ["https://jsonplaceholder.typicode.com/posts/1","https://jsonplaceholder.typicode.com/posts/2","https://jsonplaceholder.typicode.com/posts/3",];let results = fetch_urls(&urls);for (i, result) in results.iter().enumerate() {println!("Response {}: {}", i + 1, result);}
}
📝 完整示例:天氣查詢工具
use reqwest::blocking::Client;
use serde::Deserialize;
use std::env;#[derive(Debug, Deserialize)]
struct WeatherData {name: String,main: Main,weather: Vec<Weather>,
}#[derive(Debug, Deserialize)]
struct Main {temp: f32,feels_like: f32,humidity: u32,
}#[derive(Debug, Deserialize)]
struct Weather {description: String,
}fn main() -> Result<(), Box<dyn std::error::Error>> {let api_key = env::var("OPENWEATHER_API_KEY").expect("請設置 OPENWEATHER_API_KEY 環境變量");let city = env::args().nth(1).unwrap_or_else(|| "London".to_string());let url = format!("https://api.openweathermap.org/data/2.5/weather?q={}&appid={}&units=metric",city, api_key);let client = Client::new();let response = client.get(&url).send()?;if !response.status().is_success() {return Err(format!("API請求失敗: {}", response.status()).into());}let weather: WeatherData = response.json()?;println!("\n{} 的天氣:", weather.name);println!("溫度: {:.1}°C", weather.main.temp);println!("體感溫度: {:.1}°C", weather.main.feels_like);println!("濕度: {}%", weather.main.humidity);println!("天氣狀況: {}", weather.weather[0].description);Ok(())
}
?? 同步方法的局限性
- 阻塞主線程:每個請求都會阻塞當前線程
- 低并發能力:不適合高并發場景
- 資源利用低效:線程在等待響應時無法處理其他任務
- 擴展性差:大規模請求需要大量線程
💡 何時使用同步方法
- 命令行工具:簡單的數據獲取工具
- 腳本任務:一次性數據處理腳本
- 低流量服務:內部工具或低流量API
- 學習階段:理解HTTP請求的基礎
- 簡單嵌入式系統:資源受限環境
📚 總結
同步 REST API 訪問核心步驟:
- 使用
reqwest::blocking
模塊 - 創建
Client
實例(可復用) - 構建請求(GET/POST/PUT/DELETE)
- 發送請求并獲取響應
- 檢查狀態碼
- 解析響應體(文本/JSON/二進制)
最佳實踐:
- 復用 Client:減少連接開銷
- 設置超時:防止無限等待
- 添加重試:處理臨時故障
- 優雅錯誤處理:使用自定義錯誤類型
- 環境變量管理:安全存儲API密鑰
對于需要高并發或高性能的場景,建議使用異步方法(如 reqwest
的異步API + tokio
運行時)。但對于許多應用場景,同步方法提供了簡單直接的解決方案。