Rust实战课程--网络资源监控器(初版)

关于本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

过程问题&&解决方案

  1. 如果使用了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,把同步调用改成异步调用就可以了,不然同步调用会直接阻塞主进程。

  2. tokio的作用是啥?

    答:仔细观察 main函数的上面增加一个宏声明:#[tokio::main],如果不增加的话我们的异步调用逻辑就会报错 'main' function is not allowed to be 'async'

写在最后

**前路漫漫 道阻且长 与君共勉 呼~ ** 山有顶峰,湖有彼岸,万物皆有回甘

相关推荐
神秘的猪头4 小时前
html5与js今日笔记
前端
程序猿小蒜4 小时前
基于springboot的基于智能推荐的卫生健康系统开发与设计
java·javascript·spring boot·后端·spring
渣哥4 小时前
IOC 容器的进化:ApplicationContext 在 Spring 中的核心地位
javascript·后端·面试
Zyx20074 小时前
🎹用 HTML5 打造“敲击乐”钢琴:前端三剑客的第一次交响曲
前端
小时前端4 小时前
面试官:我为什么总在浏览器存储问题上追问IndexedDB?
前端·浏览器
前端小菜哇4 小时前
前端如何优雅的写一个记忆化函数?
前端
Gu_yyqx4 小时前
Spring 框架
java·后端·spring
今禾4 小时前
Git完全指南(下篇):Git高级技巧与问题解决
前端·git·github
llq_3504 小时前
为什么 JS 代码执行了,但页面没马上变?
前端·javascript