前言
你有没有遇到过这种情况:
ping 能通,curl 也能下载,但就是慢得离谱。一个HTTP请求要3秒才返回,其中2.5秒花在"等待"上。
用 tcpdump 抓包太原始,用 wireshark 分析太重,用 strace 看不懂。
你需要一个专门诊断TCP连接耗时的工具。
今天,我们手写一个TCP连接追踪器,彻底搞清楚:
· 一个TCP连接到底卡在哪一步
· DNS解析花了多久
· TCP握手花了多久
· TLS握手(如果是HTTPS)花了多久
· 首字节响应等了多久
---
一、一个HTTP请求的时间都花在哪了
```
客户端 服务器
| |
|---- 1. DNS解析 (几十到几百毫秒) ------->|
|<---- 返回IP地址 -----------------------|
| |
|---- 2. TCP三次握手 (RTT) ------------->|
|<---- SYN+ACK --------------------------|
|---- ACK ------------------------------>|
| |
|---- 3. TLS握手 (1-2个RTT, HTTPS) ---->|
|<---- Server Hello ---------------------|
|---- ... ------------------------------>|
| |
|---- 4. 发送HTTP请求 ------------------>|
| |
|---- 5. 等待首字节响应 (服务器处理时间)->|
|<---- 响应数据 -------------------------|
| |
|---- 6. 接收响应体 -------------------->|
```
核心指标:
· dns_time:DNS解析耗时
· connect_time:TCP握手耗时
· ssl_time:TLS握手耗时
· wait_time:从发送请求到收到首字节的时间
· total_time:总耗时
---
二、完整代码实现
1. 核心结构体定义
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <errno.h>
#include <signal.h>
// 时间统计结构体
typedef struct {
struct timeval dns_start;
struct timeval dns_end;
struct timeval connect_start;
struct timeval connect_end;
struct timeval ssl_start;
struct timeval ssl_end;
struct timeval send_start;
struct timeval send_end;
struct timeval recv_start;
struct timeval recv_end;
} timing_t;
// 连接配置
typedef struct {
char host[256];
char port[16];
int use_ssl;
char request[4096];
} conn_config_t;
// 诊断结果
typedef struct {
double dns_ms;
double connect_ms;
double ssl_ms;
double wait_ms;
double total_ms;
int http_code;
char error_msg[256];
} diag_result_t;
```
2. DNS解析耗时检测
```c
int resolve_dns(const char *host, struct sockaddr_in *addr, timing_t *timing) {
gettimeofday(&timing->dns_start, NULL);
struct hostent *he = gethostbyname(host);
if (!he) {
return -1;
}
gettimeofday(&timing->dns_end, NULL);
memcpy(&addr->sin_addr, he->h_addr_list[0], he->h_length);
addr->sin_family = AF_INET;
addr->sin_port = 0;
return 0;
}
```
3. TCP连接耗时检测(非阻塞模式)
```c
int tcp_connect(const char *host, int port, timing_t *timing) {
gettimeofday(&timing->connect_start, NULL);
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) return -1;
// 设置非阻塞
int flags = fcntl(sock, F_GETFL, 0);
fcntl(sock, F_SETFL, flags | O_NONBLOCK);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
inet_pton(AF_INET, host, &addr.sin_addr);
int ret = connect(sock, (struct sockaddr*)&addr, sizeof(addr));
if (ret < 0 && errno != EINPROGRESS) {
close(sock);
return -1;
}
// 等待连接完成
fd_set wset;
FD_ZERO(&wset);
FD_SET(sock, &wset);
struct timeval tv = {5, 0}; // 5秒超时
ret = select(sock + 1, NULL, &wset, NULL, &tv);
if (ret <= 0) {
close(sock);
return -1;
}
// 检查连接是否成功
int error = 0;
socklen_t len = sizeof(error);
getsockopt(sock, SOL_SOCKET, SO_ERROR, &error, &len);
if (error != 0) {
close(sock);
return -1;
}
gettimeofday(&timing->connect_end, NULL);
// 恢复阻塞模式
fcntl(sock, F_SETFL, flags);
return sock;
}
```
4. 发送请求并计时
```c
int send_request(int sock, const char *request, timing_t *timing) {
gettimeofday(&timing->send_start, NULL);
int len = strlen(request);
int sent = 0;
while (sent < len) {
int n = send(sock, request + sent, len - sent, 0);
if (n <= 0) return -1;
sent += n;
}
gettimeofday(&timing->send_end, NULL);
return 0;
}
```
5. 接收首字节并计时
```c
int recv_first_byte(int sock, char *buffer, int bufsize, timing_t *timing) {
gettimeofday(&timing->recv_start, NULL);
int n = recv(sock, buffer, bufsize - 1, 0);
if (n <= 0) return -1;
gettimeofday(&timing->recv_end, NULL);
buffer[n] = '\0';
return n;
}
```
6. 主诊断函数
```c
int diagnose_url(const char *url, diag_result_t *result) {
timing_t timing = {0};
conn_config_t config = {0};
// 解析URL
if (parse_url(url, &config) != 0) {
snprintf(result->error_msg, sizeof(result->error_msg),
"URL解析失败");
return -1;
}
// 1. DNS解析
struct sockaddr_in addr;
if (resolve_dns(config.host, &addr, &timing) != 0) {
snprintf(result->error_msg, sizeof(result->error_msg),
"DNS解析失败: %s", config.host);
return -1;
}
// 2. TCP连接
int sock = tcp_connect(config.host, atoi(config.port), &timing);
if (sock < 0) {
snprintf(result->error_msg, sizeof(result->error_msg),
"TCP连接失败: %s:%s", config.host, config.port);
return -1;
}
// 3. TLS握手(如果是HTTPS)
if (config.use_ssl) {
// 简化版:这里需要集成openssl
// 实际代码会用SSL_connect
}
// 4. 发送请求
build_http_request(&config);
if (send_request(sock, config.request, &timing) != 0) {
snprintf(result->error_msg, sizeof(result->error_msg),
"发送请求失败");
close(sock);
return -1;
}
// 5. 接收首字节
char buffer[4096];
if (recv_first_byte(sock, buffer, sizeof(buffer), &timing) < 0) {
snprintf(result->error_msg, sizeof(result->error_msg),
"接收响应失败");
close(sock);
return -1;
}
// 6. 解析HTTP状态码
result->http_code = parse_http_code(buffer);
// 7. 计算各项耗时
result->dns_ms = timeval_diff_ms(&timing.dns_start, &timing.dns_end);
result->connect_ms = timeval_diff_ms(&timing.connect_start, &timing.connect_end);
result->wait_ms = timeval_diff_ms(&timing.send_end, &timing.recv_start);
result->total_ms = timeval_diff_ms(&timing.dns_start, &timing.recv_end);
close(sock);
return 0;
}
```
7. 辅助函数
```c
// 时间差计算(毫秒)
double timeval_diff_ms(struct timeval *start, struct timeval *end) {
return (end->tv_sec - start->tv_sec) * 1000.0 +
(end->tv_usec - start->tv_usec) / 1000.0;
}
// URL解析
int parse_url(const char *url, conn_config_t *config) {
// 判断协议
if (strncmp(url, "https://", 8) == 0) {
config->use_ssl = 1;
url += 8;
strcpy(config->port, "443");
} else if (strncmp(url, "http://", 7) == 0) {
config->use_ssl = 0;
url += 7;
strcpy(config->port, "80");
} else {
return -1;
}
// 提取主机名
const char *slash = strchr(url, '/');
if (slash) {
int len = slash - url;
strncpy(config->host, url, len);
config->host[len] = '\0';
} else {
strcpy(config->host, url);
}
return 0;
}
// 构建HTTP请求
void build_http_request(conn_config_t *config) {
snprintf(config->request, sizeof(config->request),
"GET / HTTP/1.1\r\n"
"Host: %s\r\n"
"User-Agent: TCPDiag/1.0\r\n"
"Connection: close\r\n"
"\r\n",
config->host);
}
```
8. 主函数和输出格式化
```c
void print_result(diag_result_t *result) {
printf("\n========== TCP连接诊断报告 ==========\n");
printf("DNS解析耗时: %8.2f ms\n", result->dns_ms);
printf("TCP握手耗时: %8.2f ms\n", result->connect_ms);
printf("首字节等待耗时: %8.2f ms\n", result->wait_ms);
printf("总耗时: %8.2f ms\n", result->total_ms);
printf("HTTP状态码: %d\n", result->http_code);
// 给出优化建议
printf("\n========== 优化建议 ==========\n");
if (result->dns_ms > 100) {
printf("⚠️ DNS解析较慢,建议:\n");
printf(" - 使用DNS缓存(如dnsmasq)\n");
printf(" - 更换DNS服务器(如114.114.114.114或8.8.8.8)\n");
}
if (result->connect_ms > 100) {
printf("⚠️ TCP握手较慢,建议:\n");
printf(" - 检查网络延迟\n");
printf(" - 使用长连接(Keep-Alive)\n");
}
if (result->wait_ms > 500) {
printf("⚠️ 服务器响应较慢,建议:\n");
printf(" - 检查服务器负载\n");
printf(" - 优化后端接口性能\n");
printf(" - 增加缓存\n");
}
}
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("用法: %s <URL>\n", argv[0]);
printf("示例: %s https://www.baidu.com\n", argv[0]);
printf(" %s http://example.com\n", argv[0]);
return 1;
}
diag_result_t result = {0};
if (diagnose_url(argv[1], &result) == 0) {
print_result(&result);
} else {
printf("诊断失败: %s\n", result.error_msg);
}
return 0;
}
```
---
三、编译与使用
编译
```bash
gcc -o tcpdiag tcpdiag.c -Wno-deprecated-declarations
```
使用示例
```bash
./tcpdiag https://www.baidu.com
```
输出效果:
```
========== TCP连接诊断报告 ==========
DNS解析耗时: 12.34 ms
TCP握手耗时: 25.67 ms
首字节等待耗时: 45.12 ms
总耗时: 83.45 ms
HTTP状态码: 200
========== 优化建议 ==========
✅ DNS解析正常
✅ TCP握手正常
✅ 服务器响应正常
```
慢网站的示例输出
```bash
./tcpdiag https://slow-api.example.com
```
```
========== TCP连接诊断报告 ==========
DNS解析耗时: 234.56 ms
TCP握手耗时: 189.23 ms
首字节等待耗时: 2156.78 ms
总耗时: 2581.12 ms
HTTP状态码: 200
========== 优化建议 ==========
⚠️ DNS解析较慢,建议:
- 使用DNS缓存(如dnsmasq)
- 更换DNS服务器(如114.114.114.114或8.8.8.8)
⚠️ TCP握手较慢,建议:
- 检查网络延迟
- 使用长连接(Keep-Alive)
⚠️ 服务器响应较慢,建议:
- 检查服务器负载
- 优化后端接口性能
- 增加缓存
```
---
四、进阶功能:持续监控
```c
// 持续监控模式
void continuous_monitor(const char *url, int interval_sec) {
printf("开始持续监控 %s,间隔 %d 秒\n", url, interval_sec);
while (1) {
diag_result_t result = {0};
if (diagnose_url(url, &result) == 0) {
time_t now = time(NULL);
struct tm *tm_info = localtime(&now);
char time_str[20];
strftime(time_str, sizeof(time_str), "%H:%M:%S", tm_info);
printf("[%s] DNS:%.1f TCP:%.1f Wait:%.1f Total:%.1f\n",
time_str,
result.dns_ms, result.connect_ms,
result.wait_ms, result.total_ms);
}
sleep(interval_sec);
}
}
```
---
五、使用libcurl的简化版本
如果不想手写socket,可以用 libcurl 自带的耗时统计:
```c
#include <curl/curl.h>
static size_t write_cb(void *ptr, size_t size, size_t nmemb, void *data) {
return size * nmemb;
}
void diagnose_with_curl(const char *url) {
CURL *curl = curl_easy_init();
if (!curl) return;
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
CURLcode res = curl_easy_perform(curl);
if (res == CURLE_OK) {
double dns_time, connect_time, ssl_time, wait_time, total_time;
curl_easy_getinfo(curl, CURLINFO_NAMELOOKUP_TIME, &dns_time);
curl_easy_getinfo(curl, CURLINFO_CONNECT_TIME, &connect_time);
curl_easy_getinfo(curl, CURLINFO_APPCONNECT_TIME, &ssl_time);
curl_easy_getinfo(curl, CURLINFO_STARTTRANSFER_TIME, &wait_time);
curl_easy_getinfo(curl, CURLINFO_TOTAL_TIME, &total_time);
printf("DNS解析: %.3f ms\n", dns_time * 1000);
printf("TCP握手: %.3f ms\n", (connect_time - dns_time) * 1000);
if (ssl_time > 0) {
printf("TLS握手: %.3f ms\n", (ssl_time - connect_time) * 1000);
}
printf("首字节等待: %.3f ms\n", (wait_time - ssl_time) * 1000);
printf("总耗时: %.3f ms\n", total_time * 1000);
}
curl_easy_cleanup(curl);
}
```
编译:
```bash
gcc -o curldiag curldiag.c -lcurl
```
---
六、常用诊断场景
场景1:CDN节点选型
```bash
# 测试不同CDN节点的连接速度
for ip in 1.1.1.1 2.2.2.2 3.3.3.3; do
echo "测试节点 $ip"
./tcpdiag http://$ip/test.jpg
done
```
场景2:API接口性能监控
```bash
# 持续监控,输出到CSV
while true; do
./tcpdiag https://api.example.com/v1/status | \
grep -E "总耗时" | awk '{print $3}' >> latency.log
sleep 60
done
```
场景3:对比HTTP/1.1和HTTP/2
```bash
# 需要支持HTTP/2的工具
./tcpdiag https://http2.example.com
```
---
七、常见问题排查表
现象 可能原因 诊断方法
DNS耗时 > 200ms DNS服务器慢 换用114.114.114.114或8.8.8.8
TCP耗时 > 100ms 物理距离远、丢包 用 mtr 看路由
首字节等待 > 1s 服务器处理慢 检查后端服务、数据库
总耗时稳定但偏大 带宽不足 用 iperf 测带宽
偶尔超时 网络抖动 抓包看重传率
---
结语
通过这篇文章,你学会了:
· TCP连接各阶段的耗时如何测量
· DNS解析、TCP握手、首字节等待的诊断方法
· 手写一个HTTP诊断工具的完整代码
· 用libcurl快速实现的简化版本
下次有人跟你说“网络很慢”,不要让他去ping。给他这个工具,让他告诉你到底慢在哪一步。
下一篇预告:《手写一个HTTP压测工具:从单线程到百万并发》
---
评论区分享一下你遇到过的最诡异的网络问题~