WebSocket 双工通信实战:从协议握手到跨域原理,彻底搞懂实时通信的"另一条路"
上一篇讲了 SSE——服务器单向推送给浏览器的流式输出。但 QQ、微信的在线聊天呢?弹幕呢?这些场景需要双向通信,SSE 就不够用了。本文从 HTTP 的"单向"本质出发,到 WebSocket 的协议升级握手、ws 库完整实现、跨域原理、事件机制,再到"WebSocket 双工这么强,为什么 LLM 流式输出还是选 SSE?"——彻底搞懂实时通信的"另一条路"。建议收藏后动手实操。
一、从 HTTP 单向到 WebSocket 双工
1.1 HTTP 的单向本质
HTTP 协议 = 单向通信
┌──────────────────────────────────────────────────────────┐
│ HTTP 请求-响应模型 │
│ │
│ 浏览器 服务器 │
│ │ │ │
│ │──── 请求 ────────────────────→│ │
│ │ │ (处理...) │
│ │←─── 响应 ────────────────────│ │
│ │ │ │
│ │ (连接断开) │ │
│ │ │ │
│ │
│ 特点: │
│ ① 客户端发起请求,服务器才能响应 │
│ ② 服务器不能主动向客户端推送数据 │
│ ③ 响应完就断开(HTTP/1.1 keep-alive 保持连接但仍是请求驱动)│
│ ④ 服务器只能"被动"回答,不能"主动"说话 │
└──────────────────────────────────────────────────────────┘
这就带来了问题:
场景一:在线聊天
→ A 发消息给 B
→ B 的客户端需要"被通知"有新消息
→ 但 HTTP 是客户端发起请求
→ B 怎么"知道"A 发了消息?
→ 轮询?每秒问一次服务器"有新消息吗"?→ 浪费资源
场景二:弹幕
→ 主播端推流,观众端看
→ 弹幕需要实时显示
→ 服务器需要主动推弹幕给所有观众
→ HTTP 服务器不能主动推送
场景三:在线状态
→ 好友上线/下线需要实时通知
→ 服务器需要主动告诉客户端"你好友上线了"
→ HTTP 做不到
1.2 WebSocket 的双工突破
WebSocket = 全双工通信
┌──────────────────────────────────────────────────────────┐
│ WebSocket 通信模型 │
│ │
│ 客户端 服务器 │
│ │ │ │
│ │═════════ 双向通道 ═════════════│ │
│ │ ←──── 服务器推送 ────────→ │ │
│ │ ────── 客户端发送 ────────→ │ │
│ │ │ │
│ │ 两边都可以随时发数据 │ │
│ │ 地位平等,无主从之分 │ │
│ │ │ │
│ 特点: │
│ ① 全双工:双向同时通信 │
│ ② 服务器可以主动推送 │
│ ③ 客户端也可以主动发送 │
│ ④ 连接保持不断开 │
│ ⑤ 地位平等:两边都是"发送方+接收方" │
└──────────────────────────────────────────────────────────┘
WebSocket 解决了 HTTP 的核心痛点:
→ 不需要轮询
→ 服务器可以主动推送
→ 实时性极高
→ 适合聊天、弹幕、在线游戏、协作编辑
1.3 SSE vs WebSocket 定位
┌──────────────────────────────────────────────────────────────┐
│ SSE vs WebSocket 定位对比 │
│ │
│ SSE(Server-Sent Events) │
│ → 服务器 → 浏览器(单向) │
│ → 基于 HTTP │
│ → 浏览器 API:EventSource │
│ → 自动重连 │
│ → 只能 GET │
│ → 适合:ChatGPT 打字机、通知推送、股票行情 │
│ → 本质:服务器单向流式推送 │
│ │
│ WebSocket │
│ → 双向(全双工) │
│ → 独立协议(ws://) │
│ → 浏览器 API:WebSocket │
│ → 需手动重连 │
│ → 无方法限制 │
│ → 适合:QQ/微信聊天、弹幕、在线游戏、协作编辑 │
│ → 本质:双向实时通信 │
│ │
│ 选择原则: │
│ → 只需要服务器→浏览器推送? → SSE 更简单 │
│ → 需要双向实时通信? → WebSocket │
└──────────────────────────────────────────────────────────────┘
二、WebSocket 协议握手
2.1 从 HTTP 到 WebSocket 的协议升级
WebSocket 连接建立的"两步走":
┌──────────────────────────────────────────────────────────┐
│ 第一步:HTTP 连接 │
│ │
│ 客户端发起 HTTP 请求 │
│ ws://localhost:8080/ws │
│ → 先建立 HTTP 连接 │
│ → 找到 Web Server(http://localhost:8080) │
│ → 只需要一次 │
│ │
│ 第二步:协议切换 │
│ │
│ 服务器返回 101 状态码 │
│ → 101 Switching Protocols │
│ → "我要把协议从 HTTP 切换到 WebSocket" │
│ → 基于同一个 TCP 连接 │
│ → 不需要新建连接 │
│ │
│ 切换后: │
│ → HTTP 的请求-响应模式不再适用 │
│ → 变成 WebSocket 的全双工通信 │
│ → 两边随时可以发数据 │
└──────────────────────────────────────────────────────────┘
为什么基于 HTTP 升级?
→ WebSocket 不是从零开始的全新协议
→ 它"搭便车"用 HTTP 完成初始连接
→ 因为 HTTP 能穿透防火墙、代理服务器
→ 升级后切换到 WebSocket 协议
→ 这叫"协议升级"或"协议切换"
2.2 HTTP 状态码回顾
HTTP 状态码分类:
┌──────────────────────────────────────────────────────────┐
│ 1XX 信息性 → 请求已接收,继续处理 │
│ ② 101 Switching Protocols → 切换协议 │
│ → WebSocket 握手时返回 │
│ → "HTTP 切到 WebSocket 了" │
│ │
│ 2XX 成功 → 请求被成功处理 │
│ → 200 OK │
│ → 201 Created │
│ │
│ 3XX 跳转 → 需要进一步操作 │
│ → 301 Moved Permanently │
│ → 302 Found │
│ │
│ 4XX 客户端错误 → 请求有误 │
│ → 400 Bad Request │
│ → 404 Not Found │
│ → 403 Forbidden │
│ │
│ 5XX 服务器错误 → 服务器处理失败 │
│ → 500 Internal Server Error │
│ → 502 Bad Gateway │
│ → 503 Service Unavailable │
└──────────────────────────────────────────────────────────┘
101 的特殊之处:
→ 1XX 表示"还在通信中,没有完成"
→ 101 表示"协议切换中"
→ 这是 HTTP 唯一"改变协议"的状态码
→ 其他状态码都在 HTTP 协议内
→ 101 跳出了 HTTP,进入 WebSocket
2.3 握手过程详解
WebSocket 握手的完整过程:
客户端 服务器
│ │
│── HTTP GET /ws ────────────────────────→│
│ Headers: │
│ Upgrade: websocket │
│ Connection: Upgrade │
│ Sec-WebSocket-Key: dGhlIHNhbXBsZQ== │
│ Sec-WebSocket-Version: 13 │
│ │
│←── HTTP 101 Switching Protocols ────────│
│ Headers: │
│ Upgrade: websocket │
│ Connection: Upgrade │
│ Sec-WebSocket-Accept: s3pPLMBiTxaQ... │
│ │
│═══════ WebSocket 双工通道建立 ══════════│
│ │
│←── 服务器推送数据 ──────────────────────│
│── 客户端发送数据 ──────────────────────→│
│←── 服务器推送数据 ──────────────────────│
│── 客户端发送数据 ──────────────────────→│
│ ...持续通信... │
│ │
握手的关键头部:
① Upgrade: websocket → 请求升级协议
② Connection: Upgrade → 告诉服务器要升级
③ Sec-WebSocket-Key → 客户端生成的随机 key
④ Sec-WebSocket-Accept → 服务器用 key 计算的回应
→ 服务器验证客户端身份
→ 确保不是随便的 HTTP 请求
三、ws 库完整实现
3.1 服务端
// server.js
const WebSocket = require('ws');
const http = require('http');
// 第一步:启动 HTTP Server
const server = http.createServer((req, res) => {
res.writeHead(200, {
'Content-Type': 'text/plain'
});
res.end('WebSocket Server Running!');
});
// 第二步:在 HTTP Server 之上搭建 WebSocket 服务
const wss = new WebSocket.Server({
server,
path: '/ws'
});
// 第三步:监听连接事件
wss.on('connection', (ws) => {
console.log('Client connected');
// 监听客户端发来的消息
ws.on('message', (message) => {
console.log(`Received message: ${message}`);
// 回显消息给客户端
ws.send(`Server received: ${message}`);
});
});
server.listen(8080, () => {
console.log('listening on http://localhost:8080');
});
代码解析:
① const server = http.createServer(...)
→ 先创建 HTTP 服务器
→ 处理普通 HTTP 请求(非 WebSocket)
→ 这是 WebSocket 的"宿主"
→ WebSocket 基于同一个端口(8080)
② new WebSocket.Server({ server, path: '/ws' })
→ 在 HTTP 服务器之上搭建 WebSocket 服务
→ server: 复用同一个 HTTP 服务器
→ path: '/ws' → WebSocket 路径
→ 客户端连接 ws://localhost:8080/ws
→ 访问 http://localhost:8080 → 普通 HTTP
→ 访问 ws://localhost:8080/ws → WebSocket
③ wss.on('connection', (ws) => { ... })
→ 有客户端连接时触发
→ ws 是单个客户端的连接对象
→ wss 是 WebSocket.Server 实例(管理所有连接)
→ 可以在 connection 回调里给单个 ws 发消息
④ ws.on('message', (message) => { ... })
→ 收到该客户端发来的消息时触发
→ message 是 Buffer 或字符串
⑤ ws.send(data)
→ 给该客户端发送消息
→ 服务器可以随时主动推送
→ 不需要等客户端请求
3.2 客户端
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket Cross-Origin Demo</title>
</head>
<body>
<h1>WebSocket Client</h1>
<script>
// HTML5 原生支持 WebSocket API
// https:// → wss://, http:// → ws://
const ws = new WebSocket('ws://localhost:8080/ws');
// 连接建立时触发
ws.onopen = () => {
console.log('Connected to server');
ws.send('Hello from client!');
};
// 收到服务器消息时触发
ws.onmessage = (event) => {
console.log('Received message:', event.data);
};
// 出错时触发
ws.onerror = (error) => {
console.error('Error:', error);
};
// 连接关闭时触发
ws.onclose = () => {
console.log('Connection closed');
};
</script>
</body>
</html>
WebSocket 浏览器 API 解析:
new WebSocket(url)
→ 浏览器原生 API,HTML5 新增
→ 不需要安装任何库
→ 协议:ws://(明文)或 wss://(加密,类似 https)
→ 自动完成 HTTP→WebSocket 的握手
四个核心事件:
┌──────────────────────────────────────────────────────────┐
│ 事件 触发时机 用途 │
│ │
│ onopen 连接建立成功 发送初始消息/标记在线 │
│ onmessage 收到服务器消息 处理数据/更新 UI │
│ onerror 连接出错 错误处理/重连 │
│ onclose 连接关闭 清理/重连/提示用户 │
└──────────────────────────────────────────────────────────┘
ws.send(data)
→ 客户端主动给服务器发消息
→ 可以发字符串、ArrayBuffer、Blob
→ 服务器端 ws.on('message') 接收
event.data
→ 服务器发来的数据
→ 可能是字符串或 Blob
→ 取决于服务器发送的格式
为什么不像 SSE 用 innerText?
→ SSE 是追加显示(打字机效果)
→ WebSocket 是消息通信(聊天模式)
→ 每条消息是独立的,不是拼接
→ 通常用 console.log 或渲染到消息列表
3.3 通信流程
完整通信流程:
浏览器(index.html) 服务器(server.js)
│ │
│── new WebSocket('ws://...') ────→│ 握手
│ │
│←──── 101 Switching Protocols ────│ 协议切换
│ │
│── onopen 触发 ────────────────────│ 连接建立
│── ws.send('Hello from client!')─→│ 客户端发消息
│ │
│ │── ws.on('message') 触发
│ │── console.log(message)
│ │── ws.send('Server received: ...')
│ │
│←── onmessage 触发 ───────────────│ 服务器回消息
│── console.log(event.data) │
│ │
│ ...持续通信... │
│ │
│←── onclose 触发 ─────────────────│ 连接关闭
双向通信的体现:
→ 客户端 send → 服务器收到 message
→ 服务器 send → 客户端收到 onmessage
→ 两边都是"发送方+接收方"
→ 地位平等
四、WebSocket 跨域原理
4.1 HTTP 同源策略 vs WebSocket 跨域
HTTP 同源策略:
浏览器的安全机制:
→ 不同域名 → 跨域
→ 不同端口 → 跨域
→ 不同协议 → 跨域
→ 浏览器拦截跨域请求
示例:
→ 页面在 http://localhost:3000
→ 请求 http://localhost:3001/api → 跨域!
→ 浏览器拦截(需要 CORS 或代理)
WebSocket 不受同源策略限制:
→ 页面在 http://localhost:3000
→ 连接 ws://localhost:8080/ws → 不跨域!
→ WebSocket 协议不受同源策略约束
→ 可以自由跨域通信
┌──────────────────────────────────────────────────────────┐
│ 为什么 WebSocket 不受同源策略限制? │
│ │
│ ① 同源策略是浏览器的安全策略 │
│ → 只针对 HTTP 请求 │
│ → 限制 XMLHttpRequest、fetch、EventSource │
│ → 不限制 WebSocket │
│ │
│ ② WebSocket 有自己的安全机制 │
│ → 握手时的 Sec-WebSocket-Key / Accept 机制 │
│ → 服务器可以验证 Origin 头 │
│ → 服务器可以选择拒绝特定 Origin │
│ → 安全由服务器端控制,不靠浏览器 │
│ │
│ ③ 设计理念不同 │
│ → HTTP 同源策略是"浏览器层面的保护" │
│ → WebSocket 是"连接级别的安全" │
│ → 连接建立时验证,建立后自由通信 │
└──────────────────────────────────────────────────────────┘
但 WebSocket 跨域不用于"解决常规跨域问题"
→ WebSocket 是实时通信协议
→ 不适合做常规的 HTTP API 调用
→ 常规跨域还是用 CORS / Nginx 代理
→ WebSocket 跨域是"天然不限制",不是"用来解决跨域的"
4.2 服务器端 Origin 验证
虽然 WebSocket 不受浏览器同源策略限制
但服务器端可以自行验证 Origin
wss.on('connection', (ws, req) => {
const origin = req.headers.origin;
// 服务器可以检查 Origin
const allowedOrigins = [
'http://localhost:3000',
'http://example.com'
];
if (origin && !allowedOrigins.includes(origin)) {
ws.close(1008, 'Origin not allowed');
return;
}
// 正常处理连接
ws.on('message', (message) => {
console.log(`Received: ${message}`);
ws.send(`Echo: ${message}`);
});
});
→ 安全由服务器端控制
→ 不依赖浏览器同源策略
→ 服务器可以拒绝不信任的来源
五、WebSocket 基于事件机制
5.1 事件驱动的双向通信
WebSocket 的通信模式 = 事件驱动
┌──────────────────────────────────────────────────────────┐
│ 服务端事件 │
│ │
│ wss.on('connection', (ws) => { }) │
│ → 有新客户端连接时触发 │
│ → ws 是该客户端的连接对象 │
│ │
│ ws.on('message', (message) => { }) │
│ → 该客户端发来消息时触发 │
│ → message 是消息内容 │
│ │
│ ws.on('close', () => { }) │
│ → 该客户端断开连接时触发 │
│ → 可以做清理工作 │
│ │
│ ws.on('error', (err) => { }) │
│ → 连接出错时触发 │
│ │
│ 主动操作: │
│ ws.send(data) → 发送消息给该客户端 │
│ ws.close() → 主动关闭连接 │
│ ws.ping() → 发送心跳检测 │
│ │
│ ──────────────────────────────────────────────────── │
│ │
│ 客户端事件 │
│ │
│ ws.onopen → 连接建立时触发 │
│ ws.onmessage → 收到消息时触发 │
│ ws.onerror → 出错时触发 │
│ ws.onclose → 连接关闭时触发 │
│ │
│ 主动操作: │
│ ws.send(data) → 发送消息给服务器 │
│ ws.close() → 主动关闭连接 │
└──────────────────────────────────────────────────────────┘
事件驱动的本质:
→ 不是"请求-响应"模式
→ 是"事件-回调"模式
→ 事件随时发生,回调随时触发
→ 两边都监听对方的事件
→ 这就是"实时通信"的基础
5.2 广播:服务器推送给所有客户端
WebSocket 的广播能力:
→ wss.clients 包含所有已连接的客户端
→ 遍历 clients 给每个人发消息 = 广播
wss.on('connection', (ws) => {
ws.on('message', (message) => {
// 广播给所有客户端(聊天室模式)
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(`广播: ${message}`);
}
});
});
});
应用场景:
→ 聊天室:一个人发消息,所有人都能看到
→ 弹幕:观众发弹幕,所有观众看到
→ 在线状态:有人上线/下线,通知所有人
→ 协作编辑:一个人修改,其他人实时看到
六、跨域方案全景图
6.1 六种跨域方案
┌──────────────────────────────────────────────────────────────┐
│ 跨域方案全景图 │
│ │
│ ① CORS(Cross-Origin Resource Sharing) │
│ → 跨域资源共享 │
│ → 服务器设置 Access-Control-Allow-Origin │
│ → 最标准的跨域解决方案 │
│ → 适合:常规 API 请求 │
│ │
│ ② Nginx 反向代理 │
│ → 前端请求 /api → Nginx 转发到后端 :3001 │
│ → 同源访问,不跨域 │
│ → 适合:生产环境部署 │
│ │
│ ③ Vite + Mockjs(开发环境) │
│ → Vite proxy 配置代理 │
│ → 开发环境解决跨域 │
│ → 适合:开发阶段 │
│ │
│ ④ JSONP(JSON with Padding) │
│ → 利用 <script> 标签不受同源策略限制 │
│ → 只支持 GET │
│ → 已被 CORS 取代,了解即可 │
│ │
│ ⑤ WebSocket │
│ → 不受同源策略限制 │
│ → 天然跨域 │
│ → 但不用于"解决常规跨域",而是实时通信 │
│ │
│ ⑥ postMessage │
│ → 窗口间通信(iframe、window.open) │
│ → 不同窗口/iframe 之间传递消息 │
│ → 适合:嵌入 iframe 的跨域通信 │
└──────────────────────────────────────────────────────────────┘
6.2 跨域方案选型
┌──────────────────────────────────────────────────────────┐
│ 场景 → 推荐方案 │
│ │
│ 常规 API 跨域 → CORS │
│ 生产环境部署 → Nginx 反向代理 │
│ 开发环境调试 → Vite proxy │
│ 实时聊天 / 弹幕 → WebSocket │
│ 嵌入 iframe 通信 → postMessage │
│ 老系统兼容 → JSONP(仅 GET) │
└──────────────────────────────────────────────────────────┘
核心原则:
→ CORS 是标准方案,优先考虑
→ Nginx 代理是生产标配
→ WebSocket 不是"跨域方案",是"实时通信方案"
→ 选型要看场景,不是看"能不能跨域"
七、WebSocket vs SSE:为什么 LLM 选 SSE?
7.1 双工 vs 单向
readme 中的核心问题:
"WebSocket 双工,为何不用于 LLM 的流式输出?"
→ WebSocket 双向也可以一边生成一边输出
→ 为什么 ChatGPT 等产品选 SSE 而不是 WebSocket?
┌──────────────────────────────────────────────────────────┐
│ 原因分析 │
│ │
│ ① LLM 流式输出是"单向"的 │
│ → 服务器 → 浏览器:AI 生成 token 逐个推送 │
│ → 浏览器 → 服务器:只有一开始的请求 │
│ → 不需要双向通信 │
│ → SSE 的单向推送就够了 │
│ │
│ ② SSE 更简单 │
│ → 基于 HTTP,不需要协议升级 │
│ → EventSource API 简单 │
│ → 自动重连 │
│ → 开发成本低 │
│ → WebSocket 需要维护连接状态、心跳、重连 │
│ │
│ ③ SSE 穿透性好 │
│ → 基于标准 HTTP │
│ → 防火墙、代理服务器不会拦截 │
│ → WebSocket 的 ws:// 协议可能被某些代理拦截 │
│ │
│ ④ 资源效率 │
│ → SSE 是 HTTP 连接复用 │
│ → 不需要额外的协议层 │
│ → WebSocket 需要维护帧编码、心跳 │
│ │
│ ⑤ 符合场景 │
│ → LLM 回答 = 服务器推送 → SSE 天然匹配 │
│ → 聊天 = 双向通信 → WebSocket 天然匹配 │
│ → 用对工具,而不是用最复杂的工具 │
└──────────────────────────────────────────────────────────┘
7.2 什么时候用 WebSocket?
WebSocket 的真正战场:
┌──────────────────────────────────────────────────────────┐
│ 场景 原因 │
│ │
│ QQ / 微信聊天 双向实时,A 和 B 互相发消息 │
│ 弹幕 观众发弹幕 + 实时接收弹幕 │
│ 在线游戏 低延迟双向通信 │
│ 协作编辑(Google Docs) 多人同时编辑,实时同步 │
│ 在线状态 服务器主动推送上下线通知 │
│ 直播 主播推流 + 观众互动 │
│ IoT 设备控制 远程控制设备 + 设备状态回传 │
│ │
│ 共同特点: │
│ → 两边都需要主动发数据 │
│ → 实时性要求高 │
│ → 连接需要长期保持 │
│ → 不只是"服务器→浏览器"单向推送 │
└──────────────────────────────────────────────────────────┘
八、CommonJS vs ESM
8.1 server.js 中的模块系统
server.js 中的注释:
// commonjs 老的, esm 新的
// module
┌──────────────────────────────────────────────────────────┐
│ CommonJS │
│ → Node.js 的老牌模块系统 │
│ → require / module.exports │
│ → const WebSocket = require('ws'); │
│ → 同步加载 │
│ → package.json 中 "type": "commonjs"(默认) │
│ → 文件后缀 .js │
│ │
│ ESM(ES Modules) │
│ → JavaScript 标准模块系统 │
│ → import / export │
│ → import WebSocket from 'ws'; │
│ → 异步加载(静态分析) │
│ → package.json 中 "type": "module" │
│ → 文件后缀 .mjs 或 .js(配 type: module) │
│ │
│ 对比: │
│ ┌──────────────┬────────────────┬────────────────┐ │
│ │ │ CommonJS │ ESM │ │
│ ├──────────────┼────────────────┼────────────────┤ │
│ │ 导入 │ require() │ import │ │
│ │ 导出 │ module.exports│ export default │ │
│ │ 加载方式 │ 运行时同步 │ 编译时静态 │ │
│ │ 后缀 │ .js │ .mjs / .js │ │
│ │ Tree-shake │ 不支持 │ 支持 │ │
│ │ 顶层 await │ 不支持 │ 支持 │ │
│ └──────────────┴────────────────┴────────────────┘ │
│ │
│ 本项目 server.js 使用 CommonJS │
│ → const WebSocket = require('ws') │
│ → const http = require('http') │
│ → Node.js 内置模块默认 CommonJS │
└──────────────────────────────────────────────────────────┘
九、完整知识速查
9.1 WebSocket API 速查
| API | 端 | 作用 | 说明 |
|---|---|---|---|
new WebSocket(url) | 客户端 | 创建连接 | 浏览器原生 API |
ws.onopen | 客户端 | 连接建立事件 | 发初始消息 |
ws.onmessage | 客户端 | 收到消息事件 | 处理服务器数据 |
ws.onerror | 客户端 | 错误事件 | 错误处理 |
ws.onclose | 客户端 | 关闭事件 | 清理/重连 |
ws.send(data) | 两者 | 发送消息 | 主动发送 |
ws.close() | 两者 | 关闭连接 | 主动断开 |
wss.on('connection') | 服务端 | 新连接事件 | 获取 ws 对象 |
ws.on('message') | 服务端 | 收到消息事件 | 处理客户端数据 |
ws.on('close') | 服务端 | 断开事件 | 清理资源 |
wss.clients | 服务端 | 所有连接 | 遍历广播 |
9.2 协议速查
协议对比:
┌──────────────────────────────────────────────────────────┐
│ HTTP SSE WebSocket │
│ 方向 单向 单向(服务器) 双向 │
│ 协议 HTTP HTTP ws:// │
│ 连接 短连接 长连接 长连接 │
│ 方法 任意 只能GET 无限制 │
│ 跨域 CORS限制 CORS限制 不受限制 │
│ 重连 无 自动 手动 │
│ 状态码 200等 200 101握手 │
│ 浏览器API fetch/XHR EventSource WebSocket │
│ 适合 常规API LLM流式 聊天/弹幕 │
└──────────────────────────────────────────────────────────┘
十、总结
10.1 知识体系图
WebSocket 双工通信
│
├── HTTP 的单向本质
│ ├── 客户端发起 → 服务器响应 → 断开
│ ├── 服务器不能主动推送
│ └── 不适合实时双向通信
│
├── WebSocket 的双工突破
│ ├── 全双工:双向同时通信
│ ├── 服务器可主动推送
│ ├── 客户端也可主动发送
│ ├── 连接保持不断开
│ └── 地位平等,无主从
│
├── 协议握手(两步走)
│ ├── 第一步:HTTP 连接(ws:// 找到 Web Server)
│ ├── 第二步:101 Switching Protocols(切换协议)
│ ├── 握手头部:Upgrade / Connection / Sec-WebSocket-Key
│ └── 切换后进入全双工通信
│
├── ws 库完整实现
│ ├── 服务端
│ │ ├── http.createServer(HTTP 宿主)
│ │ ├── new WebSocket.Server({ server, path })
│ │ ├── wss.on('connection')(连接事件)
│ │ ├── ws.on('message')(消息事件)
│ │ └── ws.send()(主动推送)
│ └── 客户端
│ ├── new WebSocket(url)(原生 API)
│ ├── onopen / onmessage / onerror / onclose
│ └── ws.send()(主动发送)
│
├── WebSocket 跨域
│ ├── 不受同源策略限制
│ ├── 天然跨域
│ ├── 安全由服务器端控制(Origin 验证)
│ └── 不用于"解决常规跨域"而是"实时通信"
│
├── 事件驱动通信
│ ├── 事件-回调模式(非请求-响应)
│ ├── 服务端事件(connection/message/close/error)
│ ├── 客户端事件(onopen/onmessage/onerror/onclose)
│ └── 广播能力(wss.clients.forEach)
│
├── 跨域方案全景图
│ ├── CORS(标准方案)
│ ├── Nginx 反向代理(生产标配)
│ ├── Vite proxy(开发环境)
│ ├── JSONP(仅 GET,已过时)
│ ├── WebSocket(天然跨域,实时通信)
│ └── postMessage(窗口间通信)
│
├── WebSocket vs SSE
│ ├── SSE 单向 → 适合 LLM 流式输出
│ ├── WebSocket 双向 → 适合聊天/弹幕
│ ├── LLM 选 SSE 原因:单向够用、更简单、穿透好
│ └── 用对工具,不用最复杂的工具
│
├── 应用场景
│ ├── QQ/微信聊天(双向实时)
│ ├── 弹幕(观众发 + 实时收)
│ ├── 在线游戏(低延迟双向)
│ ├── 协作编辑(多人同步)
│ ├── 在线状态(服务器主动推送)
│ └── IoT 设备控制(远程 + 状态回传)
│
└── CommonJS vs ESM
├── CommonJS:require / module.exports / 同步 / .js
└── ESM:import / export / 静态 / .mjs / Tree-shaking
10.2 一句话总结
HTTP 是单向的——客户端请求、服务器响应、连接断开,服务器不能主动推送。WebSocket 通过协议升级(HTTP 101 Switching Protocols)在同一 TCP 连接上切换到全双工通信,两边都可以随时发数据,地位平等。ws 库在 Node.js 中只需三步:
http.createServer创建 HTTP 宿主 →new WebSocket.Server({ server, path })搭建 WebSocket 服务 →wss.on('connection')监听连接。浏览器端用原生new WebSocket(url)连接,四个事件(onopen/onmessage/onerror/onclose)覆盖全部通信场景。WebSocket 不受同源策略限制,天然跨域,但不用于"解决常规跨域"——它是实时通信方案。LLM 流式输出选 SSE 而非 WebSocket,因为 AI 回答是服务器→浏览器的单向推送,SSE 更简单、穿透性更好、自动重连。WebSocket 的真正战场是双向实时通信:QQ 聊天、弹幕、在线游戏、协作编辑——这些场景两边都需要主动发数据,SSE 的单向推送就不够用了。
如果这篇文章对你有帮助,欢迎点赞和收藏!