72、网络性能测试:带宽测试、延迟测试、吞吐量计算

网络性能测试,说白了就是回答三个问题:能跑多快?延迟多久?实际能传多少?

我做网络编程这些年,见过太多「理论带宽 10Gbps,实际跑起来只有 2Gbps」的案例。你想想看,如果连自己的网络链路到底什么水平都不清楚,那上层应用优化得再好也是白搭。

这一章,我就带你手写一个轻量级的网络性能测试工具。它能测带宽、测延迟、算吞吐量。代码量不大,但背后涉及的知识点——嗯,挺扎实的。

测试的基本原理

先理清三个核心概念:

  • 带宽(Bandwidth):链路的最大传输速率,单位 bps。理论值由物理层决定。
  • 延迟(Latency):数据包从发送端到接收端的往返时间(RTT),单位 ms。
  • 吞吐量(Throughput):实际应用中单位时间内成功传输的数据量,单位 bps 或 pps。

我个人的习惯是:先测延迟,再测带宽,最后算吞吐量。为什么这个顺序?因为延迟会影响 TCP 窗口的调整策略,进而影响吞吐量。你不先摸清延迟,后面算出来的吞吐量数据就没法解释。

核心公式

吞吐量 = 数据量 / 传输时间

有效带宽 = 吞吐量 × (1 - 重传率)

SVG:网络性能测试知识体系

网络性能测试核心体系 带宽测试 最大传输速率 延迟测试 RTT 往返时间 吞吐量计算 实际传输效率 发送大量数据包 时间戳 + 序列号 数据量 / 耗时 三者结合 → 全面评估网络质量

代码实现:服务端与客户端

我习惯用 UDP 来做性能测试。为什么不用 TCP?因为 TCP 有拥塞控制、重传机制,测出来的数据会「失真」。UDP 更能反映链路的真实能力。

先看服务端代码。它负责接收数据包,记录时间戳,计算延迟和吞吐量。

// server.c - 网络性能测试服务端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/time.h>
#include <arpa/inet.h>

#define PORT 8888
#define BUFFER_SIZE 65507  // 最大UDP包大小

typedef struct {
    uint32_t seq;          // 序列号
    uint64_t send_ts;      // 发送时间戳(微秒)
    uint64_t recv_ts;      // 接收时间戳(微秒)
    uint32_t data_len;     // 数据长度
} packet_header_t;

int main() {
    int sockfd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_len = sizeof(client_addr);
    char buffer[BUFFER_SIZE];
    struct timeval tv;

    // 创建UDP socket
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket");
        exit(1);
    }

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

    if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind");
        close(sockfd);
        exit(1);
    }

    printf("Server listening on port %d...\n", PORT);

    uint64_t total_bytes = 0;
    uint64_t start_time = 0;
    uint64_t end_time = 0;
    int packet_count = 0;

    while (1) {
        int n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0,
                         (struct sockaddr*)&client_addr, &addr_len);
        if (n < 0) {
            perror("recvfrom");
            continue;
        }

        gettimeofday(&tv, NULL);
        uint64_t now = (uint64_t)tv.tv_sec * 1000000 + tv.tv_usec;

        packet_header_t *hdr = (packet_header_t *)buffer;
        hdr->recv_ts = now;

        if (packet_count == 0) {
            start_time = now;
        }
        end_time = now;
        total_bytes += n;
        packet_count++;

        // 计算延迟
        uint64_t rtt = now - hdr->send_ts;
        printf("Packet %u: RTT = %lu us, size = %u bytes\n",
               hdr->seq, rtt, hdr->data_len);

        // 每1000个包输出一次吞吐量
        if (packet_count % 1000 == 0) {
            double elapsed = (end_time - start_time) / 1000000.0;
            double throughput = (total_bytes * 8.0) / elapsed; // bps
            printf("--- Throughput: %.2f Mbps, packets: %d ---\n",
                   throughput / 1000000, packet_count);
        }

        // 回送确认包(可选)
        sendto(sockfd, buffer, sizeof(packet_header_t), 0,
               (struct sockaddr*)&client_addr, addr_len);
    }

    close(sockfd);
    return 0;
}

小技巧:我在实际项目中,会把时间戳精度做到微秒级。用 gettimeofday() 或者 clock_gettime(CLOCK_MONOTONIC, ...)。纳秒级精度在普通 Linux 上其实不太稳,微秒级刚刚好。

再看客户端代码。它负责发送数据包,并记录发送时间戳。

// client.c - 网络性能测试客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/time.h>
#include <arpa/inet.h>

#define SERVER_IP "127.0.0.1"
#define PORT 8888
#define BUFFER_SIZE 65507
#define PACKET_COUNT 10000
#define PACKET_SIZE 1400  // 避免IP分片

typedef struct {
    uint32_t seq;
    uint64_t send_ts;
    uint64_t recv_ts;
    uint32_t data_len;
} packet_header_t;

int main() {
    int sockfd;
    struct sockaddr_in server_addr;
    char buffer[BUFFER_SIZE];
    struct timeval tv;

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket");
        exit(1);
    }

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);

    printf("Sending %d packets to %s:%d...\n", PACKET_COUNT, SERVER_IP, PORT);

    uint64_t total_start, total_end;
    gettimeofday(&tv, NULL);
    total_start = (uint64_t)tv.tv_sec * 1000000 + tv.tv_usec;

    for (int i = 0; i < PACKET_COUNT; i++) {
        packet_header_t *hdr = (packet_header_t *)buffer;
        hdr->seq = i;
        hdr->data_len = PACKET_SIZE;

        gettimeofday(&tv, NULL);
        hdr->send_ts = (uint64_t)tv.tv_sec * 1000000 + tv.tv_usec;

        // 填充数据部分(模拟真实负载)
        memset(buffer + sizeof(packet_header_t), 'A', PACKET_SIZE - sizeof(packet_header_t));

        sendto(sockfd, buffer, PACKET_SIZE, 0,
               (struct sockaddr*)&server_addr, sizeof(server_addr));

        // 接收回送确认(可选)
        recvfrom(sockfd, buffer, sizeof(packet_header_t), 0, NULL, NULL);
    }

    gettimeofday(&tv, NULL);
    total_end = (uint64_t)tv.tv_sec * 1000000 + tv.tv_usec;

    double total_time = (total_end - total_start) / 1000000.0;
    double throughput = (PACKET_COUNT * PACKET_SIZE * 8.0) / total_time;

    printf("\n=== Test Results ===\n");
    printf("Total packets: %d\n", PACKET_COUNT);
    printf("Packet size: %d bytes\n", PACKET_SIZE);
    printf("Total data: %.2f MB\n", (PACKET_COUNT * PACKET_SIZE) / 1024.0 / 1024.0);
    printf("Total time: %.2f seconds\n", total_time);
    printf("Throughput: %.2f Mbps\n", throughput / 1000000);
    printf("Average packet rate: %.0f pps\n", PACKET_COUNT / total_time);

    close(sockfd);
    return 0;
}

延迟测试的细节

延迟测试,我一般用两种方式:

  1. Ping 风格:发一个小包,等回显,算 RTT。简单,但只能测到网络层的延迟。
  2. 应用层时间戳:在数据包里嵌入发送时间,服务端收到后记录接收时间,再回传。这样能测到完整的端到端延迟。

我个人更推荐第二种。为什么?因为 Ping 用的是 ICMP 协议,很多云厂商或者防火墙会限速或者直接丢弃 ICMP 包。你 Ping 出来的结果可能比实际延迟低很多——嗯,我踩过这个坑。

我曾经在一个项目中,用 Ping 测出来延迟只有 2ms,但实际应用层延迟高达 15ms。排查了半天才发现,是云主机的安全组对 ICMP 做了优先处理。从那以后,我坚持用应用层时间戳来测延迟。

吞吐量计算的注意事项

吞吐量计算看起来简单——数据量除以时间。但有几个坑:

  • 时间窗口:别只测一次。我习惯测 3 次取平均值,每次持续 10 秒以上。
  • 包大小:小包和大包的吞吐量差异很大。小包考验 PPS(每秒包数)能力,大包考验带宽。建议两种都测。
  • CPU 瓶颈:如果客户端或服务端的 CPU 跑满了,测出来的就不是网络吞吐量,而是 CPU 吞吐量。用 topperf 监控一下。
包大小典型场景关注指标
64 字节游戏、VoIPPPS(每秒包数)
1400 字节文件传输、视频流Mbps(带宽)
9000 字节(巨型帧)数据中心内部CPU 利用率、线速

运行与结果分析

编译运行:

# 编译
gcc -o server server.c -Wall
gcc -o client client.c -Wall

# 先启动服务端
./server

# 再启动客户端(另一个终端)
./client

输出示例:

Server listening on port 8888...
Packet 0: RTT = 152 us, size = 1400 bytes
Packet 1: RTT = 148 us, size = 1400 bytes
...
--- Throughput: 932.45 Mbps, packets: 1000 ---
...
=== Test Results ===
Total packets: 10000
Packet size: 1400 bytes
Total data: 13.35 MB
Total time: 0.12 seconds
Throughput: 933.12 Mbps
Average packet rate: 83333 pps

你看,在本地回环(127.0.0.1)上测,吞吐量接近 1Gbps,延迟在 150 微秒左右。如果换成真实网络环境,数据会差很多——尤其是跨公网的时候。

经验之谈:我做过一个跨洋链路的测试,理论带宽 100Mbps,实际吞吐量只有 12Mbps。原因就是延迟太高(200ms+),TCP 窗口根本打不开。这时候你就得考虑用多连接或者调整窗口缩放因子了。

扩展思路

这个工具虽然简单,但已经能应付大部分场景了。如果你想做得更专业,可以加:

  • 双向测试:同时测上行和下行吞吐量
  • 抖动(Jitter)计算:延迟的方差,对实时应用很重要
  • 丢包率统计:通过序列号判断哪些包丢了
  • 多线程发送:模拟并发流,测试链路在多连接下的表现

说白了,网络性能测试这件事,工具是次要的,理解链路特性才是核心。你拿着 iperf 也能测,但自己写一遍,你才能真正理解那些数字背后的含义。


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