关于本Rust实战教程系列:
- 定位:一份从Rust基础迈向项目实战的练习记录。
- 内容:聚焦实战中遇到的具体问题、解决思路与心得总结。
- 读者 :适合有Rust基础,但急需项目经验巩固知识的开发者。资深玩家可忽略。
- 计划:本期为系列第二篇,将根据我的学习进度持续更新。
- 导航 :文件管理命令行工具
简要说明
网络资源监控器(初版) :监控一组网站,检查它们是否可访问,并记录响应时间;
本篇为一个简易版本,用于帮助初学者能够有一个简单的过渡,然后在下一篇在实现相对完整的企业级的网络资源监控系统。
需求拆解:
- 从配置文件读取监控目标(URL列表)
- 定期(例如每5分钟)发送HTTP请求到这些目标
- 记录响应状态和响应时间
- 如果网站不可达(连接超时或非200状态码),发送告警(控制台输出)
- 将监控结果保存到文件(如CSV格式)
技术点说明
- reqwest: 用于HTTP请求
- tokio: 异步运行时
- std::fs 读取文件/操作文件
- csv: CSV格式文件处理
- clap: 命令行参数
- 结构体函数定义
实现步骤
整体按照由易到难的逻辑,按照我们对需求的理解来一点点的完善我们的功能
单个网站监测函数
实现监测单个网站的函数,入参为网站URL,出参为网站监测结果
rust
// 检查网络函数
async fn check_website(url: &str) -> (bool, u128, Option<u16>){
// 创建HTTP客户端 这里使用的是 Rust中的reqwest库来请求
// 创建一个客户端,设置超时时间为5s
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(5))
.build()
.unwrap();
// 记录开始时间
let start = std::time::Instant::now();
// 需要使用await来捕获异步请求的结果
match client.get(url).send().await {
Ok(response) => {
// start.elapsed():返回从start到现在的std::time::Druation
// .as_millis(): 把Duration转换为毫秒数(u128)
let duration = start.elapsed().as_millis();
// 获取当前的状态码,用于后续的记录
let status_code = response.status().as_u16();
(true, duration, Some(status_code))
},
Err(_) => {
// 记录响应耗时
let duration = start.elapsed().as_millis();
(false, duration, None)
}
}
}
读取监控网站列表
初版我们简单做一下,读取monitor_list.txt
文件中配置的网站列表,逐个进行监测:
rust
// monitor_list.txt
https://www.google.com
https://www.github.com
https://httpbin.org/status/200
https://httpbin.org/status/404
https://httpbin.org/status/500
通过std::fs来读取文件中的内容,然后转换成网站列表(Vec),供后面的函数调用:
rust
// 获取网站监控的列表
fn read_monitor_list(file_path: &PathBuf) -> Vec<String> {
// 读取文件内容
match fs::read_to_string(file_path) {
Ok(content) => {
// .lines():&str/String上面的方法,返回一个迭代器,按行切分字符串(按\n或者\r\n分割),返回每行的&str
return content.lines()
.map(|line| line.trim().to_string()) // 过滤首尾的空格
.filter(|line| !line.is_empty())//过滤空行
.collect();// 收集成Vec<String>
},
Err(err) => {
//
eprintln!("Error reading monitor list file: {}", err.to_string());
return vec![];
}
};
}
这个步骤结束的话我们就已经完成了一个简单的闭环,现在我们来稍微完善一下main
函数,让它能把网站的监测结果打印出来,然后我们在继续往下走:
rust
async fn main(){
// 拿到网站配置文件的URL
// std::env::current_dir():上一篇我们也用到过,获取当前的工作目录
let monitor_path = std::env::current_dir().map(|path|path.join("monitor_list.txt")).unwrap();
// 调用上面的函数得到监控网站列表
let monitor_website_list = read_monitor_list(&monitor_path);
// 增加判空提示信息
if monitor_website_list.is_empty() {
println!("监控列表为空,请在 monitor_list.txt 中添加网址。");
return;
}
for url in monitor_website_list {
let result = check_website(&url).await;
match result {
(true,duration,Some(status_code)) => {
println!("{} 响应时间:{}ms,状态码:{}",url,druation,status_code);
},
(false,duration,None) => {
println!("{} 响应时间:{},状态码:无",url,duration);
}
}
}
/** 最终输出结果:(根据我自身网络条件的测试结果,仅供参考)
https://www.google.com 响应时间: 837ms, 状态码: 200
https://www.github.com 响应时间: 523ms, 状态码: 200
https://httpbin.org/status/200 响应时间: 5002ms, 状态码: 无
https://httpbin.org/status/404 响应时间: 1262ms, 状态码: 404
https://httpbin.org/status/500 响应时间: 5002ms, 状态码: 无
*/
}
异步监测网站状态&&定时监控
其实上面的话我们已经实现了异步监测网站的状态,就是使用reqwest::Client::builder()
创建一个异步客户端,现在的话我们在把定时监控的逻辑来加上,本次也是来简单的实现:默认5分钟来监测一次
rust
// 我们来新增一个异步模块,来处理相关的逻辑
// src/async_monitor.rs
use reqwest::Client;
use tokio::time::{interval, Duration}; //定时器
// 定义异步监控结构体
pub struct AsyncMonitor{
client: Client,
interval_minutes: u64
}
// 给结构体增加函数逻辑
impl AsyncMonitor{
pub fn new(interval_minutes:u64) -> Self {
let client = Client::builder()
.timeout(Duration::from_secs(10))
.build()
.unwrap();
AsyncMonitor{
client,
interval_minutes
}
}
// 其实就是跟上面第一步里面的一样
pub async fn check_website_async(&self, url:&str) -> (bool,u128,Option<u16>){
let start = std::time::Instant::now();
let response = self.client.get(url).send().await;
match response {
Ok(response) => {
let duration = start.elapsed().as_millis();
let status_code = response.status().as_u16();
(true, duration, Some(status_code))
}
Err(_) => (false, start.elapsed().as_millis(), None),
}
}
// 增加一个定时监控的逻辑
pub async fn start_monitoring(&self,websites:Vec<String>) {
// 创建一个异步计时器
let mut interval = interval(Duration::from_secs(self.interval_minutes * 60));
loop{
// 会异步等待"下一个时间点",当到达时这个 await 完成并返回一个 Instant(通常直接忽略返回值)。
interval.tick().await;
for url in &websites {
let (status, response_time, status_code) = self.check_website_async(url).await;
println!("{} 响应时间:{}, 状态码:{}", url, response_time, status_code);
}
}
}
}
配置命令行参数
定时监控和异步查询网站状态我们都已经实现了,下面的话我就要思考一下简单的触发场景了,因为上一次我们主要讲了一下命令行工具的内容,所以我们本地还是来简单定一下命令行工具,来实现一次监控和定时监控的逻辑,我们来一起看下定义的命令行结构体:
rust
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name="网络监控器", about="一个简单的网络监控工具")]
struct Args {
// 命令行参数
// subcommand:代表该字段是`子命令`的容器,clap会根据命令行把对应的子命令解析为该字段的枚举值变体
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Once, // cargo run -- Once
Monitor {
// 检查间隔
#[arg(short, long, default_value = "5")]
interval: u64, // cargo run -- Monitor --interval 10
},
}
网站状态本地化存储(CSV)
我们最后的话把我们监测的结果存储到CSV文件中,用于后续功能(告警、数据存储等)
我们直接调用Rust中的csv库来进行文件的操作:
rust
// 定义一个csv_logger.rs模块
use std::fs::OpenOptions; // 文件打开参数
use csv::Writer;
use chrono::Local;
use std::error::Error;
// 定义记录结果的结构体
#[derive(Debug, Clone)]
pub struct UrlLogResult {
pub url: String,
pub status: bool,
pub response_time: u128,
pub status_code: Option<u16>,
}
// 日志核心struct 用于后续的日志操作
pub struct CsvLogger {
file_name: String,
}
impl CsvLogger {
pub fn new(file_name: &str) -> Self {
//判断一下 当时的file_name是否存在,不存在就创建并写入表头
if !std::path::Path::new(&file_name).exists(){
let mut wtr = Writer::from_path(&file_name).unwrap();
wtr.write_record(&["时间", "网址", "状态", "响应时间(ms)", "状态码"]).unwrap();
wtr.flush().unwrap();
}
CsvLogger { file_name: file_name.to_string() }
}
// 读写日志内容
pub fn log(&self, result: UrlLogResult) -> Result<(), Box<dyn Error>> {
let mut wtr = Writer::from_writer(
OpenOptions::new() // 创建一个新的文件打开选项构建器
.append(true) // 以追加的模式打开,不覆盖现有内容
.create(true) // 如果文件不存在则创建
.open(&self.file_name)?, // ? 跟 unwrap 都用于处理Result 跟 Option,都是如果OK 或 Some 直接返回值,但是如果Err 或 None ?会返回错误 不会直接Panic,但是unwrap会直接Panic。
);
let code = result.status_code.map_or("None".to_string(), |x| x.to_string());
wtr.write_record(&[
Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
result.url,
if result.status {"Success".to_string()} else {"Failed".to_string()},
result.response_time.to_string(),
code,
])?;
wtr.flush()?;
Ok(())
}
}
逻辑整合
我们每个模块的内容都已经完成了,现在就是需要在main中来整合所有的代码逻辑,其实就是获取命令行参数,如果是执行一次,就调用第一步单次执行函数,如果是定时监听,就调用第第三步中的定时监控函数,整体逻辑已经很清晰了,下面我们来看下main函数的代码逻辑:
rust
// src/main.rs
// 日志模块
mod csv_logger;
use csv_logger::{CsvLogger,UrlLogResult};
// 定时监控模块
mod async_monitor;
use async_monitor::AsyncMonitor;
#[tokio::main]
async fn main(){
println!("网络监控器 启动...");
// 初始化日志结构体
let logger = CsvLogger::new("moniter_log.csv");
// 获取待监控的网站列表
let monitor_path = std::env::current_dir().map(|path| path.join("monitor_list.txt")).unwrap();
let monitor_website_list = read_monitor_list(&monitor_path);
if monitor_website_list.is_empty() {
println!("监控列表为空,请在 monitor_list.txt 中添加网址。");
return;
}
// 获取命令行参数
let args = Args::parse();
// 匹配命令行参数
match args::command {
Commands::Once => {
for url in monitor_website_list{
let (status, duration, status_code) = check_website(&url).await;
let result: UrlLogResult = UrlLogResult {
url: url.to_string(),
status,
response_time: duration,
status_code,
};
logger.log(result).unwrap();
}
}
Commands::Monitor { interval } => {
let async_monitor = AsyncMonitor::new(interval);
async_monitor.start_monitoring(monitor_website_list).await;
}
}
println!("所有 URL 检查完毕");
}
功能演示
rust
// 分别执行下面两条命令,最终的结果就会写入到monitor_log.csv文件中。
- cargo run -- once
- cargo run -- monitor --interval 1
// monitor_log.csv
时间,网址,状态,响应时间(ms),状态码
2025-10-21 22:39:11,https://www.google.com,Success,812,200
2025-10-21 22:39:11,https://www.github.com,Success,466,200
2025-10-21 22:39:21,https://httpbin.org/status/200,Failed,10002,None
2025-10-21 22:39:31,https://httpbin.org/status/404,Failed,10001,None
2025-10-21 22:39:41,https://httpbin.org/status/500,Failed,10001,None
2025-10-21 22:40:10,https://www.google.com,Success,541,200
2025-10-21 22:40:11,https://www.github.com,Success,393,200
2025-10-21 22:40:12,https://httpbin.org/status/200,Success,1659,200
2025-10-21 22:40:22,https://httpbin.org/status/404,Failed,10002,None
2025-10-21 22:40:32,https://httpbin.org/status/500,Success,9217,500
过程问题&&解决方案
-
如果使用了reqwet的blocking模式的话,会在后面的异步监测网站状态时出现报错
Cannot drop a runtime in a context where blocking is not allowed. This happens when a runtime is dropped from within an asynchronous context.
答:混用了异步调用:
reqwest::Client
和同步调用reqwest::blocking::Client
,把同步调用改成异步调用就可以了,不然同步调用会直接阻塞主进程。 -
tokio的作用是啥?
答:仔细观察 main函数的上面增加一个宏声明:
#[tokio::main]
,如果不增加的话我们的异步调用逻辑就会报错'main' function is not allowed to be 'async'
写在最后
**前路漫漫 道阻且长 与君共勉 呼~ ** 山有顶峰,湖有彼岸,万物皆有回甘