THE TCP/IP GUIDE 学习:主机配置协议详解:BOOTP 与 DHCP
只支持静态地址分配(硬件地址固定对应某个 IP)。笔记本电脑频繁切换网络IP 地址数量有限,需要重复利用设备数量可能超过可用 IP 数量动态地址分配。RARP(1984年)- 功能:只能提供 IP 地址- 依赖:同网络内必须有 RARP 服务器- 问题:不能跨网络,功能太少↓ 升级BOOTP(1985年)- 功能:IP 地址 + 更多参数 + 启动文件名- 依赖:UDP(高层协议,可跨网络)- 改
本文对《TCP/IP 指南》第三部分第三章(主机配置协议)进行中文整理,力求通俗易懂。
第一章:为什么需要主机配置协议?
网络配置的痛点
每台设备接入网络前,都需要被告知一些基本信息:
一台主机需要知道的参数:
- 自己的 IP 地址(最重要,必须唯一)
- 子网掩码
- 默认路由器(网关)的地址
- DNS 服务器地址
- IP 数据报的 TTL 值
- 本地网络的 MTU(最大传输单元)
- ……等几十个参数
手动配置的四大困境
| 困境 | 说明 |
|---|---|
| 远程配置 | 管理员不可能跑到每台机器前手动配置 |
| 移动设备 | 笔记本换了网络就要换 IP,手动改很麻烦 |
| 无存储设备 | 有些设备根本没有硬盘,无法保存配置 |
| 地址共享 | IP 地址不够用,必须动态分配重复利用 |
主机配置协议的作用:让主机开机后自动获取所有需要的配置信息,无需人工干预。
第二章:BOOTP——第一代自动配置协议
历史背景与设计目标
BOOTP 的前辈是 RARP(逆地址解析协议),但 RARP 有很多缺陷:
| RARP 的问题 | BOOTP 的改进 |
|---|---|
| 只能提供 IP 地址 | 能提供更多配置参数 |
| 依赖硬件层广播,不能跨网络 | 使用 UDP,支持跨网络(通过中继代理) |
| 每个网络都需要单独的 RARP 服务器 | 一台 BOOTP 服务器可以服务多个网络 |
BOOTP 的两阶段启动:
阶段一(BOOTP 负责):
主机开机 → 发送 BOOTP 请求 → 获得 IP 地址和配置参数
阶段二(TFTP 负责):
使用 IP 地址 → 通过 TFTP 下载操作系统软件 → 完成启动
BOOTP 的工作原理
BOOTP 使用 UDP 传输,端口分配:
- 服务器监听端口:67
- 客户端监听端口:68
为什么需要两个专用端口?因为 BOOTP 服务器可能需要广播回复,如果用客户端的临时端口,其他设备可能会误收到这个广播。
BOOTP 详细操作步骤
步骤1:客户端创建 BOOTREQUEST 消息
- Op 字段设为 1(请求)
- 把自己的硬件地址(MAC 地址)填入 CHAddr 字段
- 生成随机事务 ID 填入 XID 字段
- CIAddr 字段:如果知道自己的 IP 就填入,否则填 0
步骤2:客户端广播请求
- 发送到地址 255.255.255.255(广播地址)
步骤3:服务器收到请求
- 服务器在 UDP 67 端口监听
- 根据客户端的 CHAddr 查询表格,找到对应的 IP 地址
步骤4:服务器创建 BOOTREPLY 消息
- Op 字段改为 2(回复)
- 把查到的 IP 地址填入 YIAddr(你的 IP 地址)字段
- 填入服务器自己的 IP 和名称
步骤5:服务器发送回复
方式取决于 B 标志和 CIAddr 字段:
- B=1:广播回复
- CIAddr 非零:单播到客户端已知的 IP
- 否则:可以用 ARP 或广播
步骤6:客户端处理回复
- 保存获得的 IP 地址和其他参数
步骤7:客户端进入第二阶段
- 用 TFTP 下载操作系统软件
BOOTP 消息格式
| 字段名 | 大小(字节) | 说明 |
|---|---|---|
| Op | 1 | 操作码:1=请求,2=回复 |
| HType | 1 | 硬件类型(1=以太网) |
| HLen | 1 | 硬件地址长度(以太网=6) |
| Hops | 1 | 跳数,由中继代理使用 |
| XID | 4 | 事务 ID,用于匹配请求和回复 |
| Secs | 2 | 客户端启动后经过的秒数 |
| Flags | 2 | 标志位,含广播标志 B |
| CIAddr | 4 | 客户端 IP 地址(客户端自填) |
| YIAddr | 4 | 服务器分配给客户端的 IP |
| SIAddr | 4 | 服务器 IP 地址 |
| GIAddr | 4 | 中继代理 IP 地址(网关 IP) |
| CHAddr | 16 | 客户端硬件地址(MAC 地址) |
| SName | 64 | 服务器名称 |
| File | 128 | 启动文件名 |
| Vend | 64 | 厂商特定区域(后来用于扩展信息) |
丢包重传机制
BOOTP 使用 UDP(不可靠),所以客户端需要自己处理重传。
指数退避算法(防止大量主机同时重传把网络搞崩):
第 n 次重传等待时间 = 2 n − 1 × 4 秒 ± 随机抖动 \text{第}n\text{次重传等待时间} = 2^{n-1} \times 4 \text{ 秒} \pm \text{随机抖动} 第n次重传等待时间=2n−1×4 秒±随机抖动
具体:
- 第1次重传:随机等待 0~4 秒
- 第2次重传:随机等待 0~8 秒
- 第3次重传:随机等待 0~16 秒
- ……以此类推
就像以太网的碰撞退避:如果大家都同时重发,就再等一会儿,但等的时间是随机的,避免再次碰撞。
BOOTP 厂商扩展信息(Vendor Extensions)
Vend 字段原本供厂商自定义,后来 RFC 1048 规定了一种通用的使用方法:
Vend 字段结构:
[魔法 Cookie: 99.130.83.99][扩展字段1][扩展字段2]...[结束标记:255]
↑ 4字节,标识这是通用扩展模式
每个扩展字段使用 TLV 编码(类型-长度-值):
一个扩展字段:
[Code: 1字节][Len: 1字节][Data: Len字节]
↑类型 ↑后面数据的字节数 ↑实际数据
特殊值:
- Code=0:填充字节(对齐用)
- Code=255:结束标记
BOOTP 中继代理
问题:客户端不知道服务器在哪,只能广播。但路由器默认不转发广播,所以客户端和服务器必须在同一个网络。
解决方案:BOOTP 中继代理(Relay Agent)
没有中继代理时(服务器和客户端必须在同一网络):
客户端 → 广播 → 服务器(只有同网络的服务器能收到)
有中继代理时(服务器可以在任何地方):
客户端 → 广播 → 中继代理(在本地网络)
↓
单播转发给远端服务器
↓
服务器单播回复给中继代理
↓
中继代理转发回复给客户端
中继代理的工作逻辑:
- 检查 Hops 字段:如果超过 16,丢弃(防止无限循环);否则递增
- 把自己的 IP 地址填入 GIAddr 字段(第一个中继代理填写)
- 把请求转发给服务器
服务器看到 GIAddr 非零,就知道这是中继过来的,把回复单播发给中继代理的地址。
第三章:DHCP 概述与地址分配
DHCP 是 BOOTP 的升级版
BOOTP 的最大问题:只支持静态地址分配(硬件地址固定对应某个 IP)。
现代网络的需求:
- 笔记本电脑频繁切换网络
- IP 地址数量有限,需要重复利用
- 设备数量可能超过可用 IP 数量
**DHCP(动态主机配置协议)**解决了这些问题,它建立在 BOOTP 的基础上,添加了最重要的新功能:动态地址分配。
三种地址分配方式
| 方式 | 说明 | 适用场景 |
|---|---|---|
| 手动分配 | 管理员预先指定某设备用某 IP,DHCP 只是传达 | 服务器、路由器等需要固定 IP 的设备 |
| 自动分配 | DHCP 从地址池中选一个永久分配给设备 | 实际很少用(不如直接手动分配) |
| 动态分配 | DHCP 从地址池中租一个 IP 给设备,有时间限制 | 普通客户端,这是 DHCP 最重要的功能 |
动态分配的好处:
- 自动化:无需管理员干预
- 集中管理:所有 IP 信息都在服务器上
- 地址复用:设备不用时,地址自动归还
- 支持移动设备:任何设备都能获取地址
- 避免冲突:统一分配不会重复
租约(Lease)——DHCP 最核心的概念
在 DHCP 里,设备不"拥有"IP 地址,而是"租用"IP 地址。
用租房来类比:
传统 IP 分配:
→ 你购买了这套房子(永久拥有这个 IP)
DHCP 动态分配:
→ 你租了这套房子,租期 X 天(临时使用这个 IP)
→ 租期到了需要续租,否则被收回
→ 你可以提前退租(告诉服务器不需要了)
租约时长策略
租约时间的长短是一个权衡:
长租约 ⇒ 地址稳定 短租约 ⇒ 地址利用率高 \text{长租约} \Rightarrow \text{地址稳定} \quad \text{短租约} \Rightarrow \text{地址利用率高} 长租约⇒地址稳定短租约⇒地址利用率高
| 租约时长 | 适用场景 |
|---|---|
| 1小时以内 | 地址紧缺,设备频繁上下线 |
| 1天 | 访客设备,每天重新申请 |
| 3天 | Microsoft DHCP 服务器的默认值 |
| 1周 | 适中的折中方案 |
| 3个月 | 稳定环境,设备偶尔长期离线(如大学暑假) |
| 1年 | 近似于永久分配 |
注意:默认情况下,租约过了一半时间,客户端就会开始尝试续租。
地址池与范围(Scope)
管理员配置一段可供分配的 IP 范围:
示例:分配 111.14.56.20 到 111.14.56.254
↑
为什么不从 .1 开始?
.1 到 .19 手动分配给服务器和路由器
多服务器的地址范围划分:
服务器A 和 服务器B 共用同一段地址空间:
总范围:111.14.56.1 ~ 111.14.56.254
保留地址(两个服务器都排除):.1 ~ .19
服务器A 负责:.20 ~ .199
服务器B 负责:.200 ~ .254
好处:一台服务器宕机,管理员可以快速调整让另一台接管全部地址
第四章:DHCP 详细操作
客户端与服务器的职责
DHCP 服务器的职责:
- 存储和管理 IP 地址池
- 存储和管理配置参数
- 管理租约(记录哪个设备用哪个 IP 用到什么时候)
- 响应客户端的各种请求
- 提供管理界面给管理员
DHCP 客户端的职责: - 主动发起通信(服务器不会主动联系客户端)
- 管理自己的配置参数
- 在合适的时间续租或终止租约
- 检测消息丢失并重传
DHCP 客户端状态机
DHCP 客户端在不同状态之间切换:
| 状态 | 说明 |
|---|---|
| INIT | 初始状态,没有 IP 地址 |
| SELECTING | 等待服务器的 OFFER |
| REQUESTING | 已选定服务器,等待确认 |
| INIT-REBOOT | 重启后有旧租约,尝试恢复 |
| REBOOTING | 等待旧租约确认 |
| BOUND | 正常工作状态,有有效的 IP 地址 |
| RENEWING | T1 到期,向原服务器申请续租 |
| REBINDING | 续租失败,T2 到期,向任意服务器申请 |
两个关键计时器
每次获得租约后,客户端启动两个计时器:
T 1 = 50 % × 租约时长 (续租计时器) T1 = 50\% \times \text{租约时长} \quad \text{(续租计时器)} T1=50%×租约时长(续租计时器)
T 2 = 87.5 % × 租约时长 = 7 8 × 租约时长 (重绑定计时器) T2 = 87.5\% \times \text{租约时长} = \frac{7}{8} \times \text{租约时长} \quad \text{(重绑定计时器)} T2=87.5%×租约时长=87×租约时长(重绑定计时器)
租约时间轴示例(假设租约8天):
|----|----|----|----|----|----|----|----|
0 1 2 3 4 5 6 7 8天
↑T1到期(第4天) ↑T2到期(第7天) ↑租约到期(第8天)
开始续租 续租失败,向任意服务器重绑定
初始租约分配流程(14步详解)
步骤1:客户端创建 DHCPDISCOVER 消息
- 填入自己的 MAC 地址(CHAddr)
- 生成随机事务 ID(XID)
- 可选:请求特定 IP、特定租约时长、特定参数列表
步骤2:客户端广播 DHCPDISCOVER
- 发到 255.255.255.255
- 进入 SELECTING 状态
步骤3:各服务器收到并处理
- 查询数据库,决定是否响应
步骤4:服务器创建 DHCPOFFER 消息
- 填入准备分配的 IP(YIAddr)
- 填入租约时长
- 填入各种配置参数(通过 DHCP Options)
步骤5:服务器探测地址(可选但推荐)
- 用 ICMP ping 检测该 IP 是否已被使用
- 预留该地址
步骤6:服务器发送 DHCPOFFER
步骤7:客户端收集所有 OFFER
- 可以等一会儿收集多个 OFFER
- 选择最好的(如租约最长的)
- 如果没收到任何 OFFER,重传 DHCPDISCOVER
步骤8:客户端创建 DHCPREQUEST
- 包含选定服务器的标识(Server Identifier 选项)
- 包含请求的 IP 地址(Requested IP Address 选项)
步骤9:客户端广播 DHCPREQUEST
- 广播是为了告知所有服务器(包括未被选中的)
- 进入 REQUESTING 状态
步骤10:服务器处理 DHCPREQUEST
- 未被选中的服务器:等待一段时间再释放预留地址
(因为客户端可能还会回来选它作为备用)
步骤11:被选中的服务器发送 DHCPACK 或 DHCPNAK
- DHCPACK:确认租约,包含所有配置参数
- DHCPNAK:拒绝(如该地址已被其他人用了)
步骤12:客户端处理回复
- 收到 DHCPACK:继续下一步
- 收到 DHCPNAK:回到步骤1重新来
步骤13:客户端检查 IP 是否被占用(ARP 探测)
- 发送 ARP 请求:"有没有人用这个 IP?"
- 有人回复:发送 DHCPDECLINE 告知服务器,回到步骤1
- 无人回复:说明地址可用
步骤14:客户端完成分配
- 设置 IP 地址和所有参数
- 启动 T1 和 T2 计时器
- 进入 BOUND 状态
租约续租与重绑定流程
续租(Renewal):
T1 到期 → 客户端单播发送 DHCPREQUEST 给原服务器
↓
服务器回复 DHCPACK → 续租成功,重置 T1 和 T2
↓
服务器回复 DHCPNAK → 续租失败,回到 INIT 状态
↓
服务器无回复 → 保持 RENEWING 状态,继续重试,直到 T2 到期
重绑定(Rebinding):
T2 到期(原服务器无响应)→ 客户端广播 DHCPREQUEST
↓
任意服务器回复 DHCPACK → 绑定到新服务器,续租成功
↓
服务器回复 DHCPNAK → 回到 INIT 状态
↓
租约到期仍无回复 → 失去 IP 地址,回到 INIT 状态
为什么两步?先单播(省流量,通常就够了),再广播(兜底,让其他服务器接管)。
重启后的租约恢复(Reallocation)
客户端重启后发现还有有效租约,不需要完整的分配过程:
完整分配(4步网络交互): DISCOVER → OFFER → REQUEST → ACK
租约恢复(2步网络交互): REQUEST → ACK
主动释放租约
客户端发送 DHCPRELEASE 消息给原服务器:
场景:
- 设备要换到另一个网络
- 管理员要重置 IP 分配
- 修复某些 DHCP 问题
客户端单播发送 DHCPRELEASE → 服务器记录该租约已结束
(服务器不需要回复;释放是"礼貌"行为,不是强制的)
已有地址的设备请求参数
有时候设备已经有 IP(手动配置的),但还想从 DHCP 服务器获取其他配置参数:
设备(已有 IP)→ 发送 DHCPINFORM(单播或广播)
↓
DHCP 服务器 → 回复 DHCPACK(包含请求的参数)
常见场景:DHCP 服务器本身用 DHCP 获取其他参数(服务器对另一个服务器发 DHCPINFORM)。
第五章:DHCP 消息格式与选项
DHCP 消息格式
DHCP 消息格式和 BOOTP 基本一样(为了向后兼容),主要差别是:
Vend字段改名为Options字段,且长度变为可变长度(最少 312 字节)- 具体的 DHCP 消息类型通过
Options里的 DHCP Message Type 选项指定
| 字段名 | 大小(字节) | 与 BOOTP 的差异 |
|---|---|---|
| Op | 1 | 相同:1=请求,2=回复 |
| HType | 1 | 相同 |
| HLen | 1 | 相同 |
| Hops | 1 | 相同,供中继代理使用 |
| XID | 4 | 相同 |
| Secs | 2 | DHCP 中定义为:客户端尝试获取/续租以来的秒数 |
| Flags | 2 | 相同,含广播标志 B |
| CIAddr | 4 | DHCP 中仅在 BOUND/RENEWING/REBINDING 状态下填入 |
| YIAddr | 4 | 相同,服务器分配给客户端的 IP |
| SIAddr | 4 | DHCP 中是下一步引导过程中客户端应使用的服务器 IP |
| GIAddr | 4 | 相同,中继代理 IP |
| CHAddr | 16 | 相同 |
| SName | 64 | 可被 Option Overload 征用来承载 Options |
| File | 128 | 可被 Option Overload 征用来承载 Options |
| Options | 可变 | BOOTP 中是固定 64 字节的 Vend 字段 |
DHCP 的 8 种消息类型
| 选项53值 | 消息类型 | 谁发 | 用途 |
|---|---|---|---|
| 1 | DHCPDISCOVER | 客户端 | 寻找服务器,请求租约 |
| 2 | DHCPOFFER | 服务器 | 提供一个 IP 地址和租约条款 |
| 3 | DHCPREQUEST | 客户端 | 接受某个服务器的 OFFER,或续租/重绑定 |
| 4 | DHCPDECLINE | 客户端 | 告知服务器提供的地址已被占用 |
| 5 | DHCPACK | 服务器 | 确认分配/续租,包含最终配置参数 |
| 6 | DHCPNAK | 服务器 | 拒绝或撤回之前的 OFFER |
| 7 | DHCPRELEASE | 客户端 | 主动释放租约 |
| 8 | DHCPINFORM | 客户端 | 已有 IP,只请求其他配置参数 |
DHCP Options 结构
Options 字段格式:
[魔法 Cookie: 99.130.83.99(4字节)]
[Option1: Code(1B) + Len(1B) + Data(Len bytes)]
[Option2: Code(1B) + Len(1B) + Data(Len bytes)]
......
[End: 255(1字节,无 Len 和 Data)]
Option Overload(选项超载):
当 Options 字段放不下所有选项时,可以把 SName(64字节)和 File(128字节)也用来存放选项。
通过 Option 52(Option Overload 选项)告知接收方如何解析这两个字段。
重要 DHCP 选项速查
RFC 1497 厂商扩展(最常用的基础选项):
| Code | 含义 |
|---|---|
| 0 | 填充字节(对齐用) |
| 1 | 子网掩码 |
| 2 | 时区偏移(相对 UTC 的秒数) |
| 3 | 路由器(默认网关)地址列表 |
| 6 | DNS 服务器地址列表 |
| 12 | 主机名 |
| 15 | DNS 域名 |
| 255 | 结束标记 |
DHCP 专属扩展选项:
| Code | 含义 |
|---|---|
| 50 | 客户端请求的 IP 地址 |
| 51 | IP 地址租约时长(秒) |
| 52 | Option Overload 标志 |
| 53 | DHCP 消息类型(最重要!) |
| 54 | 服务器标识符 |
| 55 | 客户端请求参数列表 |
| 57 | 客户端/服务器能接受的最大消息长度 |
| 58 | T1 续租计时器值 |
| 59 | T2 重绑定计时器值 |
| 61 | 客户端标识符(替代默认的 MAC+子网 标识) |
第六章:DHCP 实现与特性
APIPA——服务器找不到时的自救机制
问题:DHCP 服务器宕机了,客户端怎么办?
解决方案:APIPA(Automatic Private IP Addressing,自动私有 IP 地址分配)
APIPA 工作流程:
1. DHCP 获取失败(多次重试无响应)
2. 在 169.254.0.1 ~ 169.254.255.254 中随机选一个地址
3. 用 ARP 探测该地址是否被占用
4. 有冲突 → 重新选地址,回到步骤2
5. 无冲突 → 用这个地址临时工作
6. 每隔5分钟继续尝试联系 DHCP 服务器
7. 找到服务器后,换回 DHCP 分配的正式地址
地址块 169.254.0.0/16 是专门为自动配置保留的:
169.254.0.1 ∼ 169.254.255.254 (共 65534 个可用地址) 169.254.0.1 \sim 169.254.255.254 \quad \text{(共 65534 个可用地址)} 169.254.0.1∼169.254.255.254(共 65534 个可用地址)
APIPA 的局限性:
169.254.x.x是私有地址,不能访问互联网- 只能和同一网段内的其他 APIPA 设备通信
- 不提供默认网关等其他配置参数
服务器冲突检测
问题:如果两台 DHCP 服务器都从同一地址池分配,可能给两个客户端分配同一个 IP。
解决方案:DHCP 服务器冲突检测
服务器要给客户端分配地址之前:
1. 先 ping 这个 IP 地址
2. 有回应 → 说明已被占用,换一个地址
3. 无回应 → 地址可用,放心分配
多台服务器都这样做 → 可以安全地使用重叠地址范围
DHCP 与 BOOTP 的互操作
DHCP 服务器如何识别对方是 BOOTP 还是 DHCP 客户端?
→ 看消息中是否有 DHCP Message Type(选项53):
- 有:是 DHCP 客户端
- 没有:是 BOOTP 客户端
| 组合 | 处理方式 |
|---|---|
| DHCP 服务器 + BOOTP 客户端 | 服务器用手动或自动分配(不能动态分配),不发送 DHCP 专属选项 |
| DHCP 客户端 + BOOTP 服务器 | 客户端把 BOOTP 回复当作无限期租约处理 |
DHCP 安全问题
DHCP 协议本身没有任何安全机制,主要威胁:
| 威胁 | 说明 | 应对 |
|---|---|---|
| 流氓 DHCP 服务器 | 攻击者架设假服务器,分配错误配置(如错误网关) | 物理网络访问控制、802.1X 认证 |
| 流氓 DHCP 客户端 | 伪造大量 DHCP 请求耗尽地址池(DHCP 饥饿攻击) | 交换机端口安全(DHCP Snooping) |
RFC 3118 定义了 DHCP 认证选项,但由于兼容性问题,实际部署很少。
第七章:DHCPv6——为 IPv6 设计的 DHCP
为什么需要 DHCPv6?
IPv6 地址从 32 位变成了 128 位,地址格式、分配方式都完全不同,需要重写 DHCP。
IPv6 的两种自动配置方式
IPv6 主机配置方式:
├── 无状态自动配置(Stateless)
│ - 主机自己计算地址(基于接口 MAC 地址 + 路由器前缀)
│ - 不需要服务器
│ - RFC 2462 定义
│
└── 有状态自动配置(Stateful)
- 由 DHCPv6 服务器分配地址和参数
- 和 DHCPv4 类似,但协议全部重写
DHCPv6 的主要变化
| 特性 | DHCPv4 | DHCPv6 |
|---|---|---|
| 基础 | 基于 BOOTP | 完全重写 |
| 传输 | UDP 67/68 | UDP 546/547(新端口) |
| 发现机制 | 广播 | 链路本地多播 |
| 消息格式 | BOOTP 格式扩展 | 全新格式 |
| 兼容性 | 兼容 BOOTP | 不兼容 DHCPv4 |
DHCPv6 基本消息交换
四步交换(获取新地址):
客户端 → 多播 Solicit(寻找服务器)
↓
服务器 → Advertise(我可以服务你)
↓
客户端 → Request(我选择你,确认地址)
↓
服务器 → Reply(确认,给你地址和参数)
两步交换(只获取参数,已有地址):
客户端 → 多播 Information-Request(只要参数)
↓
服务器 → Reply(给你参数)
第八章:协议演进总结
三代主机配置协议的对比:
RARP(1984年)
- 功能:只能提供 IP 地址
- 依赖:同网络内必须有 RARP 服务器
- 问题:不能跨网络,功能太少
↓ 升级
BOOTP(1985年)
- 功能:IP 地址 + 更多参数 + 启动文件名
- 依赖:UDP(高层协议,可跨网络)
- 改进:中继代理支持跨网络
- 问题:只支持静态地址映射
↓ 升级
DHCP(1993年)
- 功能:动态 IP 分配 + 完整配置参数
- 依赖:基于 BOOTP,向后兼容
- 改进:租约机制、地址池管理、APIPA
- 现状:现代网络的标准配置协议
关键数字速记:
T 1 = 50 % × 租约时长 T1 = 50\% \times \text{租约时长} T1=50%×租约时长
T 2 = 87.5 % = 7 8 × 租约时长 T2 = 87.5\% = \frac{7}{8} \times \text{租约时长} T2=87.5%=87×租约时长
BOOTP 服务器端口 = 67 , 客户端端口 = 68 \text{BOOTP 服务器端口} = 67, \quad \text{客户端端口} = 68 BOOTP 服务器端口=67,客户端端口=68
APIPA 地址范围 = 169.254.0.1 ∼ 169.254.255.254 \text{APIPA 地址范围} = 169.254.0.1 \sim 169.254.255.254 APIPA 地址范围=169.254.0.1∼169.254.255.254
DHCP 魔法 Cookie = 99.130.83.99 \text{DHCP 魔法 Cookie} = 99.130.83.99 DHCP 魔法 Cookie=99.130.83.99
TCP/IP 网络管理框架与 SNMP 协议详解
目录
- 为什么需要网络管理?
- SNMP 是什么?名字的两层含义
- SNMP 的整体架构:谁管谁?
- SNMP 框架的四大组件
- 管理信息的描述:SMI 和 MIB
- MIB 对象的命名体系
- SNMP 协议操作详解
- SNMP 消息格式详解
- SNMP 的安全问题
- SNMP 版本演化史
- RMON:远程网络监控
1. 为什么需要网络管理?
形象比喻
想象一个棒球裁判——他做得越好,你越感觉不到他的存在。网络管理员也一样:网络顺畅运行时,用户忘了管理员的存在;一出问题,所有人都找他。
小网络 vs 大网络的管理方式
以前网络很小,管理员可以:
- 直接走到那台电脑旁边操作
- 用简单的底层链路协议检查设备
但现代网络: - 设备遍布全球各地
- 使用不同的底层技术(光纤、WiFi、以太网……)
- 唯一的共同点:都跑 TCP/IP 协议
所以,利用 TCP/IP 网络本身来传递"管理信息",是最合理的方案。这就是 SNMP 框架诞生的背景。
SNMP 的历史起源
1980 年代,出现了三个竞争者:
| 方案 | 全称 | RFC |
|---|---|---|
| HEMS/HEMP | 高层实体管理系统/协议 | RFC 1021~1024 |
| SGMP | 简单网关监控协议 | RFC 1028 |
| CMIP | 公共管理信息协议(OSI 体系) | — |
1988 年,IETF 发布 RFC 1052,决定以 SGMP 为基础,开发新标准——SNMP(简单网络管理协议)。
2. SNMP 是什么?名字的两层含义
"SNMP"这个词有两层意思,经常让人混淆:
SNMP(广义)= 互联网标准管理框架(Internet Standard Management Framework)
包含:SMI + MIB + SNMP协议 + 安全管理
SNMP(狭义)= 具体的通信协议
只负责:在设备之间传递管理信息
本文用"SNMP 框架"指广义,用"SNMP 协议"指狭义。
"Simple"为什么说简单?
SNMP 跟更复杂的协议(比如 CMIP)相比才算"简单"。它的设计目标是:
- 提供一种通用方式描述任意设备的管理信息
- 将"定义信息"和"传输信息"解耦,互不干扰
- 协议操作本身只有几个,容易理解
- 厂商实现起来相对简单
3. SNMP 的整体架构:谁管谁?
两类设备
SNMP 网络中有两种角色:
被管节点 (Managed Node)
- 就是普通的网络设备(路由器、交换机、服务器……)
- 安装了 SNMP 代理软件
- 被动响应管理站的请求
网络管理站 (NMS, Network Management Station)
- 专门运行管理软件的设备
- 主动向被管节点发请求、收数据
- 是整个管理系统的"大脑"
每台设备上的软件组件
被管节点上运行:
SNMP 代理 (Agent)
- 实现 SNMP 协议
- 响应 NMS 的请求
- 主动发送 Trap 通知(出问题时)
MIB(管理信息库)
- 存储该设备所有可管理的变量
- 比如:IP 地址、接口状态、错误计数……
NMS 上运行:
SNMP 管理器 (Manager)
- 实现 SNMP 协议
- 向各代理发请求,收集信息
SNMP 应用程序
- 提供图形界面或报告
- 让人类管理员能看懂数据
架构示意图
+-----------------------+
| NMS |
| +----------------+ |
| | SNMP 管理器 | |
| +----------------+ |
| | SNMP 应用程序 | |
| +----------------+ |
+-----------+-----------+
|
SNMP 消息(通过 TCP/IP 网络传输)
________________________|________________________
| | |
+----------+--------+ +----------+--------+ +----------+--------+
| 被管节点 A | | 被管节点 B | | 被管节点 C |
| +-------------+ | | +-------------+ | | +-------------+ |
| | SNMP 代理 | | | | SNMP 代理 | | | | SNMP 代理 | |
| +-------------+ | | +-------------+ | | +-------------+ |
| | MIB | | | | MIB | | | | MIB | |
| +-------------+ | | +-------------+ | | +-------------+ |
+-------------------+ +-------------------+ +-------------------+
4. SNMP 框架的四大组件
组件关系
把三个核心组件的关系理解成三层抽象:
第三层:SNMP 协议
负责"怎么传"——收发消息、请求响应
第二层:MIB
负责"传什么"——定义设备有哪些可管理的变量
第一层:SMI
负责"怎么定义"——规定 MIB 中的变量用什么格式描述
5. 管理信息的描述:SMI 和 MIB
5.1 信息导向 vs 命令导向
传统管理协议是"命令导向"的,比如:
- 发命令:
读取设备运行小时数 - 发命令:
切换到测试模式
问题: 每种设备需要不同的命令集,新设备一出来就要改协议,维护成本极高。
SNMP 改为"信息导向":
不再发命令,而是读写"变量"
读操作:NMS 读取 MIB 变量 "sysUpTime"(已运行时间)
写操作:NMS 修改 MIB 变量 "currentMode" 的值为 "test"
这样,协议本身只需要"读"和"写"两种操作,不关心数据的具体含义——协议和数据完全解耦。
5.2 MIB 对象的属性
每个 MIB 对象(变量)都有以下属性:
| 属性 | SMIv1 名称 | SMIv2 名称 | 说明 |
|---|---|---|---|
| 名称 | Object Name | Object Name | 文本名(对象描述符)+ 数字 OID |
| 语法 | Syntax | Syntax | 数据类型(整数、字符串等) |
| 访问权限 | Access | Max-Access | 只读/读写/只写/不可访问 |
| 状态 | Status | Status | mandatory/optional/obsolete 等 |
| 描述 | Definition | Description | 文字说明 |
SMIv2 的访问权限层级(从高到低):
read-create 可读、可写、可创建(最高权限)
read-write 可读可写
read-only 只读
accessible-for-notify 只用于 Trap 通知
not-accessible 不可访问(特殊用途)
5.3 SMI 支持的数据类型
| 数据类型 | 说明 | SMIv1 | SMIv2 |
|---|---|---|---|
| Integer/Integer32 | 32位有符号整数,范围 − 2 31 -2^{31} −231 到 2 31 − 1 2^{31}-1 231−1 | 有 | 有 |
| Octet String | 可变长度字节串(文本或二进制) | 有 | 有 |
| Null | 空值 | 有 | 无 |
| Bits | 位枚举(一组位标志) | 无 | 有 |
| Unsigned32 | 32位无符号整数,范围 0 0 0 到 4294967295 4294967295 4294967295 | 无 | 有 |
| IpAddress | IP 地址(4字节) | 有 | 有 |
| Counter/Counter32 | 计数器,从 0 0 0 增到 4294967295 4294967295 4294967295 后归零 | 有 | 有 |
| Gauge/Gauge32 | 可升可降的量值,类似仪表盘 | 有 | 有 |
| TimeTicks | 从某时刻起的百分之一秒数(时间戳) | 有 | 有 |
| Opaque | 不透明数据,原样传输不解析 | 有 | 有 |
| Counter64 | 64位计数器,最大值 2 64 − 1 2^{64}-1 264−1 | 无 | 有 |
5.4 MIB 对象定义示例
下面是 RFC 3418 中的一个真实 MIB 对象定义(SMIv2 格式):
-- 对象名(描述符)
sysLocation OBJECT-TYPE
-- 数据类型:显示字符串,长度 0 到 255
SYNTAX DisplayString (SIZE (0..255))
-- 最大访问权限:可读可写
MAX-ACCESS read-write
-- 状态:当前有效
STATUS current
-- 描述:该节点的物理位置,不知道则为空串
DESCRIPTION
"The physical location of this node
(e.g., 'telephone closet, 3rd floor').
If the location is unknown, the value is
the zero-length string."
-- 对象标识符:system 组下的第 6 个对象
::= { system 6 }
这表示:sysLocation 是 system 组下的第 6 个对象,即 OID 为 1.3.6.1.2.1.1.6。
6. MIB 对象的命名体系
6.1 两种名称
每个 MIB 对象有两个名字:
文本名(对象描述符):sysLocation
- 人类友好,便于阅读和记忆
数字名(对象标识符 OID):1.3.6.1.2.1.1.6
- 机器友好,全球唯一,层级结构
6.2 全球对象树
SNMP 使用一棵全球统一的层级树来组织所有对象,由 ISO 和 ITU 共同维护:
6.3 关键路径记忆
所有标准 MIB 对象都在: 1.3.6.1.2.1
iso.org.dod.internet.mgmt.mib-2
所有厂商私有对象都在: 1.3.6.1.4.1
iso.org.dod.internet.private.enterprise
举个例子:
- 思科(Cisco)的所有私有对象都以
1.3.6.1.4.1.9开头(9 是思科的企业编号)
6.4 标准 MIB 对象组
mib-2(1.3.6.1.2.1)下的标准对象组:
| 组名 | 代码 | 编号 | 完整 OID | 用途 |
|---|---|---|---|---|
| system | sys | 1 | 1.3.6.1.2.1.1 | 通用设备信息 |
| interfaces | if | 2 | 1.3.6.1.2.1.2 | 网络接口信息 |
| at | at | 3 | 1.3.6.1.2.1.3 | 地址转换(已废弃) |
| ip | ip | 4 | 1.3.6.1.2.1.4 | IP 层信息 |
| icmp | icmp | 5 | 1.3.6.1.2.1.5 | ICMP 统计 |
| tcp | tcp | 6 | 1.3.6.1.2.1.6 | TCP 统计 |
| udp | udp | 7 | 1.3.6.1.2.1.7 | UDP 统计 |
| egp | egp | 8 | 1.3.6.1.2.1.8 | EGP(已过时) |
| snmp | snmp | 11 | 1.3.6.1.2.1.11 | SNMP 自身管理 |
OID 递归定义示例:
mib-2 = 1.3.6.1.2.1
system = { mib-2 1 } => 1.3.6.1.2.1.1
sysUpTime = { system 3 } => 1.3.6.1.2.1.1.3
sysLocation = { system 6 } => 1.3.6.1.2.1.1.6
7. SNMP 协议操作详解
7.1 两种通信模式
SNMP 使用两种方式通信:
轮询(Poll-Driven)
- NMS 主动向 Agent 请求数据
- 类比:每天去信箱取信
- 适用于:定期收集统计信息
中断(Interrupt-Driven)
- Agent 主动向 NMS 发送通知
- 类比:电话铃响了你才知道有事
- 适用于:紧急事件(链路断了、认证失败……)
7.2 PDU(协议数据单元)分类
SNMP 消息叫做 PDU(Protocol Data Unit),按功能分为四类:
| 类别 | 说明 | SNMPv1 PDU | SNMPv2/v3 PDU |
|---|---|---|---|
| Read(读) | 轮询读取信息 | GetRequest, GetNextRequest | GetRequest, GetNextRequest, GetBulkRequest |
| Write(写) | 修改设备变量 | SetRequest | SetRequest |
| Response(响应) | 回复请求 | GetResponse | Response |
| Notification(通知) | 主动通知 NMS | Trap | Trapv2, InformRequest |
7.3 读操作:GetRequest / Response
流程:
NMS Agent
| |
| 1. 创建 GetRequest-PDU |
| (包含要查询的 MIB 对象名称) |
|-------------------------------------->|
| | 2. 收到请求
| | 3. 查找 MIB 变量值
| | 4. 创建 Response-PDU
|<--------------------------------------|
| 5. 处理返回的 MIB 对象值 |
| |
7.4 表格遍历:GetNextRequest 和 GetBulkRequest
设备的某些信息是"表格"形式,例如一台路由器有多个 IP 地址,存在 ipAddrTable 中。
SNMPv1 的解决方案:GetNextRequest
NMS 发送:GetNextRequest(ipAddrTable[1])
Agent 返回:ipAddrTable[2] 的值
NMS 发送:GetNextRequest(ipAddrTable[2])
Agent 返回:ipAddrTable[3] 的值
...(直到返回表格外的对象,说明遍历完毕)
缺点: 每次只取一条,效率低,产生大量来回消息。
SNMPv2 的改进:GetBulkRequest
一次请求,批量返回多行:
// GetBulkRequest 结构示意
struct GetBulkRequest {
int pdu_type; // 类型 = 5
int request_id; // 请求 ID
int non_repeaters; // 普通变量的数量(表格前面的那些)
int max_repetitions; // 表格要返回几行
VarBindList var_binds; // 变量列表
};
// 例如:查 4 个普通变量 + 表格的前 3 行
// non_repeaters = 4, max_repetitions = 3
7.5 写操作:SetRequest
NMS 通过 SetRequest 修改 Agent 上的 MIB 变量,从而控制设备行为:
NMS Agent
| |
| SetRequest-PDU |
| (变量名 + 新值) |
|-------------------------------------->|
| | 验证:
| | - 变量名是否存在?
| | - 是否有写权限?
| | - 值的类型/大小是否合法?
| | - 安全检查通过?
| | 执行修改
|<--------------------------------------|
| Response-PDU(成功或错误码) |
7.6 通知操作:Trap / InformRequest
Trap(陷阱):
Agent 检测到重要事件(链路故障、重启、认证失败……)时,主动发给 NMS,不需要确认:
Agent NMS
| |
| Trapv2-PDU(无需等待请求) |
|-------------------------------------->|
| | (NMS 不需要回复)
InformRequest(通知请求):
用于 NMS 之间互相传递信息,需要确认:
NMS-1 NMS-2
| |
| InformRequest-PDU |
| (转发收到的 Trap 信息) |
|-------------------------------------->|
|<--------------------------------------|
| Response-PDU(确认收到) |
8. SNMP 消息格式详解
8.1 消息结构概览
SNMP 使用 ASN.1(抽象语法标记1)定义消息格式——和定义 MIB 对象用的语言一样。
每条 SNMP 消息 = 消息头(Message Header) + 消息体(PDU):
+------------------+---------------------------+
| 消息头 | PDU |
| (版本/安全信息) | (控制字段 + 变量绑定列表) |
+------------------+---------------------------+
变量绑定(Variable Binding):
PDU 里的每个变量都是一个"名值对":
+--------------------+--------------------+
| Object Name (OID) | Object Value (值) |
| 如:1.3.6.1.2.1.1.6| 如:"机房三楼" |
+--------------------+--------------------+
8.2 SNMPv1 消息格式
消息头极简,只有三个字段:
+----------+----------------+----------+
| Version | Community | PDU |
| (4字节) | (可变长字符串) | (可变长) |
+----------+----------------+----------+
值=0(v1) 类似密码,明文 实际协议体
SNMPv1 通用 PDU 格式(Get/GetNext/Set/GetResponse):
+----------+-----------+-------------+-------------+----------------+
| PDU Type | Request ID | Error Status | Error Index | Variable Binds |
| (4字节) | (4字节) | (4字节) | (4字节) | (可变长) |
+----------+-----------+-------------+-------------+----------------+
PDU Type 枚举值:
0 = GetRequest-PDU
1 = GetNextRequest-PDU
2 = GetNextRequest-PDU(注:原文如此,2实为GetResponse)
3 = SetRequest-PDU
4 = Trap-PDU(特殊格式)
Error Status 错误码:
| 值 | 代码 | 含义 |
|---|---|---|
| 0 | noError | 无错误 |
| 1 | tooBig | 响应太大无法传输 |
| 2 | noSuchName | 对象名不存在 |
| 3 | badValue | 值的类型或大小不对 |
| 4 | readOnly | 试图写只读变量 |
| 5 | genErr | 其他通用错误 |
SNMPv1 Trap-PDU(格式特殊):
+----------+------------+-----------+---------+-----------+----------+----------------+
| PDU Type | Enterprise | Agent Addr| Generic | Specific | TimeStamp| Variable Binds |
| (4字节) | (可变长OID)| (4字节IP) | Trap码 | Trap码 | (4字节) | (可变长) |
+----------+------------+-----------+---------+-----------+----------+----------------+
8.3 SNMPv2 消息格式
SNMPv2c(社区字符串版)消息头:
与 SNMPv1 基本相同,只是版本号变为 1(没错,是 1 不是 2,这是个历史遗留设计)。
SNMPv2 通用 PDU 格式(与 v1 类似但 PDU Type 更多):
PDU Type 取值:
0 = GetRequest-PDU
1 = GetNextRequest-PDU
2 = Response-PDU
3 = SetRequest-PDU
4 = 废弃(原 Trap)
5 = GetBulkRequest-PDU(独特格式,见下)
6 = InformRequest-PDU
7 = Trapv2-PDU
8 = Report-PDU
GetBulkRequest-PDU 特殊格式:
+----------+-----------+--------------+----------------+----------------+
| PDU Type | Request ID | NonRepeaters | MaxRepetitions | Variable Binds |
| (4字节) | (4字节) | (4字节) | (4字节) | (可变长) |
+----------+-----------+--------------+----------------+----------------+
8.4 SNMPv3 消息格式
SNMPv3 在消息头里增加了完整的安全字段,结构更复杂:
+-------------+---------+------------+----------+----------------+------------+
| MsgVersion | MsgID | MsgMaxSize | MsgFlags | MsgSecurityModel| MsgSecurity|
| (4字节) | (4字节) | (4字节) | (1字节) | (4字节) | Parameters |
+-------------+---------+------------+----------+----------------+------------+
|
+-------------------+
| Scoped PDU |
| (上下文ID + PDU) |
+-------------------+
MsgFlags(1字节)位结构:
位 7~3 位 2 位 1 位 0
+---------+-----------+-------------+----------+
| 保留(5位)| Reportable | Priv Flag | Auth Flag|
| 未使用 | 需回报告 | =1 启用加密 | =1 启用认证|
+---------+-----------+-------------+----------+
传输层:
SNMP 跑在 UDP 之上(而非 TCP),使用两个端口:
UDP 端口 161:普通 SNMP 消息(Get/Set/Response)
UDP 端口 162:Trap 消息
选择 UDP 而非 TCP 的理由:
- SNMP 的请求/响应模式简单,不需要 TCP 的连接管理开销
- UDP 头部更短,处理更快
但 UDP 的问题: - 消息可能丢失 → NMS 需要设置超时,超时重发
- 消息大小有限 → GetBulkRequest 的 MaxRepetitions 要谨慎设置,避免响应超出 UDP 上限
9. SNMP 的安全问题
9.1 SNMPv1 的安全有多弱?
SNMPv1 安全机制极其简单,只有一个"社区字符串(Community String)":
消息头里带一个字段,就像密码:
- 发送方在消息里附上社区字符串(明文!)
- 接收方比对字符串,不匹配则拒绝
问题:
- 明文传输——网络抓包就能看到密码
- 一旦密码泄露,任何人都能控制你的设备
- 就像把钥匙用透明袋子装着——能防君子,不防小人
9.2 SNMPv2/v3 的安全模型
| 模型 | 用于 | 核心思路 |
|---|---|---|
| 基于 Party 的安全(PSM) | SNMPv2p | 定义"通信方",指定认证和加密协议 |
| 基于用户的安全(USM) | SNMPv2u, SNMPv3 | 基于用户身份,支持多种认证/加密算法 |
| 基于视图的访问控制(VACM) | SNMPv3 | 细粒度控制:哪个用户能访问哪些 MIB 对象 |
VACM 的概念:
视图(View)= 一组可访问的 MIB 对象
用户组(Group)= 一批用户
上下文(Context)= 特定的管理信息范围
访问控制 = 哪个用户组 + 在什么上下文 + 能访问哪个视图
10. SNMP 版本演化史
版本对比
| 特性 | SNMPv1 | SNMPv2c | SNMPv3 |
|---|---|---|---|
| 安全机制 | 社区字符串(明文) | 社区字符串(明文) | USM(加密+认证) |
| 访问控制 | 无 | 无 | VACM |
| GetBulk | 无 | 有 | 有 |
| InformRequest | 无 | 有 | 有 |
| 当前状态 | 仍广泛使用 | 部分使用 | 推荐使用 |
SNMPv2 分裂的教训:
在争论安全方案时,各方无法达成共识,导致出现 v2p、v2c、v2u、v2* 等多个互不兼容的版本。大多数用户选择"等等看",继续用 SNMPv1。这正说明了:标准的统一性比完美性更重要。
11. RMON:远程网络监控
11.1 RMON 是什么?
RMON 常被称为"协议",但准确地说:
RMON 不是独立协议,它是 SNMP 的一个 MIB 模块,专门描述网络监控探针的管理对象。
它的 OID 是1.3.6.1.2.1.16(mib-2 下的第 16 组)。
11.2 为什么需要 RMON?
普通设备(路由器、主机)的 SNMP Agent 收集的信息有限——它们的主业是路由/主机工作,不能全力搞监控。
RMON 针对专用网络探针(网络分析仪)定义了一套更丰富的 MIB 对象。
11.3 RMON 的 9 个子组
rmon (1.3.6.1.2.1.16)
├── statistics (16.1) - 网络流量统计(包大小分布、广播数、错误计数……)
├── history (16.2) - 历史采样数据(控制采样频率)
├── alarm (16.3) - 告警配置(超过阈值时触发事件)
├── hosts (16.4) - 各主机的流量统计
├── hostTopN (16.5) - 主机排行榜(如发包最多的前10台)
├── matrix (16.6) - 任意两主机之间的流量矩阵
├── filter (16.7) - 数据包过滤(捕获特定类型的包)
├── packetCapture(16.8) - 数据包捕获缓冲区
└── event (16.9) - 事件记录与 Trap 触发
11.4 RMON 的告警机制
管理员配置:
监控哪个变量?(如 etherStats 的碰撞计数)
多久采样一次?(如每 30 秒)
阈值是多少?(如超过 100 次/秒则告警)
触发后:
记录一条日志
发送 Trap 到 NMS
NMS 管理员收到告警,决定下一步处理
总结
整个 SNMP 体系可以用一句话概括:
用统一的方式(SMI)定义设备变量(MIB),
用标准协议(SNMP)远程读写这些变量,
让管理员通过 NMS 监控和控制网络中的所有设备。
核心设计哲学:信息导向而非命令导向
这使得 SNMP 协议本身保持极简(只有几种 PDU),
而通过 MIB 模块的扩展来适应无限多种设备类型,
真正做到了协议与数据的分离。
TCP/IP 应用层寻址与文件传输详解
URI、URL、URN 及文件/消息传输概述
目录
- 为什么需要应用层寻址?
- URI 总览:三个容易混淆的概念
- URL 详解:怎么找到资源
- URL 的完整语法结构
- 常见 URL 方案(Scheme)
- 相对 URL 与绝对 URL
- URL 的长度与复杂性问题
- URL 混淆与欺骗手段
- URN 详解:用名字而非位置标识资源
- 文件与消息传输概述
1. 为什么需要应用层寻址?
已有地址,为何还需要更多?
学过网络的人可能会问:我们已经有了这么多地址,还需要更多吗?
第 2 层(数据链路层):MAC 地址 例如:AA:BB:CC:DD:EE:FF
第 3 层(网络层): IP 地址 例如:192.168.1.100
第 4 层(传输层): 端口号 + 套接字 例如::80, :443
有了 IP 地址和端口号,理论上可以访问网络上任何资源——但问题是找不到。
形象类比
假设你知道某本书在某栋楼里(IP 地址),也知道去哪个房间(端口号),但楼里有 100 万本书,你怎么知道要哪一本、在哪个书架、第几排、第几本?
应用层寻址就是解决这个"最后一公里"定位问题的。它服务的是人类,不是计算机——就像 DNS 让你用 www.google.com 代替 142.250.80.46 一样。
URI 的诞生
TCP/IP 定义了一套 统一资源标识符(Uniform Resource Identifier,URI) 系统,让每个文件、对象、程序都可以被唯一标记和访问。
URI 最初随万维网(WWW)诞生,现在也用于其他很多协议。
2. URI 总览:三个容易混淆的概念
三者的区别
| 概念 | 全称 | 作用 | 类比 |
|---|---|---|---|
| URI | Uniform Resource Identifier | 统称,包含 URL 和 URN | 身份标识的总概念 |
| URL | Uniform Resource Locator | 指明访问方法 + 位置 | 给你详细行车路线 |
| URN | Uniform Resource Name | 只给名字,不管位置 | 书的 ISBN 编号 |
生动类比
URL 的方式:
“坐火车到上海,转地铁 2 号线,到人民广场站 B 出口,右转走 100 米,进红色大楼 3 楼 302 室,书架最下层右数第 3 本”
URN 的方式:
“ISBN: 978-7-115-54558-0”(这本书永远是这个号,不管它在哪)
URL 实用,URN 稳定——这就是为什么 URL 几乎统治了互联网,而 URN 虽然概念更优雅,却难以推广。
3. URL 详解:怎么找到资源
URL 的两大核心信息
URL 必须包含两样东西:
- 访问方法(Scheme):用什么协议去拿?(HTTP?FTP?)
- 资源位置(Location):在哪台服务器的哪个路径下?
URL 为什么强大?
用一个字符串就能替代完整的操作流程。比如访问 FTP 文件,传统方式需要:
1. 打开 FTP 客户端
2. 连接服务器 ftp.example.com
3. 登录(用户名/密码)
4. 切换目录:cd /drivers/
5. 下载文件:get widgetdriver.zip
用 URL 一步搞定:
ftp://ftp.example.com/drivers/widgetdriver.zip
URI 相关标准
| RFC 编号 | 发布年份 | 内容 |
|---|---|---|
| RFC 1630 | 1994 | URI 在 WWW 中的使用(综述) |
| RFC 1737 | 1994 | URN 的功能需求 |
| RFC 1738 | 1994 | URL 详细规范 |
| RFC 1808 | — | 相对 URL 的定义与使用 |
| RFC 2141 | — | URN 语法 |
| RFC 2396 | 1998 | URI 通用语法(权威标准) |
4. URL 的完整语法结构
最通用的 URL 格式
<scheme>://<user>:<password>@<host>:<port>/<url-path>;<params>?<query>#<fragment>
各部分说明
http:// john:secret123 @ www.mysite.org : 8080 /cgi-bin/photos.php ;mode=full ?page=wedding #Reception07
| | | | | | | |
scheme 用户名:密码 主机名 端口 路径 参数 查询字符串 片段锚点
| 元素 | 是否必须 | 说明 | 示例 |
|---|---|---|---|
scheme |
必须 | 协议/访问方法 | http, ftp, mailto |
user:password |
可选 | 登录认证,很少用 | admin:123 |
host |
通常必须 | DNS 名称或 IP 地址 | www.example.com |
port |
可选 | 省略则用默认端口 | :8080(HTTP 默认 80) |
url-path |
通常需要 | 目录 + 文件名路径 | /docs/readme.txt |
params |
可选 | 方案特定参数 | ;type=i |
query |
可选 | 传给服务器的查询 | ?name=Alice&age=30 |
fragment |
可选 | 页面内锚点,不发给服务器 | #chapter2 |
注意:fragment 的特殊性
#fragment 严格来说不是 URL 的一部分——它不会发送给服务器,只由客户端(浏览器)自己使用,用来定位页面内某个位置(比如滚动到某个书签)。
URL 中的特殊字符编码
URL 只允许使用安全字符(字母、数字和少数符号)。其他字符必须用 %XX 的十六进制编码表示:
| 字符 | 编码 | 字符 | 编码 | 字符 | 编码 |
|---|---|---|---|---|---|
| 空格 | %20 |
# |
%23 |
% |
%25 |
/ |
%2F |
? |
%3F |
@ |
%40 |
: |
%3A |
& |
%26 |
= |
%3D |
< |
%3C |
> |
%3E |
{ |
%7B |
} |
%7D |
[ |
%5B |
] |
%5D |
\ |
%5C |
^ |
%5E |
~ |
%7E |
示例:
文件名 are you there? 编码后变成:
are%20you%20there%3F
文件名 {ABC Corp} budget; draft #3 编码后:
%7BABC%20Corp%7D%20budget%3B%20draft%20%233
建议: 文件命名时用下划线代替空格,避免编码:
原来:my file name.txt → URL: my%20file%20name.txt
改为:my_file_name.txt → URL: my_file_name.txt (干净多了)
5. 常见 URL 方案(Scheme)
HTTP(万维网)
http://<user>:<password>@<host>:<port>/<url-path>?<query>#<bookmark>
- 默认端口:80(HTTPS 为 443)
- 最常见的 URL 类型
?query常用于向服务器传参数
示例:
http://www.example.com/search?keyword=TCP&page=2#results
FTP(文件传输)
ftp://<user>:<password>@<host>:<port>/<url-path>;type=<typecode>
- 默认端口:21
- 省略用户名/密码 → 匿名访问
type=a表示 ASCII 文件,type=i表示二进制文件,type=d请求目录列表
示例:
ftp://ftp.example.com/drivers/widgetdriver.zip
等价于:登录 FTP → 进入 drivers 目录 → 下载 widgetdriver.zip
mailto(发送电子邮件)
mailto:<username>@<domainname>
示例:
mailto:support@example.com
这是特殊 URL——它指向的不是"文件",而是一个"动作"(发邮件给某人)。
news / nntp(新闻组)
news://<newsgroup-name>
news://<message-id>
nntp://<host>:<port>/<newsgroup-name>/<article-number>
news://使用本地默认新闻服务器,端口默认 119nntp://显式指定服务器
telnet(远程登录)
telnet://<user>:<password>@<host>:<port>
- 默认端口:23
- 省略密码 → 服务器提示输入
- 指向的是"服务"而非"文件"
file(本地文件)
file://<host>/<url-path> # 局域网文件
file:///<url-path> # 本机文件(三个斜杠)
Windows 路径的特殊处理:
Windows 路径:C:\WINDOWS\SYSTEM32\DRIVERS\ETC\HOSTS
URL 形式: file:///C|/WINDOWS/SYSTEM32/DRIVERS/ETC/HOSTS
注意:反斜杠→正斜杠,冒号→竖线符号|
6. 相对 URL 与绝对 URL
什么是绝对 URL?
包含访问资源所需全部信息的 URL,不依赖任何上下文:
http://www.longdomainnamesareirritating.com/photos/wedding/reception.jpg
什么是相对 URL?
省略了部分信息,需要依赖基准 URL(Base URL) 来补全:
photos/wedding/reception.jpg # 省略了 scheme 和 host
相对 URL 的三种基准来源(按优先级)
优先级 1:文档内显式声明的基准 URL(如 HTML 的 <base> 标签)
优先级 2:包含该文档的父实体的 URL(如 MIME 消息)
优先级 3:获取该文档时使用的 URL(最常见的默认方式)
实际解析规则
以下面这个基准 URL 为例:
http://site.net/dir1/subdir1/file1?query1#bookmark1
| 相对 URL | 解析结果 | 说明 |
|---|---|---|
#bookmark2 |
http://site.net/dir1/subdir1/file1?query1#bookmark2 |
只换书签(页内跳转) |
?query2 |
http://site.net/dir1/subdir1/file1?query2 |
换查询参数,书签被丢弃 |
file2 |
http://site.net/dir1/subdir1/file2 |
换文件名,查询和书签都丢弃 |
/file2 |
http://site.net/file2 |
单斜杠 = 从根目录开始 |
.. |
http://site.net/dir1/ |
两点 = 上一级目录 |
../file2 |
http://site.net/dir1/file2 |
上一级目录中的 file2 |
../subdir2/file2 |
http://site.net/dir1/subdir2/file2 |
上一级再进 subdir2 |
../../dir2/subdir2/file2 |
http://site.net/dir2/subdir2/file2 |
上两级再往下 |
//file2 |
http://file2 |
双斜杠替换主机名,慎用! |
ftp://othersite.net/x |
ftp://othersite.net/x |
有新 scheme → 视为绝对 URL |
相对 URL 的优势:可移植性
使用相对 URL,整个网站迁移域名时,不需要修改任何内部链接:
原域名:www.longdomainnamesareirritating.com
新域名:www.shortname.com
使用绝对URL:每个页面里的链接都要改 → 工程浩大
使用相对URL:什么都不用改 → 自动适配新域名
7. URL 的长度与复杂性问题
导致 URL 变长的常见原因
1. DNS 名称过长
不好:www.superautobodyshopandpizzaparlor.com
好: www.superauto.com
2. 文件名使用不安全字符
文件名:{ABC Corp} budget; draft #3; 3Q2024.htm
URL: %7BABC%20Corp%7D%20budget%3B%20draft%20%233%3B%203Q2024.htm
改进后文件名:ABC_budget_draft3_3Q2024.htm
改进后URL: ABC_budget_draft3_3Q2024.htm (清晰多了)
3. 查询参数字符串
http://groups.google.com/groups?q=%22potato+salad%22&hl=en&lr=&ie=
UTF-8&safe=off&selm=B826FB57.89C0%25sbrooks%40ev1.net&rnum=2
URL 换行与分隔问题
大多数程序每行只显示 78~80 个字符。URL 如果超过这个长度,就会换行,容易导致:
- 用户复制时遗漏末尾部分
- 程序不能正确识别 URL 的结束位置
解决方案:用尖括号包裹 URL
原始写法:
请访问 http://www.networkingistoodarnedcomplicated.com 获取更多信息。
推荐写法(用尖括号明确边界):
请访问 <http://www.networkingistoodarnedcomplicated.com> 获取更多信息。
也可加 URL: 标签:
<URL:http://www.networkingistoodarnedcomplicated.com>
另一个方案:使用短链接服务(如 TinyURL),将长 URL 转为短 URL。
8. URL 混淆与欺骗手段
URL 语法的灵活性本是优点,却也被人利用来制造欺骗性链接。以下是常见手法(用于了解和防范,不用于作恶):
手法 1:添加大量无意义查询字符串
http://malicious.com/q=asdkfjhaksjdhfkajsdhfkjasdhfkjsdhfkjashdfkjasdhfkjh...
让人眼花缭乱,看不清楚真实目标。
手法 2:直接用 IP 地址代替域名
http://209.68.14.80 ← 和 http://www.example.com 是同一个网站
大多数人不知道可以直接用 IP 地址,所以看不出是什么网站。
手法 3:伪装成 IP 地址的数字域名
http://42.12.205.114.com ← 看起来像 IP 地址,其实是域名
手法 4:伪造认证信息
HTTP URL 支持 用户名:密码@主机 格式,可以用来伪装:
http://www.cnn.com@www.malicioussite.com
↑
这才是真正要访问的地址
前面的 www.cnn.com 只是"用户名",会被忽略
手法 5:用 % 编码混淆域名
http://%57%57%57.%50%43%47%55%49%44%45.%43%4F%4D
W W W . P C G U I D E . C O M
解码后就是 http://www.PCGUIDE.COM,但一般人根本认不出来。
手法 6:IP 地址的各种数值表示
一个 IP 地址 209.68.14.80 可以用多种等价形式表示:
| 表示形式 | 示例 | 说明 |
|---|---|---|
| 标准点分十进制 | http://209.68.14.80/ |
正常形式 |
| 点分八进制 | http://0321.0104.016.0120/ |
每段前加 0 |
| 点分十六进制 | http://0xD1.0x44.0x0E.0x50/ |
每段前加 0x |
| 单一十进制整数 | http://3510898256/ |
32 位数字 |
| 单一十六进制 | http://0xd1440e50/ |
32 位十六进制 |
| 单一八进制 | http://032121007120/ |
32 位八进制 |
IP 地址转为 32 位整数的计算方式:
N = A × 2 24 + B × 2 16 + C × 2 8 + D N = A \times 2^{24} + B \times 2^{16} + C \times 2^{8} + D N=A×224+B×216+C×28+D
对于 209.68.14.80:
N = 209 × 16777216 + 68 × 65536 + 14 × 256 + 80 = 3510898256 N = 209 \times 16777216 + 68 \times 65536 + 14 \times 256 + 80 = 3510898256 N=209×16777216+68×65536+14×256+80=3510898256
组合攻击示例
原始:http://www.example.com
↓ 转为 IP
http://209.68.14.80
↓ 加伪装认证信息
http://www.cnn.com@209.68.14.80
↓ IP 转单一十进制数
http://www.cnn.com@3510898256
↓ IP 转带大量前导零的八进制
http://www.cnn.com@0000000000000321.00000000104.00000000000016.00000120
看起来像 CNN,实际上访问的是完全不同的网站。
9. URN 详解:用名字而非位置标识资源
URL 的根本缺陷
URL 把"资源"和"位置"绑定在了一起,一旦资源移动:
原来:http://old-server.com/papers/myresearch.pdf
移动后:http://new-server.com/archive/2024/myresearch.pdf
所有指向原来 URL 的链接全部失效,出现经典的:
HTTP 404 - NOT FOUND
类比:如果用"44 号格伦代尔新月路,悉尼"来标识一个人,那这个人一搬家,描述就失效了。
URN 的设计思路
URN 用永久名字而非位置来标识资源,就像:
- 书有 ISBN,全球唯一,无论这本书在哪个书架
- 人有身份证号,无论住在哪里
URN 的语法
URN:<namespace-ID>:<resource-identifier>
示例:
URN:isbn:0-679-73669-7 ← 标识某本书(用 ISBN)
URN:ietf:rfc:2396 ← 标识某个 RFC 文档
URN 的命名空间(Namespace)
因为不同类型的资源用不同编号系统,URN 用命名空间来区分:
URN:isbn:0-679-73669-7 ← isbn 命名空间(书籍)
URN:tel:+1-555-1234567 ← tel 命名空间(电话号码)
注意:4167819249 这串数字既可能是 ISBN 也可能是电话号码
命名空间 ID 确保不会混淆
URN 为什么没有普及?
URN 面临的核心问题:知道名字,但不知道在哪里。
给你一个 URN:isbn:0-679-73669-7
→ 你知道这是某本书
→ 但你在哪里下载/购买/阅读它?
→ URN 不告诉你!
要让 URN 实用,需要一套 URN 解析系统——类似 DNS 把域名转为 IP,把 URN 转为可访问的 URL。这套系统的设计极其复杂,至今仍在开发中(RFC 3401~3406 定义了 DDDS 动态委托发现系统)。
URL vs URN 对比
10. 文件与消息传输概述
什么是文件?
文件是计算机系统中最基本的信息单元:
- 一个独立的数据集合
- 存储在文件系统的目录/文件夹中
- 由字节序列组成
- 有文件属性(名称、大小、修改时间等)
有趣的是:文件传输不是互联网的应用,而是互联网被发明出来的原因之一——早期的 FTP 协议比 IPv4、TCP、UDP 都出现得早。
两大应用类别
通用文件传输
把文件当"黑盒"处理,不关心内容是什么,只管从 A 搬到 B:
FTP(File Transfer Protocol):功能完整,交互式,支持登录
TFTP(Trivial FTP):极简版,通常用于网络设备启动配置
消息传输应用
处理有特定格式的文件,这些文件本身就是为通信而生的:
| 应用 | 特点 | 类比 |
|---|---|---|
| 电子邮件(Email) | 一对一或一对少数,有明确收件人 | 普通信件 |
| 网络新闻(Usenet) | 一对多,所有人可见可回复 | 公告板/论坛 |
| 万维网(WWW) | 超文本,可嵌入图片/多媒体/链接,一文档链接另一文档 | 百科全书+图书馆 |
边界正在模糊
现代 TCP/IP 应用让文件传输和消息传输的界限越来越模糊:
Email → 可以附件传文件,可以包含 HTML 超文本
Web浏览器 → 可以访问 FTP 服务器下载文件
Web应用 → 可以实时收发消息(即时通讯、邮件客户端)
总结
应用层寻址体系:
URI(总称)
├── URL(最常用)= 访问方法 + 位置
│ 格式:scheme://user:pass@host:port/path;params?query#fragment
│ 特点:实用、直接,但资源移动后链接失效
│
└── URN(较新)= 永久名字
格式:URN:命名空间:资源标识符
特点:稳定不变,但需要解析系统才能找到资源
文件传输体系:
通用文件传输(FTP/TFTP)→ 不关心内容,只管搬运
消息传输(Email/Usenet/WWW)→ 处理有格式的通信文件
URL 是互联网上最成功的"应用层地址"方案,其简单、灵活、人类可读的特性使其无处不在——从网页链接到电视广告,从代码配置到命令行工具。理解 URL 的完整语法,不仅能帮助你更好地使用网络,也能让你识破那些试图利用 URL 复杂性进行欺骗的恶意手段。
TCP/IP 通用文件传输协议详解
FTP 与 TFTP 深度解析
目录
- 概述:为什么需要文件传输协议
- FTP 的历史与标准
- FTP 的整体架构模型
- FTP 连接建立与认证
- FTP 数据连接:主动模式与被动模式
- FTP 传输模式
- FTP 数据表示:类型、格式与结构
- FTP 命令详解
- FTP 回复码系统
- FTP 用户界面与用户命令
- FTP 会话示例分析
- TFTP:精简版文件传输协议
- TFTP 操作流程详解
- TFTP 选项协商
- TFTP 消息格式
- FTP 与 TFTP 综合对比
1. 概述:为什么需要文件传输协议
文件传输是网络的起点
有个让人惊讶的历史事实:文件传输协议的历史比 TCP 和 IP 还要早。网络的发明,很大程度上就是为了传输文件,而不是文件传输作为网络的应用出现的。
早期开发者把网络用途分为两类:
直接使用(Direct):远程登录到另一台计算机,就像在用本地机器
→ 演化为 Telnet 协议
间接使用(Indirect):从远程主机取回资源,在本地使用
→ 演化为 FTP 协议
FTP 与 TFTP 的关系
就像 TCP 和 UDP 的关系一样:
FTP ↔ TCP 功能全面、可靠、复杂、面向流
TFTP ↔ UDP 极度精简、快速、简单、面向数据报
2. FTP 的历史与标准
1971年 RFC 114 FTP 诞生(此时 TCP/IP 还不存在,用的是 NCP)
1972年 RFC 354 首次描述现代通信模型
1973年 RFC 542 协议形态已与现代非常接近
1980年 RFC 765 第一个基于 TCP/IP 的 FTP 标准
1985年 RFC 959 当前基础标准(沿用至今,超过 40 年!)
FTP 是 TCP/IP 应用层协议中寿命最长的之一,核心设计在 1985 年之后几乎没有变化。
3. FTP 的整体架构模型
3.1 两个核心概念:控制连接 vs 数据连接
FTP 最重要、最独特的设计是使用两条独立的 TCP 连接:
控制连接(Control Connection)
├── 端口:服务器端口 21
├── 生命周期:整个 FTP 会话期间持续存在
├── 用途:传输命令和回复
└── 不传输文件数据
数据连接(Data Connection)
├── 端口:动态分配
├── 生命周期:每次传输数据时临时建立,完成后关闭
├── 用途:传输文件或目录列表等实际数据
└── 每次传输都创建新连接
类比:控制连接像"电话里的谈判",数据连接像"快递公司派来的货车"——每次需要发货时派一辆,送完就走。
3.2 FTP 软件组件
| 组件 | 位置 | 职责 |
|---|---|---|
| 用户界面(User Interface) | 客户端 | 翻译用户友好命令为 FTP 协议命令 |
| 用户-PI(User-PI) | 客户端 | 管理控制连接,发送命令,接收回复 |
| 用户-DTP(User-DTP) | 客户端 | 管理数据连接,发送/接收文件数据 |
| 服务器-PI(Server-PI) | 服务器 | 监听端口 21,处理命令,发送回复 |
| 服务器-DTP(Server-DTP) | 服务器 | 管理数据连接,与本地文件系统交互 |
注意:客户端有 3 个组件(多一个用户界面),服务器只有 2 个组件。
3.3 代理 FTP(第三方传输)
FTP 标准还定义了一种"代理模式":用户在自己机器上操控,让两台服务器之间直接传文件。由于安全隐患较大,现实中几乎不用。
4. FTP 连接建立与认证
4.1 建立控制连接的过程
客户端(用户-PI) 服务器(服务器-PI)
| |
| TCP 连接请求(随机端口→端口21) |
|------------------------------------>|
| | 服务器在端口 21 持续监听
| 220 Service ready for new user |
|<------------------------------------|
| USER ixl(发送用户名) |
|------------------------------------>|
| 331 User ixl okay, need password |
|<------------------------------------|
| PASS xxxxxxxx(发送密码) |
|------------------------------------>|
| 230 Logged in.(登录成功) |
|<------------------------------------|
| 控制连接已建立 |
4.2 安全问题
FTP 的认证机制极其简单,用户名和密码以明文传输,任何中间设备都能截获。RFC 2228(FTP 安全扩展)提供了加密选项,但基础 FTP 本身不安全。
4.3 匿名 FTP(Anonymous FTP)
许多组织想向公众提供文件下载(如驱动程序),但不可能给每个访客创建账号。解决方案:匿名 FTP(RFC 1635):
用户名:anonymous 或 ftp
密码: 任意邮件地址(仅用于日志记录,不做验证)
权限限制:
- 只能访问特定公开目录
- 通常只读,不能删除/重命名
- 普通已认证用户权限更高
现在大多数组织改用网站提供下载,但早年(1980~1990 年代)匿名 FTP 是发布软件的主要方式。
5. FTP 数据连接:主动模式与被动模式
这是 FTP 中最容易让人困惑的部分,也是最重要的实践概念。
5.1 主动模式(Active / Normal)
谁发起数据连接:服务器
客户端(端口 1678→21,控制连接已建立)
|
| PORT 命令(告诉服务器:请连接我的 1742 端口)
|------------------------------------>|
| |
| 服务器主动从端口 20 连接客户端 1742
|<================================== | 数据连接
| |
| 数据传输(双向均可) |
关键点:
- 服务器使用固定端口 20 作为数据连接的源端口
- 客户端用 PORT 命令告诉服务器应连哪个端口
- 控制连接(1678)和数据连接(1742)通常用不同端口,避免 TCP 复用冲突
为什么建议用不同端口: TCP 规定连接关闭后,同一端口号需等待一段时间才能复用(避免延迟包混入新会话)。连续传多个文件时,若端口相同会产生延迟。
5.2 被动模式(Passive)
谁发起数据连接:客户端
客户端(端口 1678→21,控制连接已建立)
|
| PASV 命令(告诉服务器:你被动等,我来连你)
|------------------------------------>|
| 227 Entering Passive Mode (...,p1,p2)
| 服务器告知客户端:请连接我的 2223 端口
|<------------------------------------|
| | 服务器在 2223 监听
| 客户端从 1742 连接服务器 2223 |
|==================================> | 数据连接
| |
| 数据传输(双向均可) |
5.3 为什么被动模式更常用?
这与防火墙有关:
主动模式的问题:
服务器主动连接客户端 → 从客户端视角看,这是"入站连接"
现代防火墙默认拦截未主动请求的入站连接
→ 文件传输失败!
被动模式的解决:
客户端主动发起所有连接 → 从客户端视角看,都是"出站连接"
防火墙通常允许出站连接
→ 文件传输成功!
代价:把问题转移到服务器——服务器需要为被动模式开放一段端口范围(如 5000~6000),并在防火墙上允许这些端口的入站连接。这对管理员来说更可控,因为服务器数量远少于客户端数量。
5.4 违反分层原则的问题
PORT 和 PASV 命令在应用层消息中直接传递 IP 地址和端口号,这违反了 TCP/IP 的分层原则。这在使用 NAT(网络地址转换) 时会造成麻烦,因为 NAT 修改了 IP 地址,但 FTP 消息体里的 IP 地址不会自动更新,导致连接失败。需要专门的 NAT 穿越机制来处理。
6. FTP 传输模式
数据连接建立后,实际数据的发送方式有三种:
| 模式 | 机制 | 特点 | 使用率 |
|---|---|---|---|
| 流模式(Stream) | 直接推送字节流,靠关闭连接标志结束 | 最简单、最高效、无额外开销 | 最常用 |
| 块模式(Block) | 数据打包成块,每块有 3 字节头部 | 支持断点续传,可检测中断 | 较少用 |
| 压缩模式(Compressed) | 游程编码(Run-Length Encoding)压缩 | 减少数据量 | 几乎不用 |
为什么压缩模式没人用:
- 调制解调器已在硬件层做了压缩
- 大文件通常已经是 ZIP/RAR 等压缩格式
- 二次压缩效果微乎其微,甚至可能反而变大
游程编码(Run-Length Encoding)简单示意:
原始数据:AAAAABBBBCCC(12字节)
编码后:5A4B3C(6字节)——用"重复次数+字符"表示连续重复的字节,节省空间。
7. FTP 数据表示:类型、格式与结构
7.1 为什么不能把所有文件都当"字节流"?
不同操作系统对文本文件的换行符约定不同:
UNIX / Linux: LF (0x0A,\n)
旧版 Mac OS: CR (0x0D,\r)
Windows: CR+LF (0x0D 0x0A,\r\n)
如果把 Windows 文本文件直接复制到 UNIX,每行末尾会多出一个 \r,程序读取时会出现乱码。
7.2 FTP 的四种数据类型
| 类型 | 用途 | 传输方式 |
|---|---|---|
| ASCII | ASCII 文本文件 | 自动转换换行符到 CR+LF(通用格式),接收方再转回本地格式 |
| EBCDIC | IBM 大型机文本文件 | 类似 ASCII,但字符集不同 |
| Image(二进制) | 图片、压缩包、可执行文件等 | 原样传输,一字节不改 |
| Local | 特殊位宽的本地格式 | 按指定比特数解释 |
实际使用: 90% 的场合只用 ASCII 和 Image(Binary)两种。
7.3 ASCII 模式的转换流程
UNIX 系统 → 发送文本文件 → Windows 系统
发送方(UNIX):
每个 LF → 转为 CR+LF(NETASCII 通用格式)
传输中:
CR+LF(NETASCII 标准换行)
接收方(Windows):
CR+LF → 保持 CR+LF(Windows 本地格式)
结果:Windows 可以正常打开该文件
反向传输(Windows → UNIX):
发送方(Windows):CR+LF → CR+LF(不变,本来就是标准)
接收方(UNIX):CR+LF → LF(去掉 CR)
重要警告: 二进制文件(ZIP、JPG、EXE)必须用 Image 模式,绝对不能用 ASCII 模式!否则 FTP 会把文件里碰巧值为 0x0D 或 0x0A 的字节当成换行符处理,导致文件损坏。这是 FTP 传输中最常见的错误之一。
7.4 数据结构
| 结构 | 说明 | 使用情况 |
|---|---|---|
| 文件结构(File) | 连续字节流,无内部结构 | 默认,绝大多数场合 |
| 记录结构(Record) | 有序记录,每条有结束标记 | 某些文本文件场景 |
| 页结构(Page) | 索引数据页 | 历史遗留,几乎不用 |
8. FTP 命令详解
FTP 命令用 3~4 个字母的代码表示,分三组:
8.1 访问控制命令
| 命令 | 全称 | 说明 |
|---|---|---|
| USER | User name | 发送用户名 |
| PASS | Password | 发送密码 |
| ACCT | Account | 指定账户(多账户系统) |
| CWD | Change Working Directory | 切换工作目录 |
| CDUP | Change Directory UP | 切到上级目录 |
| SMNT | Structure Mount | 挂载文件系统 |
| REIN | Reinitialize | 重置会话(不断开连接) |
| QUIT | Logout | 登出并关闭控制连接 |
8.2 传输参数命令
| 命令 | 全称 | 说明 |
|---|---|---|
| PORT | Data Port | 告诉服务器客户端监听哪个端口(主动模式) |
| PASV | Passive | 请求被动模式,服务器等待客户端连接 |
| TYPE | Representation Type | 设置数据类型(ASCII/EBCDIC/Image/Local) |
| STRU | File Structure | 设置数据结构(File/Record/Page) |
| MODE | Transfer Mode | 设置传输模式(Stream/Block/Compressed) |
8.3 服务命令(最重要的一组)
| 命令 | 全称 | 说明 |
|---|---|---|
| RETR | Retrieve | 从服务器下载文件 |
| STOR | Store | 上传文件到服务器 |
| STOU | Store Unique | 上传并确保文件名唯一(防止覆盖) |
| APPE | Append | 上传并追加到已有文件 |
| ALLO | Allocate | 预留存储空间 |
| REST | Restart | 断点续传(仅 Block/Compressed 模式) |
| RNFR | Rename From | 重命名源文件名 |
| RNTO | Rename To | 重命名目标文件名 |
| ABOR | Abort | 中止当前传输 |
| DELE | Delete | 删除文件 |
| RMD | Remove Directory | 删除目录 |
| MKD | Make Directory | 创建目录 |
| PWD | Print Working Directory | 显示当前目录 |
| LIST | List | 列出目录内容(含详细信息) |
| NLST | Name List | 列出文件名(仅名称) |
| SYST | System | 查询服务器操作系统类型 |
| STAT | Status | 查询传输状态 |
| HELP | Help | 获取帮助 |
| NOOP | No Operation | 空操作(用来验证控制连接是否存活) |
9. FTP 回复码系统
9.1 三位数字编码体系
每个命令都会收到一个三位数字回复码 + 文字描述。数字部分由软件解析,文字部分供人阅读。
三位数字的含义:
第一位(x):总体结果
1xx → 正在进行(初步肯定,继续等待)
2xx → 成功完成
3xx → 暂停,等待更多信息
4xx → 临时错误(可重试)
5xx → 永久错误(不必重试)
第二位(y):功能分类
x0x → 语法相关
x1x → 信息查询
x2x → 连接相关
x3x → 认证和账户
x4x → 未定义
x5x → 文件系统
第三位(z):具体类型(每个分类内的细分)
9.2 解码示例
回复码 530:
5 3 0
↑ ↑ ↑
│ │ └── 具体错误类型 0
│ └───── 认证/账户相关(x3x)
└──────── 永久错误(5xx)
含义:Not logged in(登录失败,认证相关的永久错误)
9.3 常用回复码速查
| 代码 | 典型文字 | 含义 |
|---|---|---|
| 220 | Service ready for new user | 服务器就绪,等待登录 |
| 221 | Service closing control connection | 再见,控制连接关闭 |
| 227 | Entering Passive Mode (…) | 进入被动模式,含端口信息 |
| 230 | User logged in, proceed | 登录成功 |
| 250 | Requested file action okay | 文件操作成功 |
| 331 | User name okay, need password | 用户名已接受,请输密码 |
| 421 | Service not available | 服务不可用 |
| 425 | Can’t open data connection | 数据连接打不开 |
| 426 | Connection closed; transfer aborted | 连接断了,传输中止 |
| 500 | Syntax error | 命令语法错误 |
| 530 | Not logged in | 未登录(认证失败) |
| 550 | File unavailable | 文件不存在或无权访问 |
9.4 多行回复
当回复文字需要多行时,每行前缀规则:
230-Welcome user to FTP server. ← 代码+连字符,表示"还没结束"
230-You are user #17 of 100 allowed. ← 继续
230-Please see the file "faq.txt". ← 继续
230 Logged in. ← 代码+空格,表示"最后一行"
这种编码方式被后来的 SMTP(邮件)、NNTP(新闻组)、HTTP(Web)等协议借鉴。
10. FTP 用户界面与用户命令
用户命令 vs FTP 协议命令
用户不需要直接使用 RETR、STOR 等协议命令,用户界面提供了更友好的命令:
| 用户命令 | 对应 FTP 协议命令 | 说明 |
|---|---|---|
get 文件名 |
PASV + RETR 文件名 |
下载文件(自动处理被动模式) |
put 文件名 |
PASV + STOR 文件名 |
上传文件 |
mget *.txt |
多次 RETR |
批量下载 |
mput *.jpg |
多次 STOR |
批量上传 |
ls 或 dir |
PASV + LIST |
列目录 |
cd 目录 |
CWD 目录 |
切换目录 |
ascii |
TYPE A |
设置 ASCII 模式 |
binary |
TYPE I |
设置二进制模式 |
passive |
切换主/被动模式 | |
bye / quit / exit |
QUIT |
退出(三个是同义词) |
pwd |
PWD |
显示当前目录 |
delete 文件 |
DELE 文件 |
删除远程文件 |
rename 旧名 新名 |
RNFR + RNTO |
重命名 |
? 或 help |
本地帮助 | 显示可用命令列表 |
用户界面的三大好处:
- 更友好:
get比RETR好记 - 可定制:
binary和image都能设置二进制模式 - 自动化:
get会自动先发PASV再发RETR,用户不用知道这些细节
11. FTP 会话示例分析
下面是一个真实 FTP 会话(启用调试模式,可以看到底层协议命令):
用户命令 协议层交互 说明
─────────────────────────────────────────────────────────────────────
ftp -d pcguide.com (建立 TCP 控制连接) -d 开启调试模式
220 ftp199.pair.com ready 服务器就绪
ixl → USER ixl 发送用户名
← 331 User ixl okay, need password 需要密码
**** → PASS XXXX 发送密码(显示为****)
← 230 Logged in. 登录成功
→ SYST 自动查询系统类型
← 215 UNIX Type: L8 服务器是 UNIX
pwd → PWD 查当前目录
← 257 "/usr/home/ixl" is cwd 当前在家目录
cd ftptest → CWD ftptest 尝试进入 ftptest 目录
← 550 No such directory. 目录不存在!
cd ftpdemo → CWD ftpdemo 进入 ftpdemo 目录
← 250 ".../ftpdemo" is new cwd 成功
dir → PASV 请求被动模式
← 227 Entering Passive Mode (...) 服务器给出端口 193,224
→ LIST 请求目录列表
← 150 Data connection accepted... 数据连接建立
-rw-r--r-- 1 ixl 16 May 22 testfile.txt
← 226 Listing completed. 目录列表传输完毕
asc → TYPE A 设置 ASCII 模式
← 200 Type okay. 模式设置成功
get testfile.txt → PASV 再次请求被动模式
← 227 Entering Passive Mode (...) 新端口 193,226(不同端口!)
→ RETR testfile.txt 下载文件
← 150 ...transfer starting (16 bytes) 开始传输
← 226 Transfer completed. 传输完成
17 bytes received in 0.10 seconds
del testfile.txt → DELE testfile.txt 删除服务器上的文件
← 250 Deleted. 删除成功
cdup → CDUP 返回上级目录
← 250 "...ixl" is new cwd
rmdir ftpdemo → RMD ftpdemo 删除目录
← 250 Directory removed. 删除成功
quit → QUIT 退出
← 221 Goodbye. 再见
观察两次数据连接的端口变化:
dir时:服务器端 193,224 端口,客户端使用某端口get时:服务器端 193,226 端口,客户端使用另一端口
每次数据连接都用新端口,避免 TCP 复用延迟问题。
12. TFTP:精简版文件传输协议
12.1 为什么需要 TFTP?
考虑一种设备:无盘工作站(没有硬盘)。
启动过程(Bootstrapping):
第一步:通过 BOOTP/DHCP 获取 IP 地址(自己解决)
第二步:从网络服务器下载操作系统镜像 → 需要文件传输
问题:
FTP 需要完整的 TCP 实现
TCP 的代码量相当大
启动用的 ROM 芯片空间有限
→ FTP 太重,放不进 ROM!
解决方案:TFTP(极度精简的文件传输)
TFTP 基于 UDP(代码量极小)
只支持读/写两个操作
整个实现可以塞进 ROM 芯片
12.2 FTP vs TFTP 全面对比
| 特性 | FTP | TFTP |
|---|---|---|
| 传输层协议 | TCP | UDP |
| 连接性 | 有明确的连接状态 | 逻辑连接(无 TCP 握手) |
| 认证 | 用户名/密码 | 无 |
| 命令数量 | 数十条 | 5~6 种消息类型 |
| 文件操作 | 读/写/删/改/目录管理 | 仅读/写 |
| 数据类型 | ASCII/EBCDIC/Image/Local | netascii / octet |
| 适用场景 | 通用文件传输 | 嵌入式设备、网络启动 |
| 程序大小 | 较大 | 极小(可放 ROM) |
| 性能 | 高(TCP 流水线) | 低(锁步确认) |
| 安全性 | 基本(明文密码) | 无 |
13. TFTP 操作流程详解
13.1 连接标识与端口机制
TFTP 没有 TCP 连接,用**传输标识符(TID)**区分不同传输:
客户端随机选一个端口(如 3145)作为 TID
↓
发送初始请求到服务器的 UDP 端口 69
↓
服务器随机选一个端口(如 1114)作为自己的 TID
↓
后续通信:客户端 3145 ↔ 服务器 1114
端口 69 不再参与此次传输
为什么服务器不用 69 端口回复?
因为服务器可能同时处理多个客户端请求,用不同的随机端口来区分不同的传输会话,端口 69 始终保持空闲以接受新连接。
13.2 锁步通信(Lock-Step)
TFTP 每发一个数据包,必须等对方确认后才能发下一个:
发送方 接收方
| |
| DATA block 1 |
|-------------------->|
| ACK block 1 |
|<--------------------|
| DATA block 2 |
|-------------------->|
| ACK block 2 |
|<--------------------|
| ... |
优点: 简单,实现容易,不需要滑动窗口等复杂机制
缺点: 慢!每次只有 512 字节在"路上",高延迟网络尤其差
13.3 文件结束标志
TFTP 用一种巧妙方式标记文件结束:
规则:每个数据块标准大小 512 字节
如果最后一块 < 512 字节 → 传输结束!
特殊情况:如果文件大小恰好是 512 的倍数(如 1024 字节)
第 1 块:512 字节(未结束)
第 2 块:512 字节(还没结束,因为收到了 512 字节)
第 3 块:0 字节(这才是结束信号!)
因此,最后一个数据包包含 0 到 511 字节,收到后即知传输完成,无需额外的结束消息。
13.4 读操作(RRQ)流程
以传输 1200 字节文件为例(3 块:512 + 512 + 176):
客户端 服务器
| |
| RRQ "filename" netascii |
| (读请求,到服务器端口 69) |
|---------------------------------------->|
| |
| DATA block 1 (512 bytes) |
| (从服务器的随机 TID 端口发出) |
|<----------------------------------------|
| ACK block 1 |
|---------------------------------------->|
| |
| DATA block 2 (512 bytes) |
|<----------------------------------------|
| ACK block 2 |
|---------------------------------------->|
| |
| DATA block 3 (176 bytes) | ← 少于512字节 = 最后一块!
|<----------------------------------------|
| ACK block 3 |
|---------------------------------------->|
| |
传输完成,逻辑连接自动结束
13.5 写操作(WRQ)流程
同样是 1200 字节文件,但客户端上传:
客户端 服务器
| |
| WRQ "filename" netascii |
|---------------------------------------->|
| |
| ACK block 0 | ← 用 block 0 确认收到写请求
|<----------------------------------------|
| |
| DATA block 1 (512 bytes) |
|---------------------------------------->|
| ACK block 1 |
|<----------------------------------------|
| |
| DATA block 2 (512 bytes) |
|---------------------------------------->|
| ACK block 2 |
|<----------------------------------------|
| |
| DATA block 3 (176 bytes) | ← 最后一块
|---------------------------------------->|
| ACK block 3 |
|<----------------------------------------|
传输完成
读和写的区别:
- 读:服务器先发数据(用第一块数据作为"确认了读请求"的信号)
- 写:服务器先发 ACK block 0(明确告诉客户端"可以开始发了")
13.6 巫师学徒 Bug(Sorcerer’s Apprentice Bug)
原始 TFTP 规范有一个著名的 Bug,名字来自迪士尼电影《幻想曲》中米老鼠砍扫帚的片段:
问题场景:
1. 服务器发送 DATA block 2
2. ACK block 2 延迟(网络拥塞),服务器超时重发 DATA block 2
3. 客户端收到 DATA block 2,发出 ACK block 2
4. 服务器收到 ACK,发出 DATA block 3
5. 延迟的原始 DATA block 2 也到了!客户端再发 ACK block 2
6. 服务器收到重复 ACK block 2,再发一次 DATA block 2!
7. 此后每个数据块都被发两次,ACK 也两次
→ 消息数量翻倍,越来越混乱,就像砍扫帚越砍越多
修复方案:
只有收到重复 DATA 的一方才能重发 ACK
收到重复 ACK 的一方 不 重发 DATA
(单向允许重复,打破反馈循环)
14. TFTP 选项协商
1995 年 RFC 1782 为 TFTP 加入了选项协商机制,允许在传输开始前商定参数:
14.1 三种可协商选项
| 选项代码 | 含义 | 说明 |
|---|---|---|
blksize |
数据块大小 | 默认 512 字节,可协商更大值(如 1468)提高效率 |
interval |
超时间隔(秒) | 调整重传计时器,适应不同网络延迟 |
tsize |
传输文件大小 | 提前告知文件总大小,接收方可预分配空间 |
14.2 带选项的读操作流程
正常读(无选项):
客户端→服务器:RRQ
服务器→客户端:DATA block 1
客户端→服务器:ACK block 1
...
带选项的读:
客户端→服务器:RRQ + 选项列表(如 blksize=1024)
服务器→客户端:OACK(选项确认,列出接受的选项)
客户端→服务器:ACK block 0(确认收到 OACK)
服务器→客户端:DATA block 1(用协商好的参数)
客户端→服务器:ACK block 1
...
注意: 读操作多了一个"ACK block 0"的步骤,因为 TFTP 不允许同一设备连续发两个数据报,服务器的 OACK 需要被确认后才能发第一块数据。
写操作带选项:
客户端→服务器:WRQ + 选项列表
服务器→客户端:OACK(代替普通的 ACK block 0)
客户端→服务器:DATA block 1
...(后续与普通写相同)
15. TFTP 消息格式
TFTP 共定义 6 种消息类型,所有消息的第一个字段都是操作码(Opcode):
15.1 读请求(RRQ, Opcode=1)/ 写请求(WRQ, Opcode=2)
+----------+------------+-----+------+-----+-------+-----+
| Opcode | Filename | 0 | Mode | 0 | opts | 0 |
| 2 bytes | variable | | var | | var.. | |
+----------+------------+-----+------+-----+-------+-----+
- Filename:文件名字符串,以 0 字节结尾
- Mode:
netascii或octet,以 0 字节结尾 - opts(可选):选项名=选项值对,各以 0 字节结尾
15.2 数据消息(DATA, Opcode=3)
+----------+-----------+------------------+
| Opcode=3 | Block # | Data |
| 2 bytes | 2 bytes | 0 to 512 bytes |
+----------+-----------+------------------+
- Block #:从 1 开始顺序编号
- Data:0~512 字节,小于 512 字节表示最后一块
15.3 确认消息(ACK, Opcode=4)
+----------+-----------+
| Opcode=4 | Block # |
| 2 bytes | 2 bytes |
+----------+-----------+
- Block #:被确认的数据块编号;0 表示确认写请求或 OACK
15.4 错误消息(ERROR, Opcode=5)
+----------+------------+-----------+-----+
| Opcode=5 | Error Code | Error Msg | 0 |
| 2 bytes | 2 bytes | variable | |
+----------+------------+-----------+-----+
| 错误码 | 含义 |
|---|---|
| 0 | 未定义错误,见错误消息文字 |
| 1 | 文件未找到 |
| 2 | 访问违规 |
| 3 | 磁盘满或分配超限 |
| 4 | 非法 TFTP 操作 |
| 5 | 未知传输 ID |
| 6 | 文件已存在 |
| 7 | 无此用户 |
| 8 | 不接受选项协商结果(客户端拒绝 OACK) |
15.5 选项确认(OACK, Opcode=6)
+----------+-------+-----+--------+-----+
| Opcode=6 | opt1 | 0 | value1 | 0 | ...(重复多个选项)
| 2 bytes | var | | var | |
+----------+-------+-----+--------+-----+
服务器在 OACK 中列出所有接受的选项及其值(可能与客户端请求的值不同)。
16. FTP 与 TFTP 综合对比
使用场景选择指南
典型应用场景
| 场景 | 推荐协议 | 原因 |
|---|---|---|
| 网站文件上传管理 | FTP | 需要目录操作、权限控制 |
| 软件分发下载 | 匿名 FTP 或 HTTP | 公开访问,无需复杂认证 |
| 网络设备启动(路由器/交换机) | TFTP | 设备内存有限,只需下载配置或固件 |
| 无盘工作站引导 | TFTP | 同上,空间极度有限 |
| 备份路由器配置 | TFTP | 快速、简单,内部网络无需认证 |
| 跨系统文本文件传输 | FTP(ASCII 模式) | 自动处理换行符转换 |
关键数字速记
FTP 控制端口: 21
FTP 数据端口: 20(主动模式)
TFTP 端口: 69
TFTP 数据块: 512 字节/块(默认)
FTP 回复码: 3位数字,百位=状态,十位=类别
TCP/IP 电子邮件系统详解
目录
- 什么是电子邮件系统?
- 电子邮件的发展历史
- 邮件传输的五个步骤
- 邮件通信模型——四个关键角色
- 电子邮件地址格式
- 邮件消息格式:RFC 822
- MIME:让邮件支持图片和附件
- SMTP:邮件投递协议
- POP3:收取邮件协议
- IMAP:更强大的邮件访问协议
- 整体架构总结
1. 什么是电子邮件系统?
电子邮件(Email)不是一个单一的协议或程序,而是一个完整的系统,由多个协议和组件共同配合完成。
你可以把它想象成一套现代邮政系统的电子版:
现实邮政系统 电子邮件系统
----------- -------------
写信 ←→ 撰写邮件
投递到邮箱 ←→ 提交给 SMTP 服务器
邮局转运 ←→ SMTP 服务器之间传递
送达对方邮箱 ←→ 存入收件人邮箱
收件人取信 ←→ POP3 / IMAP 收取邮件
电子邮件的核心优势:
- 极快速度:几秒内送达全球
- 一对多:一封信同时发给多人
- 异步通信:发件人和收件人不需要同时在线
2. 电子邮件的发展历史
时间轴:
1971年
└─ RFC 196:最早的邮件协议 "Mail Box Protocol",用于远程打印文档
1970年代中期
└─ 基于 FTP 实现邮件传输(把邮件当文件传)
1980年
└─ RFC 772:邮件传输协议 MTP(Mail Transfer Protocol),基于 Telnet + FTP
1981年
└─ SMTP 诞生(RFC 788):第一个专门为邮件设计的传输协议
1982年
└─ RFC 821(SMTP正式标准)+ RFC 822(邮件消息格式标准)同时发布
1984年
└─ POP(邮局协议)诞生,让用户可以把邮件下载到本地电脑
1988年
└─ POP3 发布,成为最流行的邮件收取协议
1992年
└─ MIME 标准发布,让邮件可以携带图片、音频、附件等
1990年代末
└─ Web 邮件出现(如 Hotmail),通过浏览器收发邮件
2001年
└─ RFC 2821(新版 SMTP)+ RFC 2822(新版消息格式)发布
3. 邮件传输的五个步骤
电子邮件从发出到被读取,经历五个标准步骤:
步骤1:撰写邮件(Mail Composition)
用户用邮件客户端写邮件
邮件 = 邮件头(Header)+ 邮件正文(Body)
↓ ↓
类似信封上的地址 类似信的内容
步骤2:提交邮件(Mail Submission)
用户点击"发送",邮件通过 SMTP 提交给本地邮件服务器
↓
类似把信投进邮箱
步骤3:投递邮件(Mail Delivery)
本地 SMTP 服务器通过 DNS 找到收件人的服务器地址
然后通过 SMTP 直接发送过去
↓
类似邮局用飞机/卡车运送信件
步骤4:接收与存储(Mail Receipt)
收件人的 SMTP 服务器收到邮件
存入收件人的"邮箱(Inbox)"等待取件
↓
类似信件放入收件人的信箱格
步骤5:读取邮件(Mail Access)
收件人用 POP3 或 IMAP 协议从服务器取回邮件阅读
↓
类似收件人去邮局取信
关键思想:发件人和收件人不需要同时在线。邮件会在服务器上等待,直到收件人来取。
4. 邮件通信模型——四个关键角色
整个邮件系统涉及四个设备/角色:
[发件人的电脑] [发件人的SMTP服务器]
用户撰写邮件 → 接收并转发邮件
存入"发件队列" (通常由ISP运营)
| |
| SMTP协议 | SMTP协议
↓ ↓
[发件人客户端] → [收件人的SMTP服务器]
|
| 存入收件人邮箱
↓
[收件人的电脑]
用 POP3 或 IMAP 取件
用一张流程图表示:
5. 电子邮件地址格式
标准格式
邮件地址的格式大家都很熟悉:
用户名 @ 域名 \text{用户名} @ \text{域名} 用户名@域名
例如:zhangsan@gmail.com
zhangsan→ 用户名(指定是哪个人)gmail.com→ 域名(指定邮件服务器在哪里)
DNS MX 记录的作用
当 SMTP 服务器要给 zhangsan@gmail.com 发邮件时,它不直接用域名连接,而是先查询 DNS MX 记录:
发件服务器问 DNS:
"gmail.com 的邮件服务器是哪台?"
DNS 回答:
"MX记录指向 smtp.gmail.com,IP地址是 x.x.x.x"
发件服务器:
直接连接 smtp.gmail.com 发送邮件
MX记录的好处:
- 公司可以用
用户@公司名.com这样好记的地址,背后实际由另一台服务器处理 - 换服务器时不需要改用户地址
- 可以设多个服务器分担负载
历史上的特殊地址格式
| 类型 | 格式示例 | 说明 |
|---|---|---|
| 标准DNS | user@domain.com |
现在通用的格式 |
| UUCP格式 | host1!host2!host3!user |
早期用感叹号分隔路径,已淘汰 |
| FidoNet格式 | p4.f205.n141.z1.fidonet.org |
早期独立网络的地址 |
| 网关格式 | user%domain1@domain2 |
跨系统网关用,现已少见 |
6. 邮件消息格式:RFC 822
为什么需要统一格式?
不同电脑、不同软件要能互相读懂邮件,必须遵守同一套格式规范。这个规范叫 RFC 822(2001年更新为 RFC 2822,但习惯上还叫 RFC 822)。
邮件的基本结构
+----------------------------------+
| 邮件头(Header) | ← 控制信息(收件人、发件人、日期等)
| From: alice@example.com |
| To: bob@example.com |
| Date: Mon, 15 May 2026 10:00:00 |
| Subject: 你好 |
+----------------------------------+
| 空行 | ← 必须有一个空行分隔
+----------------------------------+
| 邮件正文(Body) | ← 实际内容
| 嗨 Bob, |
| 这是一封测试邮件... |
+----------------------------------+
RFC 822 的重要特点
- 所有内容都是纯 ASCII 文本(美国英文字符)
- 每行不超过 998 个字符(推荐78个以内)
- 每行以 回车+换行(CRLF) 结尾
常用邮件头字段说明
| 字段名 | 是否必须 | 作用说明 |
|---|---|---|
Date: |
必须 | 邮件发送的日期和时间 |
From: |
必须 | 发件人邮件地址 |
To: |
通常必须 | 主要收件人列表 |
Cc: |
可选 | 抄送收件人(所有人都能看到) |
Bcc: |
可选 | 密送收件人(其他收件人不知道) |
Subject: |
通常存在 | 邮件主题 |
Message-ID: |
推荐有 | 邮件唯一标识码 |
In-Reply-To: |
回复时有 | 指明这是对哪封邮件的回复 |
Reply-To: |
可选 | 希望对方回复到这个地址 |
Bcc(密送)是怎么实现的?
发送给:
To: 张三
Cc: 李四
Bcc: 王五 ← 密送
实际情况:
张三收到:能看到 To(张三)、Cc(李四),看不到 Bcc
李四收到:能看到 To(张三)、Cc(李四),看不到 Bcc
王五收到:服务器在发给王五前把 Bcc 字段删掉或修改
7. MIME:让邮件支持图片和附件
RFC 822 的局限性
RFC 822 规定邮件只能包含 ASCII 文本,这意味着:
- 不能发图片、音频、视频
- 不能发 Word、Excel 等二进制文件
- 中文、日文等非英文字符无法正确显示
MIME 的解决思路
MIME(多用途互联网邮件扩展)的核心思想很巧妙:
“RFC 822 规定只能用 ASCII 文本,但没说这些文本不能是经过编码的数据!”
所以 MIME 把二进制数据编码成 ASCII 文本,然后附上说明告诉收件方怎么解码。
原始图片数据(二进制):
11010100 00100111 11110111 ...(不是合法ASCII)
经过 Base64 编码后(ASCII文本):
SDc9Pjv/2wBDAQoLCw4NDhwQ...(可以放进邮件)
收件方解码后还原成原始图片
MIME 添加的关键头字段
| 头字段 | 作用 |
|---|---|
MIME-Version: 1.0 |
标记这是MIME邮件 |
Content-Type: |
说明内容类型(如图片/文本/音频) |
Content-Transfer-Encoding: |
说明编码方式(如base64) |
Content-ID: |
内容唯一标识 |
Content-Description: |
内容描述 |
Content-Type 的格式
Content-Type: 大类型 ⏟ type / 子类型 ⏟ subtype [; 参数] \text{Content-Type: } \underbrace{\text{大类型}}_{\text{type}} / \underbrace{\text{子类型}}_{\text{subtype}} \text{ [; 参数]} Content-Type: type
大类型/subtype
子类型 [; 参数]
例如:
text/plain→ 纯文本text/html→ HTML网页image/jpeg→ JPEG图片audio/mpeg→ MP3音频application/pdf→ PDF文件multipart/mixed→ 混合内容(多个附件)
MIME 支持的内容大类
| 大类型 | 含义 | 常见子类型 |
|---|---|---|
text |
文本 | plain, html, css |
image |
图片 | jpeg, gif, tiff |
audio |
音频 | mpeg(MP3), basic |
video |
视频 | mpeg, quicktime |
application |
应用文件 | pdf, msword, zip |
multipart |
多部分组合 | mixed, alternative |
message |
嵌套邮件 | rfc822 |
model |
三维模型 | vrml, iges |
多部分邮件(multipart)的结构
当一封邮件包含正文文字和一张图片附件时,结构如下:
邮件头:
Content-Type: multipart/mixed; boundary="分隔符ABC"
邮件体:
--分隔符ABC
Content-Type: text/plain
你好,这是邮件正文。
--分隔符ABC
Content-Type: image/jpeg; name="photo.jpg"
Content-Transfer-Encoding: base64
SDc9Pjv/2wBDAQoLCw4NDhwQ...(编码后的图片数据)
--分隔符ABC-- ← 最后加两个横线表示结束
Base64 编码原理
Base64 的原理是把每 3个字节(24位)变成 4个ASCII字符(每个代表6位):
3 字节 × 8 位 = 24 位 = 4 × 6 位 3 \text{ 字节} \times 8 \text{ 位} = 24 \text{ 位} = 4 \times 6 \text{ 位} 3 字节×8 位=24 位=4×6 位
编码示意:
原始3字节(二进制):
11010100 00100111 11110111
拆成4个6位组:
110101 | 000010 | 011111 | 110111
53 2 31 55
查表得到ASCII字符:
53→'1' 2→'C' 31→'f' 55→'3'
最终发送: "1Cf3"(4个ASCII字符代替原来3个字节)
Base64 的缺点:数据量会增加约 1 3 \frac{1}{3} 31,因为3字节变成4字符(每字符仍占1字节):
膨胀率 = 4 3 ≈ 133 % \text{膨胀率} = \frac{4}{3} \approx 133\% 膨胀率=34≈133%
8. SMTP:邮件投递协议
SMTP 是什么?
SMTP(简单邮件传输协议)负责把邮件从一台服务器传送到另一台服务器,就像邮政系统中的运输环节。
SMTP 工作在 TCP 端口 25 上。
现代 SMTP 投递流程(基于 DNS)
早期 SMTP 用"中继"(Relay)方式,像接力赛一样一站一站传。现在有了 DNS 的 MX 记录,可以直接投递:
SMTP 会话的三个阶段
阶段1:建立连接(握手)
收件方:220 mail.example.com 服务就绪
发件方:EHLO smtp.sender.org
收件方:250-mail.example.com 你好
250-SIZE ← 支持的扩展功能列表
250-DSN
250 PIPELINING
阶段2:传送邮件(事务)
发件方:MAIL FROM:<alice@sender.org> ← 说明发件人
收件方:250 发件人OK
发件方:RCPT TO:<bob@example.com> ← 说明收件人
收件方:250 收件人OK
发件方:DATA ← 开始发送邮件内容
收件方:354 请输入邮件,以单独一行的"."结束
发件方:(发送完整邮件内容)
. ← 单独一行的点表示结束
收件方:250 邮件已接收
阶段3:关闭连接
发件方:QUIT
收件方:221 再见,关闭连接
SMTP 命令速查
| 命令 | 作用 |
|---|---|
HELO |
普通问候,开始会话 |
EHLO |
扩展问候,支持新功能 |
MAIL FROM: |
指定发件人地址 |
RCPT TO: |
指定收件人地址(可多次使用) |
DATA |
开始传输邮件内容 |
QUIT |
结束会话 |
VRFY |
验证邮件地址是否有效 |
RSET |
取消当前邮件事务 |
NOOP |
什么都不做(测试连接用) |
SMTP 回复码结构
回复码是三位数字 x y z xyz xyz,每位有特定含义:
x ⏟ 结果类型 y ⏟ 功能分类 z ⏟ 具体含义 \underbrace{x}_{\text{结果类型}} \underbrace{y}_{\text{功能分类}} \underbrace{z}_{\text{具体含义}} 结果类型
x功能分类
y具体含义
z
第一位(x)表示成功/失败:
| 第一位 | 含义 |
|---|---|
| 2xx | 成功完成 |
| 3xx | 命令接受,等待更多信息 |
| 4xx | 临时错误(可以重试) |
| 5xx | 永久错误(不要重试) |
常见回复码:
| 回复码 | 含义 |
|---|---|
| 220 | 服务就绪(连接成功) |
| 250 | 命令执行成功 |
| 354 | 请开始发送邮件内容 |
| 221 | 再见,关闭连接 |
| 421 | 服务暂时不可用 |
| 550 | 邮件地址不存在/拒绝 |
| 552 | 邮箱已满 |
SMTP 安全问题
SMTP 早期设计时互联网很小,大家互相信任,因此没有任何安全机制。这带来了严重问题:
- 垃圾邮件(Spam):攻击者可以用任意发件人地址发大量邮件
- 身份伪造:SMTP 本身无法验证发件人身份
- 开放中继:早期服务器会帮任何人转发邮件
现代的防护措施: - 检查连接 IP,只接受授权来源
- 限制中继功能
- 要求 AUTH 身份验证
- 限制单次发送数量和邮件大小
- 记录所有访问日志
9. POP3:收取邮件协议
POP3 是什么?
POP3(邮局协议第3版)是最流行的邮件收取协议,工作在 TCP 端口 110。
它使用离线模式:把邮件从服务器下载到本地,通常下载后从服务器删除。
服务器端 本地电脑
[邮箱] ──POP3──> [邮件客户端]
(清空) (保存邮件)
POP3 会话的三个状态
POP3 会话是线性的,依次经过三个状态:
状态1:授权状态(Authorization)
服务器:+OK POP3 服务就绪
客户端:USER bob@example.com
服务器:+OK
客户端:PASS 123456
服务器:+OK bob@example.com 有 3 封邮件
状态2:事务状态(Transaction)
客户端:STAT ← 查询邮箱状态
服务器:+OK 3 1500 ← 3封邮件,共1500字节
客户端:LIST ← 列出所有邮件
服务器:+OK
1 600
2 500
3 400
. ← 单独一行的点表示列表结束
客户端:RETR 1 ← 下载第1封邮件
服务器:+OK
(邮件内容)
.
客户端:DELE 1 ← 标记第1封为待删除
服务器:+OK 邮件1已标记删除
状态3:更新状态(Update)
客户端:QUIT ← 退出
(服务器此时才真正删除被标记的邮件)
服务器:+OK 再见
POP3 命令速查
| 命令 | 作用 |
|---|---|
USER 用户名 |
发送用户名 |
PASS 密码 |
发送密码 |
STAT |
查询邮箱中邮件数量和总大小 |
LIST |
列出所有邮件编号和大小 |
RETR 编号 |
下载指定编号的邮件 |
DELE 编号 |
标记指定邮件为待删除 |
RSET |
撤销所有删除标记 |
TOP 编号 行数 |
只下载邮件头和前N行(可选) |
QUIT |
退出并执行实际删除 |
POP3 的优缺点
优点:
- 实现简单,几乎所有客户端都支持
- 下载后可离线阅读
- 占用服务器空间少
缺点: - 邮件下载到一台电脑后,其他设备看不到
- 没有服务器端邮件夹管理
- 难以追踪哪些邮件已读/未读
10. IMAP:更强大的邮件访问协议
IMAP 与 POP3 的根本区别
IMAP(互联网消息访问协议)让邮件始终保留在服务器上,支持多设备同步访问。
POP3 模式(离线): IMAP 模式(同步):
服务器 ──下载──> 手机 服务器 <──> 手机
(服务器删除) 服务器 <──> 电脑
服务器 <──> 平板
(三端同步,都能看到)
IMAP 会话的四个状态
IMAP 的核心功能
状态1:未认证状态 - 只能登录
服务器:* OK mail.example.com 服务就绪
客户端:a001 LOGIN bob@example.com 密码
服务器:a001 OK 登录成功
注意:IMAP 每条命令前有一个标签(如 a001),服务器回复时带同样标签,这样可以并发发送多条命令。
状态2:已认证状态 - 管理邮件夹
客户端:a002 LIST "" "*" ← 列出所有邮件夹
服务器:* LIST () "/" INBOX
* LIST () "/" Sent
* LIST () "/" Drafts
a002 OK LIST 完成
客户端:a003 SELECT INBOX ← 选择收件箱
服务器:* 15 EXISTS ← 共15封邮件
* 3 RECENT ← 3封新邮件
* FLAGS (\Seen \Answered \Flagged \Deleted \Draft)
a003 OK INBOX 已选择
状态3:已选择状态 - 操作具体邮件
客户端:a004 FETCH 1:3 (FLAGS ENVELOPE) ← 获取第1-3封邮件的标志和摘要
客户端:a005 SEARCH UNSEEN ← 搜索未读邮件
服务器:* SEARCH 2 5 7 ← 第2、5、7封未读
客户端:a006 STORE 2 +FLAGS (\Seen) ← 标记第2封为已读
客户端:a007 FETCH 5 BODY[] ← 下载第5封完整内容
IMAP 命令分组
| 状态 | 可用命令 |
|---|---|
| 任意状态 | CAPABILITY, NOOP, LOGOUT |
| 未认证状态 | LOGIN, AUTHENTICATE, STARTTLS |
| 已认证状态 | SELECT, EXAMINE, CREATE, DELETE, RENAME, LIST, STATUS, APPEND |
| 已选择状态 | FETCH, STORE, SEARCH, COPY, EXPUNGE, CLOSE, CHECK |
IMAP vs POP3 对比
| 对比项 | POP3 | IMAP |
|---|---|---|
| 邮件保存位置 | 下载到本地 | 保留在服务器 |
| 多设备支持 | 差(只有一台有邮件) | 好(所有设备同步) |
| 服务器端文件夹 | 不支持 | 支持 |
| 部分下载 | 不支持 | 支持(只下载头部或附件) |
| 搜索功能 | 需要本地搜索 | 服务器端搜索 |
| 协议复杂度 | 简单 | 较复杂 |
| 端口 | 110 | 143 |
11. 整体架构总结
各协议的分工
协议端口总结
| 协议 | 端口 | 作用 |
|---|---|---|
| SMTP | 25 | 服务器之间传送邮件 |
| SMTP(提交) | 587 | 客户端向服务器提交邮件 |
| POP3 | 110 | 客户端收取邮件(离线模式) |
| IMAP | 143 | 客户端访问邮件(同步模式) |
邮件格式的层次关系
一封完整的邮件(MIME格式)
├── RFC 822 邮件头
│ ├── From:、To:、Date:、Subject: 等
│ └── MIME-Version:、Content-Type: 等MIME头
└── 邮件正文
├── 如果是纯文本:直接就是文字
└── 如果是MIME多部分:
├── 第一部分:正文文字(text/plain 或 text/html)
├── 第二部分:图片附件(image/jpeg)Base64编码
└── 第三部分:文件附件(application/pdf)Base64编码
三种邮件访问模式对比
| 模式 | 代表协议 | 邮件保存在 | 适合场景 |
|---|---|---|---|
| 在线模式 | Telnet直连 / IMAP | 服务器 | 专业用户,多地访问 |
| 离线模式 | POP3 | 本地电脑 | 单台设备,低带宽 |
| 断开同步模式 | IMAP | 服务器+本地副本 | 多设备用户,需要离线阅读 |
| Web模式 | HTTP(浏览器) | 服务器 | 临时设备,无需安装客户端 |
TCP/IP 万维网(WWW)与 HTTP 协议详解
目录
- 万维网是什么?
- 超文本与 HTML
- URL 地址格式
- HTTP 版本演进
- HTTP 的工作模型
- HTTP 连接方式:短连接与持久连接
- HTTP 消息格式
- HTTP 方法(命令)
- HTTP 状态码
- HTTP 头部字段详解
- HTTP 实体与编码
- 内容协商与质量值
- HTTP 缓存机制
- 代理服务器
- HTTP 安全与 Cookie
- 整体架构总结
1. 万维网是什么?
一句话理解
万维网(WWW,World Wide Web)不是互联网本身,而是运行在互联网上的一个应用系统,它让我们可以通过点击链接,在无数文档之间自由跳转。
超文本的核心思想
在没有超文本之前,文档是"孤岛":
文档A 文档B 文档C
[内容] [内容] [内容]
↑ ↑ ↑
互相不知道彼此的存在
有了超文本之后:
文档A ──点击链接──> 文档B ──点击链接──> 文档C
↑ | |
└───────────────────┴───────────────────┘
相互连接,形成"网"
这就是 Web(网)这个名字的由来。
发展历史时间线
1945年
└─ Vannevar Bush 提出"Memex"概念:用"索引路径"连接相关文档
1960年
└─ Ted Nelson 创造"hypertext(超文本)"这个词,设计 Xanadu 系统
1989年
└─ Tim Berners-Lee 在 CERN(欧洲核研究组织)提出万维网构想
1990年
└─ 第一个极简版 HTTP(HTTP/0.9)诞生,只能传输文本
1993年
└─ 第一个图形化浏览器 Mosaic 诞生(NCSA 开发)
↑ Marc Andreessen 后来创立了 Netscape
1993年初:全球仅 50 台 HTTP 服务器
1993年底:超过 1000 台
1995年底:每天数千个新网站上线
1996年
└─ HTTP/1.0 正式发布(RFC 1945)
1999年
└─ HTTP/1.1 正式发布(RFC 2616),沿用至今
万维网的三大核心组件
2. 超文本与 HTML
HTML 是什么?
HTML(HyperText Markup Language,超文本标记语言)是一种用标签来描述文档结构的语言。本质上是一个 ASCII 文本文件,但加了特殊的标记。
HTML 的基本结构
<html> <!-- 整个文档的根标签 -->
<head> <!-- 头部:文档的元信息(标题、编码等) -->
<title>网页标题</title>
</head>
<body> <!-- 正文:用户看到的内容 -->
<!-- 各种内容标签放这里 -->
</body>
</html>
常用 HTML 标签速查
| 标签 | 作用 | 示例 |
|---|---|---|
<p>...</p> |
段落 | <p>这是一段文字</p> |
<h1>~<h6> |
标题(从大到小) | <h1>大标题</h1> |
<br> |
换行 | 第一行<br>第二行 |
<b>...</b> |
加粗 | <b>加粗文字</b> |
<i>...</i> |
斜体 | <i>斜体文字</i> |
<a href="URL"> |
超链接 | <a href="http://www.example.com">点我</a> |
<img src="URL"> |
显示图片 | <img src="logo.gif"> |
<ul><li> |
无序列表 | 显示为项目符号列表 |
<table><tr><td> |
表格 | 行列布局 |
<form> |
表单 | 用户输入和提交数据 |
超链接的工作原理
HTML 文件中:
<a href="http://www.pcguide.com">点击访问PC指南</a>
用户点击后:
浏览器读取 href 属性中的 URL
↓
发送 HTTP GET 请求到 www.pcguide.com
↓
服务器返回对应的 HTML 文件
↓
浏览器显示新页面
3. URL 地址格式
URL 的完整语法
http ⏟ 协议 :// user:pass ⏟ 认证(可选) @ host ⏟ 主机名 : port ⏟ 端口 /path ⏟ 路径 ? query ⏟ 查询 # bookmark ⏟ 书签 \underbrace{\text{http}}_{\text{协议}} \text{://} \underbrace{\text{user:pass}}_{\text{认证(可选)}} \text{@} \underbrace{\text{host}}_{\text{主机名}} \text{:} \underbrace{\text{port}}_{\text{端口}} \underbrace{\text{/path}}_{\text{路径}} \text{?} \underbrace{\text{query}}_{\text{查询}} \text{\#} \underbrace{\text{bookmark}}_{\text{书签}} 协议 http://认证(可选) user:pass@主机名 host:端口 port路径 /path?查询 query#书签 bookmark
各部分的含义
| 部分 | 含义 | 示例 | 是否必须 |
|---|---|---|---|
http:// |
使用 HTTP 协议 | http:// |
必须 |
user:pass@ |
身份验证信息 | admin:123@ |
可选,少见 |
host |
服务器域名或IP | www.example.com |
必须 |
:port |
端口号 | :8080(默认80可省略) |
可选 |
/path |
资源路径 | /news/article.html |
通常有 |
?query |
查询参数 | ?id=123&page=2 |
可选 |
#bookmark |
页面内书签 | #chapter3 |
可选 |
实际例子解析
http://www.myfavoritewebsite.com:8080/chatware/chatroom.php
协议: http
主机: www.myfavoritewebsite.com
端口: 8080(非默认80)
路径: /chatware/chatroom.php
浏览器发送给服务器的请求:
GET /chatware/chatroom.php HTTP/1.1
Host: www.myfavoritewebsite.com:8080
注意:http:// 和主机名被分开了:路径放在请求行,主机名放在 Host 头部。
默认文档
当 URL 只有域名没有路径时,如 http://www.cnn.com:
用户输入:http://www.cnn.com
↓
服务器自动返回默认文档(通常是 index.html 或 default.html)
↓
等同于访问:http://www.cnn.com/index.html
4. HTTP 版本演进
各版本核心特点对比
| 特性 | HTTP/0.9 | HTTP/1.0 | HTTP/1.1 |
|---|---|---|---|
| 文件类型 | 只有文本 | 多媒体 | 多媒体 |
| 连接方式 | 每次新建 | 每次新建 | 持久连接(默认) |
| 虚拟主机 | 不支持 | 不支持 | 支持(一IP多域名) |
| 缓存支持 | 无 | 基础 | 完整(26页规范) |
| 管线化 | 无 | 无 | 支持 |
| 分块传输 | 无 | 无 | 支持 |
| 内容协商 | 无 | 基础 | 完整 |
HTTP/1.0 的主要问题
HTTP/1.0 时代出现了"万维网等待"(World Wide Wait)这个讽刺称号,原因:
加载一个网页需要:
1. 下载 HTML 文件 → 建立TCP连接 → 请求 → 响应 → 关闭TCP
2. 下载图片1 → 建立TCP连接 → 请求 → 响应 → 关闭TCP
3. 下载图片2 → 建立TCP连接 → 请求 → 响应 → 关闭TCP
4. 下载图片3 → 建立TCP连接 → 请求 → 响应 → 关闭TCP
... (一个页面可能有几十张图片)
每次建立 TCP 连接都要三次握手,浪费大量时间!
5. HTTP 的工作模型
基本通信:请求/响应
HTTP 是最简单的一类协议——客户问,服务器答:
带中间代理的通信链
这整条路径称为请求/响应链(Request/Response Chain)。
代理的特点:
- 对客户端来说,代理像服务器
- 对服务器来说,代理像客户端
- 可以透明转发,也可以修改消息(添加缓存、过滤内容等)
缓存对通信链的影响
正常流程(无缓存):
客户端 → 代理1 → 代理2 → 服务器
↓(找到资源)
客户端 ← 代理1 ← 代理2 ← 服务器
有缓存时(代理2有缓存):
客户端 → 代理1 → 代理2
↓(缓存命中!)
客户端 ← 代理1 ← 代理2
(服务器完全不知道这次请求的存在)
6. HTTP 连接方式:短连接与持久连接
短连接(HTTP/0.9 和 HTTP/1.0)
短连接模式(每次请求都要重新握手):
浏览器请求HTML:
[TCP三次握手] → [HTTP请求] → [HTTP响应] → [TCP四次挥手]
浏览器请求图片1:
[TCP三次握手] → [HTTP请求] → [HTTP响应] → [TCP四次挥手]
浏览器请求图片2:
[TCP三次握手] → [HTTP请求] → [HTTP响应] → [TCP四次挥手]
... 浪费大量时间在握手上
类比:每问一个问题,就挂断电话,然后重新拨号,再问下一个问题。
持久连接(HTTP/1.1 默认)
持久连接模式:
[TCP三次握手] → 一次建立,多次使用
↓
[HTTP请求HTML] → [HTTP响应HTML]
[HTTP请求图片1] → [HTTP响应图片1]
[HTTP请求图片2] → [HTTP响应图片2]
[HTTP请求图片3] → [HTTP响应图片3]
↓
[TCP四次挥手] → 全部完成后才断开
管线化(Pipelining)
持久连接的进一步优化:不等上一个响应,就发下一个请求。
普通持久连接(顺序):
→ 请求A → 请求B → 请求C
← 响应A ← 响应B ← 响应C
管线化(流水线):
→ 请求A → 请求B → 请求C
← 响应A ← 响应B ← 响应C
(请求连续发出,响应按顺序返回)
关闭连接
客户端在最后一次请求中加上:
Connection: close
服务器处理完后关闭 TCP 连接。
二进制指数退避
当服务器忙碌而意外断开连接时,客户端重试不能立刻疯狂重连,而是采用二进制指数退避:
等待时间 = 2 n 秒(n为重试次数) \text{等待时间} = 2^n \text{ 秒(n为重试次数)} 等待时间=2n 秒(n为重试次数)
例如:第1次失败等1秒,第2次失败等2秒,第3次等4秒,第4次等8秒……这样避免加重服务器负担。
7. HTTP 消息格式
通用消息结构
所有 HTTP 消息(不管是请求还是响应)都遵循同一个基本格式:
┌─────────────────────────────┐
│ 起始行 (Start Line) │ ← 请求行 或 状态行
├─────────────────────────────┤
│ 消息头部 (Headers) │ ← 多行,每行 "名字: 值"
│ Header1: Value1 │
│ Header2: Value2 │
│ ... │
├─────────────────────────────┤
│ 空行 (CRLF) │ ← 标志头部结束
├─────────────────────────────┤
│ 消息正文 (Body) │ ← 可选,携带实际数据
│ (文件内容、表单数据等) │
├─────────────────────────────┤
│ 消息尾部 (Trailers) │ ← 可选,分块传输时使用
└─────────────────────────────┘
请求消息格式
GET /chatware/chatroom.php HTTP/1.1 ← 请求行:方法 路径 版本
Host: www.myfavoritewebsite.com:8080 ← 请求头(必须有Host)
User-Agent: Mozilla/5.0 ... ← 告诉服务器浏览器类型
Accept: text/html,image/jpeg ← 告诉服务器客户端能接受的格式
Accept-Language: zh-CN,en;q=0.9 ← 语言偏好
Connection: keep-alive ← 要求持久连接
← 空行(头部结束)
(通常GET请求没有正文)
响应消息格式
HTTP/1.1 200 OK ← 状态行:版本 状态码 原因短语
Date: Mon, 15 May 2026 10:00:00 GMT ← 响应日期
Content-Type: text/html; charset=utf-8 ← 告诉客户端内容类型
Content-Length: 1256 ← 内容长度(字节)
Server: Apache/2.4 ← 服务器软件信息
← 空行(头部结束)
<html> ← 响应正文(HTML文件内容)
<body>
<h1>Hello World!</h1>
</body>
</html>
8. HTTP 方法(命令)
HTTP 用"方法"(Method)这个词代替"命令",来自面向对象编程的概念。
三大常用方法
GET - 获取资源
用途:请求服务器把指定资源发给我
场景:在浏览器地址栏输入URL、点击超链接
示例:
GET /news/article.html HTTP/1.1
Host: www.example.com
HEAD - 只要头部,不要正文
用途:和 GET 一样,但服务器只返回头部(不含文件内容)
场景:检查文件是否存在、获取文件大小、查看最后修改时间
示例:
HEAD /bigfile.zip HTTP/1.1
Host: www.example.com
(响应中有文件大小等信息,但不传输文件本身)
POST - 提交数据给服务器
用途:把数据发送给服务器上的某个程序处理
场景:提交登录表单、发布评论、网上购物结账
示例:
POST /login.php HTTP/1.1
Host: www.example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 27
username=alice&password=123
其他方法
| 方法 | 作用 | 是否常用 |
|---|---|---|
GET |
获取资源 | 最常用 |
HEAD |
只获取头部信息 | 常用 |
POST |
提交数据 | 常用 |
PUT |
上传文件到指定位置 | 少用 |
DELETE |
删除指定资源 | 少用 |
OPTIONS |
查询服务器支持哪些方法 | 少用 |
TRACE |
回显请求,用于调试 | 诊断用 |
安全方法 vs 幂等方法
安全方法:不会对服务器造成改变的方法
- 安全:GET、HEAD、OPTIONS、TRACE
- 不安全:POST(提交数据会改变服务器状态)、PUT、DELETE
幂等方法:重复执行多次,结果和执行一次相同
f ( f ( x ) ) = f ( x ) (幂等的数学定义) f(f(x)) = f(x) \quad \text{(幂等的数学定义)} f(f(x))=f(x)(幂等的数学定义) - 幂等:GET、HEAD(多次获取同一资源,结果一样)
- 非幂等:POST(提交两次订单 = 下了两笔订单,结果不同)
实际意义:非幂等方法(如POST)不应该使用管线化,因为如果连接中断需要重试,重复执行可能造成问题。
9. HTTP 状态码
状态码格式
HTTP 状态码是三位数 x y z xyz xyz,第一位表示大类:
x ⏟ 类别 y y ⏟ 具体编号 \underbrace{x}_{\text{类别}} \underbrace{yy}_{\text{具体编号}} 类别
x具体编号
yy
| 第一位 | 含义 | 通俗理解 |
|---|---|---|
| 1xx | 信息性消息 | “收到了,继续…” |
| 2xx | 成功 | “搞定!” |
| 3xx | 重定向 | “不在这里,去那里找” |
| 4xx | 客户端错误 | “你的请求有问题” |
| 5xx | 服务器错误 | “我这边出问题了” |
常见状态码详解
2xx 成功类
| 状态码 | 原因短语 | 含义 |
|---|---|---|
| 200 | OK | 请求成功,资源已返回 |
| 201 | Created | 资源创建成功(常用于PUT) |
| 204 | No Content | 成功但无返回内容 |
| 206 | Partial Content | 部分内容(用于断点续传) |
3xx 重定向类
| 状态码 | 原因短语 | 含义 |
|---|---|---|
| 301 | Moved Permanently | 永久移走了,以后用新URL |
| 302 | Found | 临时移到别处,还用原URL |
| 304 | Not Modified | 资源没变,用你缓存的版本 |
| 307 | Temporary Redirect | 临时重定向(和302类似) |
4xx 客户端错误
| 状态码 | 原因短语 | 含义 |
|---|---|---|
| 400 | Bad Request | 请求格式错误 |
| 401 | Unauthorized | 需要身份验证 |
| 403 | Forbidden | 服务器拒绝,无关权限 |
| 404 | Not Found | 资源不存在(最常见的错误) |
| 405 | Method Not Allowed | 该资源不支持此方法 |
| 408 | Request Timeout | 请求超时 |
5xx 服务器错误
| 状态码 | 原因短语 | 含义 |
|---|---|---|
| 500 | Internal Server Error | 服务器内部错误 |
| 501 | Not Implemented | 服务器不支持此功能 |
| 503 | Service Unavailable | 服务器暂时不可用(过载/维护) |
| 504 | Gateway Timeout | 网关等待上游服务器超时 |
100 Continue 的特殊用途
当客户端要上传一个很大的文件时,可以先问服务器"你接受吗?"
客户端发送:
POST /upload HTTP/1.1
Expect: 100-continue
Content-Length: 100000000 ← 1亿字节,很大!
服务器回应:
HTTP/1.1 100 Continue ← "可以,发过来吧"
或
HTTP/1.1 413 Entity Too Large ← "太大了,我不收"
如果是 100:客户端继续发送文件内容
如果是错误:客户端不必浪费流量传输大文件
10. HTTP 头部字段详解
HTTP 的大部分功能都通过头部字段来实现。头部字段分四大类:
通用头部(General Headers)
| 头部名 | 作用 | 示例 |
|---|---|---|
Cache-Control |
缓存控制指令 | Cache-Control: no-cache |
Connection |
连接管理 | Connection: close |
Date |
消息发出时间 | Date: Mon, 15 May 2026 10:00:00 GMT |
Transfer-Encoding |
传输编码方式 | Transfer-Encoding: chunked |
Via |
经过哪些代理 | Via: 1.1 proxy.example.com |
请求头部(Request Headers)
| 头部名 | 作用 |
|---|---|
Host |
目标主机名(HTTP/1.1 中唯一强制头部) |
Accept |
客户端能接受的媒体类型 |
Accept-Language |
客户端偏好的语言 |
Accept-Encoding |
客户端支持的压缩方式 |
User-Agent |
浏览器/客户端名称和版本 |
Referer |
从哪个页面跳转过来的 |
Authorization |
身份验证凭据 |
If-Modified-Since |
条件请求:只有资源修改过才发送 |
Range |
请求资源的某一部分(断点续传) |
响应头部(Response Headers)
| 头部名 | 作用 |
|---|---|
Server |
服务器软件信息 |
Location |
重定向目标URL |
WWW-Authenticate |
告诉客户端如何认证 |
ETag |
资源的唯一标识(用于缓存验证) |
Age |
资源在缓存中存放的时间 |
实体头部(Entity Headers)
| 头部名 | 作用 |
|---|---|
Content-Type |
资源的媒体类型(如 text/html) |
Content-Length |
资源的字节数 |
Content-Encoding |
资源的压缩方式(如 gzip) |
Content-Language |
资源使用的语言 |
Last-Modified |
资源最后修改时间 |
Expires |
资源过期时间 |
Cache-Control 指令详解
缓存控制是 HTTP/1.1 最复杂的头部之一:
| 指令 | 用于 | 含义 |
|---|---|---|
no-cache |
请求/响应 | 每次使用前必须向服务器验证 |
no-store |
请求/响应 | 绝对不要缓存(敏感数据用) |
max-age=秒数 |
请求/响应 | 缓存最长有效时间 |
public |
响应 | 允许共享缓存存储 |
private |
响应 | 只允许私有缓存(不能存到代理) |
must-revalidate |
响应 | 过期后必须向服务器重新验证 |
11. HTTP 实体与编码
实体(Entity)是什么?
在 HTTP 中,消息正文中携带的数据叫做实体。通常就是服务器返回的文件(HTML、图片、视频等)。
HTTP 与 MIME 的关系
HTTP 借鉴了邮件系统的 MIME 规范,但不完全兼容:
MIME(用于邮件): HTTP(用于Web):
- 只能用 ASCII 文本 - 可以直接传二进制数据
- 需要 Base64 编码图片 - 不需要 Base64,直接传字节
- 有 MIME-Version 头 - 没有 MIME-Version 头
- Content-Transfer-Encoding - 不用这个头
关键区别:MIME 必须把图片用 Base64 编码变成文字再传(因为邮件系统只支持文字),而 HTTP 可以直接传二进制字节,效率更高。
Base64 编码会增加 1 3 \frac{1}{3} 31 的数据量:
HTTP 不需要 Base64,节省了 ≈ 33 % 的传输量 \text{HTTP 不需要 Base64,节省了} \approx 33\% \text{ 的传输量} HTTP 不需要 Base64,节省了≈33% 的传输量
HTTP 的两级编码体系
HTTP 把编码分成了两层,比 MIME 更灵活:
内容编码(Content Encoding):
- 作用于实体本身
- 整个端到端有效
- 主要用于压缩:
gzip、deflate、compress
传输编码(Transfer Encoding): - 作用于整条消息
- 每一跳可以不同
- 主要用于解决消息长度问题:
chunked(分块传输)
分块传输(Chunked Transfer)
当服务器不知道响应有多长时(动态生成的内容),用分块传输:
HTTP/1.1 200 OK
Content-Type: text/html
Transfer-Encoding: chunked
Trailer: Expires
29 ← 第1块的长度(16进制,= 41字节)
<html><body><p>The file you requested is
5 ← 第2块长度(5字节)
3,400
23 ← 第3块长度(35字节)
bytes long and was last modified:
1d ← 第4块长度(29字节)
Sun, 20 Mar 2005 21:12:00 GMT
13 ← 第5块长度(19字节)
.</p></body></html>
0 ← 长度为0,表示结束
Expires: Sun, 27 Mar 2005 21:12:00 GMT ← 尾部字段(Trailer)
块长度都用十六进制表示,0 代表最后一块。
对比普通响应(已知长度):
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 129 ← 直接告诉长度
<html><body>...</body></html>
12. 内容协商与质量值
为什么需要内容协商?
同一个资源可能有多个版本:
- 同一篇文章有中文版、英文版、法文版
- 同一张图片有 JPEG 版(小文件)和 TIFF 版(高质量)
内容协商让客户端和服务器商量,返回最合适的版本。
两种协商方式
服务器驱动协商:客户端说"我的偏好是…",服务器自己选:
Accept: text/html, text/plain;q=0.5
Accept-Language: zh-CN, en;q=0.8
Accept-Encoding: gzip, deflate
代理驱动协商:服务器先返回可用版本列表,客户端再选:
第一步:客户端请求资源
第二步:服务器返回 300 Multiple Choices(有多个版本)
第三步:客户端选择想要的版本,再次请求
质量值(Quality Value)
客户端用 q=数值 来表示偏好程度,范围 [ 0 , 1 ] [0, 1] [0,1]:
- q = 1 q = 1 q=1:最喜欢(不写 q 值时默认为 1)
- q = 0.5 q = 0.5 q=0.5:一般
- q = 0 q = 0 q=0:绝对不要
例子:三语用户,偏好中文,其次西班牙语,法语凑合,德语不要:
Accept-Language: zh-CN, sp;q=0.7, fr;q=0.3, de;q=0
翻译成人话:“请给我中文版;没有的话西班牙语也行;再没有法语也凑合;反正不要德语。”
Accept 头的通配符用法:
Accept: text/html, text/*;q=0.6, */*;q=0.1
含义:
- 最想要 HTML 文档(q=1)
- 其他文本格式也行(q=0.6)
- 实在不行,什么格式都收(q=0.1)
13. HTTP 缓存机制
缓存的核心思想
Web 用户经常反复请求同样的资源。与其每次都从服务器取,不如把最近用过的存起来:
不用缓存(每次都要完整走一遍):
客户端 → 代理1 → 代理2 → 服务器
客户端 ← 代理1 ← 代理2 ← 服务器
使用缓存(代理2有缓存,直接返回):
客户端 → 代理1 → 代理2
客户端 ← 代理1 ← 代理2
服务器根本不知道有这次请求!
好处:
- 减少网络流量
- 加快响应速度(就近取,不用跑远路)
缓存在哪里?
[用户电脑上的浏览器缓存] ←── 最近,只有你自己受益
↑
[公司/ISP 代理服务器缓存] ←── 居中,整个组织受益
↑
[Web 服务器缓存] ←── 最远,所有用户受益,
但每次还是要走网络
三角关系:离用户越近 = 个人受益越大;离用户越远 = 受益人数越多。
缓存的主要问题
缓存老化(Staleness):缓存里的内容可能已经过时。
你今天早上缓存了 CNN 首页
你下午再看,但缓存里还是早上的新闻
用户看到的是"旧新闻"——这就是缓存过期问题
解决方法:
- 服务器在响应头中加
Expires或Cache-Control: max-age=秒数告诉缓存多久过期 - 缓存过期后,必须向服务器验证(Validation)是否真的变了
条件请求用于验证:
客户端(有旧缓存):
GET /news.html HTTP/1.1
If-Modified-Since: Mon, 14 May 2026 08:00:00 GMT
服务器回应(如果没变):
HTTP/1.1 304 Not Modified ← 没变,用你的缓存就行,我不发内容了
服务器回应(如果变了):
HTTP/1.1 200 OK ← 变了,给你新版本
(附带新的 HTML 内容)
304 状态码节省的流量
假设一个网页 200KB,每天被访问 100 万次,内容很少改变:
- 没有 304 机制:每次传 200KB → 每天 200 KB × 100 万 = 200 GB 200\text{KB} \times 100\text{万} = 200\text{GB} 200KB×100万=200GB
- 有 304 机制:大部分请求只返回几十字节的"没变" → 流量可以减少 90 % 90\% 90% 以上
14. 代理服务器
代理是什么?
代理(Proxy)是位于客户端和服务器之间的中间人:
代理的主要用途
安全过滤:
客户端的请求 → 代理检查 → 发现包含恶意内容/访问违规网站 → 拦截
缓存加速:
用户A请求 baidu.com → 代理获取并缓存
用户B请求 baidu.com → 代理直接从缓存返回(不去服务器)
卫星网络延迟优化(实例):
问题:卫星来回延迟 500ms,一个有 20 张图片的页面需要:
500ms(HTML)+ 20×500ms(每张图)= 10500ms ≈ 10秒!
解决:ISP 的代理预取页面中所有图片:
500ms(HTML + 代理同时获取所有图片)= 约 1-2秒
代理 vs 缓存的区别
| 对比项 | 代理服务器 | 缓存 |
|---|---|---|
| 是什么 | 独立的中间设备 | 可以在任何设备上实现 |
| 如何使用 | 客户端需要手动配置 | 自动启用 |
| 主要功能 | 转发+过滤+缓存+认证 | 存储最近使用的资源 |
| 关系 | 代理可以包含缓存 | 缓存可以在代理里 |
15. HTTP 安全与 Cookie
HTTP 认证方式
基本认证(Basic Auth):
1. 客户端请求受保护资源:
GET /secret.html HTTP/1.1
2. 服务器要求认证:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="My Site"
3. 客户端发送凭据(Base64编码的 用户名:密码):
GET /secret.html HTTP/1.1
Authorization: Basic dXNlcjpwYXNz
4. 验证通过,返回资源
问题:Base64 只是编码,不是加密,容易被截获。
摘要认证(Digest Auth):使用 MD5 等加密算法,比基本认证更安全,但实现更复杂。
HTTPS = HTTP + SSL
对于敏感数据(网银、购物、登录),需要使用 HTTPS:
普通 HTTP:
http://www.bank.com → 端口 80,明文传输,可被监听
安全 HTTPS:
https://www.bank.com → 端口 443,SSL/TLS 加密,无法被监听
Cookie:状态管理
HTTP 本身是无状态的:服务器处理完每个请求就"忘记"了,不记得上一次请求是谁。
这对购物车、登录状态等应用来说是大问题:
无状态的问题:
第1次请求:用户登录
第2次请求:服务器不认识你了,要你再登录
第3次请求:服务器还是不认识你...
Cookie 解决了这个问题:
Cookie 就是服务器存到客户端电脑上的一小块数据,记录用户信息。
Cookie 的隐患
| 隐患 | 描述 |
|---|---|
| 敏感信息泄露 | Cookie 被截获可能暴露登录信息 |
| 跨站追踪 | 广告商通过第三方 Cookie 追踪你访问了哪些网站 |
| 第三方 Cookie | 加载了广告图片的网站可以悄悄给你设 Cookie |
第三方 Cookie 的例子:
你访问 www.news.com:
→ 页面里有一个来自 ads.tracker.com 的广告图片
→ ads.tracker.com 给你设置了 Cookie
→ 下次你访问 www.shopping.com(也有该广告商的广告)
→ ads.tracker.com 看到了你的 Cookie
→ 它知道你同时访问了 news.com 和 shopping.com
→ 开始给你推送相关广告
这就是为什么现代浏览器开始限制第三方 Cookie。
16. 整体架构总结
一次完整的 Web 访问流程
用户在浏览器输入 http://www.example.com/index.html,到页面显示出来的全过程:
步骤1:DNS 解析
www.example.com → 查询 DNS → 得到 IP 地址 1.2.3.4
步骤2:建立 TCP 连接
浏览器 → TCP 三次握手 → 1.2.3.4:80
步骤3:发送 HTTP 请求
GET /index.html HTTP/1.1
Host: www.example.com
Accept: text/html
...
步骤4:服务器处理并响应
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 5000
...
(HTML内容)
步骤5:浏览器解析 HTML
发现有 3 张图片引用 → 发出 3 个新的 GET 请求(持久连接,不重新握手)
步骤6:接收图片数据,渲染页面
步骤7:发送 Connection: close,关闭 TCP 连接
各协议和技术的分工
端口号速查
| 协议 | 端口 | 说明 |
|---|---|---|
| HTTP | 80 | 标准网页访问 |
| HTTPS | 443 | 加密网页访问 |
| HTTP(备用) | 8080 | 有时用于测试环境 |
HTTP 状态码分类速查
| 类别 | 范围 | 通俗记忆 |
|---|---|---|
| 1xx | 100-199 | “收到了,继续” |
| 2xx | 200-299 | “成功!” |
| 3xx | 300-399 | “去别处找” |
| 4xx | 400-499 | “你的锅” |
| 5xx | 500-599 | “我的锅” |
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)