一、网络应用体系结构

写一个网络应用,你只需要为端系统写代码。你不需要为路由器写任何东西——这是分层带给应用开发者最大的礼物。

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?三个理由:

  1. 不想被拥塞控制限速(实时音视频宁可丢包也不降速)
  2. 不想为可靠性付出重传时延(游戏里迟到的位置信息不如丢掉)
  3. 需要自己控制一切(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 连接上串行发送多个请求/响应,服务器发送完不立即关闭。

两个额外好处(常被忽略但很重要):

  1. 省下 TCP 握手的 RTT
  2. ⭐ 保住了 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:给无状态协议加上状态

四个组成部分:

  1. HTTP 响应报文中的 Set-Cookie: 首部
  2. HTTP 请求报文中的 Cookie: 首部
  3. 用户端浏览器里保存的 cookie 文件
  4. 网站后端的数据库
客户机                                  服务器
   │—— 首次 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 越大(移动网络、跨洲访问)时越显著。


六、随堂自测

  1. HTTP 为什么设计成无状态的?无状态带来了什么代价,用什么补上?
  2. Host: 首部为什么在 HTTP/1.1 中变成必需的?
  3. 一个网页有 20 个对象,RTT = 200 ms。持续连接(流水线)相比非持续串行,能省多少时间(忽略传输时间)?
  4. 401 和 403 的区别是什么?502 和 504 呢?
  5. 为什么浏览器刷新 POST 结果页会弹出确认框,而刷新 GET 页面不会?
  6. 条件 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 讲展开。