Learn
HTTP/01-layers

网络分层与 HTTP 的位置

💡🤖 AI 时代,还要学 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)

HTTP/1.x 的报文是人类可读的纯文本,这让它极易调试——用 telnet 或 socket 直接敲字符就能发请求。HTTP/2 之后改为二进制帧(第 13 章),但语义不变。

4. 一次请求的完整旅程

在浏览器输入 https://example.com/index.html 并回车后:

  1. URL 解析:拆出协议 https、域名 example.com、路径 /index.html(第 2 章);
  2. DNS 解析:把域名翻译成 IP 地址,如 93.184.216.34(第 2 章);
  3. TCP 三次握手:与目标 IP 的 443 端口建立连接(第 3 章);
  4. TLS 握手:协商加密算法、验证证书、交换密钥(第 12 章);
  5. 发送 HTTP 请求:请求行 + 请求头 + 请求体(第 4 章);
  6. 服务器处理并返回响应:状态行 + 响应头 + 响应体(第 6 章);
  7. 浏览器渲染:解析 HTML,继续发起 CSS/JS/图片等子请求;
  8. 连接复用或关闭:keep-alive 决定连接去留(第 11 章)。

这份清单几乎就是本课程的目录——接下来的每一章都在放大其中一步。

5. 动手观察

本课程的沙箱里有 curl 和 python3,但没有外网。没关系:我们在容器里自己启动一个 HTTP 服务器,然后请求自己——这反而能看得更清楚。

先用 curl -v(verbose)观察一次完整的请求/响应交换:

curl -v 观察一次完整的 HTTP 交互
# 后台启动一个静态文件服务器,监听 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 参数把一次请求按阶段计时,对应上面的"旅程"步骤:

分阶段计时:DNS / TCP / 首字节 / 总耗时
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 手写请求字节:

用原生 socket 手发 HTTP 请求
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 行为最趁手的工具,贯穿全课程。
🎯练习
  1. 修改 Playground 1:把请求路径换成一个不存在的文件(如 /nope.html),观察响应状态行有什么变化。
  2. 在 Playground 3 的 socket 代码里,把请求行的 HTTP/1.1 改成 HTTP/1.0,再把 Host 头删掉,观察服务器是否仍然响应;思考为什么 HTTP/1.1 强制要求 Host 头(提示:虚拟主机)。
  3. 画出你访问一个 HTTPS 网站时的完整旅程图,标注每一步属于四层模型的哪一层。
  4. 用 curl -w 的其他变量(如 %{size_download}、%{http_code})扩展 Playground 2 的输出。