Module 13: 网络与协议
📖 深度参考手册 — 本模块属于理论参考,非主线必读。 主线学习路径见 README.md。 当你在项目实战中遇到相关问题时,回来查阅。
所有系统设计最终都跑在网络上。理解网络协议,才能理解为什么系统会慢、为什么会断、为什么要用不同的通信方式。
13.1 DNS 工作原理
Section titled “13.1 DNS 工作原理”定义:DNS(Domain Name System) 是互联网的”电话簿”,将人类可读的域名(如 short.ly)翻译成机器可读的 IP 地址(如 93.184.216.34)。DNS 解析是一个多级递归查询过程:
浏览器输入 short.ly → 1. 浏览器缓存(最近访问过?直接用) → 2. 操作系统缓存(/etc/hosts 或 OS DNS 缓存) → 3. 递归解析器(通常是 ISP 或 8.8.8.8 提供) → 4. 根 DNS 服务器("我不知道 short.ly,但 .ly 去问那边") → 5. TLD DNS 服务器("short.ly 的权威服务器在这里") → 6. 权威 DNS 服务器("short.ly → 93.184.216.34") → 结果沿原路返回并在各级缓存常见 DNS 记录类型:
- A 记录:域名 → IPv4 地址(最基本)
- AAAA 记录:域名 → IPv6 地址
- CNAME 记录:域名 → 另一个域名(别名,如
www.example.com→example.com) - MX 记录:域名 → 邮件服务器地址
- TXT 记录:任意文本,常用于域名所有权验证、SPF 邮件反垃圾
- NS 记录:指定域名的权威 DNS 服务器
每条 DNS 记录都有 TTL(Time To Live),告诉缓存”这条记录可以缓存多久”。TTL 过期后必须重新查询。
为什么重要:DNS 是用户访问你系统的第一步,如果 DNS 解析慢或出错,用户根本到不了你的服务器。在系统设计中,DNS 还承担着流量分发的重要角色:通过 DNS 轮询实现负载均衡,通过 GeoDNS 将用户引导到最近的数据中心,通过低 TTL 实现故障快速切换。
案例:URL Shortener — 短链接服务对 DNS 有特殊要求。short.ly 这个域名每天可能被解析数亿次,因此:
- A 记录指向负载均衡器 IP,而不是单台服务器
- TTL 设置为较短的值(如 60 秒),这样如果需要切换到新的负载均衡器(比如故障转移),60 秒内全球用户就能切换过来
- 使用多个权威 DNS 服务器(如 Route53 + Cloudflare 双备份),防止 DNS 本身成为单点故障
- 如果服务面向全球用户,用 GeoDNS 让亚洲用户解析到亚洲服务器、欧洲用户解析到欧洲服务器
先想一想 🤔 为什么 URL Shortener 的 DNS TTL 不能设得太长(比如 24 小时),也不能设得太短(比如 1 秒)?
点击查看解析
TTL 太长(24 小时)的问题:
- 如果服务器出故障需要切换 IP,全球的 DNS 缓存最长要 24 小时才会刷新
- 这意味着故障切换时间可能长达 24 小时——对一个短链接服务来说不可接受
- 修改 DNS 记录后无法快速生效
TTL 太短(1 秒)的问题:
- 每次访问都要重新进行 DNS 解析,增加延迟(DNS 解析通常需要 20-100ms)
- 给权威 DNS 服务器带来巨大的查询压力
- 短链接服务的核心指标是跳转速度,多 50ms 的 DNS 延迟直接影响用户体验
平衡点:60-300 秒。正常运行时 DNS 缓存能有效降低延迟和查询压力,故障时最多等几分钟就能完成切换。这是延迟、可靠性和 DNS 服务器负载之间的权衡。
13.2 HTTP/HTTPS
Section titled “13.2 HTTP/HTTPS”定义:HTTP(HyperText Transfer Protocol) 是 Web 的基础协议,采用无状态的请求/响应模型——客户端发请求,服务端回响应,每次请求独立,服务端不记住之前的请求。
HTTP 状态码(系统设计面试必知):
2xx 成功: 200 OK — 请求成功 201 Created — 资源创建成功(POST 创建) 204 No Content — 成功但无返回体(DELETE)
3xx 重定向: 301 Moved Permanently — 永久重定向(浏览器会缓存) 302 Found — 临时重定向(浏览器不缓存) 304 Not Modified — 缓存有效,无需重新传输
4xx 客户端错误: 400 Bad Request — 请求格式错误 401 Unauthorized — 未认证(没登录) 403 Forbidden — 已认证但无权限 404 Not Found — 资源不存在 429 Too Many Requests — 限流触发
5xx 服务端错误: 500 Internal Server Error — 服务端未知错误 502 Bad Gateway — 网关/代理无法连接后端 503 Service Unavailable — 服务暂时不可用(过载/维护)HTTP 协议演进:
- HTTP/1.1:每个 TCP 连接上请求排队(队头阻塞),浏览器对同一域名最多 6 个并发连接
- HTTP/2:单连接上多路复用(多个请求并行不阻塞)、头部压缩(HPACK)、服务端推送
- HTTP/3:基于 QUIC(UDP 之上),0-RTT 建立连接,无队头阻塞(即使丢包也不影响其他流)
为什么重要:HTTP 状态码的选择直接影响系统行为。301 和 302 的区别看起来很小,但在实际系统中影响巨大。HTTP 版本的选择影响系统性能——如果你的页面需要加载 50 个资源,HTTP/1.1 下需要排队等待,HTTP/2 下可以并行加载。
案例:URL Shortener — 短链接跳转时 301 还是 302 是一个经典的设计决策:
301 Moved Permanently(永久重定向): ✅ 浏览器缓存跳转,第二次访问不经过服务器 → 更快 ❌ 不经过服务器 → 无法统计点击次数 ❌ 缓存后无法更改目标 URL
302 Found(临时重定向): ✅ 每次访问都经过服务器 → 可以精确统计点击次数 ✅ 可以随时修改目标 URL ❌ 每次都要经过服务器 → 增加服务器负载
结论:大多数短链接服务(bit.ly、tinyurl)选择 302, 因为**点击统计**是核心商业价值(广告主按点击付费), 性能牺牲可以通过 CDN 和缓存补偿。先想一想 🤔 当你在浏览器里看到 502 Bad Gateway 错误时,问题出在哪里?和 503 有什么区别?
点击查看解析
502 Bad Gateway:
- 问题出在反向代理/负载均衡器和后端服务器之间
- 负载均衡器(Nginx)收到了你的请求,但它联系后端应用服务器时,后端没响应、返回了无效响应、或者连接被拒绝
- 常见原因:后端服务挂了、后端服务正在重启、后端端口没开、后端处理超时
503 Service Unavailable:
- 问题出在服务本身——服务还活着,但暂时无法处理请求
- 常见原因:服务过载(CPU/内存满了)、正在维护、触发了熔断器
- 通常会带一个 Retry-After 头,告诉客户端多久后可以重试
简单记忆:
- 502 = 门卫说”后面的人找不到了”(代理层到应用层连接断了)
- 503 = 门卫说”后面的人太忙了,等一会儿”(应用层还在,但处理不过来)
在系统设计中,区分两者很重要——502 通常说明需要检查后端服务健康状况,503 通常说明需要扩容或限流。
13.3 TLS/SSL
Section titled “13.3 TLS/SSL”定义:TLS(Transport Layer Security) 是 HTTPS 中的”S”,为 HTTP 通信提供三层保护:加密(第三方无法窃听通信内容)、完整性(数据无法被篡改)、身份验证(确认你连接的确实是目标服务器)。
TLS 握手过程(TLS 1.3 简化版):
客户端 服务端 │ │ │── ClientHello ────────────────→ │ (支持的加密算法列表、随机数) │ │ │←── ServerHello + 证书 ────────── │ (选定的算法、服务端证书、随机数) │ │ │ [验证证书是否由信任的CA签发] │ │ [生成预主密钥, 用服务端公钥加密] │ │ │ │── 加密的预主密钥 ────────────→ │ │ │ │ [双方用相同的材料计算会话密钥] │ │ │ │←─────── 加密通信开始 ──────────→ │证书链:服务端证书由 CA(Certificate Authority,证书颁发机构)签发。浏览器内置信任的 CA 列表(如 Let’s Encrypt、DigiCert)。验证过程:服务端证书 → 中间 CA 证书 → 根 CA 证书(浏览器信任)。如果证书不在信任链中,浏览器会显示”不安全”警告。
为什么重要:没有 TLS 的 HTTP 是明文传输,意味着:
- 窃听:在公共 WiFi 上,任何人都能用抓包工具看到你的密码、Cookie、聊天内容
- 篡改:ISP 可以在你的网页中注入广告、恶意代码
- 冒充:攻击者可以伪装成银行网站,你无法分辨真假
现在 HTTPS 已经是标配——Chrome 对所有 HTTP 网站标记”不安全”,搜索引擎对 HTTPS 网站有排名加分,Let’s Encrypt 提供免费证书。
案例:Hotel Reservation — 酒店预订系统必须使用 HTTPS,理由非常充分:
- 支付信息:信用卡号在 HTTP 下明文传输,任何中间人都能截获。PCI DSS(支付卡行业数据安全标准)明确要求加密传输
- 个人信息:用户姓名、手机号、身份证号等个人隐私信息
- 登录凭证:用户名和密码如果被截获,攻击者可以冒充用户预订酒店
- 支付接口要求:Stripe、支付宝、微信支付等支付渠道,都要求回调 URL 必须是 HTTPS
没有 HTTPS 的酒店预订网站不仅不安全,而且根本无法接入任何支付渠道。
先想一想 🤔 为什么 TLS 握手要用非对称加密交换密钥,后续通信却切换为对称加密?
点击查看解析
核心原因:性能。
- 非对称加密(RSA/ECDSA):安全性高,但速度极慢(比对称加密慢 100-1000 倍)
- 对称加密(AES):速度快,但密钥分发困难(怎么在不安全的网络上安全地传递密钥?)
TLS 的巧妙设计:两者结合:
- 先用非对称加密安全地交换一个对称密钥(握手阶段,只做一次)
- 后续所有通信都用这个对称密钥加密(数据传输阶段,速度快)
这样既解决了密钥分发问题(非对称加密的优势),又保证了通信效率(对称加密的优势)。
类比:你要给朋友寄一箱保密文件。你先用一个公开的保险箱(非对称加密)把钥匙(对称密钥)安全地传过去,然后后续所有文件都用这把钥匙锁(对称加密)——因为用保险箱锁每一箱文件太慢了。
13.4 WebSocket
Section titled “13.4 WebSocket”定义:WebSocket 是一种在单个 TCP 连接上提供全双工通信的协议。它基于 HTTP 升级机制建立连接,建立后双方可以随时互相发送消息,无需等待对方请求。
连接建立过程:
客户端 服务端 │ │ │── HTTP GET /chat ──────────────────→ │ │ Upgrade: websocket │ │ Connection: Upgrade │ │ Sec-WebSocket-Key: dGhlIHNhbXBsZQ== │ │ │ │←── HTTP 101 Switching Protocols ───── │ │ Upgrade: websocket │ │ Sec-WebSocket-Accept: s3pPLMBiTxa... │ │ │ │←──────── WebSocket 全双工通信 ────────→ │ │ (双方随时可以发送消息帧) │关键机制:
- 心跳保活:客户端定期发送 ping 帧,服务端回 pong 帧,检测连接是否存活。如果连续 N 次 ping 无回复,判定连接断开
- 断线重连:客户端检测到连接断开后,自动尝试重新建立 WebSocket 连接,重连时可能需要重新认证
- 消息帧:WebSocket 传输的是帧(frame),支持文本帧和二进制帧,可以发送任意格式的数据
为什么重要:传统 HTTP 是”你问我答”模式——客户端不发请求,服务端就不能发数据。但很多场景需要服务端主动推送:聊天消息、股票行情、实时通知、协同编辑… 如果用 HTTP 轮询(每秒发一次请求问”有新消息吗?”),既浪费带宽又增加延迟。WebSocket 建立连接后,服务端有新消息就直接推过来,延迟可以低至毫秒级。
案例:Chat System — 聊天系统是 WebSocket 最经典的应用场景。
为什么聊天必须用 WebSocket(或类似技术):
HTTP 轮询方案(❌ 不可行): 客户端每 500ms 发一次 GET /messages - 100万在线用户 = 每秒200万次请求 - 99%的请求返回"没有新消息" → 极大浪费 - 消息延迟最多 500ms,实时性不够
WebSocket 方案(✅ 推荐): 每个在线用户维持一个 WebSocket 长连接 - 100万在线用户 = 100万个长连接(服务端需要足够资源维持) - 有新消息时服务端直接推送 → 延迟 < 50ms - 无消息时只有心跳包 → 几乎无流量开销
架构考量: - 单台服务器通常维持 10万-50万 WebSocket 连接 - 100万用户需要 10-20 台 WebSocket 服务器 - 用户 A 和用户 B 可能连在不同服务器 → 需要消息路由 - 消息路由方案:Redis Pub/Sub 或 消息队列先想一想 🤔 如果 Chat System 的用户从手机切换到 WiFi(网络变化),WebSocket 连接会怎样?系统应该如何处理?
点击查看解析
连接会断开。网络切换时 TCP 连接的源 IP 变了,原来的连接无法维持。
系统处理流程:
- 客户端检测断线:
- 心跳超时(连续 3 次 ping 无 pong)或直接收到连接关闭事件
- 触发自动重连机制
- 自动重连策略:
- 使用指数退避:第 1 次立即重连,第 2 次等 1 秒,第 3 次等 2 秒,第 4 次等 4 秒…
- 加入随机抖动,防止所有客户端同时重连(“惊群效应”)
- 设置最大重试次数或最大等待时间
- 重连后恢复状态:
- 客户端携带 JWT 重新认证
- 携带”上次收到的最后一条消息 ID”
- 服务端返回断线期间的所有未读消息(补推)
- 关键设计:消息必须有全局唯一 ID 和时间戳,才能实现精确的断点续推
- UI 反馈:
- 断线时显示”连接中…”提示
- 重连成功后消除提示,补推的消息无缝出现在聊天窗口
这也是为什么 Chat System 需要消息持久化(存数据库)而不是只走内存——断线期间的消息必须被保存。
13.5 gRPC 与 Protocol Buffers
Section titled “13.5 gRPC 与 Protocol Buffers”定义:gRPC 是 Google 开源的高性能远程过程调用(RPC)框架,基于 HTTP/2 传输,使用 Protocol Buffers(protobuf) 作为接口定义语言和序列化格式。
Protocol Buffers:用 .proto 文件定义数据结构和服务接口,然后用编译器生成各语言的代码:
syntax = "proto3";
service SearchService { rpc Search(SearchRequest) returns (SearchResponse); // 普通调用 rpc StreamResults(SearchRequest) returns (stream Result); // 服务端流}
message SearchRequest { string query = 1; int32 page = 2; int32 page_size = 3;}
message SearchResponse { repeated Result results = 1; int32 total_count = 2;}
message Result { string title = 1; string url = 2; float score = 3;}gRPC 的四种通信模式:
- 一元 RPC(Unary):一个请求,一个响应(类似 REST)
- 服务端流(Server Streaming):一个请求,服务端返回一系列响应
- 客户端流(Client Streaming):客户端发送一系列请求,服务端返回一个响应
- 双向流(Bidirectional Streaming):双方都可以随时发送消息
gRPC vs REST+JSON:
| 维度 | gRPC + Protobuf | REST + JSON |
|---|---|---|
| 序列化 | 二进制(体积小 3-10 倍) | 文本 JSON(可读但较大) |
| 类型安全 | 编译时检查(.proto 定义) | 运行时检查(容易出错) |
| 流支持 | 原生四种流模式 | 不支持(需 SSE/WebSocket) |
| 浏览器 | 不直接支持(需 gRPC-Web) | 原生支持 |
| 调试 | 二进制难以直接查看 | JSON 人眼可读 |
| 适合场景 | 服务间内部通信 | 对外 API、前后端通信 |
为什么重要:在微服务架构中,服务间调用的性能至关重要。一个请求可能经过 5-10 个微服务的调用链,每个环节省下的序列化/反序列化时间、传输体积,累积起来非常可观。gRPC 的强类型定义还避免了”这个字段是字符串还是数字”之类的联调问题。
案例:Search Engine — 搜索引擎内部有多个微服务:Web Crawler(爬虫)、Indexer(索引构建)、Query Service(查询服务)、Ranking Service(排序服务)。这些服务之间的调用量极大(每秒数百万次),用 gRPC 的优势明显:
查询流程(gRPC 调用链):
用户 → HTTP/REST → API Gateway → gRPC → Query Service ├── gRPC → Index Service(查索引) ├── gRPC → Ranking Service(排序) └── gRPC → Snippet Service(生成摘要)
为什么内部用 gRPC:- Index Service 返回大量文档 ID 列表 → 二进制传输比 JSON 小 5 倍- Ranking Service 用服务端流返回排序结果 → 边排序边返回,延迟更低- 强类型 .proto 文件 → 多个团队(爬虫团队、索引团队、查询团队)靠接口定义协作- 所有服务都是内部服务 → 不需要浏览器兼容
为什么对外用 REST:- 用户浏览器不支持 gRPC- 外部开发者(搜索 API)更习惯 REST + JSON先想一想 🤔 YouTube 的视频上传功能适合用 gRPC 的哪种通信模式?为什么?
点击查看解析
客户端流(Client Streaming)。
视频上传的特点:
- 文件可能很大(几 GB),不能一次性发送
- 需要分片上传:客户端将视频切成多个小块,逐个发送
- 所有片段上传完成后,服务端返回一个最终响应(上传成功/失败、视频 ID)
这正是客户端流的场景:客户端发送一系列消息(视频分片),服务端最后返回一个响应。
service VideoService {rpc Upload(stream VideoChunk) returns (UploadResponse);}message VideoChunk {bytes data = 1; // 视频分片数据int32 chunk_index = 2; // 分片序号string upload_id = 3; // 上传会话 ID(用于断点续传)}不过实际上,YouTube 的上传更可能用 HTTP 分片上传(如 tus 协议),因为:
- 上传主要来自浏览器 → 浏览器不支持 gRPC
- HTTP 分片上传天然支持断点续传
- gRPC 更适合内部服务间通信
但如果是移动客户端或桌面客户端上传,gRPC 客户端流是一个很好的选择。
13.6 GraphQL
Section titled “13.6 GraphQL”定义:GraphQL 是 Facebook 开发的 API 查询语言,核心理念是客户端定义想要的数据结构,服务端按需返回。它用一个端点(POST /graphql)替代了 REST 的多个端点。
# 客户端请求:精确指定需要的字段query { post(id: "123") { title content author { name avatar } comments(first: 5) { text createdAt } }}
# 服务端响应:只返回请求的字段,不多不少{ "data": { "post": { "title": "系统设计入门", "content": "...", "author": { "name": "张三", "avatar": "https://..." }, "comments": [ { "text": "写得好!", "createdAt": "2024-01-01" }, ... ] } }}GraphQL 解决的两个 REST 痛点:
- 过度获取(Over-fetching):REST 的
/api/posts/123返回帖子的所有 30 个字段,但你只需要 3 个字段 - 欠获取(Under-fetching):获取帖子详情需要调 3 个 API(帖子、作者、评论),即 N+1 请求问题
GraphQL 的代价:
- 缓存复杂:REST 的 GET 请求天然可以被 HTTP 缓存(CDN),GraphQL 的 POST 请求不行,需要应用层缓存
- N+1 查询:如果不做优化(DataLoader),一个查询可能触发数百次数据库查询
- 安全控制:客户端可以构造深度嵌套查询消耗服务端资源,需要查询深度限制和复杂度分析
- 学习曲线:Schema 设计、Resolver 编写、类型系统,比 REST 复杂得多
为什么重要:在需要灵活获取异构数据的场景中,GraphQL 可以显著减少网络请求和数据传输量。但它不是 REST 的”升级版”——而是一个不同的工具,适用于不同的场景。选择 GraphQL 还是 REST,取决于你的数据模型复杂度和客户端需求多样性。
案例:News Feed — 信息流是 GraphQL 最经典的应用场景(Facebook 正是为了解决 News Feed 的数据获取问题才发明了 GraphQL)。
信息流的挑战:每条帖子的数据结构不同
纯文本帖子:只需要 text图片帖子:需要 text + images(含缩略图 URL)视频帖子:需要 text + video_url + thumbnail + duration分享帖子:需要 text + shared_post(递归嵌套原帖数据)每条帖子都有:author(name, avatar) + like_count + comment_count
REST 方案(痛点): GET /api/feed → 返回所有帖子的所有字段(过度获取) 或者 GET /api/feed → 只返回基本信息 GET /api/posts/1/author → 获取作者(N+1 请求,10条帖子就是10次请求) GET /api/posts/1/comments → 获取评论
GraphQL 方案(精确获取): 一次请求,每种帖子只获取需要的字段 用 Fragment 复用公共字段 用 Connection 分页(cursor-based)先想一想 🤔 如果 Google Maps 的地图展示 API 也用 GraphQL,合适吗?
点击查看解析
不太合适。原因:
数据结构固定:地图瓦片(tile)、路线规划、地点搜索——每个 API 的请求和响应格式都是固定的,不存在”不同客户端需要不同字段”的问题
缓存是核心:地图瓦片需要被 CDN 大量缓存,REST 的 GET 请求天然支持 HTTP 缓存(
GET /tiles/z/x/y.png),GraphQL 的 POST 请求无法利用这一优势性能敏感:地图加载需要极低延迟,GraphQL 的 Schema 解析和 Resolver 执行会增加服务端延迟
二进制数据:地图瓦片是图片/矢量数据,GraphQL 处理二进制数据不是强项
GraphQL 适合的场景特征:
- 数据关系复杂(社交图谱、内容管理)
- 不同客户端需要不同字段(移动端/Web端/第三方)
- 多个关联资源需要在一次请求中获取
REST 更适合的场景特征:
- 数据结构固定、可预测
- 需要重度 HTTP 缓存
- 资源是独立的、非关联的
- 二进制数据传输
Google Maps 显然属于后者。
13.7 CORS 跨域
Section titled “13.7 CORS 跨域”定义:CORS(Cross-Origin Resource Sharing,跨域资源共享) 是浏览器的一种安全机制。浏览器的同源策略规定:协议、域名、端口三者必须完全一致,才能发起 AJAX 请求。不一致就是”跨域”,浏览器会拦截请求。
同源(✅ 允许): https://example.com/page1 → https://example.com/api (同协议+域名+端口)
跨域(❌ 默认拦截): https://example.com → https://api.example.com (子域名不同) https://example.com → http://example.com (协议不同) http://localhost:3000 → http://localhost:8080 (端口不同)CORS 的工作流程:
- 简单请求(GET、POST + 表单Content-Type):浏览器直接发送,带
Origin头。服务端响应中带Access-Control-Allow-Origin,浏览器检查是否匹配 - 预检请求(Preflight):非简单请求(PUT/DELETE、自定义头、JSON Content-Type),浏览器先发一个 OPTIONS 请求询问服务端”允许哪些方法和头部”,收到允许后才发真正请求
预检请求流程:浏览器 ─── OPTIONS /api/users ───→ 服务端 Origin: http://localhost:3000 Access-Control-Request-Method: PUT Access-Control-Request-Headers: Content-Type, Authorization
服务端 ──── 200 OK ───────────────→ 浏览器 Access-Control-Allow-Origin: http://localhost:3000 Access-Control-Allow-Methods: GET, POST, PUT, DELETE Access-Control-Allow-Headers: Content-Type, Authorization Access-Control-Max-Age: 86400 (预检结果缓存24小时)
浏览器 ─── PUT /api/users ───────→ 服务端 (真正的请求)为什么重要:前后端分离架构几乎必然遇到 CORS 问题。前端开发服务器(localhost:3000)调用后端 API(localhost:8080)就是跨域。如果不处理 CORS,浏览器会直接拦截请求,控制台报错 Access to XMLHttpRequest has been blocked by CORS policy——这是前端开发者最常遇到的错误之一。
案例:在我们的 11 个系统案例中,所有前后端分离的系统都需要处理 CORS。以开发环境为例:
URL Shortener: 前端 http://localhost:3000 → 后端 http://localhost:8080 ← 跨域!
解决方案(按场景选择):
1. 开发环境:Vite/Webpack 配置代理 前端请求 /api/shorten → Vite 代理到 localhost:8080/api/shorten 浏览器看到的是同源请求 → 无跨域问题 (本项目就是这样做的:Vite 代理 /api 到 localhost:8080)
2. 生产环境方案一:Nginx 反向代理 前端和 API 都在 example.com 下 example.com/ → 前端静态文件 example.com/api/ → 后端服务 同域名 → 无跨域问题
3. 生产环境方案二:后端配置 CORS 头 后端设置 Access-Control-Allow-Origin: https://example.com 适合前端和后端部署在不同域名的情况
注意:永远不要设置 Access-Control-Allow-Origin: *(允许所有来源), 除非你的 API 是公开的(如 Google Maps API)。先想一想 🤔 为什么 CORS 是浏览器的限制,而不是服务端的限制?用 Postman 或 curl 调用 API 为什么不会有跨域问题?
点击查看解析
因为同源策略是浏览器为了保护用户而实施的安全机制,不是 HTTP 协议本身的限制。
浏览器要保护什么? 假设你正在浏览器中登录着银行网站 bank.com(有 Cookie),同时你不小心打开了恶意网站 evil.com。如果没有同源策略,evil.com 的 JavaScript 可以:
// evil.com 的恶意代码fetch('https://bank.com/api/transfer', {method: 'POST',credentials: 'include', // 自动带上 bank.com 的 Cookiebody: JSON.stringify({ to: 'attacker', amount: 100000 })})浏览器会自动携带 bank.com 的 Cookie,银行服务器无法区分这是用户操作还是恶意请求——这就是 CSRF(跨站请求伪造) 攻击。同源策略正是为了阻止这种攻击。
为什么 Postman/curl 没有跨域限制?
- Postman/curl 是开发工具,由开发者自己控制,不存在”用户被恶意网站欺骗”的场景
- 它们不会自动携带某个网站的 Cookie
- 它们不在浏览器的安全沙箱中运行
服务端角度:服务端根本无法区分请求来自浏览器还是 curl——HTTP 请求长得一模一样。CORS 头部只是服务端”告诉浏览器”是否允许跨域,最终执行拦截的是浏览器。
13.8 长轮询 vs SSE vs WebSocket
Section titled “13.8 长轮询 vs SSE vs WebSocket”定义:三种实现服务端向客户端实时推送数据的方案,各有优劣:
长轮询(Long Polling):
客户端 ─── GET /updates ──────→ 服务端 │ (hold 住连接,等待有数据) │ ... 30秒后有新数据了 ...客户端 ←── 200 + 数据 ──────── 服务端客户端 ─── GET /updates ──────→ 服务端 (立即再发一个请求) │ (继续 hold...)- 基于普通 HTTP,兼容性最好
- 每次响应后连接关闭,需要重新建立
- 适合更新频率低的场景
SSE(Server-Sent Events):
客户端 ─── GET /stream ──────→ 服务端客户端 ←── Content-Type: text/event-stream data: {"score": 100}
data: {"score": 150} ← 服务端持续推送
data: {"score": 200}- 基于 HTTP 长连接,服务端单向推送
- 自动重连(浏览器原生支持)
- 支持事件类型和事件 ID(断线续推)
- 文本格式(不支持二进制)
WebSocket:
- 全双工,双方都可以随时发消息
- 支持文本和二进制
- 需要自行实现心跳和重连
- 连接建立开销比 SSE 大
三者对比:
| 维度 | 长轮询 | SSE | WebSocket |
|---|---|---|---|
| 方向 | 模拟服务端推送 | 服务端→客户端 | 双向 |
| 协议 | HTTP | HTTP | WS(升级自HTTP) |
| 重连 | 客户端实现 | 浏览器自动 | 客户端实现 |
| 二进制 | 不支持 | 不支持 | 支持 |
| 兼容性 | 最好 | 好(IE 不支持) | 好 |
| 连接开销 | 高(频繁建立断开) | 低 | 低 |
| 适合场景 | 更新少、兼容性优先 | 单向通知、数据流 | 双向交互、实时性要求高 |
为什么重要:选错实时通信方案会导致要么资源浪费(用 WebSocket 做只需要 SSE 的功能),要么功能受限(用 SSE 做需要双向通信的功能)。在系统设计面试中,能根据场景选择合适的方案是一个重要的加分点。
案例:Gaming Leaderboard — 排行榜的更新是一个服务端→客户端的单向推送场景,SSE 是最合适的选择:
场景分析:- 数据流向:排名变化只需要从服务端推送到客户端(单向)- 客户端不需要通过这个连接向服务端发数据(提交分数用普通 HTTP POST)- 更新频率:排名可能每几秒更新一次,不需要毫秒级延迟
SSE 方案: 客户端 ─── GET /api/leaderboard/stream ───→ 服务端 客户端 ←── event: rank_update data: {"player": "Alice", "score": 9500, "rank": 1}
event: rank_update data: {"player": "Bob", "score": 9200, "rank": 2}
优势: ✅ 浏览器原生支持(EventSource API),几行代码搞定 ✅ 自动重连(断线后浏览器自动重新建立连接) ✅ 支持 Last-Event-ID(断线后从上次位置继续推送) ✅ 基于 HTTP → CDN 友好、防火墙友好 ✅ 比 WebSocket 简单得多(无需心跳、握手升级等)
不用 WebSocket 的原因: ❌ 排行榜不需要双向通信 ❌ WebSocket 需要额外实现心跳、重连逻辑 ❌ WebSocket 无法利用 HTTP 缓存和代理 ❌ 增加了服务端复杂度(WebSocket 服务器 vs 普通 HTTP 服务器)先想一想 🤔 Web Crawler(爬虫系统)的爬取任务状态展示(正在爬取、已完成、失败等),应该用哪种实时通信方案?
点击查看解析
SSE 最合适,理由:
单向推送:任务状态是服务端→客户端的单向更新(“正在爬取第 1000/5000 个页面”),管理员不需要通过这个连接操作什么
更新频率适中:任务状态每几秒更新一次(进度百分比、已爬取页面数),不需要毫秒级实时性
天然适合事件流:
event: task_progressdata: {"task_id": "abc", "status": "running", "crawled": 1000, "total": 5000}event: task_progressdata: {"task_id": "abc", "status": "running", "crawled": 2500, "total": 5000}event: task_completedata: {"task_id": "abc", "status": "done", "crawled": 5000, "total": 5000, "errors": 23}自动重连:管理员页面长时间开着,网络偶尔断一下,SSE 自动重连并从上次的事件 ID 继续推送,不会丢失状态更新
如果用长轮询:每次只能收到一个状态更新,然后重新发请求——对于频繁更新的进度条来说太笨重。
如果用 WebSocket:杀鸡用牛刀——任务监控不需要双向通信,引入 WebSocket 只会增加复杂度。
练习一:对比三个系统的通信方式选择
Section titled “练习一:对比三个系统的通信方式选择”问题:Chat System 用 WebSocket,News Feed 用 SSE(或长轮询),URL Shortener 用普通 HTTP。它们各自为什么选择不同的通信方式?
提示:从以下维度思考每个系统:
- 数据流向是单向还是双向?
- 实时性要求有多高(毫秒 vs 秒级 vs 分钟级)?
- 客户端数量和连接开销?
- 是否需要浏览器兼容性?
参考分析框架:
数据流向 实时性 连接成本 最佳方案Chat System 双向 毫秒级 长连接 WebSocketNews Feed 单向推送 秒级 长连接 SSEURL Shortener 请求-响应 无要求 短连接 HTTP
深层原因:- Chat System:用户之间互相发消息 → 必须双向 → WebSocket 如果用 SSE:客户端收消息用 SSE,发消息用 HTTP POST → 可行但两条通道,不如 WebSocket 优雅
- News Feed:新帖子推送到 Feed → 单向即可 → SSE 如果用 WebSocket:能工作但大材小用,增加复杂度 实际中很多产品用"下拉刷新"(普通 HTTP)也够用 → 取决于产品需求
- URL Shortener:用户发一个短链接请求,得到结果 → 经典的请求-响应 → HTTP 没有任何实时推送需求 → 不需要长连接练习二:画出一次 HTTPS 请求的完整链路
Section titled “练习二:画出一次 HTTPS 请求的完整链路”要求:从用户在浏览器输入 https://hotel.example.com/rooms 开始,画出以下完整链路:
用户输入 URL → DNS 解析 (递归查询,可能命中缓存) → TCP 三次握手 (SYN → SYN-ACK → ACK) → TLS 握手 (ClientHello → ServerHello+证书 → 密钥交换 → 加密开始) → HTTP 请求 (GET /rooms,带 Host/Cookie/Authorization 等头部) → 服务端处理 (路由 → 认证 → 查数据库 → 构造响应) → HTTP 响应 (200 OK + HTML/JSON) → 浏览器渲染/前端处理
标注每个步骤的大致耗时: DNS:0-100ms(有缓存则 0ms) TCP:1个 RTT(约 10-100ms,取决于距离) TLS:1-2个 RTT(TLS 1.3 为 1-RTT,首次连接可能 0-RTT) HTTP:取决于服务端处理时间 + 数据大小
总延迟 = DNS + TCP + TLS + 服务端处理 + 数据传输 ≈ 0 + 30 + 60 + 50 + 10 = 150ms(理想情况)思考:HTTP/2 和 HTTP/3 分别在这个链路的哪些环节做了优化?