82、网络流量统计:流量统计实现、带宽计算、实时监控
网络流量统计,说白了就是搞清楚你的网口上到底跑了多少数据。我早年做嵌入式网关的时候,客户反馈设备偶尔会卡死,查了半天才发现是流量统计模块的内存泄漏——每秒记录一次数据,结果内存越吃越多,最后系统直接挂了。从那以后,我对流量统计这块就格外上心。
这一讲,咱们就聊聊怎么用C语言实现流量统计、带宽计算,以及如何做到实时监控。嗯,内容不复杂,但坑不少,我一个个给你拆开讲。
流量统计的核心思路
流量统计,本质上就是数包。你想想看,网卡每收一个包,我们就记一笔。统计什么呢?无非是:
- 总字节数:所有收发的字节加起来
- 总包数:收发了多少个数据包
- 当前速率:每秒多少字节(Bps)或多少比特(bps)
- 峰值速率:历史最高速率
我个人习惯用结构体来组织这些数据,清晰又好维护。你看下面这个定义:
typedef struct {
unsigned long long rx_bytes; // 接收总字节数
unsigned long long tx_bytes; // 发送总字节数
unsigned long long rx_packets; // 接收总包数
unsigned long long tx_packets; // 发送总包数
double rx_rate; // 接收速率(Bps)
double tx_rate; // 发送速率(Bps)
double peak_rx; // 接收峰值(Bps)
double peak_tx; // 发送峰值(Bps)
struct timeval last_time; // 上次采样时间
} traffic_stats_t;
这里我用了 unsigned long long,为什么?因为在高带宽环境下,32位整数很快会溢出。我曾经见过一个项目,用 int 存字节数,千兆网络下跑了几分钟就溢出了,统计结果直接变成负数——那画面太美我不敢看。
从 /proc/net/dev 读取数据
在Linux系统上,获取流量数据最直接的方式就是读 /proc/net/dev。这个虚拟文件会列出所有网络接口的收发统计。格式大概是这样的:
Inter-| Receive | Transmit
face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
lo: 1234567 12345 0 0 0 0 0 0 7654321 54321 0 0 0 0 0 0
eth0: 987654321 98765 0 0 0 0 0 0 123456789 12345 0 0 0 0 0 0
解析这个文件其实很简单。我一般用 fopen 打开,然后跳过前两行表头,再用 sscanf 按格式读。给你看个例子:
int read_proc_net_dev(const char *iface, traffic_stats_t *stats) {
FILE *fp = fopen("/proc/net/dev", "r");
if (!fp) return -1;
char line[256];
// 跳过前两行
fgets(line, sizeof(line), fp);
fgets(line, sizeof(line), fp);
while (fgets(line, sizeof(line), fp)) {
char name[32];
unsigned long long rx_bytes, rx_packets;
unsigned long long tx_bytes, tx_packets;
// 注意:sscanf 的格式要匹配好
if (sscanf(line, " %31[^:]: %llu %llu %*u %*u %*u %*u %*u %*u %llu %llu",
name, &rx_bytes, &rx_packets, &tx_bytes, &tx_packets) == 5) {
if (strcmp(name, iface) == 0) {
stats->rx_bytes = rx_bytes;
stats->rx_packets = rx_packets;
stats->tx_bytes = tx_bytes;
stats->tx_packets = tx_packets;
fclose(fp);
return 0;
}
}
}
fclose(fp);
return -1; // 没找到指定接口
}
%*u 就是用来跳过这些整数的。我第一次写的时候没注意,结果读出来的数据全是错的,排查了半天才发现是格式串没对齐。
带宽计算:两次采样做差
带宽计算,说白了就是算速率。你想想看,第一次采样拿到字节数 bytes1,第二次采样拿到 bytes2,中间隔了 delta_t 秒,那么速率就是:
rate = (bytes2 - bytes1) / delta_t
这里有个细节:delta_t 怎么算?我建议用 gettimeofday 获取微秒级时间戳,这样精度更高。看代码:
double calc_rate(unsigned long long cur_bytes,
unsigned long long prev_bytes,
struct timeval *cur_time,
struct timeval *prev_time) {
double delta_bytes = (double)(cur_bytes - prev_bytes);
double delta_sec = (cur_time->tv_sec - prev_time->tv_sec)
+ (cur_time->tv_usec - prev_time->tv_usec) / 1000000.0;
if (delta_sec <= 0) return 0.0;
return delta_bytes / delta_sec; // 返回 Bps
}
delta_sec 为0,一定要做保护,否则除零错误会让你程序直接崩溃。
实时监控:定时采样与显示
实时监控,其实就是循环采样、计算、显示。我一般用 sleep(1) 做1秒间隔的轮询。当然,如果你需要更高精度,可以用 nanosleep 或者 timerfd。
下面是一个简单的实时监控循环:
void monitor_traffic(const char *iface) {
traffic_stats_t prev = {0}, cur = {0};
// 第一次采样
if (read_proc_net_dev(iface, &prev) != 0) {
fprintf(stderr, "无法读取接口 %s 的数据\n", iface);
return;
}
gettimeofday(&prev.last_time, NULL);
while (1) {
sleep(1);
// 第二次采样
if (read_proc_net_dev(iface, &cur) != 0) break;
gettimeofday(&cur.last_time, NULL);
// 计算速率
double rx_rate = calc_rate(cur.rx_bytes, prev.rx_bytes,
&cur.last_time, &prev.last_time);
double tx_rate = calc_rate(cur.tx_bytes, prev.tx_bytes,
&cur.last_time, &prev.last_time);
// 显示(单位换算:Bps -> Mbps)
printf("\rRX: %.2f Mbps | TX: %.2f Mbps",
rx_rate * 8 / 1000000, tx_rate * 8 / 1000000);
fflush(stdout);
// 更新上一次数据
prev = cur;
}
}
这里我用了 \r 回车符,让输出在同一行刷新,看起来就像实时仪表盘一样。嗯,这个小技巧在写命令行监控工具时特别实用。
知识体系总览
下面这张图帮你理清流量统计的整体流程:
避坑指南与经验之谈
做流量统计,有几个坑我踩过,今天一并告诉你:
- 计数器回绕问题:
/proc/net/dev里的计数器是64位的,但某些老内核或虚拟设备可能用32位。当计数器达到最大值后会回绕到0。如果你不做差值保护,算出来的速率会突然变成负数。我建议在差值计算时加个判断:如果当前值小于上一次值,说明发生了回绕,这时候应该用(最大值 - 上一次值) + 当前值来修正。 - 多线程安全:如果你的监控程序是多线程的,读写
traffic_stats_t结构体时记得加锁。我曾经在一个项目中忘了加锁,结果统计线程和显示线程同时读写,显示出来的速率忽大忽小,排查了好久才发现是竞态条件。 - 性能开销:每秒读一次
/proc/net/dev其实开销很小,但如果你要监控几十个接口,建议用libpcap或者 netlink 的方式,效率更高。
核心要点总结:
- 用
/proc/net/dev获取原始数据,解析时注意格式串对齐 - 用结构体保存两次采样数据,做差计算速率
- 采样间隔建议1秒,避免波动过大
- 注意计数器回绕、多线程安全、性能开销等细节
好了,流量统计这块就聊到这儿。代码虽然简单,但实际项目中用到的场景非常多——从简单的网速监控,到复杂的流量计费系统,核心逻辑都是这一套。你把这个基础打牢了,后面遇到更复杂的场景也能从容应对。