网络通了但很慢:手写一个TCP连接耗时诊断工具

前言

你有没有遇到过这种情况:

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;

```

  1. 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;

}

```

  1. 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;

}

```

  1. 发送请求并计时

```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;

}

```

  1. 接收首字节并计时

```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;

}

```

  1. 主诊断函数

```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;

}

```

  1. 辅助函数

```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);

}

```

  1. 主函数和输出格式化

```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压测工具:从单线程到百万并发》


评论区分享一下你遇到过的最诡异的网络问题~

相关推荐
良木生香2 小时前
【C++初阶】:STL——String从入门到应用完全指南(3)
c语言·开发语言·数据结构·c++·算法
爱编码的小八嘎2 小时前
C语言完美演绎8-14
c语言
程序员老邢2 小时前
【技术底稿 18】FTP 文件处理 + LibreOffice Word 转 PDF 在线预览 + 集群乱码终极排查全记录
java·经验分享·后端·pdf·word·springboot
三品吉他手会点灯3 小时前
C语言学习笔记 - 8.C概述 - 学习的目标
c语言·笔记·学习
三品吉他手会点灯3 小时前
C语言学习笔记 - 10.C概述 - C语言课程总纲要
c语言·笔记·学习
LaughingZhu4 小时前
Product Hunt 每日热榜 | 2026-04-22
人工智能·经验分享·深度学习·神经网络·产品运营
流年如夢4 小时前
结构体:定义、使用与内存布局
c语言·开发语言·数据结构·c++·算法
三品吉他手会点灯4 小时前
C语言学习笔记 - 6.C概述 - C的重要性
c语言·笔记·学习
louiseailife4 小时前
企业级AI智能体安全实践:从不可控到受控执行
经验分享