72、网络性能测试:带宽测试、延迟测试、吞吐量计算
网络性能测试,说白了就是回答三个问题:能跑多快?延迟多久?实际能传多少?
我做网络编程这些年,见过太多「理论带宽 10Gbps,实际跑起来只有 2Gbps」的案例。你想想看,如果连自己的网络链路到底什么水平都不清楚,那上层应用优化得再好也是白搭。
这一章,我就带你手写一个轻量级的网络性能测试工具。它能测带宽、测延迟、算吞吐量。代码量不大,但背后涉及的知识点——嗯,挺扎实的。
测试的基本原理
先理清三个核心概念:
- 带宽(Bandwidth):链路的最大传输速率,单位 bps。理论值由物理层决定。
- 延迟(Latency):数据包从发送端到接收端的往返时间(RTT),单位 ms。
- 吞吐量(Throughput):实际应用中单位时间内成功传输的数据量,单位 bps 或 pps。
我个人的习惯是:先测延迟,再测带宽,最后算吞吐量。为什么这个顺序?因为延迟会影响 TCP 窗口的调整策略,进而影响吞吐量。你不先摸清延迟,后面算出来的吞吐量数据就没法解释。
核心公式:
吞吐量 = 数据量 / 传输时间
有效带宽 = 吞吐量 × (1 - 重传率)
SVG:网络性能测试知识体系
代码实现:服务端与客户端
我习惯用 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;
}
延迟测试的细节
延迟测试,我一般用两种方式:
- Ping 风格:发一个小包,等回显,算 RTT。简单,但只能测到网络层的延迟。
- 应用层时间戳:在数据包里嵌入发送时间,服务端收到后记录接收时间,再回传。这样能测到完整的端到端延迟。
我个人更推荐第二种。为什么?因为 Ping 用的是 ICMP 协议,很多云厂商或者防火墙会限速或者直接丢弃 ICMP 包。你 Ping 出来的结果可能比实际延迟低很多——嗯,我踩过这个坑。
我曾经在一个项目中,用 Ping 测出来延迟只有 2ms,但实际应用层延迟高达 15ms。排查了半天才发现,是云主机的安全组对 ICMP 做了优先处理。从那以后,我坚持用应用层时间戳来测延迟。
吞吐量计算的注意事项
吞吐量计算看起来简单——数据量除以时间。但有几个坑:
- 时间窗口:别只测一次。我习惯测 3 次取平均值,每次持续 10 秒以上。
- 包大小:小包和大包的吞吐量差异很大。小包考验 PPS(每秒包数)能力,大包考验带宽。建议两种都测。
- CPU 瓶颈:如果客户端或服务端的 CPU 跑满了,测出来的就不是网络吞吐量,而是 CPU 吞吐量。用
top或perf监控一下。
| 包大小 | 典型场景 | 关注指标 |
|---|---|---|
| 64 字节 | 游戏、VoIP | PPS(每秒包数) |
| 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 也能测,但自己写一遍,你才能真正理解那些数字背后的含义。