HTTP请求的完整过程详解:从DNS解析到TCP挥手的微秒级实战分析

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 参数可以获取请求各阶段的精确耗时:

bash 复制代码
$ 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/
yaml 复制代码
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请求从发起到结束的完整时序图:

ini 复制代码
  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配置:

bash 复制代码
$ cat /etc/resolv.conf
sql 复制代码
nameserver 127.0.0.53
options edns0 trust-ad
search openstacklocal

server-3使用systemd-resolved本地DNS存根解析器(127.0.0.53)。如果请求的是域名(如www.baidu.com),解析过程如下:

arduino 复制代码
Client → 127.0.0.53 (systemd-resolved) → 上游DNS服务器 → 返回A记录
         UDP 53端口                      华为云内部DNS

实测解析www.baidu.com的结果:

bash 复制代码
$ nslookup www.baidu.com
yaml 复制代码
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.comwww.a.shifen.com)和两个A记录。如果是域名请求,DNS解析阶段耗时通常在1-10毫秒之间,远大于本次的18微秒。

五、阶段二:TCP三次握手(18 - 218微秒)

DNS解析完成后,curl开始建立TCP连接。从tcpdump抓包数据可以精确看到三次握手的每个包:

5.1 第一次握手:SYN

bash 复制代码
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

bash 复制代码
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

ini 复制代码
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发送和接收的每一字节数据:

bash 复制代码
$ curl -v --trace-ascii /tmp/curl_trace.txt http://192.168.0.201/

请求发送部分:

makefile 复制代码
=> 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请求包

yaml 复制代码
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确认

ini 复制代码
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连接:

ini 复制代码
# 后端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请求

yaml 复制代码
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 后端响应

javascript 复制代码
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,响应完成后立即关闭连接:

ini 复制代码
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将后端响应处理后返回给客户端:

javascript 复制代码
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 接收分析

yaml 复制代码
<= 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 可以看到更直观的请求和响应头:

bash 复制代码
$ curl -v http://192.168.0.201/
ruby 复制代码
*   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连接的关闭:

ini 复制代码
# 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 各阶段耗时占比

scss 复制代码
总耗时 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请求的微秒级剖析,我们可以看到:

  1. DNS解析仅耗时18微秒,因为使用IP地址直接访问。域名解析通常耗时1-10毫秒,是外网请求的重要开销。
  2. TCP三次握手耗时200微秒,包含SYN、SYN-ACK、ACK三个包,是建立可靠连接的基础。
  3. HTTP请求发送仅21微秒,76字节的请求被封装在一个TCP段中传输。
  4. 服务端处理耗时565微秒,占总耗时的68%,其中包含Nginx反向代理向后端转发请求的开销(额外的TCP握手和HTTP交互)。
  5. 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

相关推荐
南青1 小时前
Vue 3 中后台实战:我踩过的 10 个坑和最佳实践
前端
江华森1 小时前
HTTPS协议详解——SSL/TLS握手、证书与加密通信
前端
江华森1 小时前
从一个HTTP请求看网络分层原理:基于华为云ECS的真实抓包实战
前端
江华森1 小时前
TCP协议详解——三次握手、四次挥手与连接状态
前端
甲维斯1 小时前
《钢铁洪流》官网搞定,纯AI制作,Opus5操刀!
前端·人工智能·游戏开发
szephyr2 小时前
WebSocket 实战:心跳、断线重连、鉴权,一次讲清
前端·websocket·node.js·长连接·实时通信
默_笙2 小时前
🚋 从流水线到地铁网:为什么复杂 AI 都要拆成多 Agent(上)——LangGraph 基础入门
前端·javascript
科技苑2 小时前
前后端分离与微服务架构如何协同?
前端·后端·前端框架