网络分层与 HTTP 的位置
报文格式和状态码不必死记,用到时让 AI 查即可;但分层模型、无状态本质,以及缓存、CORS、HTTPS 这些安全与边界问题必须懂,否则 AI 写错的接口调用你根本发现不了。这部分概念与边界判断是 AI 替代不了的。建议达到概念级:理解协议为什么这样设计,具体的报文细节交给 AI 生成并人工核对。
打开浏览器输入网址、按下回车、页面出现——这背后是一场跨越多个协议层的接力赛。HTTP 只是其中最上层的一棒。理解 HTTP 之前,先要知道它站在哪里、脚下踩着什么。
1. 为什么要分层
网络通信要解决的问题非常多:比特如何在网线上传输、数据发给谁、丢包了怎么办、内容怎么表达……如果用一个巨型协议解决所有问题,任何改动都会牵一发而动全身。
分层的思想是每层只解决一类问题,向上层提供服务,对上层隐藏细节:
- HTTP 不关心丢包重传——那是 TCP 的事;
- TCP 不关心数据包怎么路由到对端——那是 IP 的事;
- IP 不关心比特如何变成电信号——那是链路层的事。
这与写代码的分层架构如出一辙:Controller 调用 Service,Service 调用 DAO,每层各司其职。
2. TCP/IP 四层模型
教科书上的 OSI 七层模型是理论参考,工程实践中真正落地的是 TCP/IP 四层模型:
| 层 | 典型协议 | 职责 | 数据单位 |
|---|---|---|---|
| 应用层 | HTTP、DNS、SMTP、WebSocket | 定义应用间交换数据的格式与语义 | 报文(Message) |
| 传输层 | TCP、UDP | 端口到端口,提供可靠(TCP)或尽力(UDP)传输 | 段(Segment)/ 数据报 |
| 网络层 | IP、ICMP | 主机到主机,跨网络路由寻址 | 包(Packet) |
| 链路层 | Ethernet、Wi-Fi、ARP | 相邻设备间传输比特 | 帧(Frame) |
记住两组关键词:
- IP 管"送到哪台机器"(IP 地址定位主机);
- TCP/UDP 管"送给哪个进程"(端口号定位进程,HTTP 服务器通常监听 80/443)。
3. 封装与解封装
发送方每往下一层,就给数据套一层"信封"(头部);接收方每往上一层,就拆掉一层:
发送方 接收方
应用层 [HTTP 报文] [HTTP 报文] 应用层
↓ 加 TCP 头 ↑ 拆 TCP 头
传输层 [TCP头|HTTP报文] [TCP头|HTTP报文] 传输层
↓ 加 IP 头 ↑ 拆 IP 头
网络层 [IP头|TCP头|HTTP报文] [IP头|TCP头|HTTP报文] 网络层
↓ 加帧头帧尾 ↑ 拆帧头帧尾
链路层 [帧头|IP头|TCP头|HTTP|帧尾] → 物理传输 → ...所以一个 HTTP 请求在网线上的真身,是被 TCP 头、IP 头、以太网帧层层包裹的字节序列。抓包工具(如 Wireshark)看到的就是这个洋葱结构。
HTTP/1.x 的报文是人类可读的纯文本,这让它极易调试——用 telnet 或 socket 直接敲字符就能发请求。HTTP/2 之后改为二进制帧(第 13 章),但语义不变。
4. 一次请求的完整旅程
在浏览器输入 https://example.com/index.html 并回车后:
- URL 解析:拆出协议 https、域名 example.com、路径 /index.html(第 2 章);
- DNS 解析:把域名翻译成 IP 地址,如 93.184.216.34(第 2 章);
- TCP 三次握手:与目标 IP 的 443 端口建立连接(第 3 章);
- TLS 握手:协商加密算法、验证证书、交换密钥(第 12 章);
- 发送 HTTP 请求:请求行 + 请求头 + 请求体(第 4 章);
- 服务器处理并返回响应:状态行 + 响应头 + 响应体(第 6 章);
- 浏览器渲染:解析 HTML,继续发起 CSS/JS/图片等子请求;
- 连接复用或关闭:keep-alive 决定连接去留(第 11 章)。
这份清单几乎就是本课程的目录——接下来的每一章都在放大其中一步。
5. 动手观察
本课程的沙箱里有 curl 和 python3,但没有外网。没关系:我们在容器里自己启动一个 HTTP 服务器,然后请求自己——这反而能看得更清楚。
先用 curl -v(verbose)观察一次完整的请求/响应交换:
# 后台启动一个静态文件服务器,监听 127.0.0.1:8000
python3 -m http.server 8000 --bind 127.0.0.1 >/dev/null 2>&1 &
for _i in $(seq 1 50); do (exec 3<>/dev/tcp/127.0.0.1/8000) 2>/dev/null && break; sleep 0.1; done
echo hello-http > index.html
# -v 打印请求头(> 开头的行)和响应头(< 开头的行)
curl -v http://127.0.0.1:8000/index.html输出里以 * 开头的行是连接过程,> 开头的是我们发出的请求,< 开头的是服务器的响应——HTTP 的全部秘密都在这几行文本里。
再用 curl 的 -w 参数把一次请求按阶段计时,对应上面的"旅程"步骤:
python3 -m http.server 8000 --bind 127.0.0.1 >/dev/null 2>&1 &
for _i in $(seq 1 50); do (exec 3<>/dev/tcp/127.0.0.1/8000) 2>/dev/null && break; sleep 0.1; done
curl -s -o /dev/null -w 'DNS 解析耗时: %{time_namelookup}s
TCP 连接完成: %{time_connect}s
首字节到达: %{time_starttransfer}s
总耗时: %{time_total}s
' http://127.0.0.1:8000/本地回环地址没有真实的 DNS 查询和网络延迟,所以数字都很小;换成公网站点,time_connect 里就包含一个 RTT(往返时延)的三次握手成本。
最后证明"HTTP 就是 TCP 之上的文本":不用任何 HTTP 库,直接用 socket 手写请求字节:
python3 -m http.server 8000 --bind 127.0.0.1 >/dev/null 2>&1 &
for _i in $(seq 1 50); do (exec 3<>/dev/tcp/127.0.0.1/8000) 2>/dev/null && break; sleep 0.1; done
python3 -c '
import socket
s = socket.create_connection(("127.0.0.1", 8000))
# HTTP 请求就是这几行文本,行尾必须是 \r\n,空行表示头部结束
s.sendall(b"GET / HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n")
data = b""
while True:
chunk = s.recv(4096)
if not chunk:
break
data += chunk
s.close()
print(data.decode(errors="replace")[:500])
'我们没有用任何 HTTP 客户端库,只是往 TCP 连接里写了四行文本,就得到了标准的 HTTP 响应。HTTP 的本质:约定好格式的文本(1.x),跑在 TCP 提供的可靠字节流上。
本课程所有 Playground 都遵循同一个套路:先后台启动本地服务(python3 -m http.server 或内嵌自定义 Handler),sleep 0.3 等它就绪,再用 curl 观察。容器无外网,但 127.0.0.1 回环完全可用。
小结
- 网络按层分工:链路层传比特、IP 层跨网络送包、TCP 层保证可靠字节流、应用层定义语义;
- HTTP 是应用层协议,默认跑在 TCP 上(HTTP/3 换成了 UDP 上的 QUIC,第 14 章);
- IP 定位主机,端口定位进程;发送时层层封装,接收时层层解封;
- 一次请求 = URL 解析 → DNS → TCP 握手 → (TLS) → HTTP 请求/响应 → 渲染;
curl -v和curl -w是观察 HTTP 行为最趁手的工具,贯穿全课程。
- 修改 Playground 1:把请求路径换成一个不存在的文件(如 /nope.html),观察响应状态行有什么变化。
- 在 Playground 3 的 socket 代码里,把请求行的
HTTP/1.1改成HTTP/1.0,再把Host头删掉,观察服务器是否仍然响应;思考为什么 HTTP/1.1 强制要求 Host 头(提示:虚拟主机)。 - 画出你访问一个 HTTPS 网站时的完整旅程图,标注每一步属于四层模型的哪一层。
- 用
curl -w的其他变量(如%{size_download}、%{http_code})扩展 Playground 2 的输出。