HTTP请求的完整过程详解:从DNS解析到TCP挥手的微秒级实战分析
引言
当我们在终端执行一条 curl http://192.168.0.201/ 命令时,从按下回车到看到响应,整个过程不到1毫秒。但在这1毫秒内,网络协议栈完成了DNS解析、TCP三次握手、HTTP请求传输、服务端处理、HTTP响应返回、TCP四次挥手等一系列精密操作。
本文将基于华为云ECS(Ubuntu 24.04)上的真实curl计时数据和tcpdump抓包日志,逐阶段剖析HTTP请求的完整生命周期。所有数据均来自实际操作,时间精确到微秒级别。
一、实验环境
| 项目 | 值 |
|---|---|
| 客户端 | server-3,192.168.0.23,华为云ECS Ubuntu 24.04 |
| 服务端 | server-1,192.168.0.201,nginx/1.24.0 (Ubuntu) |
| curl版本 | curl/8.5.0 |
| 抓包工具 | tcpdump 4.99.1 |
| 网络环境 | 同子网直连(192.168.0.0/24),1跳可达 |
| 请求命令 | curl http://192.168.0.201/ |
二、curl计时数据总览
使用curl的 -w 参数可以获取请求各阶段的精确耗时:
$ curl -s -o /dev/null -w 'DNS Lookup: %{time_namelookup}s\nTCP Connect: %{time_connect}s\nTLS Handshake: %{time_appconnect}s\nRequest Sent: %{time_pretransfer}s\nFirst Byte: %{time_starttransfer}s\nTotal: %{time_total}s\n' http://192.168.0.201/
DNS Lookup: 0.000018s
TCP Connect: 0.000218s
TLS Handshake: 0.000000s
Request Sent: 0.000239s
First Byte: 0.000804s
Total: 0.000832s
各计时指标含义如下:
| 指标 | 耗时 | 含义 |
|---|---|---|
| time_namelookup | 0.000018s (18微秒) | DNS解析耗时,从开始到域名解析完成 |
| time_connect | 0.000218s (218微秒) | TCP连接耗时,从开始到三次握手完成 |
| time_appconnect | 0.000000s | TLS握手耗时,本次为HTTP无TLS故为0 |
| time_pretransfer | 0.000239s (239微秒) | 从开始到请求发送就绪 |
| time_starttransfer | 0.000804s (804微秒) | TTFB,从开始到收到第一个响应字节 |
| time_total | 0.000832s (832微秒) | 总耗时,从开始到连接关闭 |
关键耗时分解:
- DNS解析耗时 = 0.000018s = 18微秒
- TCP握手耗时 = 0.000218 - 0.000018 = 0.000200s = 200微秒
- 请求发送耗时 = 0.000239 - 0.000218 = 0.000021s = 21微秒
- 服务端处理耗时 = 0.000804 - 0.000239 = 0.000565s = 565微秒
- 响应传输耗时 = 0.000832 - 0.000804 = 0.000028s = 28微秒
整个请求总耗时仅832微秒,其中服务端处理(含Nginx反向代理转发到后端)占了约68%的时间。
三、完整过程时序图
以下是HTTP请求从发起到结束的完整时序图:
Client (server-3) Server-1 (Nginx LB) Backend (server-3:80)
192.168.0.23 192.168.0.201 192.168.0.23:80
│ │ │
│ [1] DNS Lookup (18us) │ │
│ 查询本地DNS缓存 │ │
│ │ │
│ [2] TCP SYN (seq=3992942498) │ │
│ ─────────────────────────────────> │ │
│ │ │
│ [3] TCP SYN-ACK (seq=4094577964, │ │
│ ack=3992942499) │ │
│ <───────────────────────────────── │ │
│ │ │
│ [4] TCP ACK (ack=4094577965) │ │
│ ─────────────────────────────────> │ TCP握手完成 (200us) │
│ │ │
│ [5] HTTP GET / HTTP/1.1 (76字节) │ │
│ ─────────────────────────────────> │ │
│ │ │
│ [6] TCP ACK │ │
│ <───────────────────────────────── │ │
│ │ │
│ │ [7] TCP SYN (新建代理连接) │
│ │ ─────────────────────────────────> │
│ │ │
│ │ [8] TCP SYN-ACK │
│ │ <───────────────────────────────── │
│ │ │
│ │ [9] TCP ACK │
│ │ ─────────────────────────────────> │
│ │ │
│ │ [10] GET / HTTP/1.0 (151字节) │
│ │ X-Real-IP: 192.168.0.23 │
│ │ X-Forwarded-For: .23 │
│ │ ─────────────────────────────────> │
│ │ │
│ │ [11] TCP ACK │
│ │ <───────────────────────────────── │
│ │ │
│ │ [12] HTTP 403 Forbidden (349字节) │
│ │ X-Static-Server: backend-2 │
│ │ <───────────────────────────────── │ 服务端处理 (565us)
│ │ │
│ │ [13] TCP FIN │
│ │ ─────────────────────────────────> │
│ │ [14] TCP ACK │
│ │ <───────────────────────────────── │
│ │ [15] TCP FIN │
│ │ <───────────────────────────────── │
│ │ [16] TCP ACK │
│ │ ─────────────────────────────────> │
│ │ │
│ [17] HTTP 403 Forbidden (338字节) │ │
│ X-Static-Server: backend-2 │ │
│ <───────────────────────────────── │ │
│ │ │
│ [18] TCP ACK │ │
│ ─────────────────────────────────> │ │
│ │ │
│ [19] TCP FIN │ │
│ ─────────────────────────────────> │ │
│ [20] TCP FIN │ │
│ <───────────────────────────────── │ │
│ [21] TCP ACK │ │
│ ─────────────────────────────────> │ │
│ │ │
│ 连接关闭,总耗时 832us │ │
四、阶段一:DNS解析(0 - 18微秒)
由于本次请求直接使用IP地址 192.168.0.201,DNS解析阶段仅需18微秒。curl内部会调用 getaddrinfo() 函数,即便目标是IP地址而非域名,该函数仍会被调用以确认地址格式。
查看server-3的DNS配置:
$ cat /etc/resolv.conf
nameserver 127.0.0.53
options edns0 trust-ad
search openstacklocal
server-3使用systemd-resolved本地DNS存根解析器(127.0.0.53)。如果请求的是域名(如www.baidu.com),解析过程如下:
Client → 127.0.0.53 (systemd-resolved) → 上游DNS服务器 → 返回A记录
UDP 53端口 华为云内部DNS
实测解析www.baidu.com的结果:
$ nslookup www.baidu.com
Server: 127.0.0.53
Address: 127.0.0.53#53
Non-authoritative answer:
www.baidu.com canonical name = www.a.shifen.com.
Name: www.a.shifen.com
Address: 110.242.69.21
Name: www.a.shifen.com
Address: 110.242.70.57
DNS返回了CNAME记录(www.baidu.com → www.a.shifen.com)和两个A记录。如果是域名请求,DNS解析阶段耗时通常在1-10毫秒之间,远大于本次的18微秒。
五、阶段二:TCP三次握手(18 - 218微秒)
DNS解析完成后,curl开始建立TCP连接。从tcpdump抓包数据可以精确看到三次握手的每个包:
5.1 第一次握手:SYN
12:05:21.578379 IP (tos 0x0, ttl 64, id 49954, offset 0, flags [DF], proto TCP (6), length 60)
192.168.0.23.49916 > 192.168.0.201.80: Flags [S], cksum 0x825f (incorrect -> 0xdfb7),
seq 3992942498, win 64240, options [mss 1460,sackOK,TS val 18333984 ecr 0,nop,wscale 7], length 0
客户端(192.168.0.23)从临时端口49916向服务端(192.168.0.201)的80端口发送SYN包。关键参数:
| 参数 | 值 | 说明 |
|---|---|---|
| seq | 3992942498 | 客户端初始序列号(ISN) |
| win | 64240 | 告知服务端自己的接收窗口 |
| mss | 1460 | 最大分段大小,告知服务端每个TCP段最多携带1460字节数据 |
| wscale | 7 | 窗口缩放因子,实际窗口=64240*128=8222720字节 |
| TS val | 18333984 | 时间戳,用于计算RTT |
| length | 0 | SYN包不携带应用数据 |
5.2 第二次握手:SYN-ACK
12:05:21.578515 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
192.168.0.201.80 > 192.168.0.23.49916: Flags [S.], cksum 0xaded (correct),
seq 4094577964, ack 3992942499, win 65160, options [mss 1460,sackOK,TS val 4061133525 ecr 18333984,nop,wscale 7], length 0
服务端回复SYN-ACK包。关键变化:
| 参数 | 值 | 说明 |
|---|---|---|
| seq | 4094577964 | 服务端的初始序列号 |
| ack | 3992942499 | 确认号=客户端seq+1,表示期望下次收到3992942499 |
| win | 65160 | 服务端接收窗口 |
| ecr | 18333984 | 回显客户端的时间戳,用于RTT计算 |
从时间戳计算,第一个RTT(SYN到SYN-ACK)= 578515 - 578379 = 136微秒。
5.3 第三次握手:ACK
12:05:21.578528 IP (tos 0x0, ttl 64, id 49955, offset 0, flags [DF], proto TCP (6), length 52)
192.168.0.23.49916 > 192.168.0.201.80: Flags [.], cksum 0x8257 (incorrect -> 0xd94c),
ack 4094577965, win 502, options [nop,nop,TS val 18333984 ecr 4061133525], length 0
客户端发送纯ACK包,确认号ack=4094577965(服务端seq+1)。此时TCP连接进入ESTABLISHED状态。
三次握手总耗时:578528 - 578379 = 149微秒(tcpdump层面),curl统计为200微秒(含系统调用开销)。
六、阶段三:HTTP请求发送(218 - 239微秒)
6.1 curl --trace-ascii 字节级分析
使用 curl --trace-ascii 可以看到curl发送和接收的每一字节数据:
$ curl -v --trace-ascii /tmp/curl_trace.txt http://192.168.0.201/
请求发送部分:
=> Send header, 76 bytes (0x4c)
0000: GET / HTTP/1.1
0010: Host: 192.168.0.201
0025: User-Agent: curl/8.5.0
003d: Accept: */*
004a:
左侧的十六进制偏移量表示每行起始字节的位置。HTTP请求总共76字节,结构如下:
| 偏移 | 内容 | 字节数 | 说明 |
|---|---|---|---|
| 0000 | GET / HTTP/1.1\r\n | 16 | 请求行(方法+路径+协议版本) |
| 0010 | Host: 192.168.0.201\r\n | 21 | Host头 |
| 0025 | User-Agent: curl/8.5.0\r\n | 24 | UA头 |
| 003d | Accept: /\r\n | 13 | Accept头 |
| 004a | \r\n | 2 | 空行,标志请求头结束 |
注意0x4c=76,与tcpdump中 length 76: HTTP 完全吻合。HTTP/1.1请求头以 \r\n 分隔,以空行 \r\n 结束。
6.2 tcpdump中的HTTP请求包
12:05:21.578558 IP (tos 0x0, ttl 64, id 49956, offset 0, flags [DF], proto TCP (6), length 128)
192.168.0.23.49916 > 192.168.0.201.80: Flags [P.], cksum 0x82a3 (incorrect -> 0x5608),
seq 3992942499:3992942575, ack 4094577965, win 502, options [nop,nop,TS val 18333984 ecr 4061133525], length 76: HTTP, length: 76
GET / HTTP/1.1
Host: 192.168.0.201
User-Agent: curl/8.5.0
Accept: */*
Flags [P.] 表示PSH+ACK。seq范围 3992942499:3992942575,差值为76,即HTTP请求的字节数。IP包总长度128字节 = 20(IP头)+ 32(TCP头含选项)+ 76(HTTP载荷)。
6.3 服务端ACK确认
12:05:21.578655 IP ... 192.168.0.201.80 > 192.168.0.23.49916: Flags [.],
ack 3992942575, win 509, options [nop,nop,TS val 4061133526 ecr 18333984], length 0
服务端确认收到HTTP请求,ack=3992942575(客户端最后发送的seq+length),确认序号推进了76字节。
七、阶段四:服务端处理与反向代理(239 - 804微秒)
这是耗时最长的阶段(565微秒),因为server-1作为Nginx反向代理,需要将请求转发到后端服务器。
7.1 Nginx向后端发起新连接
tcpdump抓到了server-1主动向后端(server-3的80端口)发起的TCP连接:
# 后端SYN
12:05:21.578743 IP ... 192.168.0.201.15218 > 192.168.0.23.80: Flags [S],
seq 1876997722, win 64240, options [mss 1460,sackOK,TS val 4061133526 ecr 0,nop,wscale 7], length 0
# 后端SYN-ACK
12:05:21.578752 IP ... 192.168.0.23.80 > 192.168.0.201.15218: Flags [S.],
seq 676700932, ack 1876997723, win 65160, ...
# 后端ACK
12:05:21.578856 IP ... 192.168.0.201.15218 > 192.168.0.23.80: Flags [.],
ack 676700933, win 502, ...
7.2 Nginx转发HTTP请求
12:05:21.578867 IP ... 192.168.0.201.15218 > 192.168.0.23.80: Flags [P.],
length 151: HTTP, length: 151
GET / HTTP/1.0
Host: 192.168.0.201
X-Real-IP: 192.168.0.23
X-Forwarded-For: 192.168.0.23
Connection: close
User-Agent: curl/8.5.0
Accept: */*
Nginx转发请求时的关键区别:
| 对比项 | 客户端原始请求 | Nginx转发请求 |
|---|---|---|
| HTTP版本 | HTTP/1.1 | HTTP/1.0 |
| X-Real-IP | 无 | 192.168.0.23 |
| X-Forwarded-For | 无 | 192.168.0.23 |
| Connection | 隐式keep-alive | close(短连接) |
| 总长度 | 76字节 | 151字节 |
7.3 后端响应
12:05:21.578972 IP ... 192.168.0.23.80 > 192.168.0.201.15218: Flags [P.],
length 349: HTTP, length: 349
HTTP/1.1 403 Forbidden
Server: nginx/1.24.0 (Ubuntu)
Date: Thu, 17 Sep 2026 04:05:22 GMT
Content-Type: text/html
Content-Length: 162
Connection: close
X-Static-Server: backend-2
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.24.0 (Ubuntu)</center>
</body>
</html>
后端返回403 Forbidden,响应头中 X-Static-Server: backend-2 标识了处理请求的后端服务器。响应体162字节,加上HTTP头共349字节。
7.4 代理与后端的四次挥手
后端由于设置了 Connection: close,响应完成后立即关闭连接:
12:05:21.578988 192.168.0.23.80 > 192.168.0.201.15218: Flags [F.] (后端FIN)
12:05:21.579048 192.168.0.201.15218 > 192.168.0.23.80: Flags [.] (代理ACK)
12:05:21.579066 192.168.0.201.15218 > 192.168.0.23.80: Flags [F.] (代理FIN)
12:05:21.579069 192.168.0.23.80 > 192.168.0.201.15218: Flags [.] (后端ACK)
八、阶段五:HTTP响应返回(804微秒)
8.1 Nginx返回响应给客户端
server-1将后端响应处理后返回给客户端:
12:05:21.579085 IP ... 192.168.0.201.80 > 192.168.0.23.49916: Flags [P.],
length 338: HTTP, length: 338
HTTP/1.1 403 Forbidden
Server: nginx
Date: Thu, 17 Sep 2026 04:05:22 GMT
Content-Type: text/html
Content-Length: 162
Connection: keep-alive
X-Static-Server: backend-2
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.24.0 (Ubuntu)</center>
</body>
</html>
注意Nginx返回给客户端的响应与后端原始响应的区别:
| 对比项 | 后端原始响应 | Nginx返回客户端 |
|---|---|---|
| Server头 | nginx/1.24.0 (Ubuntu) | nginx(隐藏版本号) |
| Connection | close | keep-alive |
| 响应总长度 | 349字节 | 338字节 |
8.2 curl --trace-ascii 接收分析
<= Recv header, 24 bytes (0x18)
0000: HTTP/1.1 403 Forbidden
<= Recv header, 15 bytes (0xf)
0000: Server: nginx
<= Recv header, 37 bytes (0x25)
0000: Date: Thu, 17 Sep 2026 04:05:25 GMT
<= Recv header, 25 bytes (0x19)
0000: Content-Type: text/html
<= Recv header, 21 bytes (0x15)
0000: Content-Length: 162
<= Recv header, 24 bytes (0x18)
0000: Connection: keep-alive
<= Recv header, 28 bytes (0x1c)
0000: X-Static-Server: backend-2
<= Recv header, 2 bytes (0x2)
0000:
<= Recv data, 162 bytes (0xa2)
0000: <html>
0008: <head><title>403 Forbidden</title></head>
0033: <body>
003b: <center><h1>403 Forbidden</h1></center>
0064: <hr><center>nginx/1.24.0 (Ubuntu)</center>
0090: </body>
0099: </html>
== Info: Connection #0 to host 192.168.0.201 left intact
curl将接收到的数据分为header(逐行接收)和data(响应体)两类。最后的 Connection #0 to host 192.168.0.201 left intact 表示连接保持打开状态(keep-alive),可供后续请求复用。
8.3 curl -v 的完整请求/响应头
通过 curl -v 可以看到更直观的请求和响应头:
$ curl -v http://192.168.0.201/
* Trying 192.168.0.201:80...
* Connected to 192.168.0.201 (192.168.0.201) port 80
> GET / HTTP/1.1
> Host: 192.168.0.201
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/1.1 403 Forbidden
< Server: nginx
< Date: Thu, 17 Sep 2026 04:05:22 GMT
< Content-Type: text/html
< Content-Length: 162
< Connection: keep-alive
< X-Static-Server: backend-2
<
{ [162 bytes data]
100 162 100 162 0 0 182k 0 --:--:-- --:--:-- --:--:-- 158k
* Connection #0 to host 192.168.0.201 left intact
其中 > 开头的行是curl发送的请求头,< 开头的行是服务端返回的响应头,{ [162 bytes data] 表示接收到162字节的响应体数据。
九、阶段六:TCP四次挥手(832微秒)
请求完成后,客户端发起TCP连接的关闭:
# 1. 客户端发送FIN
12:05:21.579166 IP ... 192.168.0.23.49916 > 192.168.0.201.80: Flags [F.],
seq 3992942575, ack 4094578303, length 0
# 2. 服务端ACK确认
12:05:21.579246 IP ... 192.168.0.201.80 > 192.168.0.23.49916: Flags [.],
ack 3992942576, length 0
# 3. 服务端发送FIN
12:05:21.579248 IP ... 192.168.0.201.80 > 192.168.0.23.49916: Flags [F.],
seq 4094578303, ack 3992942576, length 0
# 4. 客户端ACK确认
12:05:21.579249 IP ... 192.168.0.23.49916 > 192.168.0.201.80: Flags [.],
ack 4094578304, length 0
四次挥手过程说明:
| 步骤 | Flags | 发送方 | seq/ack变化 | 状态变化 |
|---|---|---|---|---|
| 1 | FIN+ACK | 客户端 | seq=3992942575 | 客户端→FIN_WAIT_1 |
| 2 | ACK | 服务端 | ack=3992942576 | 服务端→CLOSE_WAIT, 客户端→FIN_WAIT_2 |
| 3 | FIN+ACK | 服务端 | seq=4094578303 | 服务端→LAST_ACK |
| 4 | ACK | 客户端 | ack=4094578304 | 客户端→TIME_WAIT, 服务端→CLOSED |
客户端在发送最后一个ACK后进入TIME_WAIT状态,持续2*MSL(约60秒)后完全关闭。这是为了确保服务端收到了最后的ACK,如果服务端没收到会重传FIN。
十、耗时分析与优化建议
10.1 各阶段耗时占比
总耗时 832微秒
├── DNS解析 18us ( 2.2%) █
├── TCP握手 200us (24.0%) ██████
├── 请求发送 21us ( 2.5%) █
├── 服务端处理 565us (67.9%) █████████████████
└── 响应传输 28us ( 3.4%) █
10.2 优化建议
| 阶段 | 耗时 | 优化方向 |
|---|---|---|
| DNS解析 | 18us | 已使用本地缓存,无需优化。若使用域名可启用DNS预解析 |
| TCP握手 | 200us | 启用HTTP Keep-Alive或HTTP/2多路复用,避免重复握手 |
| 请求发送 | 21us | 减少不必要的请求头,启用HTTP/2头部压缩(HPACK) |
| 服务端处理 | 565us | 含代理转发开销,可优化Nginx upstream连接池(keepalive) |
| 响应传输 | 28us | 启用gzip压缩减少传输量,或使用HTTP/2减少头部开销 |
其中最大的优化空间在于TCP握手(200us)和服务端处理(565us)。通过启用HTTP Keep-Alive,可以将后续请求的TCP握手开销降为零;通过配置Nginx upstream keepalive,可以消除代理到后端的重复握手。
十一、总结
通过对一次HTTP请求的微秒级剖析,我们可以看到:
- DNS解析仅耗时18微秒,因为使用IP地址直接访问。域名解析通常耗时1-10毫秒,是外网请求的重要开销。
- TCP三次握手耗时200微秒,包含SYN、SYN-ACK、ACK三个包,是建立可靠连接的基础。
- HTTP请求发送仅21微秒,76字节的请求被封装在一个TCP段中传输。
- 服务端处理耗时565微秒,占总耗时的68%,其中包含Nginx反向代理向后端转发请求的开销(额外的TCP握手和HTTP交互)。
- HTTP响应返回和TCP四次挥手合计约28微秒,连接优雅关闭。
理解HTTP请求的每个阶段耗时,对于性能优化至关重要。通过curl的 -w 计时功能和tcpdump抓包,我们可以精确定位性能瓶颈在DNS、TCP、服务端处理还是网络传输,从而有针对性地进行优化。
实验环境:华为云ECS Ubuntu 24.04,nginx/1.24.0,curl/8.5.0,tcpdump 4.99.1