网络知识点 - TCP/IP 四层模型知识大扫盲
一、计网基础概念
第一章先总体回顾一下
1.1 OSI 七层模型与 TCP/IP 四层模型
| OSI 模型 | TCP/IP | 核心职责 | 常见协议 | iOS 关联 |
|---|---|---|---|---|
| 7.应用层 | 4. 应用层 | 提供应用服务接口,定义数据格式与交互语义 | HTTP, HTTPS, DNS, WebSocket | NSURL、URLSession |
| 6.表示层 | (并入应用层) | 加密、压缩、格式转化 | SSL/TLS, JSON, JPEG, UTF-8 | |
| 5.会话层 | (并入应用层) | 会话管理、状态保持 | TLS 握手、RPC、Session | |
| 4.传输层 | 3. 传输层 | 端到端通信、可靠传输 | TCP、UDP、QUIC | Socket |
| 3.网络层 | 2. 网络层 | 路由寻址、分片 | IP、路由、ICMP、ARP | |
| 2.数据链路层 | 1. 网络接口层 | MAC寻址、帧封装 | MAC、Wi-Fi、Ethernet | |
| 1.物理层 | (并入网络接口层) | 比特流传输 | 光线、电缆、无线信号 |
网络通信是一条从 应用层 → 内核网络栈 → 网络接口 → 传输介质 → 对端主机 的完整路径。
1.2 数据封装与解封装
数据在网络中是如何传递的?
- 以 TCP 为例:
你的 App 数据:{"name":"张三"}
│
▼ ④ 应用层:加 HTTP 头(方法、路径、Host、…)
┌──────────────────────────────────────┐
│ HTTP Header │ JSON: {"name":"张三"} │ ← 消息(Message)
└──────────────────────────────────────┘
│
▼ ③ 传输层:加 TCP 头(源端口、目的端口、序列号、…)
┌────────────────────────────────────┐
│ TCP Header │ HTTP Header │ JSON │ ← 段(Segment)
└────────────────────────────────────┘
│
▼ ② 网络层:加 IP 头(源IP、目的IP、TTL、协议号=6)
┌──────────────────────────────────────────────┐
│ IP Header │ TCP Header │ HTTP Header │ JSON │ ← 包(Packet)
└──────────────────────────────────────────────┘
│
▼ ① 网络接口层:加 MAC 头(源MAC、目的MAC) + 尾部校验(FCS)
┌────────────────────────────────────────────────────────────────┐
│ MAC Header │ IP Header │ TCP Header │ HTTP Header │ JSON │ FCS │ ← 帧(Frame)
└────────────────────────────────────────────────────────────────┘
- 以 UDP 为例:
你的 App 数据:{"query":"example.com"}
│
▼ ④ 应用层:加上应用层协议头(例如 DNS / 自定义协议)
┌──────────────────────────────────────────────┐
│ DNS Header │ JSON: {"query":"example.com"} │ ← 消息(Message)
└──────────────────────────────────────────────┘
│
▼ ③ 传输层:加 UDP 头(源端口、目的端口、长度、校验和)
┌──────────────────────────────────────────────┐
│ UDP Header │ DNS Header │ JSON │ ← 数据报(Datagram)
└──────────────────────────────────────────────┘
│
▼ ② 网络层:加 IP 头(源IP、目的IP、TTL、协议号=17)
┌────────────────────────────────────────────────────────┐
│ IP Header │ UDP Header │ DNS Header │ JSON │ ← 包(Packet)
└────────────────────────────────────────────────────────┘
│
▼ ① 网络接口层:加 MAC 头(源MAC、目的MAC) + 尾部校验(FCS)
┌────────────────────────────────────────────────────────────────────────────┐
│ MAC Header │ IP Header │ UDP Header │ DNS Header │ JSON │ FCS │ ← 帧(Frame)
└────────────────────────────────────────────────────────────────────────────┘
接下来按照数据封装的顺序,依次回顾一下各个层级的知识。
二、应用层(应用层 + 表示层 + 会话层)
App 层逻辑,包括 DNS、HTTP、HTTPS、WebSocket、缓存、认证体系。
2.1 DNS 域名解析
作用: 将域名 https://some.com 解析为 IP 地址,是一切请求的起点。
-
从 输入网址 -> 页面返回,过程是怎样的?
- 解析顺序: 浏览器缓存 → OS 缓存 → hosts → 本地 DNS → 根服务器 → 顶级域服务器 → 权威服务器;
- 返回 IP 后进行 TCP(三次握手)→ TLS 握手 → HTTP 请求。
┌──────────────────────────────────────┐ │ 访问 https://api.example.com/path │ └──────────────────────────────────────┘ ① 用户输入网址 └──> 浏览器解析 URL 协议 = https、主机 = api.example.com、端口 = 443、路径 = /path ② DNS 解析域名(api.example.com → IP) │ ├─ 1) 查 [本机缓存](浏览器 DNS 缓存 → OS DNS 缓存 → hosts 文件) │ ├─ 2) 未命中 → 请求 [本地 DNS 服务器](路由器 / 运营商) │ └─ 3) 仍未命中 → [本地 DNS 服务器] 发起 [迭代查询](由它代跑,浏览器只等结果) │ ├─ 问 [根 DNS](.) │ └─ 回答:"去问 .com 的服务器" ├─ 问 [顶级域 DNS](.com) │ └─ 回答:"去问 example.com 的权威服务器" └─ 问 [权威 DNS](example.com) └─ 回答:"api.example.com = 203.0.113.8" ✓ └─ 本地 DNS 缓存结果(根据 TTL),返回给浏览器 ③ 建立 TCP 连接(三次握手) ④ TLS 握手(HTTPS 特有,在 TCP 之上建立加密信道) ⑤ 发送 HTTP 请求(数据从上到下逐层封装) └──> [1.2 数据封装与解封装] 知识点 ⑥ 服务器处理请求并返回响应 │ ├─ 1) 解封帧(拆 MAC -> ... -> 拿到 HTTP 请求) │ ├─ 2) 业务处理(路由匹配 -> 鉴权 -> 查库 -> 生成结果) │ └─ 3) 构建 HTTP 响应,逐层封装,发回客户端 ⑦ 浏览器接收响应 ⑧ 连接关闭(四次挥手,或保持复用) ├─ HTTP/1.1 Keep-Alive → 连接放入连接池,后续请求复用 ├─ HTTP/2 → 同一连接上继续多路复用 └─ 不再需要时 → 四次挥手断开: -
DNS 劫持
-
现象:
- App -> 运营商 DNS 服务器(可能被劫持)-> 返回错误的 IP
-
解决方案:HTTPDNS(绕过运营商,App 直连 DNS 服务)
- App -> 通过 HTTP 直接请求 HTTPDNS 服务器(绕过运营商)-> 返回正确的 IP
-
现象:
2.2 HTTP 协议
HTTP(80)、HTTPS(443)
HTTP 是一种基于 “请求-响应” 的无状态的应用层协议,每次请求都是独立的。
最初就是为浏览器与 Web 服务器设计的。
2.2.1 HTTP 基本信息
- HTTP 请求 = 请求行 + 请求头 + 空行 + 请求体
POST /api/users HTTP/1.1 ← 请求行:方法、路径、版本
Host: api.example.com ← 请求头:多行参数
...
...
← 空行:分隔头和体
{"name":"张三","email":"z@example.com"} ← 请求体
- HTTP 响应 = 状态行 + 响应头 + 空行 + 响应体
HTTP/1.1 201 Created ← 状态行:版本、状态码、描述
Content-Type: application/json ← 响应头
...
...
← 空行
{"id":456,"name":"张三","created":true} ← 响应体
2.2.2 HTTP 常见的请求方法
| 方法 | 语义 | 幂等? | 安全? | 有请求体? | 典型场景 |
|---|---|---|---|---|---|
| GET | 获取资源 | ✅ | ✅ | 通常没有 | 获取用户列表、详情页 |
| POST | 创建资源/提交数据 | ❌ | ❌ | ✅ | 注册、下单、上传 |
| DELETE | 删除资源 | ✅ | ❌ | 通常没有 | 删除订单 |
| HEAD | 同 GET 但只要头部 | ✅ | ✅ | 没有 | 检查资源是否更新 |
-
幂等: 执行 1 次和执行 N 次效果相同。
- GET 请求 10 次,拿到的是同一份数据,幂等✅;
- DELETE 删 10 次,资源还是被删了(第 2 次返回 404),幂等✅;
- 但 POST 创建订单 10 次,可能创建 10 哥订单,不幂等❌;
-
安全: 不会修改服务器资源。
- GET、HEAD 不会修改服务器资源,只读,安全✅;
- POST/DELETE 有写操作,不安全❌;
思考: 实际项目中,为什么大部分是 POST 而非 GET?大部分场景不是只需要 “读” 吗?
- 为安全、加密、签名、防重放、复杂度等。
-
请求体加密
- 很多项目会对请求参数做 AES 等对称加密,将整个参数序列化后加密放在 request body 中传输。GET 请求没有 body,参数只能拼在 URL query string 里,无法做 body 级别的加密。
-
签名机制与 HTTP 方法绑定
NSString *source = [NSString stringWithFormat:@"POST&%@&%@", pathEncoding, paramEncoding]; NSString *hash = [self HmacSha1:kAppKey data:source];- 常见的 API 签名方案会把 HTTP 方法作为签名原文的一部分,客户端和服务端按同一规则生成签名并校验。一旦签名协议绑定了 POST,改用 GET 会导致签名不一致、请求被拒绝。
-
公共参数太多,URL 长度受限
- 每个请求通常会自动携带大量公参(设备信息、版本号、签名、时间戳、MD5、...),GET 的请求参数在 URL 里:
- URL 容易超长,超出 中间代理/CDN 的长度限制;
- 参数结构复杂时,编码笨重;
- 每个请求通常会自动携带大量公参(设备信息、版本号、签名、时间戳、MD5、...),GET 的请求参数在 URL 里:
-
敏感信息不易暴露在 URL 中
- GET 请求的参数在 URL 里,会被以下环节明文记录:
- 服务端 access log
- CDN 日志
- 浏览器/WebView历史记录
- 网络抓包/运营商等
- 而用户凭证(uid/sid/token)、设备指纹、签名等都是敏感数据。
- GET 请求的参数在 URL 里,会被以下环节明文记录:
-
防重放机制的需要
- 为了防止请求被截获后重放,通常每个请求都会带上 nonce(随机数)和 millisecond(时间戳),并参与签名计算,GET 请求的缓存机制反而会造成干扰。
- POST 天然不会被缓存,与防重放设计更契合。
-
统一方案降低复杂度
- 如果 GET 和 POST 混用,就需要【签名逻辑、加密方案、服务端解析逻辑、网关/中间件的安全策略】 等都需要区分两套,维护成本升高。
2.2.3 HTTP 常见的状态码
|状态码 | 说明 |
| --- | -- |
|`1xx`(上传前试探)|`100`: 服务器说"继续发吧",用于大文件上传前的试探<br>`101`: 协议升级,WebSocket 握手就用这个|
|`2xx`(成功)|`200`: 最常见的 OK|
|`3xx`(重定向)|`301`: 永久重定向|
|`4xx`(客户端错误)|`400`: 请求格式错误<br>`401`: 没登录或Token过期<br>`403`: 登录了但没权限<404>资源不存在|
|`5xx`(服务端错误)|`500`: 服务器崩了|
2.3 HTTP 缓存机制
HTTP 缓存分为两阶段机制: 强缓存(freshness)→ 协商缓存(validation)
“先看时间(强缓存),再问服务器(协商缓存)。”
-
首次请求
- 服务器返回资源 + 缓存控制信息(如 Cache-Control, ETag, Last-Modified)。
-
强缓存阶段(freshness)
- 客户端检查本地缓存是否仍在有效期(由 Cache-Control: max-age 或 Expires 判断)。
- 若未过期 → 直接使用本地副本,不访问服务器。
-
协商缓存阶段(validation)
- 若 强缓存过期或被标记需验证,则 客户端带验证头请求服务器:
If-None-Match: <etag>- 或
If-Modified-Since: <time>
- 服务器判断资源是否变化:
-
未变化 →
304 Not Modified(仅返回头部,客户端复用旧内容); -
已变化 →
200 OK(新资源内容)。
-
未变化 →
- 若 强缓存过期或被标记需验证,则 客户端带验证头请求服务器:
- 常见 Header:
| 分类 | Header | 作用 |
|---|---|---|
| 强缓存 | Cache-Control: max-age=3600 |
指定可直接使用的秒数 |
| Expires: | 老式写法,被 Cache-Control 覆盖 |
|
| 协商缓存 |
ETag / If-None-Match
|
内容标签验证 |
Last-Modified / If-Modified-Since
|
修改时间验证 | |
| 其他 | Vary |
声明缓存与哪些请求头有关 |
private / public
|
是否允许代理缓存 |
- 常见配置:
| 场景 | Header 示例 |
|---|---|
| 静态资源 | Cache-Control: public, max-age=31536000, immutable |
| 动态接口 | Cache-Control: no-cache + ETag |
| 敏感数据 | Cache-Control: no-store |
2.4 Cookie、Session、Token —— 认证三兄弟
HTTP 是无状态协议 ———— 服务器不记得你是谁。每次请求都是独立的,但是有很多场景需要 “记住用户”(登录态、用户身份等),于是就有了他们仨。
| 名称 | 存储位置 | 主要作用 | 特点 |
|---|---|---|---|
| Cookie | 浏览器 / 客户端 | 存放少量数据,携带 Session ID
|
每次请求自动携带到服务器 |
| Session | 服务端 | 保存用户状态(如登录态) | 有状态,需要共享。 依赖 Cookie 或 URL 中的 Session ID
|
| Token | 客户端 | 身份凭证(常为加密签名) | 服务端无状态验证,跨端通用 |
Web 用 Session,App 常用 Token 鉴权。
-
Cookie
-
本质: HTTP 头中由服务器通过
Set-Cookie下发的 键值对。客户端保存后,在后续请求 自动携带 Cookie 头。 -
示例:
Set-Cookie: session_id=abc123; HttpOnly; Secure; Max-Age=3600Cookie: session_id=abc123
-
本质: HTTP 头中由服务器通过
-
Session(服务端状态)
-
流程:
- 用户登录 -> 服务端验证成功,生成唯一
Session ID; - 服务端保存登录信息(uid、权限等)到内存或 redis;
- 将
Session ID下发给客户端(通常通过 Cookie); - 后续请求客户端自动携带
Session ID, 服务器查表恢复状态。
- 用户登录 -> 服务端验证成功,生成唯一
-
特点:
- 状态保存在服务器(有状态);
- 适合小规模 / 单机服务;
- 分布式时需要共享 Session (如 Redis 集中存储)。
-
-
Token(无状态身份验证)
- 原理: 服务端不保存状态,只验证 Token 的合法性。
-
常见类型:
-
JWT(JSON Web Token):Header.Payload.Signature 三段式 Base64 编码。
-
-
验证流程:
- 登录成功后生成 Token(带用户信息 + 过期时间 + 签名)。
- 客户端保存(如 iOS Keychain).
- 每次请求带头部:
Authorization: Bearer <token> - 服务端验签(是否过期、签名是否匹配)。
-
特点:
- 无需服务器保存状态;
- 一旦签发,撤销复杂(需要黑名单机制);
- Token 通常短期有效,需要配合 Refresh Token 使用。
Session 与 Token 对比
| 项目 | Session | Token |
|---|---|---|
| 状态保存 | 服务端 | 客户端(无状态) |
| 可扩展性 | 弱(需共享 Session) | 强(验证即可) |
| 安全性 | 依赖 Cookie 保护 | 依赖签名/加密 |
| 登出控制 | 服务端可立即失效 | 需黑名单或等待过期 |
| 常见场景 | Web 登录态 | 移动端 / API 鉴权 |
2.5 HTTPS 与 TLS
TLS 是 SSL 的后继协议,SSL 已被淘汰。
HTTPS = HTTP + TLS。TLS 在 TCP 之上、HTTP 之下,提供三大安全保障:
| 保障 | 含义 | 实现方式 |
|---|---|---|
| 加密(Encryption) | 防窃听 | 对称加密(AES) |
| 认证(Authentication) | 防伪造 | 数字证书 + CA 体系 |
| 完整性(Integrity) | 防篡改 | MAC(消息认证码) |
即 HTTPS = HTTP + 加密 + 认证 + 完整性
TLS 握手流程:
![]()
- 客户端发送 随机数与算法列表;
- 服务端返回 证书与随机数;
- 客户端 验证证书,生成 会话密钥;
- 使用 非对称算法 安全交换 对称密钥,后续双方使用 对称密钥加密通信。
TLS 1.3 优化了流程:
- 握手仅需 1-RTT(一次往返),更快;
- 默认强加密算法(如 AES-GCM、ChaCha20);
- 支持 0-RTT 快速重连。
iOS 强制使用 TLS1.2+(ATS),支持证书 Pinning。
| 类型 | 用途 | 特点 |
|---|---|---|
| 非对称加密(RSA、ECDHE) | 握手阶段,用于密钥协商 | 安全但慢 |
| 对称加密(AES、ChaCha20) | 传输阶段,用同一密钥加解密数据 | 快速 |
2.6 WebSocket 协议
HTTP 是"你问我答"(请求-响应)模式。客户端不问,服务器不答。但很多场景需要服务器 主动推送.
- 在 WebSocket 之前,人们用各种"土办法"模拟:
方案 原理 缺点 轮询(Polling) 客户端定时发 HTTP 请求(如每 3 秒一次) 浪费带宽和电量,延迟高 长轮询(Long Polling) 客户端发请求,服务器有数据时才响应,否则挂起直到超时 服务器资源开销大 SSE(Server-Sent Events) 服务器单向推送 只能服务器→客户端,不支持双向
而 WebSocket 才是真正的解决方案:全双工、持久连接、双方可以随时发数据。
- iOS 实践: 使用 URLSessionWebSocketTask。
WebSocket 握手
- 本质: 就握手本质就是一个 HTTP 请求,只不过请求目的不是获取数据,而是 请求协议升级。
-
步骤:
- 客户端发送一个特殊的 HTTP 请求 GET /chat HTTP/1.1 ← 还是普通的 HTTP 请求 Host: server.example.com Upgrade: websocket ← "我想升级为 WebSocket 协议" Connection: Upgrade ← "这是一个升级请求" Sec-WebSocket-Key: dGhlIHNhbXBsZQ== ← 随机 Base64 值(防伪造) Sec-WebSocket-Version: 13 ← WebSocket 协议版本 Origin: example.com ← 来源(可选,用于安全校验)
- 服务器同意升级 HTTP/1.1 101 Switching Protocols ← 101 = "我同意切换协议了" Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGz... ← 基于 Key 计算的值(验证对方确实懂 WebSocket)
- 协议升级完成
- 从此刻起,这条 TCP 连接不再说 HTTP 了,双方改说 WebSocket 的帧协议(Frame Protocol),双向随时发数据,没有请求-响应的限制。
握手前(HTTP): 握手后(WebSocket):
┌──────────────┐ ┌──────────────┐
│ HTTP/1.1 │ ── 升级 ──→ │ WebSocket │
│ 文本协议 │ │ 二进制帧协议 │
│ 请求-响应 │ │ 全双工 │
│ 头部几百字节 │ │ 帧头 2~14字节 │
└──────────────┘ └──────────────┘
↑ ↑
还是跑在 TCP 之上 还是同一条 TCP 连接
(如果 wss:// 则还有 TLS) (连接没断,只是协议变了)
-
Sec-WebSocket-Key/Sec-WebSocket-Accept是做什么的?- 简单的 防伪造机制, 不是加密,验证对方是否是 WebSocket 服务器。
WebSocket vs HTTP 对比
| 维度 | HTTP | WebSocket |
|---|---|---|
| 通信模式 | 请求-响应(半双工) | 全双工 |
| 连接 | 短连接或 Keep-Alive | 持久连接 |
| 头部开销 | 每次请求带完整头部(几百字节) | 握手后每帧只有 2~14 字节头部 |
| 服务器推送 | 不支持(只能客户端发起) | 支持 |
| URL 协议 | http:// / https:// | ws:// / wss:// |
| 适用场景 | API 调用、资源获取 | 实时通信、推送、游戏 |
iOS 一般使用原生的
URLSessionWebSocketTask(iOS 13+)。
为什么有些项目使用自定义协议(例如[包长度|protobuf序列化数据]),不使用 WebSocket?
| WebSocket | 自定义协议 | |
|---|---|---|
| 建连开销 | TCP 握手 + HTTP 升级握手(多一个 RTT) | TCP 握手后直接发数据 |
| 帧头开销 | 2~14字节(opcode/mask/长度) | 4字节(只写长度就够了) |
| 心跳 | 固定的 ping/pong | 自定义 Protobuf 心跳(可带业务数据) |
| 压缩 | 整个连接统一压缩策略 | 不同消息可选不同的压缩策略 |
| 传输层 | 只能 TCP | 可以 TCP / KCP 等随时切换 |
| 浏览器兼容 | ✅这是它存在的意义 | ❌但是 App 不需要 |
2.5 HTTP 版本演进(HTTP/1.0 -> HTTP/1.1 → HTTP/2 → HTTP/3)
-
HTTP/1.0
- 引入请求头、状态码。
-
问题:
- 最初版本,每次请求都需要重新建立连接。
-
HTTP/1.0 -> HTTP/1.1()
- 问题: HTTP/1.0 每次请求都要新建 TCP 连接,用完就断开,资源浪费;
-
改进:
- 持久连接(Keep-Alive): 默认不断开,复用同一个 TCP 连接发送多个请求;
- 管道化(Pipelining): 连续发送 多个请求,不用等待上一个响应回来。
-
仍有问题:
- 对头阻塞(HTTP 层): HTTP/1.1 规定响应必须按照请求顺序返回,如果第 1 个请求慢了,后续全阻塞。
-
HTTP/1.1 -> HTTP/2(大幅性能提升,显著减少延迟与连接数量)
-
改进:
- 二进制分帧: 不再是文本协议,把数据拆成带有 Stream ID 标记的帧(Frame)传输;
-
多路复用: 一个 TCP 连接上 并行传输 多个请求和响应的帧,互不阻塞;
- ‼️每个帧都标记了 Stream ID,接收方根据 ID 把帧重新组装成各自的响应。帧可以交错发送,谁先好就先发谁—— HTTP 层的队头阻塞 解决了。
- 头部压缩(HPACK): HPACK 用静态表+动态表+哈夫曼编码大幅压缩头部;
- 服务器推送(Server Push): 服务器主动推送资源;
-
仍有问题:
- ‼️多路复用虽然解决了 HTTP 层的队头阻塞,但底层 TCP 的对头阻塞还在——TCP 丢一个包,整个连接上所有 Stream 都得等重传。
- ‼️因为底层用的还是 一条 TCP 连接,TCP 的特性 保证字节流严格有序到达,所有 Stream 共享一条字节流,空洞就等,等到补齐为止。
- ‼️多路复用虽然解决了 HTTP 层的队头阻塞,但底层 TCP 的对头阻塞还在——TCP 丢一个包,整个连接上所有 Stream 都得等重传。
-
改进:
-
HTTP/2 -> HTTP/3(基于 QUIC,连更低的头部延迟与更快的握手)
-
改进:
特性 HTTP/2 (TCP) HTTP/3 (QUIC/UDP) 传输层 TCP UDP(自己实现可靠传输) 队头阻塞 TCP 层有 完全消除(各 Stream 独立) 握手延迟 TCP 握手 + TLS 握手 QUIC 内置 TLS,1-RTT 重连 全新握手(WiFi→蜂窝) 0-RTT 恢复(快速重连) 连接迁移 基于四元组,换 IP 就断 Connection ID 标识,换 IP 不断 -
‼️解决 TCP 层对头阻塞
- 不用 TCP 了,在 UDP 上自己造一个传输层,让每个 Stream 都有独立的序号和重传机制,每个 Stream 有自己独立的字节流,互不干扰。
- 对 iOS 端的意义: 用户在 WiFi 和蜂窝之间可以做到无感切换
传统 TCP(HTTP/2): 用户走出公司(WiFi)→ 走上街(蜂窝) → IP 地址变了 → TCP 连接断了 → 重新握手 QUIC(HTTP/3): 用户走出公司(WiFi)→ 走上街(蜂窝) → IP 变了但 Connection ID 没变 → 连接无缝切 -
‼️解决 TCP 层对头阻塞
-
三、传输层:TCP、UDP、QUIC、KCP
3.1 TCP
TCP 是一个 面向连接、可靠、有序、全双工 的传输协议。在建立连接前,双方需要达成 3 个共识:
- 确认通信能力: 双向通信,双方都能收能发。
- 交换初始序列号(ISN): 每个字节都有序号,双方要各自告知自己的起始序号。
- 防止历史连接的干扰: 网络中的旧报文不能让新连接误认为是有效的。
三次握手就是为了解决这三件事。
3.1.1 TCP 三次握手
![]()
-
过程:
- 第 1 次: C -> 连接请求 (SYN) -> S
- 第 2 次: S -> 收到请求后,发送连接请求的确认请求 (SYN+ACK) -> C
- 第 3 次: C -> 收到请求后,发送确认请求的确认请求 (ACK) -> S
常见问题:
1. 为什么不是 2/4 次握手,而是 3 次握手?
-
2 次握手,双端状态可能不一致,存在 “半开连接” 和 “旧连接干扰” 问题。
-
3 次握手 的作用:
-
确认双向通信可达:
- 3 次握手保证 C、S 双方相互得知对方的收发能力正常;
- 如果只有 2 次握手,S 无法得知 C 的接收能力是否正常。
-
防止旧连接干扰:
- 3 次握手通过新的 ISN 和确认机制,可以识别出过期连接并丢弃。
- 如果只有 2 次握手,S 可能错误建立 “脏连接”,造成状态混乱。
-
建立可靠传输基础:
- 3 次握手双方交换初始序列号,建立有序的、基于字节流的协议。
-
确认双向通信可达:
2. 如果第 3 次握手丢失,服务端、客户端分别如何处理?
第 3 次握手丢失会造成短暂的 “半开连接”,但 TCP 的 重传与超时机制 最终会恢复一致性。
-
客户端
- 进入
ESTABLISHED,认为连接成功,继续等待 S 响应或者发送数据。
- 进入
-
服务端
- 仍在
SYN_RECEIVED,等待 ACK,定时器到期后会重传 SYN+ACK。 - C 收到重传的 SYN+ACK 后,会再次发送 ACK;
- 若多次重传仍无回应,S 会放弃连接,并释放 半连接资源;
- C 尝试发数据,若 S 未进入
ESTABLISHED,可能收到 RST 响应。
- 仍在
3. 为什么 ISN(初始序列号)不固定?
-
防止旧连接干扰:
- 网络中可能残留旧包,若新连接的 ISN 与旧连接相同,旧报可能被误认为是当前连接的数据。
-
增强安全性:
- 防止攻击者通过预测序列号来伪造 TCP 报文(TCP 盲注入攻击、会话劫持);
- RFC 要求 ISN 必须 随时间变化且不可预测,现代操作系统(Linux/iOS/macOS)也都采用 “时间戳+随机扰动” 动态生成方式。
4. 3 次握手的过程中是否携带数据?
标准的 TCP 行为 是不会在握手阶段携带数据的:
- 前 2 次握手 不携带数据,是因为 连接尚未建立,接收方未分配接收缓冲区,若携带数据,可能造成安全与资源浪费问题(攻击者伪造 SYN 报文携带大量数据)。
-
第 3 次握手 理论上可以携带数据,因为 C 已经进入
ESTABLISHED状态了。但是大多实现出于简化和安全考虑,也不在 ACK 中携带数据。-
实现简化:
- S 的 接收缓冲区和应用层 socket 可能还没准备好,处理逻辑复杂。
-
安全问题:
- 防止 DoS 攻击和数据误处理(若 ACK 丢失或重传,可能导致同一份数据被重复处理,S 在连接未确认时读渠道未认证的数据,会产生安全隐患(伪造、注入等风险))。
-
实现简化:
特殊情况: TCP Fast Open (TFO) 允许在 SYN 报文中携带应用数据并提前发出,实现 “0- RTT 建连”,但需要 S 支持。
5. “半开连接” 是怎么产生的?
- 概念: 半开连接(Half-Open Connection) 是指连接的两端状态不同步:一方认为连接已建立或仍存在,另一方实际上未建立或已断开。
- 后果: 半连接堆积(如 SYN Flood 攻击)。
- 检测手段: 内核(SYN Cookies、防火墙限速)、应用层(心跳检测、超时挥手)。
-
握手阶段丢包: 第 3 次握手丢失,S 进入
SYN_RECEIVED,客户端已进入ESTABLISHED。- 服务端 会将半开连接存放在 半连接队列 中。
-
已连接后异常断线: C 退出 App,S仍认为连接存在,处于
ESTABLISHED,等待数据。- 需要依赖 KeepAlive 心跳 检测。
-
关闭阶段异常: 某端未正确完成四次挥手(如
FIN/ACK丢失)。- 双方状态不一致(
FIN_WAIT/CLOSE_WAIT)。
- 双方状态不一致(
扩展一下
1. 全双工 vs 半双工
全双工: 指通信双方可以 同时 进行发送和接收数据。
半双工: 双方都可以发送数据,但 不能同时,只能 “你说完我再说”。
| 模式 | 含义 | 能否同时发送/接收 | TCP 中体现 |
|---|---|---|---|
| 单工 | 单向通信 | 否 | 几乎不用 |
| 半双工 | 双向但不同时 | 否 | 早期物理层通信 |
| 全双工 | 双向同时 | 是 | TCP 是全双工协议 |
| 半关闭 | 一方发送关闭,但可接收 | 仅一方向关闭 | TCP 四次挥手中的状态 |
2. 全连接队列 vs 半连接队列
服务端 TCP 内核实现层面的关键概念,
全连接队列: 存放 3 次握手已完成,但还未被应用层 accept() 接收的连接。
半连接队列: 存放已收到客户端 SYN,但 3 次握手尚未完成 的连接。
握手过程:
| 步骤 | 状态 | 队列 |
|---|---|---|
| C -> 发送 SYN | S 进入 SYN_RECIEVED
|
加入 半连接队列 |
| S -> 发送 SYN+ACK,等待 ACK | C 进入 ESTABLISHED
|
半连接队列 中 等待确认 |
| C -> 回 ACK 包 | S 进入 ESTABLISHED
|
从 半连接队列 移入 全连接队列 |
特点:
- 如果第 3 次握手迟迟不到,连接会在队列中等待一段时间,知道超市或超过最大重传次数,就删除这个 半连接。
- 若 全连接队列 已🈵,新握手完成的连接会被 丢弃/拒绝。
- 应用层调用 accept() 是,才会将连接从 全连接队列 中取出。
3.1.2 TCP 四次挥手
![]()
-
过程:
- 第 1 次: C -> FIN -> S,客户端主动发送 FIN 关闭发送通道,服务端收到 FIN 后 关闭接收通道。
- 第 2 次: S -> ACK -> C,服务器仍可继续 发送剩余数据。
- 第 3 次: S -> FIN -> C,服务端发送完所有数据后,主动发送 FIN 关闭发送通道,客户端收到 FIN 后 关闭接收通道。
- 第 4 次: C -> ACK -> S,服务端收到 ACK 后 立即关闭连接,客户等待 2MSL 后 彻底关闭连接,确保最后的 ACK 能被对方收到并防止旧包干扰。
常见问题:
1. 为什么是 “4 次挥手”,而不是 “3 次”?
- 因为 TCP 是 全双工协议,双方的发送和接收通道是 独立的,每个方向都必须 单独关闭,因此有了 “4 次” 挥手。
- 当一方(主动方)发出 FIN,只表示 “不再发送数据”,但是 “还能接收数据”;
- 另一方(被动方)可能还没有发完数据,所以不能立即 FIN;
- 另一方(被动方)必须等到自己也准备关闭时,再单独发出 FIN;
- 因此需要一次 FIN 一次 ACK,再一次 FIN 一次 ACK,总共四次,确保双方都能 安全、完整地 关闭。
2.
TIME_WAIT是什么?
- 主动关闭连接的一方,在发送最后一个 ACK 后会先进入
TIME_WAIT状态,等待一段时间(2MSL)再彻底关闭连接(进入CLOSED)。- MSL(Max Segment Lifetime): 网络中一个 TCP 报文可能存在的最长时间。
- 为什么不直接进入
CLOSED呢?-
保证 “最后一个 ACK” 可靠传输
- 如果 C 立即关闭,S 没收到 ACK,会重发 FIN;
- 若 C 已关闭,就无法回应,导致服务端一直处于
LAST_ACK状态。
-
防止旧连接的干扰
- 如果立即关闭,并重新使用相同四元组(IP、PORT、协议),网络中延迟处理的旧包可能被当作新连接的数据;
- TIME_WAIT 期间确保旧包在网络中全部消失后才关闭。
-
保证 “最后一个 ACK” 可靠传输
3. 为什么
TIME_WAIT要持续 2 x MSL?
- 一段 MSL 是为了 确保本连接最后发送的 ACK 能到达对方;
- 另一段 MSL 是为了 确保旧连接中的报文在网络中彻底消失。
4.
TIME_WAIT常见问题
| 问题 | 原因 | 对策 |
|---|---|---|
| TIME_WAIT 太多,占用端口 | 客户端大量主动关闭连接 | 1. 调整 tcp_tw_reuse、tcp_tw_recycle(Linux 旧版);2. 使用长连接;3. 服务端主动关闭 |
| TIME_WAIT 导致端口耗尽 | 并发短连接太多 | 增大临时端口范围;采用连接池或 HTTP keep-alive |
| 服务端出现大量 CLOSE_WAIT | 服务端未调用 close()正常关闭 | 检查应用层逻辑,确保及时释放 |
5.
TIME_WAIT与CLOSE_WAIT区别
| 状态 | 所在端 | 触发条件 | 表示意义 |
|---|---|---|---|
| TIME_WAIT | 主动关闭方 | 发完最后 ACK 等待 2MSL | 等待旧包消失,保证连接彻底关闭 |
| CLOSE_WAIT | 被动关闭方 | 收到对方 FIN,尚未发送自己的 FIN | 应用层未 close(),导致资源占用 |
6. RST 什么时候出现?
- 对 已关闭的连接 发送数据;
- 向 未建立的连接 发送请求;
- 半连接超时被清理;
- 异常关闭;
3.1.3 TCP vs UDP
一句口诀:
- TCP 是一种面向连接、可靠的、一对一的、面向字节流的、首部 20-60 字节的、传输层通信协议。
- UDP 是一种无连接的、不可靠的、任意连接个数的、面向报文的、首部 8 字节的、传输层通信协议。
| 维度 | TCP | UDP |
|---|---|---|
| 连接性 | 面向连接(三次握手) | 无连接的(不需要事先建立连接) |
| 可靠性 | 可靠(数据包校验、包重排、丢弃重复数据包、ACK 机制、超时重传机制、拥塞控制、流量控制) | 不可靠(可能丢包、乱序) |
| 通信模式 | 一对一 | 一对任意数量(单播、多播、广播) |
| 数据传输单元 | 面向字节流(无边界,可能粘包/拆包) | 面向报文(有边界,保留报文长度) |
| 首部开销 | 较大,20~60 字节 | 很小,固定 8 字节 |
| 传输效率 | 相对较低(需建立连接、确认、控制机制) | 极高(无控制开销,延迟低) |
| 适用场景 | 要求 数据准确完整 的场景,如 FTP、HTTP、SMTP、SSH | 要求 实时性高、速度快 的场景,如音视频、通话、直播、DNS 查询、游戏等 |
-
TCP 特性详解
-
核心特性:
- 面向连接: 三握四挥
- 可靠传输: 一系列复杂机制确保数据 准确、有序、不重复 地送达
- 全双工通信: 连接建立后,双方可同时进行数据收发。
-
可靠性保障机制: 0. 三次握手、四次挥手
- 序列号与确认应答(ACK): 每个字节都有唯一序列号,接收方通过返回 ACK 确认收到。
- 超时重传(RTO): 发送数据后启动计时器,若在 RTO(超时重传时间)内未收到 ACK,则重发数据。RTO 动态计算,通常略大于 RTT(往返时延)。
- 数据包排序: 利用序列号对数据包排序。
- 丢弃数据包: 根据序列号识别并丢弃重复包。
- 校验和: 校验数据在传输中是否出错,出错则丢弃等待重传。
- 流量控制: 通过接收方通告的窗口大小,动态调整发送速率,防止接收方缓冲区溢出。
-
拥塞控制: 通过拥塞窗口和四种算法(慢开始、拥塞避免、快重传、快恢复)感知并应对网络拥塞,降低整体丢包率。
-
RTT 往返时延:
- 由链路的传播时间、末端系统出的处理时间、路由缓存中的排队和处理时间组成。
- 最后一个因素会 随着网络拥塞程度而变化,所以 RTT 一定程度上也反映网络拥塞程度。
-
RTT 往返时延:
-
面向字节流与粘包问题:
- 字节流: TCP 将数据视为无结构的字节流,不保留消息边界。发送方多次写入数据可能被合并(粘包)或拆分(拆包)发送。
-
粘包解决方案:
- 先传包大小,再传包内容;
- 固定包长度;
- 设置结束标志;
-
核心机制:
-
滑动窗口:
- 是 实现流量控制的核心数据结构,它标识了在无需等待确认的情况下,发送方 能连续发送的数据范围,极大 提高传输效率。
-
流量控制:
- 目的是 保护接收方,防止接收方被淹没,控制对象为端到端(rwnd),接收方通过 ACK 包中的 “窗口大小” 字段,告知发送方自己 剩余的缓冲区容量,从而 控制发送方的发送速率。
-
拥塞控制:
- 目的是 保护网络,防止网络过载,控制对象为全网(cwnd),通过感知网络拥塞程度(如丢包、RTT增长),动态调整 “拥塞窗口” 的大小,控制 向网络注入数据的全局速率。
-
滑动窗口:
-
TCP 缓冲区
- 发送缓冲区: 存储 已发送未确认、以及待发送 的数据,每个字节都有序列号,在收到 ACK 确认的数据才会从缓冲区移除。
- 接收缓冲区: 存储 已接收但未被应用层读取,以及乱序到达 的数据,其剩余空间大小通过窗口通告给发送方,确保接收缓冲区不溢出。
-
-
UDP 特性详解
-
核心特性
- 无连接: 直接发送数据,无需建立连接,开销小。
- 不可靠传输: 不提供 ACK、重传、排序 等保障机制,数据可能 丢失、重复、乱序。
- 面向报文: 对应用层交下来的报文,既不合并也不拆分,一次发送一个完整的报文,保留消息边界。
-
优缺点
-
优点:
- 速度快、延迟低: 无连接、无控制开销。
- 头部开销小: 仅 8 字节。
- 支持多播/广播: 可以高效向多个目标发送数据。
- 无阻塞控制: 在网络拥塞时仍能保持发送速率,适合实时应用。
-
缺点:
- 不保证可靠性: 需要由 应用层 自行处理 丢包、重复、乱序 等问题。
- 易导致网络拥塞: 缺乏拥塞控制,若大量发送可能加剧网络拥塞。
-
优点:
-
UDP 报文结构
- 头部(8 字节):源端口(2)、目的端口(2)、长度(2)、校验和(2)。
-
不可靠性的应用层解决方案
- 增加序列号
- 引入ACK与重传机制
- 实现流量控制
-
UDP缓冲区
- 发送缓冲区: 发送时,将数据放入缓冲区后立即发送,并从缓冲区清除不停留;
- 接收缓冲区: 接收时,将数据放入缓冲区供应用读取。
-
单独讲一下 拥塞控制 与 流量控制
-
拥塞控制:
-
本质: 拥塞控制的本质是发送方通过一个 拥塞窗口 变量,来动态探测并适应网络传输能力,尽可能高效利用可用带宽。
-
拥塞窗口(cwnd):
- 发送方根据自己 对网络拥塞程度的评估 而维护的一个窗口值,它代表了 “在当前网络情况下,我能安全发送多少数据,而不造成拥塞”。
-
发送窗口的最终大小:
- 发送方在任一时刻,
实际能发送的数据了 = min(cwnd,rwnd)。
- 发送方在任一时刻,
-
慢启动门限(ssthresh):
- 一个状态切换的阈值。当
cwnd < ssthresh,使用慢开始算法;当cwnd >= ssthresh,使用拥塞避免算法。
- 一个状态切换的阈值。当
-
拥塞窗口(cwnd):
- 影响: 网络拥塞时 TCP 继续发包可能会导致数据包丢失或时延,这时 TCP 就会重传,导致网络更加拥塞。
-
拥塞前兆:
- 数据包延迟显著增加;
- 丢包率上升;
- 网络吞吐量下降;
-
超时重传(RTO 超时): 严重拥塞信号。
- 网络非常拥堵,必须 “急刹车”,然后从最低速重新开始指数增长。
-
收到 3 个重复的 ACK: 轻度拥塞信号。
- 网络只是轻度丢包,数据还在流动,可以适度减速。
-
超时重传(RTO 超时): 严重拥塞信号。
- 拥塞控制:
初始状态 ↓ 慢启动(指数增长) ↓ (cwnd >= ssthresh) 拥塞避免(线性增长) ↓ [网络事件发生] ├─ 超时重传 ──→ 慢启动重启(cwnd=1) └─ 3个重复ACK ──→ 快重传 + 快恢复 ──→ 拥塞避免 -
本质: 拥塞控制的本质是发送方通过一个 拥塞窗口 变量,来动态探测并适应网络传输能力,尽可能高效利用可用带宽。
-
流量控制:
- 本质: 本质是控制供需平衡,解决收发双方的速率匹配问题,防止发送方发送速率超过接收方的处理能力,导致接收方缓冲区溢出和数据丢失。
- 核心机制:滑动窗口
发送缓冲区(发送方维护) ┌───────────────────────────────────────────────────┐ │ 已发送且已确认 │ 已发送未确认 │ 可发送未发送 │ 不可发送 │ │ (可清除) │ (等待ACK) │ (在窗口内) │(超出窗口)│ ├───────────────────────────────────────────────────┤ ◄── 已确认部分 ──►◄─────────── 发送窗口 ────────────►接收缓冲区(接收方维护) ┌──────────────────────────────────────┐ │ 已接收已确认 │ 可接收未接收 │ 不可接收 │ │ (待应用读取) │ (在窗口内) │(超出窗口)│ ├──────────────────────────────────────┤ ◄── 已使用部分 ─►◄─────── 接收窗口 ───────►
四、网络层:IP
4.1 作用
逻辑寻址 + 路由转发
- 核心协议:IP、ICMP、ARP、NAT、路由协议(RIP/OSPF/BGP)。
4.2 IP 基础
| 版本 | 地址长度 | 示例 |
|---|---|---|
| IPv4 | 32 位 | 192.168.1.1 |
| IPv6 | 128 位 | 2001:db8::1 |
- IP 报文由 【头部 + 数据】 组成,头部含源 IP、目标 IP、TTL、协议号。
- 若包太大,网络层负责分片与重组。
- 负责寻址与分片;
- TTL 防死循环;
- 协议号区分上层(6=TCP,17=UDP)。
4.3 ICMP
- Internet Control Message Protocol,用于诊断和错误报告。
- 常见命令:ping(测试连通性)、traceroute(路由追踪)。
4.4 路由机制
- 静态路由:手动配置;
- 动态路由:路由器间自动交换信息(RIP、OSPF、BGP)。
- 默认网关:未知目标的转发出口。
五、网络接口层(链路层、物理层):WiFi、蜂窝
5.1 职责
- 封装帧、寻址、差错检测(FCS)、介质访问控制。
- 负责同一局域网内的 节点到节点通信。
5.2 MAC 地址与 ARP
- 每个网卡唯一的 48 位地址(00-1C-42-7A-xx-xx)。
- ARP(地址解析协议):根据 IP 查 MAC 地址。
- 逆向为 RARP。
5.3 常见协议
| 类型 | 协议 | 作用 |
|---|---|---|
| 有线 | Ethernet | 主流局域网协议 |
| 无线 | Wi-Fi(IEEE 802.11) | 无线局域网标准 |
| 点对点 | PPP | 拨号链路协议 |
六、三方库:AFNetworking
6.1 AFNetworking
AFNetworking 本质上就是对苹果 NSURLSession 的面向对象封装,把 delegate 回调模式变成更易用的 Block 模式,同时加了一套序列化体系。
整体结构:
AFNetworking 4.0
│
├── 核心层
│ ├── AFURLSessionManager 会话管理器(核心中的核心)
│ └── AFHTTPSessionManager HTTP 便捷管理器
│
├── 序列化层
│ ├── AFHTTPRequestSerializer 请求序列化(拼参数、设 Header)
│ │ ├── AFJSONRequestSerializer
│ │ └── AFPropertyListRequestSerializer
│ ├── AFHTTPResponseSerializer 响应序列化(解析数据)
│ │ ├── AFJSONResponseSerializer
│ │ ├── AFXMLParserResponseSerializer
│ │ └── AFImageResponseSerializer
│ └── AFSecurityPolicy SSL 证书校验策略
│
├── 辅助层
│ └── AFNetworkReachabilityManager 网络状态检测
│
└── UIKit 扩展 (可选)
├── UIImageView+AFNetworking
├── UIButton+AFNetworking
└── AFNetworkActivityIndicatorManager
-
AFURLSessionManager(一切的基础)-
持有:
-
NSURLSession *session← 苹果原生会话 NSOperationQueue *operationQueAFSecurityPolicy *securityPolicyAFNetworkReachabilityManager *reachabilityManager-
NSMutableDictionary *mutableTaskDelegatesKeyedByTaskIdentifier(每个 Task 对应一个 delegate, 管理回调)
-
-
做了什么?
- 实现
NSURLSessionDelegate全家桶 - 把 delegate 回调 →转化成→ Block 回调
- 给每个
NSURLSessionTask配一个AFURLSessionManagerTaskDelegate,这个内部 delegate 负责收集数据、计算进度、最终回调。 - 管理 Task 的生命周期 (创建/取消/暂停/恢复)
- SSL 证书校验 (通过
AFSecurityPolicy)
- 实现
-
关键方法:
- 数据任务(用于普通 HTTP 请求,请求与响应体都在内存中): dataTaskWithRequest:completionHandler:
- 上传任务(用于 “上传本地文件” 到服务器,底层基于 http(s)): uploadTaskWithRequest:fromFile:progress:completionHandler:
- 下载任务(用于 “下载文件” 到本地磁盘): downloadTaskWithRequest:destination:progress:completionHandler:
-
持有:
-
AFHTTPSessionManger(继承自AFURLSessionManager, HTTP的便捷入口)- 新增:
- 提供 RESTful 风格方法:
- 内部流程:
序列化体系
- 这是 AFNetworking 最核心的设计模式 —— 请求/响应序列化器可替换:
请求序列化器 (怎么把参数变成 HTTP 请求)
────────────────────────────────────────
AFHTTPRequestSerializer (默认)
· Content-Type: application/x-www-form-urlencoded
· 参数编码: key1=value1&key2=value2
· 设置通用 Header: User-Agent / Accept-Language
AFJSONRequestSerializer
· Content-Type: application/json
· 参数编码: JSON 格式 {"key1":"value1"}
响应序列化器 (怎么把响应数据变成对象)
────────────────────────────────────────
AFHTTPResponseSerializer (默认)
· 不做任何解析, 直接返回 NSData
AFJSONResponseSerializer
· 验证 Content-Type 是否为 JSON
· NSJSONSerialization 解析 → NSDictionary / NSArray
AFImageResponseSerializer
· 验证 Content-Type 是否为图片
· 解析 → UIImage
6.1.1 一个请求在 AF 内部的完整流程
manager.POST(url, parameters, headers, success, failure): 业务调用
│
▼
① AFHTTPRequestSerializer.request: 请求序列化
│
▼
返回 NSMutableURLRequest
│
▼
② AFURLSessionManager.dataTask: 创建 Task,绑定 delegate,注册回调。
│
▼
③ task.resume() ← 请求发出去了
│
▼
④ NSURLSession delegate 回调 (AF 接管): 接收数据,回调 AFURLSessionManagerTaskDelegate
│
▼
⑤ AFURLSessionManagerTaskDelegate: 汇总数据
│
▼
⑥ AFJSONResponseSerializer.response: 响应序列化
│
▼
返回 NSDictionary
│
▼
⑦ dispatch_group: 回到主线程
6.1.2 AFNetworking 使用优化
-
AFHTTPSessionManager: 可以注入 Cronet 以支持 QUIC / HTTP2‘ -
AFHTTPRequestSerializer: 可以增加 AES128 加密等; -
AFJSONResponseSerializer可以增加对应的 AES128 加密、加 GZip/Brotli 解压逻辑等。
6.1.3 AFNetworking vs 直接使用 NSURLSession
裸 NSURLSession
|
AF 封装后 |
|---|---|
| 要自己实现 4 个 delegate 协议 | Block 回调,几行代码搞定 |
| 要自己拼 URL、编码参数 | manager.POST(url, params) 一行搞定 |
| 要自己管理 Task 生命周期 | AF 自动管理 |
| 要自己解析 JSON | responseSerializer 自动解析 |
| 要自己做 SSL 校验 | AFSecurityPolicy 配置即可 |
| 要自己检测网络状态 | AFNetworkReachabilityManager 现成的 |
6.1.4 AFNetworking vs Protobuf
- AF 序列化: 把参数 → 变成 HTTP 协议能理解的格式
- Protobuf: 把数据 → 变成一种紧凑的二进制编码格式
AF 序列化把字典变成 HTTP 能传的文本(HTTP 协议规范),Protobuf 把对象变成 Socket 能传的二进制。 前者为了兼容 HTTP 标准,后者为了追求极致的小和快。
| AF 序列化 | Protobuf | |
|---|---|---|
| 格式 | 文本 (JSON / 表单) | 二进制 |
| 可读性 | 人能直接看 | 看不懂,需要 .proto 文件才能解 |
| 体积 | 大 | 小(1/3~1/5) |
| 解析速度 | 慢 | 快(10~100x) |
| Schema | 无,运行时动态解析 | 有,编译时生成代码(GPBMessage 子类) |
| 用在哪 | HTTP 请求/响应 | TCP 长连接的数据包 |
| 为什么选它 | HTTP 标准就是 JSON/表单 | 长连接要求低延迟、省流量 |
七、总结
落实到 App 开发中,常见链路可能是这样的:
整体链路关系:
┌─────────────────────── 应用层 ───────────────────────────┐
│ │
│ 业务协议: HTTP API / Protobuf / RTC │
│ 安全: AES 加密 / HMAC-SHA1 签名 / 防重放 │
│ 域名管理: 业务统一管理 │
│ 域名解析: HTTPDNS / 系统 DNS │
│ │
├─────────────────────── 传输层 ───────────────────────────┤
│ TCP (GCDAsyncSocket) │
│ UDP + KCP (GCDAsyncUdpSocket + KCP) │
│ QUIC (Cronet) │
│ │
├─────────────────────── 网络层 ───────────────────────────┤
│ IP (v4/v6) │
│ │
├─────────────────────── 接口层 ───────────────────────────┤
│ WiFi / 蜂窝 / 断网检测 │
└─────────────────────────────────────────────────────────┘
HTTP 链路:
┌─────────────────────── 应用层 ───────────────────────────┐
│ │
│ ① 业务入口 │
│ ObjC: xxxxHTTP.facebookLogin(...) │
│ Swift: xxxxAPI(params).observable.subscribe(...) │
│ │ │
│ ▼ │
│ ② 调度中心 │
│ ObjC: 业务统一错误码处理等 │
│ Swift: MoyaProvider (插件链处理) │
│ │ │
│ ▼ │
│ ③ 请求模型 (两侧做同样的事, 语言不同) │
│ · 填充公共参数: uid, sid, device_id, version... │
│ · 生成防重放: nonce (UUID) + millisecond (时间戳) │
│ · 计算签名: token (HMAC-SHA1) │
│ sign (HMAC-SHA1) │
│ · 合并业务参数 │
│ │ │
│ ▼ │
│ ④ 序列化 & 加密 │
│ ObjC: HTTPAESRequestSerialization │
│ Swift: MoyaAESHandler.prepare │
│ · 按路径判断是否需要 AES128 加密请求体 │
│ · 设置请求头 X-ENCRYPTED-VERSION │
│ │ │
│ ▼ │
│ ⑤ HTTP 库 │
│ ObjC: AFNetworking → AFHTTPSessionManager │
│ Swift: Alamofire → Session │
│ │ │
│ ▼ │
│ ⑥ NSURLSession (两侧共用) │
│ · SessionConfiguration 注入 Cronet │
│ · Cronet 尝试 QUIC → 降级 HTTP/2 → 降级 HTTP/1.1 │
│ │
├─────────────────────── 传输层 ───────────────────────────┤
│ │
│ QUIC (Cronet, UDP) 或 TCP (系统) │
│ │ │
│ ▼ │
│ TLS 握手 │
│ │
├─────────────────────── 网络层 ───────────────────────────┤
│ │
│ IP 路由 │
│ │
├─────────────────────── 接口层 ───────────────────────────┤
│ │
│ WiFi / 蜂窝 │
│ │
└──────────────────────────────────────────────────────────┘
TCP 链路:
┌─────────────────────── 应用层 ───────────────────────────┐
│ │
│ ① 建连 (App启动 / 登录成功触发) │
│ │ │
│ ▼ │
│ ② 认证 (连接建立后) │
│ · 构建请求: sid + deviceId + version │
│ │ │
│ ▼ │
│ ③ 封包:Protobuf 等 │
│ │ │
│ ▼ │
│ ④ Socket 发送 │
│ │ │
│ ▼ │
├─────────────────────── 传输层 ────────────────────────────┤
│ │
│ TCP (GCDAsyncSocket) / KCP + UDP (GCDAsyncUdpSocket) │
│ │
├─────────────────────── 网络层 ────────────────────────────┤
│ │
│ IP 路由 │
│ │
├─────────────────────── 接口层 ───────────────────────────┤
│ │
│ WiFi / 蜂窝 │
│ │
└──────────────────────────────────────────────────────────┘
保护机制:
┌─────────────────────── 应用层 ───────────────────────────┐
│ │
│ 心跳保活 │
│ → NSTimer 定时发送心跳包 │
│ │
│ 守护进程 (ConnectorDaemon) │
│ ├── TCP 守护: 连续 n 次失败 → 切备用域名 │
│ └── KCP 守护: n 秒内 n 次超时 → 切回 TCP │
│ │
│ 重连策略 │
│ TCP 断开 → 0.5s 自动重连 │
│ bind 失败 → 1s 重试 bind │
│ 域名故障 → 切备用域名 │
│ KCP 故障 → 切回 TCP 连同一域名 │
│ 前后台切换 → 发探测包验证, 失败则重连 │
│ │
└──────────────────────────────────────────────────────────┘