阅读视图

发现新文章,点击刷新页面。

跨越边界的艺术:现代 Web 开发跨域解决方案终极指南

一、跨域的本质:同源策略是什么?

想要解决跨域问题,首先要明白“跨域”从何而来。

1. 同源策略的定义

浏览器的同源策略(Same-Origin Policy) 是跨域的核心根源,它是浏览器最核心也最基本的安全功能。
所谓“同源”,要求两个页面的以下三点必须完全相同:

  • 协议(http/https)
  • 域名(包括主域名、子域名)
  • 端口号(80/443/3000等)

只要三者有其一不同,就会被判定为“跨域”。此时,浏览器会限制非同源页面的以下行为:

  • 读取非同源网页的 Cookie、LocalStorage、IndexedDB 等存储数据。
  • 获取非同源网页的 DOM 元素。
  • 向非同源地址发送 AJAX 请求(XMLHttpRequest/fetch)。

2. 为什么需要同源策略?

同源策略就像一道“防火墙”,从根本上限制了恶意网站的非法操作,保障了用户的信息安全。试想一下,如果没有同源策略:

  • 恶意网站可以轻易读取你网银页面的 Cookie,盗取账户信息。
  • 钓鱼网站可以嵌入真实的电商页面,篡改支付金额。
  • 任意网站都能向你的服务器发送伪造请求,发起 CSRF 攻击。

3. 跨域的常见场景

日常开发中,跨域几乎无处不在:

  • 前后端分离项目:前端运行在 localhost:5173,后端接口在 localhost:3000(端口不同)。
  • 调用第三方接口:如支付、地图、天气等第三方服务(域名不同)。
  • 多端协作:公司内部不同部门的系统对接(子域名不同)。

二、方案 1:JSONP——兼容性拉满的“老古董”

JSONP(JSON with Padding)是跨域方案中的“老前辈”,也是早期前端解决跨域最常用的方式,最大的优势是浏览器兼容性极好(甚至能兼容 IE6/7)。

1. JSONP 的核心原理

浏览器的同源策略限制了 AJAX 请求,但并没有限制 <script> 标签的 src 属性。<script> 可以加载任意域名的资源(比如 CDN 上的 jQuery)。
JSONP 正是利用这一“漏洞”实现跨域:

  • 前端动态创建 <script> 标签,通过 src 向跨域接口发送请求,同时传递一个回调函数名。
  • 后端接收到请求后,将数据包裹在回调函数中返回(即“JSON with Padding”)。
  • 前端的回调函数被执行,从而拿到跨域数据。

2. JSONP 实战实现

前端代码(封装 JSONP 函数)
这段代码封装了一个返回 Promise 的 JSONP 函数,便于处理异步逻辑:

// 封装JSONP请求函数,返回Promise方便异步处理
function jsonp({ url, params, callback }) {
  return new Promise((resolve, reject) => {
    // 1. 创建script标签
    let script = document.createElement('script')
    // 2. 定义全局回调函数,接收后端返回的数据
    window[callback] = function(data) {
      resolve(data) // 成功拿到数据,resolve Promise
      document.body.removeChild(script) // 移除script标签,避免污染
    }
    // 3. 拼接请求参数(包含回调函数名)
    params = { ...params, callback } // 比如:{wd: 'test', callback: 'show'}
    let arrs = []
    for (let key in params) {
      arrs.push(`${key}=${params[key]}`)
    }
    // 4. 设置script的src属性,发送请求
    script.src = `${url}?${arrs.join('&')}`
    document.body.appendChild(script)
    // 5. 处理请求失败场景
    script.onerror = function() {
      reject(new Error('JSONP请求失败'))
      document.body.removeChild(script)
    }
  })
}

// 调用JSONP请求
jsonp({
  url: 'http://localhost:3000/say',
  params: { wd: 'Iloveyou' },
  callback: 'show'
}).then(data => {
  console.log('JSONP请求结果:', data)
}).catch(err => {
  console.error(err)
})

后端代码(Node.js 原生实现)
后端需要接收回调函数名,并将数据包裹在函数调用中返回:

const http = require('http');
const server = http.createServer((req, res) => {
  // 匹配/say接口
  if (req.url.startsWith('/say')) {
    // 解析URL参数
    const url = new URL(req.url, `http://${req.headers.host}`);
    const callback = url.searchParams.get('callback'); // 获取回调函数名
    
    // 设置响应头:返回JS脚本
    res.writeHead(200, { 'Content-type': 'text/javascript' });
    // 构造返回数据,包裹在回调函数中
    const data = {
      id: 1,
      username: 'admin',
      msg: 'JSONP请求成功'
    }
    // 核心:返回 "回调函数(数据)" 格式的JS代码
    res.end(`${callback}(${JSON.stringify(data)})`);
  } else {
    res.writeHead(404);
    res.end('Not Found')
  }
})

server.listen(3000, () => {
  console.log('JSONP服务器运行在 http://localhost:3000');
})

3. JSONP 的优缺点

表格

维度 详细说明
✅ 优点 兼容性极强:支持所有主流浏览器,包括低版本 IE。 实现简单:无需复杂的配置,前端后端少量代码即可完成。
❌ 缺点 仅支持 GET 请求:因为 <script> 标签的 src 只能发起 GET 请求。 安全风险:容易遭受 XSS 攻击(加载的脚本可能包含恶意代码),需确保请求的服务器是可信的。 性能问题**:额外加载的 <script> 标签会阻塞页面渲染,影响首屏加载速度。

4. 适用场景

仅推荐在兼容老旧浏览器(如需要支持 IE6/7)的场景下使用。在现代项目中,优先选择其他方案。

三、方案 2:CORS——现代跨域的主流之选

CORS(跨域资源共享,Cross-Origin Resource Sharing)是W3C制定的标准,也是目前解决跨域问题最主流、最推荐的方式。它本质上是在HTTP协议之上,通过增加特定的请求头和响应头,让浏览器和服务器协同工作,来判断一个跨域请求是否被允许。

1. CORS 的核心原理

CORS 的核心思想是:将跨域的控制权从浏览器完全转移到服务器。

1. 浏览器发起请求:当浏览器发起一个跨域请求时,它会自动在请求头中添加 Origin 字段,标明请求的来源(协议、域名、端口)。

2. 服务器决策:服务器接收到请求后,根据 Origin 字段的值来判断是否允许这个来源的请求。

3. 服务器响应:如果服务器允许该请求,它会在响应头中添加 Access-Control-Allow-Origin 字段,其值就是被允许的源。

4. 浏览器检查:浏览器收到响应后,会检查响应头中的 Access-Control-Allow-Origin 是否与请求的 Origin 匹配。如果匹配,则将响应数据返回给前端JS代码;如果不匹配,则浏览器会拦截响应,并在控制台抛出跨域错误。

2. 简单请求 vs 预检请求

CORS 将跨域请求分为两类:简单请求预检请求

简单请求 一个请求要成为简单请求,必须同时满足以下条件:

请求方法是以下之一:GET, HEAD, POST

自定义请求头:除了浏览器自动设置的 Accept, Accept-Language, Content-Language, Content-Type 等,没有添加其他自定义请求头。

Content-Type 的值仅限于:application/x-www-form-urlencoded, multipart/form-data, text/plain

对于简单请求,浏览器会直接发送,并在请求头中带上 Origin

预检请求 不满足简单请求条件的,就是预检请求。例如,使用 PUTDELETE 方法,或者 Content-Typeapplication/json,又或者添加了自定义请求头(如 token)。

对于预检请求,浏览器会先自动发起一个 OPTIONS 方法的请求(这就是“预检”),询问服务器是否允许当前的跨域请求。只有在服务器明确回复“允许”后,浏览器才会真正发起后续的请求。

3. CORS 实战实现

后端代码(Node.js + Express) 使用 Express 框架时,可以借助 cors 中间件轻松实现 CORS。

const express = require('express');
const cors = require('cors'); // 引入cors中间件
const app = express();

// 1. 允许所有来源的跨域请求 (最宽松的配置)
app.use(cors());

// 2. 或者,进行更精细的配置
const corsOptions = {
  origin: 'http://localhost:5173', // 只允许这个源访问
  methods: ['GET', 'POST', 'PUT'], // 允许的请求方法
  allowedHeaders: ['Content-Type', 'Authorization'], // 允许的自定义请求头
  credentials: true // 允许携带Cookie等凭证
};
app.use(cors(corsOptions));

// 定义一个需要跨域访问的接口
app.get('/api/data', (req, res) => {
  res.json({ message: 'CORS请求成功!', data: [1, 2, 3] });
});

app.listen(3000, () => {
  console.log('CORS服务器运行在 http://localhost:3000');
});

后端代码(Node.js 原生实现) 如果不使用框架,也可以手动设置响应头。

const http = require('http');
const server = http.createServer((req, res) => {
  // 设置允许跨域的源,* 表示允许所有源
  res.setHeader('Access-Control-Allow-Origin', '*');
  // 允许的请求方法
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  // 允许的自定义请求头
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  // 允许携带凭证(如Cookie)
  res.setHeader('Access-Control-Allow-Credentials', 'true');

  // 处理预检请求
  if (req.method === 'OPTIONS') {
    res.writeHead(204); // 204 No Content
    res.end();
    return;
  }

  // 处理其他业务请求
  if (req.url === '/api/data') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ message: 'CORS请求成功!', data: [1, 2, 3] }));
  }
});

server.listen(3000, () => {
  console.log('CORS服务器运行在 http://localhost:3000');
});

4. 关键的 CORS 响应头

响应头 说明
Access-Control-Allow-Origin 必需。指定允许访问的源,可以是 *(通配符)或具体的源(如 http://example.com)。
Access-Control-Allow-Methods 预检请求必需。指定允许的请求方法,如 GET, POST, PUT
Access-Control-Allow-Headers 预检请求必需。指定允许的自定义请求头,如 Content-Type, Authorization
Access-Control-Allow-Credentials 可选。一个布尔值,表示是否允许浏览器发送 Cookie。如果为 true,则 Access-Control-Allow-Origin 不能为 *,必须是具体的源。
Access-Control-Max-Age 可选。指定预检请求的缓存时间(秒),避免频繁发送 OPTIONS 请求。

5. CORS 的优缺点

维度 详细说明
优点 功能强大:支持所有类型的 HTTP 请求(GET, POST, PUT, DELETE 等)。安全性高:通过服务器精确控制允许的源、方法和请求头。现代标准:被所有现代浏览器支持,是前后端分离项目的最佳实践。
缺点 需要后端配合:必须在服务器端进行配置,前端无法单方面解决。配置复杂:对于需要携带凭证或复杂请求头的场景,配置相对繁琐。兼容性问题:不支持 IE9 及以下版本。

6. 适用场景

CORS 是现代 Web 开发中解决跨域问题的首选方案,尤其适用于前后端分离的架构。只要后端能够配合修改响应头,就应该优先使用 CORS。

四、方案 3:反向代理——“曲线救国”的万能钥匙

反向代理是开发环境中解决跨域问题最常用、也最省心的方法之一。它的核心思想是 “曲线救国” :既然浏览器禁止前端直接访问后端接口,那我们就让前端请求一个和自己“同源”的代理服务器,再由这个代理服务器去请求真正的后端接口。

1. 反向代理的核心原理

1.  前端请求代理:前端应用(如运行在 localhost:5173)不再直接请求后端接口(如 localhost:3000/api),而是请求一个与自己同源的代理地址(如 localhost:5173/api)。因为源相同,所以不会触发浏览器的跨域限制。

2. 

代理服务器转发:这个代理服务器(通常是开发服务器或 Nginx)收到请求后,会以自己的身份向真正的后端接口(localhost:3000/api)发起请求。这个请求是服务器与服务器之间的通信,不受浏览器同源策略的限制。

3. 代理服务器返回:代理服务器拿到后端接口的响应数据后,再原封不动地返回给前端。 4.

通过这种方式,前端巧妙地绕过了浏览器的跨域限制,实现了数据的获取。

2. 反向代理实战实现

反向代理的实现方式多种多样,从开发环境的配置到生产环境的部署,都有其身影。

开发环境:Vite/Webpack 配置 在现代前端框架(如 Vue, React)的开发环境中,我们通常使用 Vite 或 Webpack 作为开发服务器。它们都内置了强大的代理功能,只需几行配置即可解决跨域问题。

Vite 配置示例 ( vite.config.js )

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  server: {
    // 配置代理规则
    proxy: {
      // 当前端请求 /api 路径时,触发代理
      '/api': {
        target: 'http://localhost:3000', // 代理的目标服务器地址
        changeOrigin: true, // 修改请求头中的 Origin 为目标服务器的 Origin
        // rewrite: (path) => path.replace(/^/api/, '') // 可选:重写路径,去掉 /api 前缀
      }
    }
  }
})

配置完成后,前端请求 http://localhost:5173/api/data,开发服务器会自动将其转发到 http://localhost:3000/api/data

生产环境:Nginx 配置 在项目部署到生产环境时,Nginx 是最常用的反向代理服务器。它不仅能处理跨域,还能提供负载均衡、静态资源服务、缓存等多种功能。

Nginx 配置示例 ( nginx.conf )

server {
    listen       80;
    server_name  localhost; # 或者你的域名

    # 1. 配置前端静态文件
    location / {
        root   /usr/share/nginx/html; # 前端打包文件的路径
        index  index.html index.htm;
        try_files $uri $uri/ /index.html; # 解决前端路由 history 模式刷新404的问题
    }

    # 2. 配置后端接口代理
    location /api/ {
        proxy_pass http://backend_server:3000/; # 后端服务器的地址
        proxy_set_header Host $host;             # 传递原始主机头
        proxy_set_header X-Real-IP $remote_addr; # 传递用户真实IP
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

这样配置后,无论是前端页面还是 /api/ 开头的接口请求,都由同一个 Nginx 服务器处理,完美规避了跨域问题。

3. 反向代理的优缺点

维度 详细说明
** 优点** 前端无感:前端代码无需任何修改,完全不用关心跨域问题。功能强大:除了跨域,还能实现负载均衡、请求/响应拦截、日志记录等。通用性强:适用于任何类型的请求,不受请求方法和请求头的限制。
** 缺点** 增加服务器成本:需要额外部署和维护一台代理服务器(如 Nginx)。配置相对复杂:相比 CORS,反向代理的配置(尤其是 Nginx)需要一定的运维知识。可能增加延迟:请求多了一次转发,理论上会增加一点点网络延迟。

4. 适用场景

● 开发环境:强烈推荐使用 Vite/Webpack 的代理功能,是开发阶段解决跨域问题的首选。

● 生产环境:当你无法控制后端服务器(例如调用第三方API),或者后端团队不方便配合配置 CORS 时,使用 Nginx 反向代理是最佳选择。它也是微服务架构中 API 网关的雏形。

五、方案 4:WebSocket——实时通信的“特权通道”

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它最大的特点是不受同源策略的限制,这使得它在需要实时双向通信的场景下,成为了一个天然的跨域解决方案。

1. WebSocket 的核心原理

WebSocket 的工作流程可以分为三个阶段:

1. 握手阶段:前端通过 JavaScript 创建一个 WebSocket 对象,浏览器会向服务器发起一个特殊的 HTTP 请求。这个请求头中包含 Upgrade: websocket 字段,表示希望将协议从 HTTP 升级到 WebSocket。

2. 协议升级:服务器收到请求后,如果支持 WebSocket,会返回一个状态码为 101 (Switching Protocols) 的响应,同意协议升级。

3. 数据传输:一旦握手成功,客户端和服务器之间就建立了一条持久的 TCP 连接。此后,双方可以随时主动向对方推送数据,而无需像 HTTP 那样由客户端反复发起请求。

正是因为 WebSocket 在握手成功后就脱离了 HTTP 协议的范畴,建立了一条独立的“管道”,所以浏览器不会对其应用同源策略的限制。

2. WebSocket 实战实现

前端代码 前端使用非常简单,只需几行代码即可建立连接并监听事件。

// 1. 创建 WebSocket 连接,传入服务器地址
// 注意:协议是 ws:// (或 wss:// 用于加密连接)
const socket = new WebSocket('ws://localhost:3000');

// 2. 监听连接成功事件
socket.onopen = function(event) {
  console.log('WebSocket 连接已建立');
  // 连接成功后,可以立即向服务器发送数据
  socket.send(JSON.stringify({ type: 'init', data: 'Hello Server!' }));
};

// 3. 监听服务器发来的消息
socket.onmessage = function(event) {
  console.log('收到服务器消息:', event.data);
  const data = JSON.parse(event.data);
  // 根据消息类型处理数据
  if (data.type === 'notification') {
    alert(data.message);
  }
};

// 4. 监听连接关闭事件
socket.onclose = function(event) {
  console.log('WebSocket 连接已关闭');
};

// 5. 监听连接错误事件
socket.onerror = function(event) {
  console.error('WebSocket 发生错误:', event);
};

// 随时可以向服务器发送数据
function sendMsg() {
  socket.send('这是一条新消息');
}

后端代码(Node.js + ws 库) 后端可以使用 ws 这个轻量级的 WebSocket 库来快速搭建服务器。

const WebSocket = require('ws');

// 创建一个 WebSocket 服务器,监听 3000 端口
const wss = new WebSocket.Server({ port: 3000 });

// 监听客户端连接事件
wss.on('connection', (ws) => {
  console.log('有客户端连接进来了');

  // 向当前连接的客户端发送欢迎消息
  ws.send(JSON.stringify({ type: 'welcome', message: '欢迎连接到 WebSocket 服务器' }));

  // 监听当前客户端发来的消息
  ws.on('message', (data) => {
    console.log('收到客户端消息:', data.toString());
    // 可以将消息广播给所有连接的客户端
    wss.clients.forEach((client) => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(`广播: ${data}`);
      }
    });
  });

  // 监听客户端断开连接
  ws.on('close', () => {
    console.log('客户端连接已关闭');
  });
});

console.log('WebSocket 服务器运行在 ws://localhost:3000');

3. WebSocket 的优缺点

维度 详细说明
优点 天然跨域:不受同源策略限制,无需额外配置。实时双向通信:服务器可以主动向客户端推送数据,延迟极低。持久连接:只需一次握手,即可保持长时间通信,减少了 HTTP 反复建立连接的开销。
缺点 协议不同:需要服务器和客户端都支持 WebSocket 协议,不适用于传统的 HTTP 请求场景。兼容性问题:不支持 IE9 及以下版本。连接管理复杂:需要处理连接的建立、维持、断开和重连,比简单的 HTTP 请求更复杂。

4. 适用场景

WebSocket 专为实时性要求高的场景而生,例如:

● 在线聊天/即时通讯:如微信网页版、在线客服。

● 实时数据推送:如股票行情、体育比赛比分、新闻快讯。

● 协同编辑:如在线文档、代码编辑器。

● 多人在线游戏:需要实时同步玩家状态。


六、方案 5:Vite 反向代理 —— 本地开发的 “最优解”

在前端本地开发阶段,我们经常会遇到这样的场景:前端项目运行在 http://localhost:5173,而后端接口运行在 http://localhost:3000。虽然这只是开发环境下的端口不同,但在浏览器看来这就是“跨域”。

虽然可以通过后端配置 CORS 来解决,但在开发阶段,更优雅、更安全的方式是利用开发服务器(如 Vite、Webpack)进行反向代理。这种方式不需要后端做任何改动,完全由前端工具链来处理跨域问题。

1. Vite 代理的核心原理

Vite 代理的本质是利用了开发服务器(Dev Server)作为“中间人”。

  • 前端视角:前端代码发起请求时,目标地址是 Vite 服务器(例如 /api/user)。
  • 同源策略豁免:因为 Vite 服务器就是提供前端页面的服务端,所以前端请求 /api/user 属于“同源请求”,浏览器不会拦截。
  • 服务器转发:Vite 服务器接收到请求后,发现这是一个代理请求,于是它会以“服务器身份”向真正的后端接口(例如 http://localhost:3000/api/user)发起请求。
  • 响应返回:后端接口将数据返回给 Vite 服务器,Vite 再将数据返回给前端浏览器。

关键点:浏览器与 Vite 服务器之间是同源的(无跨域);Vite 服务器与后端服务器之间是服务器间的通信(不受浏览器同源策略限制)。通过这种“曲线救国”的方式,完美绕过了浏览器的跨域限制。

2. Vite 代理实战配置

Vite 内置了强大的代理功能,基于 http-proxy 中间件实现。我们只需要在 vite.config.js 中进行简单的配置即可。

配置步骤:

  1. 打开项目根目录下的 vite.config.js 文件。
  2. 在 server 选项中添加 proxy 配置。
  3. 定义需要代理的路径前缀(如 /api)。
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  server: {
    // 配置代理规则
    proxy: {
      // 1. 定义代理前缀
      // 当请求路径以 '/api' 开头时,触发代理
      '/api': {
        // 2. 目标服务器地址
        // 这里填写后端接口的真实地址
        target: 'http://localhost:3000',
        
        // 3. 是否改变请求头中的 Origin
        // 设置为 true 时,Vite 会将请求头的 Host 改为目标服务器的 Host
        // 避免后端因为 Origin 校验不通过而拒绝请求
        changeOrigin: true,
        
        // 4. 路径重写 (可选)
        // 如果后端不需要 '/api' 这个前缀,可以将其重写为空
        // 例如:前端请求 '/api/user' -> 后端接收 '/user'
        rewrite: (path) => path.replace(/^/api/, '')
      },
      
      // 5. 多个代理配置 (可选)
      // 如果有多个不同的后端服务,可以继续添加
      '/upload': {
        target: 'http://upload-server.com',
        changeOrigin: true
      }
    }
  }
})

3. 配置项详解

表格

配置项 类型 说明
target String 必需。你要代理到的目标地址,即后端接口的真实域名或 IP。
changeOrigin Boolean 推荐开启。设为 true 时,会自动修改请求头中的 host 为 target 的值。很多后端框架(如 Nginx、Java Spring)会校验 host,不开启可能导致 403/404 错误。
rewrite Function 可选。用于重写请求路径。例如,如果后端接口不需要前端定义的前缀(如 /api),可以用此函数将其替换或删除。
secure Boolean 如果目标是 https 接口,设为 false 可以忽略 HTTPS 证书校验(开发环境常用)。

4. 优缺点分析

优点:

  • 开发环境专用神器:无需后端配合,前端开发者自己就能搞定,不影响生产环境配置。
  • 无跨域风险:完全在开发服务器层面处理,浏览器根本感知不到跨域的存在。
  • 配置极其简单:Vite 内置功能,几行代码即可完成,且支持 TypeScript 配置。
  • 支持 WebSocket:Vite 代理也支持代理 WebSocket 连接(配置 ws: true)。

缺点:

  • 仅限开发环境:Vite 代理只在 vite dev 启动的开发服务器中生效。项目打包上线后,Vite 服务器不再运行,代理配置也随之失效。
  • 无法解决生产环境问题:它只是一个开发时的“模拟器”,不能用于解决线上环境的跨域问题。

5. 适用场景

  • 前端本地开发:这是该方案的唯一且最佳场景。
  • 接口联调阶段:在后端尚未部署或无法修改响应头时,前端通过代理快速进行联调。
  • Mock 数据切换:配合环境变量,可以在代理真实接口和本地 Mock 服务之间灵活切换。

八、方案 7:postMessage —— 跨域通信的“万能信使”

前文提到的 JSONP、CORS、反向代理等方案,主要解决的是“浏览器向服务器请求数据”的跨域问题。但在现代 Web 开发中,我们经常会遇到“页面与页面”、“窗口与窗口”之间需要通信的场景,例如:

  • 父页面与嵌入的跨域 iframe 进行数据交互。
  • 主窗口与通过 window.open() 打开的跨域子窗口同步状态。
  • 主线程与 Web Worker 之间传递消息。

对于这些场景,postMessage 是 HTML5 提供的标准解决方案,它就像一个“万能信使”,允许不同源的窗口之间安全地进行双向通信。

1. postMessage 的核心原理

postMessage 的核心思想是 “消息传递” 而非“直接访问”。它打破了浏览器的同源策略,但并没有完全移除安全限制。

  • 发送方:通过 targetWindow.postMessage(message, targetOrigin) 方法,向目标窗口发送一条结构化的数据消息。
  • 接收方:通过监听 window 对象的 message 事件,来捕获并处理来自其他窗口的消息。
  • 安全校验:整个过程通过 targetOrigin(发送时指定目标源)和 event.origin(接收时检查消息来源)进行双重安全校验,确保消息只在你信任的窗口之间传递。

2. postMessage 实战实现

我们以最常见的 父页面与跨域 iframe 通信 为例,展示如何实现双向通信。

父页面 (parent.html)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>父页面</title>
</head>
<body>
    <h1>我是父页面</h1>
    <!-- 嵌入一个跨域的 iframe -->
    <iframe id="childFrame" src="https://iframe-example.com/child.html"></iframe>

    <script>
        const iframe = document.getElementById('childFrame');

        // 1. 向 iframe 发送消息
        // 注意:必须等待 iframe 加载完成后再发送
        iframe.onload = () => {
            const data = { type: 'GREETING', text: 'Hello from parent!' };
            // 精确指定目标源,这是安全的关键!
            iframe.contentWindow.postMessage(data, 'https://iframe-example.com');
        };

        // 2. 监听来自 iframe 的消息
        window.addEventListener('message', (event) => {
            // 【安全红线】必须校验消息来源!
            if (event.origin !== 'https://iframe-example.com') {
                console.warn('收到来自非法源的消息,已忽略');
                return;
            }
            console.log('父页面收到消息:', event.data);
        });
    </script>
</body>
</html>

子页面 (child.html)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>子页面 (iframe)</title>
</head>
<body>
    <h1>我是子页面 (iframe)</h1>
    <script>
        // 1. 监听来自父页面的消息
        window.addEventListener('message', (event) => {
            // 【安全红线】同样必须校验消息来源!
            if (event.origin !== 'https://parent-example.com') {
                return;
            }
            console.log('子页面收到消息:', event.data);

            // 2. 向父页面回复消息
            const replyData = { type: 'REPLY', text: 'Hello back!' };
            // event.source 是发送消息的窗口对象的引用
            event.source.postMessage(replyData, event.origin);
        });
    </script>
</body>
</html>

3. API 详解

发送消息:targetWindow.postMessage(message, targetOrigin)

表格

参数 类型 说明
message 任意类型 要发送的数据。可以是字符串、对象、数组等,数据会被浏览器使用“结构化克隆算法”进行序列化。
targetOrigin String 安全关键!  指定接收消息的窗口的源(协议+域名+端口)。必须精确指定,严禁在生产环境使用通配符 '*' ,否则可能导致敏感数据泄露给恶意网站。

接收消息:window.addEventListener('message', callback)

回调函数接收一个 MessageEvent 对象,其中包含三个关键属性:

表格

属性 类型 说明
event.data 任意类型 发送方传递的实际消息数据。
event.origin String 安全关键!  发送消息的窗口的源。接收方必须校验此属性,确保消息来自可信源。
event.source Window 对象 发送消息的窗口对象的引用。可用于向发送方回传消息,实现双向通信。

4. 优缺点分析

优点:

  • 功能强大:解决了页面间通信的跨域问题,这是 CORS 和代理无法做到的。
  • 双向通信:支持父子窗口、主副窗口之间的双向消息传递。
  • 安全性高:通过 targetOrigin 和 event.origin 的双重校验,可以有效防止恶意攻击。
  • 数据灵活:支持传递复杂的结构化数据。

缺点:

  • 使用场景特定:仅适用于窗口间通信,不适用于常规的 AJAX 请求。
  • 安全要求高:开发者必须手动进行源校验,任何疏忽都可能导致严重的安全漏洞(如 XSS)。
  • 异步通信:基于事件模型,处理复杂交互时逻辑可能变得分散。

5. 适用场景

  • 第三方组件集成:如嵌入支付宝/微信支付的 iframe,支付完成后通知父页面。
  • 跨域单点登录(SSO) :通过一个中央登录页,使用 postMessage 将登录令牌传递给其他域名的应用。
  • 微前端架构:主应用与子应用之间进行状态同步和事件通知。
  • 多窗口协作:如在线协作文档,主窗口打开多个编辑窗口,并同步光标位置和编辑内容。
  • Web Worker:主线程与后台线程进行数据交换。

面试官问SSE和WebSocket的区别?看这篇就够了(含心跳机制详解)

最近在复习计算机网络和 LLM 相关技术时,我突然意识到一个很有意思的现象:现在的 AI 聊天大多用的是 SSE,但提到真正的实时互动,还得看 WebSocket

为了搞懂这玩意儿,我手写了一个简易版的聊天室。今天就把我的学习笔记、代码实现,还有那些让人头秃的协议对比,一次性全掏出来!


🤔 为什么 HTTP 不适合聊天?

咱们先聊聊背景。作为前端,我们最熟悉的是 HTTP 协议。

HTTP 就像是一个“高冷”的客服

  • 你问一句(Request),它答一句(Response)。
  • 答完就挂电话(短连接),下次想问得重新拨号。

如果你想做一个聊天室,用 HTTP 怎么办?只能靠轮询

// 每隔 1 秒问一次服务器:“有新消息吗?有新消息吗?”
setInterval(() => {
  fetch('/api/messages').then(...)
}, 1000);

这太蠢了,对吧?性能差,延迟高,服务器都要被问烦了。

SSE 呢?SSE 适合 LLM 那种“流式输出”(我一次提问,它一直吐字),它是单向的。但聊天是双向的,我要发,你也要发。

所以,我们需要一个能建立“长连接”、双方都能主动说话的协议——WebSocket


💡 WebSocket:一次握手,终身相伴

WebSocket 是 HTML5 提供的一种在单个 TCP 连接上进行全双工通讯的协议。

  • 全双工:就像打电话,双方都可以同时说话,不需要等对方说完。
  • 长连接:一旦建立,除非主动断开,否则一直连着。

📝 核心代码逻辑拆解

这里我用 Koa + koa-websocket 来实现。为了让大家看得更清楚,我把代码拆成三个关键步骤来讲。

步骤一:搭建舞台(服务端初始化)

首先,我们需要让 Koa 具备处理 WebSocket 的能力,并准备一个“花名册”来记录所有连进来的用户。

javascript

编辑

const Koa = require('koa');
const websocket = require('koa-websocket');

// 1. 初始化 Koa 并赋予 WebSocket 能力
const app = websocket(new Koa());

// 2. 准备一个 Set 集合,用来存储所有连接的客户端
// 为什么用 Set?因为我们要保证连接对象的唯一性
const clients = new Set();

步骤二:派发请柬(处理 HTTP 请求)

WebSocket 连接通常是从一个网页开始的。所以,我们需要一个普通的 HTTP 中间件,返回给浏览器一个包含聊天界面的 HTML 页面。

javascript

编辑

// 3. 处理普通 HTTP 请求:返回我们的聊天页面
app.use(async (ctx) => {
    ctx.body = `
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <title>WebSocket Chat</title>
    </head>
    <body>
       <div id="messages" style="height:300px;overflow-y:scroll; border:1px solid #ccc;"></div>
        <input type="text" id="messageInput" placeholder="输入消息..."/>
        <button onclick="sendMessage()">发送</button>
        <script>
            // 核心在这里:前端建立 WebSocket 连接
            // 注意协议是 ws:// 而不是 http://
            const ws = new WebSocket('ws://localhost:3000/ws');
            
            // 监听服务器发来的消息
            ws.onmessage = function(event){
                const messageDiv = document.getElementById('messages');
                messageDiv.innerHTML += '<div>' + event.data + '</div>';
            }
            
            function sendMessage(){
                const input = document.getElementById('messageInput');
                ws.send(input.value); // 发送消息
                input.value = '';
            }
        </script>
    </body>
    </html>
    `
})

步骤三:建立专线(处理 WebSocket 连接)

这是最关键的一步。当浏览器执行了 new WebSocket() 后,服务器会通过 app.ws.use 捕获到这个连接请求。

这里我们主要做三件事:登记用户监听消息广播消息

javascript

编辑

// 4. 处理 WebSocket 连接
app.ws.use(async (ctx) => {
    // A. 登记:将当前连接加入集合
    clients.add(ctx.websocket);
    console.log('当前在线人数:', clients.size);

    // B. 监听:当收到某人的消息时
    ctx.websocket.on('message', message => {
        // C. 广播:把这条消息发给“花名册”里的每一个人
        for(const client of clients){
            client.send(message.toString());
        }
    });

    // D. 离场:监听断开连接,把人从花名册里删掉
    ctx.websocket.on('close', () => {
        clients.delete(ctx.websocket);
        console.log('有人离开了...');
    });
})

app.listen(3000, () => {
    console.log('🚀 服务器启动,请访问 http://localhost:3000');
});

运行效果:
打开浏览器访问 localhost:3000,你可以打开好几个标签页,在一个标签页发消息,所有标签页都会实时收到消息!这就是广播

image.png


📊 一张表看懂 HTTP、SSE 与 WebSocket

为了面试(408 计算机网络)和工作,这三个协议的区别必须门儿清:

特性 HTTP SSE WebSocket
连接方式 短连接 (请求-响应) 长连接 (单向推送) 长连接 (双向通讯)
通讯方向 客户端发起 服务端 -> 客户端 客户端 <-> 服务端
适用场景 网页加载、API 请求 AI 流式输出、股票行情 聊天室、即时游戏、协作编辑
数据格式 文本/JSON/二进制 仅限文本 (text/event-stream) 二进制帧/文本帧
  • SSE:适合“我不动,你推给我”的场景(比如 LLM 打字机效果)。
  • WebSocket:适合“你一句我一句”的场景。

❤️ 心跳机制:长连接的“异地恋”哲学

既然 WebSocket 是长连接,那就面临一个现实问题:网络是不稳定的

路由器重启、手机进电梯、防火墙拦截……都可能导致连接“静默断开”。这时候,客户端以为连着,服务器以为断了,这就尴尬了。

怎么解决?—— 心跳机制

这就好比异地恋的情侣

你们不能一直打电话(开销太大),但必须定期确认对方还在。

  • 客户端:“宝,你在吗?”(Ping)
  • 服务端:“在呢,活着呢。”(Pong)

如果客户端发了 Ping,过了 30 秒还没收到 Pong,那就判定为“分手”(连接断开),然后触发重连机制

代码逻辑示意:

// 客户端
setInterval(() => {
    if(ws.readyState === WebSocket.OPEN){
        ws.send(JSON.stringify({type: 'ping'}));
    }
}, 30000); // 每30秒问候一次

// 服务端
ws.on('message', (msg) => {
    const data = JSON.parse(msg);
    if(data.type === 'ping'){
        ws.send(JSON.stringify({type: 'pong'})); // 秒回
    }
});

📌 总结

今天我们从 HTTP 的局限性出发,手搓了一个基于 Koa 的 WebSocket 聊天室,顺便复习了 SSE 和心跳机制。

划重点:

  1. HTTP 是“一问一答”,WebSocket 是“双向奔赴”。
  2. SSE 适合流式输出(AI),WebSocket 适合即时通讯(Chat)。
  3. 心跳机制是长连接保活的关键,防止“假死”连接。

希望这篇文章能帮你搞定 WebSocket!如果觉得有用,记得点个赞 👍,我们下期见!

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

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

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


原文地址

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


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

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

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

这个差异源于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离线推送,才能最终呈现在屏幕上。

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

❌