WebSocket 双工通信实战:从协议握手到跨域原理,彻底搞懂实时通信的"另一条路"

5 阅读16分钟

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 六种跨域方案

┌──────────────────────────────────────────────────────────────┐
│              跨域方案全景图                                   │
│                                                              │
│  ① CORSCross-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 / 微信聊天           双向实时,AB 互相发消息       │
│  弹幕                   观众发弹幕 + 实时接收弹幕          │
│  在线游戏                低延迟双向通信                    │
│  协作编辑(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                                          │
│                                                          │
│  ESMES Modules)                                       │
│  → JavaScript 标准模块系统                                │
│  → import / export                                       │
│  → import WebSocket from 'ws';                          │
│  → 异步加载(静态分析)                                  │
│  → package.json"type": "module"                     │
│  → 文件后缀 .mjs 或 .js(配 type: module)              │
│                                                          │
│  对比:                                                   │
│  ┌──────────────┬────────────────┬────────────────┐     │
│  │              │  CommonJSESM            │     │
│  ├──────────────┼────────────────┼────────────────┤     │
│  │  导入        │  require()     │  import         │     │
│  │  导出        │  module.exportsexport 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限制      不受限制            │
│  重连     无            自动          手动               │
│  状态码   200200           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.createServerHTTP 宿主)
│   │   ├── 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 双向 → 适合聊天/弹幕
│   ├── LLMSSE 原因:单向够用、更简单、穿透好
│   └── 用对工具,不用最复杂的工具
│
├── 应用场景
│   ├── QQ/微信聊天(双向实时)
│   ├── 弹幕(观众发 + 实时收)
│   ├── 在线游戏(低延迟双向)
│   ├── 协作编辑(多人同步)
│   ├── 在线状态(服务器主动推送)
│   └── IoT 设备控制(远程 + 状态回传)
│
└── CommonJS vs ESM
    ├── CommonJSrequire / module.exports / 同步 / .js
    └── ESMimport / 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 的单向推送就不够用了。


如果这篇文章对你有帮助,欢迎点赞收藏