一、网络应用体系结构
写一个网络应用,你只需要为端系统写代码。你不需要为路由器写任何东西——这是分层带给应用开发者最大的礼物。
1.1 客户机–服务器(Client-Server)
┌──────────┐
客户机 ─────────→│ 服务器 │ 永久 IP、永远在线
客户机 ─────────→│ (数据中心) │
客户机 ─────────→└──────────┘
客户机之间不直接通信
特征:
- 服务器:永久在线、固定 IP、通常是数据中心里的集群
- 客户机:间歇连接、可能是动态 IP、彼此不直接通信
⚠️ 可扩展性问题:服务能力受服务器资源限制。N 个客户机同时下载 F 大小的文件,服务器至少要发 N·F 比特。服务时间随 N 线性增长。
1.2 对等(P2P)
Peer ←——→ Peer
↑ ╲ ╱ ↑
│ ╳ │ 没有永远在线的服务器
↓ ╱ ╲ ↓ 任意端系统直接通信
Peer ←——→ Peer
特征:自扩展(self-scalability)——每加入一个 peer,既增加了需求,也增加了服务能力。
代价:管理复杂(peer 随时上线下线、IP 会变)、安全难保证、激励机制难设计。
📌 量化对比在第 8 讲:我们会推导出 P2P 分发时间的下界,并看到它在大 N 时几乎不随 N 增长,而 C/S 是线性增长。
1.3 混合形态
现实中的大型系统几乎都是混合的:BitTorrent 用中心化的 tracker + P2P 的数据传输;区块链网络用 P2P 广播 + 中心化的 RPC 服务节点。
二、进程通信
2.1 进程、Socket 与消息
- 进程:运行在端系统上的程序
- 同一主机内的进程用进程间通信(由 OS 管,不归网络课)
- 不同主机上的进程通过交换报文通信
Socket 是进程与网络之间的门:
发送进程 接收进程
│ ↑
┌───▼────┐ socket ┌───┴────┐
│ 应用层 │ ←── 应用开发者控制 ──→ │ 应用层 │
├────────┤ ├────────┤
│ 传输层 │ ←── OS 控制 → │ 传输层 │
│ 网络层 │ │ 网络层 │
│ 链路层 │ │ 链路层 │
└────────┘ └────────┘
└──────── 物理网络 ─────────────┘
Socket = 应用层与传输层之间的 API 边界。 应用开发者能控制 socket 之上的一切,以及传输层协议的选择(TCP 还是 UDP)和少量参数;socket 之下的行为由操作系统决定。
第 9 讲会亲手写 socket 代码。
2.2 进程寻址:为什么需要端口号
要把报文送到某个进程,需要两级标识:
IP 地址(32 位 / 128 位) → 找到哪台主机
端口号(16 位,0–65535) → 找到主机上的哪个进程
只有 IP 是不够的:一台服务器上同时跑着 Web、SSH、数据库,报文到了主机之后必须还能分辨给谁。
知名端口(well-known ports,0–1023):
| 端口 | 服务 | 端口 | 服务 | |
|---|---|---|---|---|
| 20/21 | FTP | 143 | IMAP | |
| 22 | SSH | 443 | HTTPS | |
| 25 | SMTP | 853 | DNS-over-TLS | |
| 53 | DNS | 3306 | MySQL | |
| 80 | HTTP | 8080 | HTTP 备用 |
(在类 Unix 系统上,绑定 1024 以下端口需要 root 权限——这是一条安全设计。)
三、应用需要什么样的传输服务
设计一个应用时,先问四个问题:
3.1 四类需求
1️⃣ 数据完整性(可靠性)
- 文件传输、网页、金融交易:必须 100% 可靠,丢一个字节就错
- 音频、视频:可容忍部分丢失,丢一帧画面比卡顿好
2️⃣ 吞吐量
- 带宽敏感型(bandwidth-sensitive):多媒体应用需要最低带宽保证,低于阈值就不可用
- 弹性型(elastic):邮件、文件传输、Web,有多少用多少
3️⃣ 时延
- 交互式游戏、电话会议:要求 < 100 ms
- 邮件:几分钟都没关系
4️⃣ 安全性
- 加密、数据完整性、端点认证
3.2 常见应用的需求表
| 应用 | 数据丢失 | 吞吐量 | 时延敏感 |
|---|---|---|---|
| 文件传输 / 网页 | 不可容忍 | 弹性 | 否 |
| 电子邮件 | 不可容忍 | 弹性 | 否 |
| 实时音视频 | 可容忍 | 音频 5 kbps–1 Mbps · 视频 100 kbps–10 Mbps | 是,百毫秒级 |
| 存储音视频 | 可容忍 | 同上 | 是,秒级 |
| 交互式游戏 | 可容忍 | > 几 kbps | 是,百毫秒级 |
| 即时通讯 | 不可容忍 | 弹性 | 是与否 |
3.3 互联网提供了什么
| TCP | UDP | |
|---|---|---|
| 可靠数据传输 | ✅ | ❌ |
| 流量控制 | ✅ | ❌ |
| 拥塞控制 | ✅ | ❌ |
| 面向连接 | ✅ 需要握手 | ❌ |
| 有序交付 | ✅ | ❌ |
| 时延保证 | ❌ | ❌ |
| 带宽保证 | ❌ | ❌ |
| 头部开销 | 20 字节 | 8 字节 |
⚠️ 注意最后两行:**TCP 和 UDP 都不提供时延和带宽保证。**互联网是 best-effort 的,没有任何传输层协议能凭空创造出确定性。
那么为什么还有人用 UDP?三个理由:
- 不想被拥塞控制限速(实时音视频宁可丢包也不降速)
- 不想为可靠性付出重传时延(游戏里迟到的位置信息不如丢掉)
- 需要自己控制一切(QUIC 在 UDP 上重新实现了可靠传输和拥塞控制,因为在内核 TCP 上无法快速迭代——第 6 讲)
3.4 应用选择表
| 应用 | 应用层协议 | 传输层 |
|---|---|---|
| Web | HTTP/1.1, HTTP/2 | TCP |
| Web | HTTP/3 | UDP(QUIC) |
| 邮件 | SMTP, IMAP | TCP |
| 域名解析 | DNS | UDP(大响应回落 TCP) |
| 流媒体 | DASH(基于 HTTP) | TCP |
| 实时会议 | RTP / SIP / WebRTC | UDP |
| 网络时间 | NTP | UDP |
四、HTTP:Web 的应用层协议
4.1 基本概念
- Web 页面由若干对象(object)组成:一个 HTML 基本文件 + 若干图片、CSS、JS
- 每个对象由一个 URL 寻址
http://www.example.edu/dept/pic.gif
└─┬─┘ └──────┬──────┘└─────┬─────┘
协议 主机名 路径
- HTTP 使用 TCP(HTTP/1.1、HTTP/2),默认端口 80(HTTPS 为 443)
- ⭐ HTTP 是无状态的(stateless):服务器不保存关于客户机过去请求的任何信息
💡 **为什么设计成无状态?**因为保存状态很复杂:状态在崩溃后要恢复,客户端和服务器的状态可能不一致,并且状态占内存、限制可扩展性。无状态让服务器可以随意水平扩展——任何一台机器都能处理任何一个请求。 代价是:需要状态的时候(购物车、登录),必须用别的机制补上,这就是 Cookie(第 4.6 节)。
4.2 非持续连接 vs 持续连接
非持续连接(HTTP/1.0):每个对象用一条新的 TCP 连接。
一次对象获取的时间线:
客户机 服务器
│—— SYN ————————————————————→ │
│ ←———————————— SYN/ACK ————— │ ← 1 个 RTT(TCP 握手)
│—— ACK + HTTP GET —————————→ │
│ ←—————————————— 文件 ——————— │ ← 1 个 RTT + 传输时间
│
总计:2 × RTT + 文件传输时间
这个 2 RTT 是必须记住的数字。
📌 推导一个网页的加载时间:一个 HTML 加 10 张图片,RTT = 100 ms,忽略传输时间。
| 方式 | RTT 数 | 时间 |
|---|---|---|
| 非持续、串行 | 2 + 10×2 = 22 | 2200 ms |
| 非持续、6 条并行连接 | 2 + ⌈10/6⌉×2 = 2+4 = 6 | 600 ms |
| 持续连接、流水线 | 2 + 1 = 3 | 300 ms |
持续连接(HTTP/1.1,默认):一条 TCP 连接上串行发送多个请求/响应,服务器发送完不立即关闭。
两个额外好处(常被忽略但很重要):
- 省下 TCP 握手的 RTT
- ⭐ 保住了 TCP 的拥塞窗口。新连接要从慢启动重新开始(第 16 讲),窗口很小、速度很慢。复用连接意味着继承已经涨起来的窗口。
4.3 HTTP 请求报文
HTTP 报文是 ASCII 文本,人类可读——这是它最初能流行的重要原因(也是 Lab 1 里你能直接读懂抓包内容的原因)。
GET /index.html HTTP/1.1\r\n
Host: www.example.edu\r\n
User-Agent: Mozilla/5.0\r\n
Connection: keep-alive\r\n
Accept-Language: zh-CN,en\r\n
\r\n
结构:
请求行: 方法 URL 版本
首部行: 字段名: 值 (可以有很多行)
空行: \r\n ← 标志首部结束
实体体: (POST/PUT 时携带的数据)
⚠️ Host: 首部是必需的(HTTP/1.1 起)。原因:一个 IP 可以承载成千上万个网站(虚拟主机),服务器靠 Host 判断你要访问哪一个。这也是 CDN 得以工作的前提之一。
4.4 HTTP 方法
| 方法 | 语义 | 幂等? | 安全? |
|---|---|---|---|
| GET | 获取资源 | ✅ | ✅ 不改变服务器状态 |
| POST | 提交数据、创建资源 | ❌ | ❌ |
| PUT | 用请求体整体替换目标资源 | ✅ | ❌ |
| DELETE | 删除资源 | ✅ | ❌ |
| HEAD | 同 GET 但只要首部不要实体 | ✅ | ✅ |
| OPTIONS | 查询支持的方法(CORS 预检用) | ✅ | ✅ |
- 安全(safe):不修改服务器状态
- 幂等(idempotent):执行一次和执行 N 次效果相同
📌 为什么这个区分重要:网络会丢包和重传。幂等的请求可以安全地自动重试,非幂等的不能——这就是浏览器刷新 POST 页面时会弹「确认重新提交表单?」的原因。
4.5 HTTP 状态码
| 段 | 含义 | 常见码 |
|---|---|---|
| 1xx | 信息 | 101 Switching Protocols(WebSocket 升级) |
| 2xx | 成功 | 200 OK、201 Created、204 No Content |
| 3xx | 重定向 | 301 永久移动、302 临时、304 Not Modified ⭐ |
| 4xx | 客户端错误 | 400 Bad Request、401 未认证、403 Forbidden、404 Not Found、429 Too Many Requests |
| 5xx | 服务器错误 | 500 Internal Error、502 Bad Gateway、503 Service Unavailable、504 Gateway Timeout |
⚠️ 考试易错点:
- 401 vs 403:401 = 「我不知道你是谁,请认证」;403 = 「我知道你是谁,但你没权限」
- 301 vs 302:301 会被浏览器和搜索引擎永久缓存,改错了很难撤回
- 502 vs 504:502 = 上游返回了无效响应;504 = 上游超时没回
4.6 Cookie:给无状态协议加上状态
四个组成部分:
- HTTP 响应报文中的
Set-Cookie:首部 - HTTP 请求报文中的
Cookie:首部 - 用户端浏览器里保存的 cookie 文件
- 网站后端的数据库
客户机 服务器
│—— 首次 GET ————————————————————→ │
│ │ 创建 ID 1678
│ ←—— 200 OK │ 写入数据库
│ Set-Cookie: 1678 ————————————│
│ [浏览器存下 1678] │
│ │
│—— GET Cookie: 1678 ————————————→ │ 查库 → 「这是上次那个人」
│ ←—— 个性化响应 ——————————————————│
用途:会话管理(登录态)、购物车、个性化推荐、追踪用户。
⚠️ 隐私问题:第三方 cookie 让广告商能跨站点拼接用户的浏览轨迹。这是 GDPR、cookie 同意横幅、以及浏览器逐步淘汰第三方 cookie 的直接原因。
安全属性(面试常问):
HttpOnly:禁止 JavaScript 读取,缓解 XSS 窃取Secure:只在 HTTPS 上发送SameSite=Strict/Lax:限制跨站携带,缓解 CSRF
4.7 条件 GET:不重复传输没变的东西
目标:如果缓存里的副本还是最新的,就不要再传一遍。
客户机(有缓存副本,上次带回 Last-Modified: Wed, 9 Sep 2026 09:23:24)
│—— GET /pic.gif
│ If-Modified-Since: Wed, 9 Sep 2026 09:23:24 ——→ │
│ │ 对比修改时间
│ ←—— HTTP/1.1 304 Not Modified │
│ (空实体体!)——————————————————————————————│
304 响应不包含对象本身,只有几百字节的头部。对于没变的大图片,节省可以是 99.9%。
另一组更现代的机制是 ETag / If-None-Match:用内容哈希代替时间戳,避免了时钟精度和「内容没变但时间戳变了」的问题。
📌 第 6 讲讲 Web 缓存与 CDN 时,这个机制是基础。
五、例题(Worked Example)
题目:某网页包含 1 个 HTML 基本文件(100 KB)和 8 个小对象(各 10 KB)。RTT = 80 ms,链路速率 10 Mbps,忽略 DNS 查询与排队时延,TCP 慢启动的影响也忽略。
(a) 用 HTTP/1.0 非持续连接、串行获取,总时间? (b) 改为 6 条并行的非持续连接,总时间? (c) 用 HTTP/1.1 持续连接 + 流水线,总时间?
解答:
先算传输时间:
HTML: 100 KB × 8 = 800,000 bit ÷ 10 Mbps = 80 ms
每个小对象: 10 KB × 8 = 80,000 bit ÷ 10 Mbps = 8 ms
(a) 非持续串行:
HTML: 2×RTT + 80 ms = 160 + 80 = 240 ms
每个对象: 2×RTT + 8 ms = 160 + 8 = 168 ms
8 个对象: 8 × 168 = 1344 ms
总计: 240 + 1344 = 1584 ms
(b) 6 条并行(两批:6 个 + 2 个):
HTML: 240 ms
第一批 6 个并行: 168 ms(并行不叠加 RTT,但带宽被 6 条分摊,
此处对象很小,取近似 168 ms)
第二批 2 个并行: 168 ms
总计: 240 + 168 + 168 = 576 ms
(c) 持续 + 流水线:
TCP 握手: 1 × RTT = 80 ms
请求 HTML: 1 × RTT + 80 ms 传输 = 160 ms
8 个请求一起发出,响应连续到达:
1 × RTT + 8×8 ms = 80 + 64 = 144 ms
总计: 80 + 160 + 144 = 384 ms
结论:1584 ms → 576 ms → 384 ms。持续连接的收益主要来自消除了重复握手的 RTT,而这个收益在 RTT 越大(移动网络、跨洲访问)时越显著。
六、随堂自测
- HTTP 为什么设计成无状态的?无状态带来了什么代价,用什么补上?
Host:首部为什么在 HTTP/1.1 中变成必需的?- 一个网页有 20 个对象,RTT = 200 ms。持续连接(流水线)相比非持续串行,能省多少时间(忽略传输时间)?
- 401 和 403 的区别是什么?502 和 504 呢?
- 为什么浏览器刷新 POST 结果页会弹出确认框,而刷新 GET 页面不会?
- 条件 GET 的 304 响应节省了什么?没节省什么?
七、本讲要点回顾
- 应用体系结构:C/S 服务能力受限于服务器;P2P 自扩展但管理复杂。
- Socket 是应用层与传输层的 API 边界;寻址需要 IP + 端口两级。
- 应用需求四维:可靠性、吞吐量、时延、安全。
- **TCP 和 UDP 都不提供时延或带宽保证。**用 UDP 是为了绕开拥塞控制、避免重传时延、或自己掌控一切。
- HTTP 无状态;持续连接省的是 RTT 和已长大的拥塞窗口。
- 非持续连接每个对象 2 RTT。
- 幂等性决定了请求能否被安全重试。
- Cookie 是给无状态协议外挂状态的机制,也是追踪的技术基础。
- 条件 GET / 304 只省实体体,不省 RTT。
八、自测答案
1. 无状态让服务器可以任意水平扩展——任何一台服务器都能处理任何请求,无需在集群间同步会话状态,也无需在崩溃后恢复状态。代价是需要状态的应用(登录、购物车)必须自己实现,补救机制是 Cookie(客户端存标识,服务端存数据)以及后来的 token / session store。
2. 因为一个 IP 地址上可以托管成千上万个虚拟主机(尤其是共享主机和 CDN 边缘节点)。HTTP/1.0 的请求行只有路径,服务器无法判断客户想访问哪个站点。Host: 提供了这一信息。(HTTPS 场景下对应的机制是 TLS 的 SNI 扩展,第 28 讲。)
3. 非持续串行:(1 + 20) × 2 RTT = 42 RTT = 8400 ms。持续流水线:1 RTT(握手)+ 1 RTT(HTML)+ 1 RTT(20 个对象一批)= 3 RTT = 600 ms。节省 7800 ms,约 93%。
4. 401 Unauthorized:缺少或无效的认证凭据,响应应带 WWW-Authenticate,客户端补上凭据后可能成功。403 Forbidden:已认证但无权限,重试凭据没用。502 Bad Gateway:代理/网关从上游收到了无效响应。504 Gateway Timeout:代理/网关等待上游超时未收到响应。
5. 因为 GET 是幂等且安全的——重复执行不改变服务器状态,浏览器可以放心重发。POST 不是幂等的——重发可能造成重复下单、重复扣款,所以浏览器必须让用户确认。(正确的工程做法是 POST/Redirect/GET 模式:POST 后返回 303 重定向到一个 GET 页面。)
6. 节省了实体体的传输(可能是几百 KB 到几 MB)。没有节省的是一个完整的 RTT——你仍然必须发出请求、等待响应才知道「没变」。这正是 HTTP 缓存要引入 Cache-Control: max-age 的原因:在有效期内连请求都不发,把那个 RTT 也省掉。第 6 讲展开。