从一个HTTP请求看网络分层原理:基于华为云ECS的真实抓包实战

从一个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-IPX-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微秒)。这得益于同子网直连的低延迟网络环境。

八、总结

通过这次基于真实抓包的逐层分析,我们可以清晰地看到网络分层模型的实际运作:

  1. 网络接口层:网卡eth0(MAC: fa:16:3e:81:d9:f0)通过ARP协议将IP地址映射为MAC地址,以太网帧在物理链路上传输。
  2. 网络层:路由表决定数据包的走向,IP头部携带源/目的地址和TTL等信息,ICMP协议用于连通性检测。
  3. 传输层:TCP协议通过三次握手建立可靠连接,通过序列号和确认号保证数据有序到达,通过四次挥手优雅关闭连接。
  4. 应用层:HTTP协议定义了请求/响应格式,DNS协议将域名解析为IP地址。Nginx作为反向代理在应用层进行请求转发。

理解网络分层原理不仅仅是理论知识,它对于排查网络故障、优化应用性能、设计系统架构都有着直接的指导意义。当请求超时时,你可以逐层排查:是ARP解析失败?路由不可达?TCP握手失败?还是HTTP响应慢?分层思维让你能够快速定位问题所在。

实验环境:华为云ECS Ubuntu 24.04,nginx/1.24.0,curl/8.5.0,tcpdump 4.99.1

相关推荐
江华森1 小时前
TCP协议详解——三次握手、四次挥手与连接状态
前端
甲维斯1 小时前
《钢铁洪流》官网搞定,纯AI制作,Opus5操刀!
前端·人工智能·游戏开发
szephyr1 小时前
WebSocket 实战:心跳、断线重连、鉴权,一次讲清
前端·websocket·node.js·长连接·实时通信
默_笙2 小时前
🚋 从流水线到地铁网:为什么复杂 AI 都要拆成多 Agent(上)——LangGraph 基础入门
前端·javascript
科技苑2 小时前
前后端分离与微服务架构如何协同?
前端·后端·前端框架
神秘的猪头2 小时前
TypeScript 高级用法全解析:从泛型到 infer,把类型系统真正用起来
前端·typescript
moMo2 小时前
React Hooks 与闭包
前端·react.js
柚yuzumi3 小时前
彻底搞懂 JavaScript 类型转换:显式转换、隐式转换与 ToPrimitive
前端·javascript·node.js