前言

当局域网网速突然变慢时,我们不仅想知道“网速慢了”,更想知道“是谁在占用带宽”。(实验室的网总是不好,用这个小工具看看到底是谁在占用带宽,当做复习 Rust 的练手)

虽然 Linux 上有经典的 iftop,但它的界面有些古老。而 btop 虽然界面炫酷,但它主要监控系统资源(CPU/内存),在网络流量的具体来源分析上功能有限。

目前可以实现的功能如下,未来如果需要什么功能再增加

  • 实时流量监控:基于 libpcap 的底层抓包。
  • 绘制图表:使用 Ratatui 绘制波形图,类似于 Btop++风格
  • 统计网速:基于滑动窗口算法计算 1 分钟平均网速,过滤瞬时抖动。
  • 自动日志:支持流量快照记录,并自动轮转日志文件。
  • 识别主机名和厂商:支持 DNS 反向解析和 MAC 地址厂商识别。

代码仓库: https://github.com/ssfortynine/net_monitor

目前识别主机名和厂商,通过 mDNS识别只能识别一些简单设备(本机以及路由器),其他的设备设置了防火墙,根据这个方法无法读取到主机名。

  • 尝试使用了一些网络工具nmlookup也只能查找到小部分,大部分 PC 机查找不到
    识别厂商名比较简单,只需要识别 MAC 地址的前缀即可,但是 OUI 数据集不好搜索,现在的 MAC 地址前缀不只是前 6 位,文件夹下保存了 Nmap MAC 的前缀

虽然可以识别 Vendor ,但是我们实验室使用的机子都是一家厂商,所以 MAC 地址也都一样,所以我就把这个功能搁置了,感觉没有什么用

Rust 库选择

  • 核心库
    • pcap / pnet: 处理底层网络包捕获。
    • ratatui: 目前 Rust 生态最强大的 TUI 库,用于绘制界面。
    • crossterm: 处理终端输入和原始模式。
  • 辅助库
    • log4rs: 处理日志轮转(限制文件大小、自动删除旧日志)。
    • dns-lookup: 解析 IP 对应的域名。
    • chrono: 时间处理

架构设计:生产者-消费者模型

为了保证界面渲染流畅(500ms 刷新一次)且不丢失任何一个数据包,系统采用了经典的线程分离设计:

  1. 捕获线程(Producer):置于后台,处于混杂模式(Promiscuous Mode)监听网卡。它负责解析数据包,并累加每个 IP 产生的字节数。
  2. UI 线程(Consumer):主线程负责定时从共享内存读取数据。它计算速率、更新历史采样点,并最终渲染图形。
  3. 共享状态(SharedStats):仅保存增量,UI 线程每读取一次便将其清零,简化了锁的竞争时间。

关键算法:基于滑动窗口的速率统计

简单的“瞬时速率”会导致图表剧烈跳动,难以观察长期趋势。我在系统中引入了 IpHistory 结构体,利用双端队列(VecDeque)实现了一个滑动窗口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pub struct IpHistory {
pub samples: VecDeque<u64>, // 存储每个采样周期的字节数
pub total_sum: u64, // 窗口内字节总和
pub peak_rate: f64, // 历史峰值速率
}

impl IpHistory {
pub fn update(&mut self, bytes: u64) -> f64 {
self.samples.push_back(bytes);
self.total_sum += bytes;

// 维护滑动窗口长度(例如 60 秒)
if self.samples.len() > MAX_SAMPLES {
if let Some(removed) = self.samples.pop_front() {
self.total_sum -= removed;
}
}

// 计算平均速率:总和 / (样本数量 * 采样周期)
let duration_secs = self.samples.len() as f64 * (TICK_RATE_MS as f64 / 1000.0);
self.total_sum as f64 / duration_secs
}
}

底层逻辑:从字节流到 IP 地址

数据采集模块的核心在于对网络协议栈的逐层拆解。利用 pnet 库解析数据包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1. 解析以太网帧
if let Some(ethernet) = EthernetPacket::new(packet.data) {
// 2. 确认是否为 IPv4 协议
if ethernet.get_ethertype() == EtherTypes::Ipv4 {
// 3. 解析 IPv4 数据包
if let Some(ipv4) = Ipv4Packet::new(ethernet.payload()) {
let len = packet.header.len as u64;
let src = ipv4.get_source();
let dst = ipv4.get_destination();

// 逻辑判断:如果源 IP 是本机,则为上传(TX),反之为下载(RX)
if src == local_ip {
stats.tx_delta += len;
} else {
stats.rx_delta += len;
}
}
}
}

TUI 视觉优化

终端的字符像素通常是 1x1,绘制曲线会有严重的锯齿。我选用了 RatatuiMarker::Braille(盲文)。盲文符号由 8 个点组成,可以将一个字符单元拆分为 2x4 的微小“像素点”。

这个部分是参考 Btop++ 的 UI风格

同时,针对流量排行表(Top Talkers)设置动态颜色:

  • 红色:当平均网速超过 1MB/s 时,提醒用户该设备正在进行大流量操作。
  • 绿色:轻量级流量显示。

参考资料

OUI 数据库的来源: https://github.com/Ringmast4r/OUI-Master-Database?tab=readme-ov-file