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

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

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


原文地址

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


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

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

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

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

回顾一下HTTP的工作方式:

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

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

(一秒钟后)

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

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

(又一秒)

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

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

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

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


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

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

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

服务器回复:「没有。」

继续问。

服务器回复:「没有。」

继续问。

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

这就是短轮询(Short Polling)

实现示例

javascript 复制代码
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号客户,请入座"------无需频繁询问。

长轮询的工作流程

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

实现示例

javascript 复制代码
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,随后升级为不同的协议:

yaml 复制代码
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的请求/响应:

yaml 复制代码
┌─────────────────────────────────────────────────────────────┐
│  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字节,超过时使用扩展)

消息格式示例

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

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

WebSocket事件生命周期

yaml 复制代码
┌─────────────────────────────────────────────────────────┐
│                   WebSocket连接                            │
│                                                          │
│  onopen     →     onmessage     →     onclose           │
│  (连接建立)        (接收消息)           (连接关闭)         │
│                                                          │
│               onerror                                  │
│               (连接错误)                                 │
└─────────────────────────────────────────────────────────┘

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

WebSocket与HTTP对比

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

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

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

典型场景:

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

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

SSE的工作方式

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

实现示例

javascript 复制代码
// 客户端
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);
});
javascript 复制代码
// 服务器(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事件格式

yaml 复制代码
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 需定期发送心跳包保持连接活跃:

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

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

断线重连策略

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

javascript 复制代码
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验证方式:

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

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

简化架构

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

消息发送完整流程

yaml 复制代码
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实现幂等:

javascript 复制代码
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),未确认则重试:

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

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

javascript 复制代码
// 业务层消息确认
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 示例:

json 复制代码
{
  "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离线推送,才能最终呈现在屏幕上。

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

相关推荐
酉鬼女又兒2 小时前
零基础快速入门前端DOM 元素获取方法详解:从代码到实践(可用于备赛蓝桥杯Web应用开发)
前端·javascript·职场和发展·蓝桥杯·js
牛奶2 小时前
为什么关掉浏览器再打开,你还是登录状态?
前端·网络协议·https
bjxiaxueliang2 小时前
一文掌握Python aiohttp:异步Web开发从入门到部署
开发语言·前端·python
Liudef062 小时前
从0到1开发ReAct智能体:原理、实现与最佳实践
前端·react.js·前端框架
金豆呀2 小时前
WPS自定义公式,相似度匹配
前端·javascript·wps
jiayong232 小时前
0基础学习VUE3 第 1 课:项目启动流程
前端·vue.js·学习
今天又在摸鱼2 小时前
学习vue前必要的js语法
前端·vue.js·学习
大家的林语冰2 小时前
TypeScript 6 官宣,JS “最后之舞“,版本升级踩雷指南
前端·javascript·typescript
英俊潇洒美少年2 小时前
react useDeferredvalue和useTransition的讲解
前端·react.js·前端框架