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
}
注意:如果采样间隔太短(比如小于100毫秒),计算出的速率波动会很大。我建议采样间隔设在1秒左右,这样既平滑又及时。另外,如果 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 sscanf 解析接口数据 提取 rx_bytes / tx_bytes / rx_packets / tx_packets traffic_stats_t 结构体 保存当前 & 上一次采样数据 带宽计算:rate = Δbytes / Δtime 单位换算:Bps → Kbps → Mbps 实时显示 / 日志记录

避坑指南与经验之谈

做流量统计,有几个坑我踩过,今天一并告诉你:

  • 计数器回绕问题/proc/net/dev 里的计数器是64位的,但某些老内核或虚拟设备可能用32位。当计数器达到最大值后会回绕到0。如果你不做差值保护,算出来的速率会突然变成负数。我建议在差值计算时加个判断:如果当前值小于上一次值,说明发生了回绕,这时候应该用 (最大值 - 上一次值) + 当前值 来修正。
  • 多线程安全:如果你的监控程序是多线程的,读写 traffic_stats_t 结构体时记得加锁。我曾经在一个项目中忘了加锁,结果统计线程和显示线程同时读写,显示出来的速率忽大忽小,排查了好久才发现是竞态条件。
  • 性能开销:每秒读一次 /proc/net/dev 其实开销很小,但如果你要监控几十个接口,建议用 libpcap 或者 netlink 的方式,效率更高。

核心要点总结:

  • /proc/net/dev 获取原始数据,解析时注意格式串对齐
  • 用结构体保存两次采样数据,做差计算速率
  • 采样间隔建议1秒,避免波动过大
  • 注意计数器回绕、多线程安全、性能开销等细节

好了,流量统计这块就聊到这儿。代码虽然简单,但实际项目中用到的场景非常多——从简单的网速监控,到复杂的流量计费系统,核心逻辑都是这一套。你把这个基础打牢了,后面遇到更复杂的场景也能从容应对。


公众号:蓝海资料掘金营,微信deep3321