从一个HTTP请求看网络分层原理:基于华为云ECS的真实抓包实战
引言
在日常的DevOps工作中,我们每天都在与HTTP请求打交道。但当你在终端敲下 curl http://192.168.0.201/ 时,这背后究竟发生了什么?数据包是如何从一台服务器穿越网络到达另一台服务器的?
本文将基于华为云ECS(Ubuntu 24.04)上的真实操作日志,从一个真实的HTTP请求出发,自底向上逐层剖析网络分层原理。所有tcpdump抓包数据、ARP表、路由表、DNS解析结果均来自实际生产环境操作,绝非模拟数据。
一、实验环境架构
本次实验涉及4台服务器,均部署在华为云ECS上,操作系统为Ubuntu 24.04。架构如下:
less
华为云 VPC (192.168.0.0/24)
┌─────────────────────────────────────────────────────────┐
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ server-1 │ │ server-2 │ │
│ │ 192.168.0.201│ │ (VRRP Backup)│ │
│ │ Nginx LB │ │ Nginx LB │ │
│ │ VRRP Master │ │ │ │
│ │ VIP: .250 │<------->│ VIP: .250 │ │
│ └──────┬───────┘ └──────────────┘ │
│ │ 反向代理 │
│ ├──────────────────────┐ │
│ │ │ │
│ ┌──────▼───────┐ ┌──────▼───────┐ │
│ │ server-3 │ │ backend-1 │ │
│ │ 192.168.0.23 │ │ 192.168.0.225│ │
│ │ Client + │ │ Dynamic │ │
│ │ backend-2 │ │ Server │ │
│ │ (Static:80) │ │ │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ 网关: 192.168.0.1 (MAC: fa:16:3e:f3:87:8a) │
└─────────────────────────────────────────────────────────┘
各服务器角色说明:
| 服务器 | IP地址 | 角色 | 说明 |
|---|---|---|---|
| server-1 | 192.168.0.201 | Nginx负载均衡器 | VRRP Master,持有VIP 192.168.0.250,反向代理请求到后端 |
| server-2 | - | Nginx负载均衡器 | VRRP Backup,高可用备机 |
| server-3 | 192.168.0.23 | 客户端 + backend-2 | 本次实验的HTTP客户端,同时运行静态文件服务(backend-2) |
| backend-1 | 192.168.0.225 | 动态服务器 | 返回动态内容 "Backend-1 Dynamic Server" |
本次实验中,server-3(192.168.0.23)作为客户端发起HTTP请求,server-1(192.168.0.201)作为Nginx反向代理服务器接收请求并转发到后端。
二、网络分层模型
在深入分析之前,我们先回顾两种经典的网络分层模型。
2.1 OSI七层模型 vs TCP/IP四层模型
scss
OSI 七层模型 TCP/IP 四层模型
┌───────────────┐ ┌───────────────┐
│ 7. 应用层 │ │ │
│ (HTTP/DNS/SSH) │ │ 应用层 │
├───────────────┤ │ (Application) │
│ 6. 表示层 │ │ │
├───────────────┤ ├───────────────┤
│ 5. 会话层 │ │ │
├───────────────┤ │ 传输层 │
│ 4. 传输层 │ │ (Transport) │
│ (TCP/UDP) │ │ │
├───────────────┤ ├───────────────┤
│ 3. 网络层 │ │ 网络层 │
│ (IP/ICMP) │ │ (Internet) │
├───────────────┤ ├───────────────┤
│ 2. 数据链路层 │ │ 网络接口层 │
│ (Ethernet/ARP) │ │ (Link) │
├───────────────┤ │ │
│ 1. 物理层 │ │ │
│ (网线/光缆) │ │ │
└───────────────┘ └───────────────┘
TCP/IP模型将OSI的上三层(应用层、表示层、会话层)合并为应用层,将下两层(物理层、数据链路层)合并为网络接口层。在实际工程中,TCP/IP四层模型更为常用。
接下来,我们将沿着TCP/IP四层模型,从底层到顶层,用真实数据逐层剖析一个HTTP请求的完整旅程。
三、网络接口层:物理与数据链路
3.1 物理层 - 网络接口状态
一切从网卡开始。我们首先查看server-3的网卡状态:
bash
$ ip addr show eth0
sql
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
link/ether fa:16:3e:81:d9:f0 brd ff:ff:ff:ff:ff:ff
altname enp0s3
altname ens3
inet 192.168.0.23/24 brd 192.168.0.255 scope global dynamic noprefixroute eth0
valid_lft 315357940sec preferred_lft 315357940sec
inet6 fe80::f816:3eff:fe81:d9f0/64 scope link
valid_lft forever preferred_lft forever
关键字段解读:
| 字段 | 值 | 含义 |
|---|---|---|
| state | UP | 网卡处于启用状态 |
| LOWER_UP | - | 物理链路已连通(L1状态) |
| mtu | 1500 | 最大传输单元1500字节(以太网标准) |
| link/ether | fa:16:3e:81:d9:f0 | MAC地址(华为云ECS前缀fa:16:3e) |
| inet | 192.168.0.23/24 | IPv4地址及子网掩码(24位即255.255.255.0) |
| qlen | 1000 | 发送队列长度 |
MTU 1500意味着单个以太网帧的数据载荷最大为1500字节。超过此大小的数据需要在IP层分片。
3.2 数据链路层 - MAC地址与ARP协议
在以太网中,数据链路层依靠MAC地址进行通信。但我们的HTTP请求使用的是IP地址,如何将IP转换为MAC地址?这就是ARP协议的职责。
查看server-3的ARP缓存表:
bash
$ arp -n
css
Address HWtype HWaddress Flags Mask Iface
192.168.0.1 ether fa:16:3e:f3:87:8a C eth0
192.168.0.201 ether fa:16:3e:81:d9:a2 C eth0
ARP表解读:
| IP地址 | MAC地址 | Flags | 含义 |
|---|---|---|---|
| 192.168.0.1 | fa:16:3e:f3:87:8a | C | 网关的MAC(Complete表示已完成解析) |
| 192.168.0.201 | fa:16:3e:81:d9:a2 | C | server-1的MAC地址 |
当server-3需要向192.168.0.201发送数据时,内核会查找ARP表,找到对应的MAC地址 fa:16:3e:81:d9:a2,将其作为以太网帧的目的MAC地址。如果ARP表中没有记录,内核会先发送ARP广播请求,等待目标主机回应ARP应答后才能继续发送数据。
四、网络层:IP与路由
4.1 路由表查表过程
知道了目的MAC地址后,数据包需要知道该从哪个方向发出。这由路由表决定:
bash
$ ip route show
scss
default via 192.168.0.1 dev eth0 proto dhcp src 192.168.0.23 metric 100
169.254.169.254 via 192.168.0.1 dev eth0 proto dhcp src 192.168.0.23 metric 100
192.168.0.0/24 dev eth0 proto kernel scope link src 192.168.0.23 metric 100
| 目的网络 | 网关 | 接口 | 含义 |
|---|---|---|---|
| 192.168.0.0/24 | - (直连) | eth0 | 同子网流量直接通过eth0发出 |
| default (0.0.0.0/0) | 192.168.0.1 | eth0 | 其他所有流量走默认网关 |
| 169.254.169.254 | 192.168.0.1 | eth0 | 华为云元数据服务地址 |
由于目标地址192.168.0.201属于192.168.0.0/24网段,匹配第一条路由规则(最长前缀匹配),因此数据包直接通过eth0发出,无需经过网关。这就是为什么ARP表中192.168.0.201对应的MAC地址是server-1自身的MAC,而不是网关的MAC。
4.2 ICMP验证 - Ping与Traceroute
在发起HTTP请求前,我们先用ICMP协议验证网络连通性:
bash
$ ping -c 4 192.168.0.201
python
PING 192.168.0.201 (192.168.0.201) 56(84) bytes of data.
64 bytes from 192.168.0.201: icmp_seq=1 ttl=64 time=0.186 ms
64 bytes from 192.168.0.201: icmp_seq=2 ttl=64 time=0.111 ms
64 bytes from 192.168.0.201: icmp_seq=3 ttl=64 time=0.073 ms
64 bytes from 192.168.0.201: icmp_seq=4 ttl=64 time=0.073 ms
--- 192.168.0.201 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3057ms
rtt min/avg/max/mdev = 0.073/0.110/0.186/0.046 ms
Ping使用ICMP Echo Request/Reply,工作在网络层。0%丢包率、平均RTT 0.110ms,说明两台服务器之间的网络非常健康(同子网直连,仅1跳)。
使用tracepath验证路径跳数:
bash
$ tracepath -n 192.168.0.201
yaml
1?: [LOCALHOST] pmtu 1500
1: 192.168.0.201 0.172ms reached
1: 192.168.0.201 0.126ms reached
Resume: pmtu 1500 hops 1 back 1
仅1跳即到达目标,PMTU为1500,与网卡MTU一致。两台服务器在同一二层网络中,中间没有路由器。
4.3 DNS解析
虽然本次HTTP请求直接使用IP地址,但DNS是应用层与网络层交互的重要环节。查看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:
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
Name: www.a.shifen.com
Address: 2408:871a:2100:186c:0:ff:b07e:3fbc
Name: www.a.shifen.com
Address: 2408:871a:2100:1b23:0:ff:b07a:7ebc
DNS解析过程本身就是一次网络通信:server-3先向127.0.0.53发送DNS查询(UDP 53端口),systemd-resolved再向上游DNS服务器转发查询,最终获得CNAME记录 www.a.shifen.com 和多个A/AAAA记录。
五、传输层:TCP连接
5.1 TCP三次握手
HTTP基于TCP协议,在发送HTTP请求之前必须先建立TCP连接。我们在server-3上启动tcpdump抓包,同时发起curl请求:
bash
# 抓包命令
tcpdump -i eth0 -nn -vvv -S host 192.168.0.201 and port 80 -c 20
以下是TCP三次握手的真实抓包数据:
bash
# 第1包:SYN(客户端发起连接)
12:02:52.433574 IP (tos 0x0, ttl 64, id 63692, offset 0, flags [DF], proto TCP (6), length 60)
192.168.0.23.45480 > 192.168.0.201.80: Flags [S], cksum 0x825f (incorrect -> 0xc81b),
seq 2543468923, win 64240, options [mss 1460,sackOK,TS val 18183839 ecr 0,nop,wscale 7], length 0
# 第2包:SYN-ACK(服务端响应)
12:02:52.433706 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.45480: Flags [S.], cksum 0xa31c (correct),
seq 709383338, ack 2543468924, win 65160, options [mss 1460,sackOK,TS val 4060983381 ecr 18183839,nop,wscale 7], length 0
# 第3包:ACK(客户端确认)
12:02:52.433731 IP (tos 0x0, ttl 64, id 63693, offset 0, flags [DF], proto TCP (6), length 52)
192.168.0.23.45480 > 192.168.0.201.80: Flags [.], cksum 0x8257 (incorrect -> 0xce7a),
seq 2543468924, ack 709383339, win 502, options [nop,nop,TS val 18183840 ecr 4060983381], length 0
IP头部字段详解(以SYN包为例):
| 字段 | 值 | 含义 |
|---|---|---|
| tos | 0x0 | 服务类型,0表示普通优先级 |
| ttl | 64 | 生存时间,每经过一个路由器减1,为0则丢弃 |
| id | 63692 | IP包标识符,用于分片重组 |
| flags | DF | Don't Fragment,禁止分片 |
| proto | TCP (6) | 上层协议为TCP(协议号6) |
| length | 60 | IP包总长度60字节(20字节IP头+40字节TCP头) |
TCP头部字段详解:
| 字段 | SYN包值 | SYN-ACK包值 | 含义 |
|---|---|---|---|
| Flags | S | S. | S=SYN, .=ACK |
| seq | 2543468923 | 709383338 | 序列号 |
| ack | - | 2543468924 | 确认号=对方seq+1 |
| win | 64240 | 65160 | 接收窗口大小 |
| mss | 1460 | 1460 | 最大分段大小 |
| wscale | 7 | 7 | 窗口缩放因子(实际窗口=win*2^7) |
| TS val | 18183839 | 4060983381 | 时间戳值 |
| ecr | 0 | 18183839 | 回显时间戳(SYN-ACK回显SYN的TS val) |
三次握手的核心在于序列号的同步:客户端发送初始序列号(ISN),服务端确认并返回自己的ISN,客户端再确认。整个握手过程仅耗时约0.157毫秒(433574到433731微秒之差)。
5.2 查看Socket状态
通过ss命令可以查看当前的TCP连接状态:
bash
$ ss -tn state established | head -10; echo '---'; ss -s
yaml
Recv-Q Send-Q Local Address:Port Peer Address:Port Process
0 0 192.168.0.23:46292 100.125.12.110:33554
0 112 192.168.0.23:22 114.116.247.146:55650
---
Total: 182
TCP: 13 (estab 2, closed 4, orphaned 0, timewait 4)
Transport Total IP IPv6
RAW 0 0 0
UDP 5 4 1
TCP 9 8 1
INET 14 12 2
FRAG 0 0 0
可以看到当前有2个已建立的TCP连接、4个TIME_WAIT状态的连接。TIME_WAIT是TCP关闭后的正常状态,持续约60秒后自动回收。
六、应用层:HTTP请求与响应
6.1 HTTP请求发送
TCP连接建立后,立刻就可以发送HTTP请求了。从tcpdump抓包可以看到:
yaml
# 第4包:PSH+ACK(客户端发送HTTP请求)
12:02:52.433758 IP (tos 0x0, ttl 64, id 63694, offset 0, flags [DF], proto TCP (6), length 128)
192.168.0.23.45480 > 192.168.0.201.80: Flags [P.], cksum 0x82a3 (incorrect -> 0x4b36),
seq 2543468924:2543469000, ack 709383339, win 502, options [nop,nop,TS val 18183840 ecr 4060983381], length 76: HTTP, length: 76
GET / HTTP/1.1
Host: 192.168.0.201
User-Agent: curl/8.5.0
Accept: */*
Flags P. 中的P代表PSH(Push),指示接收方应立即将数据交给应用层。HTTP请求载荷为76字节,封装在一个TCP段中。
6.2 Nginx反向代理转发
server-1收到请求后,作为Nginx反向代理,将请求转发到后端服务器。tcpdump抓到了这一过程:
yaml
# server-1主动向后端发起TCP连接(源端口61596)
12:02:52.433952 IP ... 192.168.0.201.61596 > 192.168.0.23.80: Flags [S], ...
# 后端响应SYN-ACK
12:02:52.433967 IP ... 192.168.0.23.80 > 192.168.0.201.61596: Flags [S.], ...
# 反向代理发送HTTP请求(携带X-Real-IP和X-Forwarded-For头)
12:02:52.434083 IP ... 192.168.0.201.61596 > 192.168.0.23.80: Flags [P.], length 151: HTTP
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转发时使用了HTTP/1.0协议(而非客户端原始的HTTP/1.1),并添加了 X-Real-IP 和 X-Forwarded-For 头部,将客户端真实IP传递给后端。同时设置了 Connection: close,表示代理与后端之间使用短连接。
6.3 HTTP响应返回
后端服务器处理后返回HTTP响应。在某些请求中返回200 OK:
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 200 OK
< Server: nginx
< Date: Thu, 17 Sep 2026 04:02:55 GMT
< Content-Type: text/html
< Content-Length: 34
< Connection: keep-alive
< Last-Modified: Thu, 17 Sep 2026 03:39:21 GMT
< ETag: "6aab60e9-22"
< X-Backend-Server: backend-1-192.168.0.225
< Accept-Ranges: bytes
<
{ [34 bytes data]
100 34 100 34 0 0 38288 0 --:--:-- --:--:-- --:--:-- 34000
* Connection #0 to host 192.168.0.201 left intact
<h1>Backend-1 Dynamic Server</h1>
响应头中 X-Backend-Server: backend-1-192.168.0.225 揭示了Nginx将请求代理到了backend-1(192.168.0.225)。响应体为34字节的HTML:<h1>Backend-1 Dynamic Server</h1>。
6.4 TCP四次挥手
请求完成后,TCP连接通过四次挥手关闭。从tcpdump可以看到完整的FIN序列:
ruby
# 客户端发送FIN
192.168.0.23.45480 > 192.168.0.201.80: Flags [F.], seq 2543469000, ack 709383677, length 0
# 服务端ACK确认
192.168.0.201.80 > 192.168.0.23.45480: Flags [.], ack 2543469001, length 0
# 服务端发送FIN
192.168.0.201.80 > 192.168.0.23.45480: Flags [F.], seq 709383677, ack 2543469001, length 0
# 客户端ACK确认
192.168.0.23.45480 > 192.168.0.201.80: Flags [.], ack 709383678, length 0
Flags F. 中F代表FIN(Finish),表示发送方没有更多数据要发送了。四次挥手确保双方都完成了数据传输后再关闭连接。
七、全链路数据包总览
将整个HTTP请求过程的所有20个数据包汇总如下:
| 序号 | 时间戳 | 方向 | Flags | 协议 | 说明 |
|---|---|---|---|---|---|
| 1 | 433574 | C->S | SYN | TCP | 客户端发起连接 |
| 2 | 433706 | S->C | SYN-ACK | TCP | 服务端确认连接 |
| 3 | 433731 | C->S | ACK | TCP | 客户端完成握手 |
| 4 | 433758 | C->S | PSH-ACK | TCP+HTTP | 发送HTTP GET请求 |
| 5 | 433838 | S->C | ACK | TCP | 服务端确认收到请求 |
| 6-9 | 433952-434065 | S<->C | SYN/SYN-ACK/ACK | TCP | 代理到后端的握手 |
| 10 | 434083 | S->C | PSH-ACK | TCP+HTTP | 代理转发请求到后端 |
| 11-12 | 434087-434202 | C->S | ACK/PSH-ACK | TCP+HTTP | 后端返回响应 |
| 13-16 | 434221-434321 | S<->C | FIN/ACK | TCP | 代理与后端挥手 |
| 17 | 434334 | S->C | PSH-ACK | TCP+HTTP | 代理返回响应给客户端 |
| 18-20 | 434338-434484 | C<->S | FIN/ACK | TCP | 客户端与服务端挥手 |
从时间戳可以看出,整个请求过程从第一个SYN包到最后一个ACK包,总共耗时仅约0.91毫秒(433574到434484微秒)。这得益于同子网直连的低延迟网络环境。
八、总结
通过这次基于真实抓包的逐层分析,我们可以清晰地看到网络分层模型的实际运作:
- 网络接口层:网卡eth0(MAC: fa:16:3e:81:d9:f0)通过ARP协议将IP地址映射为MAC地址,以太网帧在物理链路上传输。
- 网络层:路由表决定数据包的走向,IP头部携带源/目的地址和TTL等信息,ICMP协议用于连通性检测。
- 传输层:TCP协议通过三次握手建立可靠连接,通过序列号和确认号保证数据有序到达,通过四次挥手优雅关闭连接。
- 应用层:HTTP协议定义了请求/响应格式,DNS协议将域名解析为IP地址。Nginx作为反向代理在应用层进行请求转发。
理解网络分层原理不仅仅是理论知识,它对于排查网络故障、优化应用性能、设计系统架构都有着直接的指导意义。当请求超时时,你可以逐层排查:是ARP解析失败?路由不可达?TCP握手失败?还是HTTP响应慢?分层思维让你能够快速定位问题所在。
实验环境:华为云ECS Ubuntu 24.04,nginx/1.24.0,curl/8.5.0,tcpdump 4.99.1