写在前面:上节课讲了 SSE------服务器往浏览器"单向推水管",AI 一个字一个字蹦出来。今天的问题来了------QQ、微信聊天是实时的,你发一句对方立刻收到,对方回一句你立刻看到。这是 SSE 能实现的吗?不能。SSE 是单向 的,只有服务器能推给浏览器,浏览器没法通过同一条连接随时发消息给服务器。readme 给出了答案------WebSocket,双工通信。今天顺着 readme 的跨域方案清单,一路讲到 WebSocket 的握手原理和代码实现。以下所有代码和概念均来自课堂真实文件。
一、通信方式的进化史
开讲之前,先把三种通信方式放进一个框架里:
| 方式 | 协议 | 方向 | 类比 | 代表 |
|---|---|---|---|---|
| 一问一答 | HTTP | 客户端→服务器→客户端 | 写信 | 普通网页请求 |
| 单向推送 | SSE | 服务器→客户端 | 电台广播 | LLM 流式输出 |
| 双向实时 | WebSocket | 双向 | 打电话 | 聊天、直播弹幕 |
readme 用一段话点明了 HTTP 的局限:
"http 之外的协议------单向传输。用户发起请求,服务器反馈,一般服务器是不可以主动向用户发送数据的。server 伺服状态,等。"
HTTP 天生是"被动"的。 服务器永远在等请求------用户不发请求,服务器就不能主动开口。就像写信------你写一封,对方回一封。对方永远不会在你没写信的时候主动给你寄信。
但聊天软件不是这样的------微信里对方发来消息,你的手机立刻"叮"一声。服务器主动推送了数据。HTTP 做不到,所以需要新协议。
二、跨域方案全家桶:六种武器
readme 开头列了一个跨域方案清单:
"nginx 反向代理;vite + mockjs dev;websocket;jsonp json with padding;cors 跨域资源分享 CORS(cross origin resource sharing);postMessage。"
六种方案,各有各的适用场景:
| 方案 | 原理 | 适用场景 |
|---|---|---|
| Nginx 反向代理 | 服务端转发,浏览器无感知 | 生产环境前后端分离 |
| Vite + MockJS | 开发服务器拦截请求返回 mock | 开发环境前端独立调试 |
| WebSocket | 协议层面不受同源策略限制 | 实时通信(不是常规跨域方案) |
| JSONP | <script> 标签不受同源限制 |
老式 GET 请求跨域 |
| CORS | 服务器主动声明允许跨域 | 现代标准方案 |
| postMessage | 窗口间消息传递 | iframe / 多窗口通信 |
Nginx 反向代理:上一节课的主角
"前端项目 index.html nginx,发出的请求 /api,:3000/。"
前端静态文件交给 Nginx,API 请求以 /api 开头,Nginx 转发到后端 3000 端口------浏览器只跟 Nginx 一个源通信,跨域从根上消失。前面讲过的"中间人方案"。
CORS:最标准的解法
"CORS 跨域资源分享(cross origin resource sharing)。"
CORS 的原理是------服务器在响应头里加 Access-Control-Allow-Origin,告诉浏览器"这个源可以访问我"。浏览器检查到许可后放行。
其他三兄弟
Vite + MockJS 是开发环境的"模拟跨域"------前端独立开发时用 mock 数据假装后端。
JSONP 是上古方案------利用 <script> 标签不受同源限制的漏洞,只支持 GET。
postMessage 是窗口间通信------iframe 里父页面和子页面传消息。
readme 对 WebSocket 的跨域能力有一句点评:
"WebSocket 协议可以跨域。http(s) 协议:不同域名,不同端口,不同协议,浏览器因安全问题,同源策略拦截跨域请求。WebSocket 协议不需要遵守同源策略,可以跨域通信。还是用于实时交流,不去用于常规的跨域解决。"
WebSocket 能跨域,但你别拿它当跨域工具用------它天生是干实时通信的。
三、WebSocket:双工通信的"打电话"协议
为什么需要 WebSocket?
readme 说的:
"QQ Wechat Socket 协议,双工通信。不再是 http 那种只有浏览器发送数据,服务器也可以。在线状态。"
"Socket 实时通信,聊天、直播。Client 端当它来到 web 端,WebSocket 协议。抖音、腾讯、哔哩哔哩、AI 弹幕。两边都可以发送数据,平等。"
WebSocket 的核心特征------双工(Full-duplex):一条连接,两边都能随时发数据,地位平等。
这就是打电话------你说、对方说、你说、对方说,不用等对方说完你才能开口。对比 HTTP 写信------你写一封等回信,再写一封再等回信。
应用场景都是实时类:
| 场景 | 为什么需要 WebSocket |
|---|---|
| QQ / 微信 | 实时收发消息 + 在线状态 |
| 直播间弹幕 | 千万人同时发弹幕 |
| 抖音 / B站 | 实时互动 |
| AI 弹幕 | 实时 AI 回复 |
四、WebSocket 握手:从 HTTP 升级到 WS
WebSocket 不是凭空出现的协议------它寄生在 HTTP 之上。
readme 拆解了连接过程:
"链接的时候,url ws://localhost:8080/ws。分两步:1. http://localhost:8080 http 链接服务器 Web Server 找到,只需要一次。2. 101 status code switch protocol 切换协议。基于 http web server 的 socket 服务双向通信建立了。"
两步握手:
bash
第一步:浏览器发起 HTTP 请求
GET ws://localhost:8080/ws (带 Upgrade 头)
↓
第二步:服务器返回 101
HTTP/1.1 101 Switching Protocols
↓
协议切换成功,双向通信建立
浏览器先以 HTTP 协议连上服务器(找到 Web Server),然后请求"升级"协议------服务器返回 101 Switching Protocols,之后这条连接就不再是 HTTP 了,变成 WebSocket 全双工通道。
一次 HTTP 握手,换来一条永久的双向管道。
101 是什么?
readme 在这里复习了 HTTP 状态码:
"1XX 还在通信中,没有完成。2XX 成功。3XX 跳转。4XX 用户错误。5XX 服务器错误。"
| 区间 | 含义 | 例子 |
|---|---|---|
| 1XX | 进行中 | 101 Switching Protocols(切换协议) |
| 2XX | 成功 | 200 OK |
| 3XX | 跳转 | 301 / 302 |
| 4XX | 客户端错误 | 404 找不到 / 403 禁止 |
| 5XX | 服务器错误 | 500 内部错误 |
101 属于 1XX------"还在通信中,没有完成"。它告诉浏览器:"你要切换协议?好,这条连接从现在开始不再是 HTTP 了,升级成 WebSocket。"
五、服务器端实现:ws 库
server.js 用 ws 库实现了 WebSocket 服务器。
完整代码
javascript
// commonjs 老的,esm 新的
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 再搭建 socket 协议
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(`listen on http://localhost:8080`);
})
逐层拆解
第一步:先起一个 HTTP 服务器
javascript
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('WebSocket Server Running!');
});
注意------WebSocket 服务器不是独立的,它寄生在 HTTP 服务器上。先有 HTTP server,才能升级成 WebSocket。这印证了握手的"两步走"。
代码注释说:
"先要把 http server 启动。"
浏览器访问 http://localhost:8080 时会看到 "WebSocket Server Running!"------HTTP 层面的响应。
第二步:在 HTTP server 上挂 WebSocket
javascript
const wss = new WebSocket.Server({ server, path: '/ws' });
new WebSocket.Server({ server, path: '/ws' }) --- 告诉 ws 库:"在已有的 HTTP server 上开一个 WebSocket 服务,路径是 /ws。"
server:复用的 HTTP 服务器path: '/ws':只有访问/ws路径才会触发协议升级
代码注释说:
"基于 http server 再搭建 socket 协议。"
WebSocket 握手需要 HTTP 服务器配合------浏览器先发 HTTP 请求到 /ws,服务器检测到 Upgrade 头后返回 101,之后这条连接交给 wss 管理。
第三步:监听连接和消息
javascript
wss.on('connection', (ws) => {
console.log('Client connected');
ws.on('message', (message) => {
console.log(`Received message: ${message}`);
ws.send(`Server received: ${message}`);
});
});
wss.on('connection') --- 有客户端连上了(握手完成),回调里拿到 ws 对象------这条双向连接。
ws.on('message') --- 收到客户端发来的消息。
ws.send(...) --- 服务器主动发消息给客户端。注意这行------服务器不需要等请求,想发就发。 这就是双工。
回声服务器:收到什么回什么------客户端发 "Hello",服务器回 "Server received: Hello"。麻雀虽小,但完整的双向通信闭环。
第四步:监听端口
javascript
server.listen(8080, () => {
console.log(`listen on http://localhost:8080`);
})
监听 8080 端口。注意监听的是 server(HTTP server)而不是 wss------WebSocket 没有独立端口,它复用 HTTP 的端口。
六、客户端实现:浏览器原生 WebSocket
index.html 是 WebSocket 客户端------浏览器原生支持,不需要任何库。
javascript
// html5 支持。
// 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('WebSocket error:', error);
}
ws.onclose = () => {
console.log('Disconnected from server');
}
协议转换:http:// → ws://
代码注释说:
"http:// -> ws://"
URL 协议从 http:// 变成 ws://(加密版是 wss://)------这是 WebSocket 的标志。同一个地址 localhost:8080/ws,协议从 HTTP 换成 WS。
四个事件
WebSocket 是事件驱动的------readme 说的:
"基于事件机制双向通信。"
| 事件 | 触发时机 | 类比 |
|---|---|---|
onopen |
连接建立成功 | 电话接通 |
onmessage |
收到服务器消息 | 对方说话 |
onerror |
出错 | 信号不好 |
onclose |
连接关闭 | 挂电话 |
连接成功后 ws.send('Hello from client!') --- 客户端主动发消息。对比 SSE 的 EventSource------SSE 客户端只能 onmessage 接收,没有 send 方法。这是 WebSocket 和 SSE 最本质的区别。
七、WebSocket vs SSE:选谁?
两节课连起来看,SSE 和 WebSocket 是实时通信的两大主力。readme 的问题也来了:
"WebSocket 双工,为何不用于 LLM 的流式输出?"
先看对比:
| 特性 | SSE | WebSocket |
|---|---|---|
| 方向 | 单向(服务器→客户端) | 双向 |
| 协议 | 基于 HTTP | 独立协议(寄生 HTTP 握手) |
| 客户端 API | EventSource | WebSocket |
| 自动重连 | 有 | 需手动实现 |
| 数据类型 | 文本(可传 JSON) | 文本 + 二进制 |
| 跨域 | 受同源策略限制 | 不受限制 |
| 服务器实现 | 任意 HTTP 服务器 | 需要 WebSocket 服务器 |
为什么 LLM 流式输出用 SSE 而不是 WebSocket?
readme 问了这个好问题,课堂的答案其实藏在 SSE 的定义里:
LLM 流式输出的本质------用户问一次,服务器持续回答。数据流向是单向的(LLM → 用户),不需要用户中途插话。SSE 恰好匹配:
- 单向够了------LLM 生成回答不需要客户端随时发消息
- 实现简单------任何 HTTP 服务器都能做 SSE,WebSocket 要单独起服务
- 自动重连------SSE 断线自动重连,WebSocket 要自己写
- 兼容性好------SSE 就是 HTTP,走 80/443 端口不会被防火墙拦
反过来说,聊天室、直播弹幕、在线游戏这类场景必须 WebSocket------因为用户要持续往服务器发消息(发弹幕、说台词),SSE 做不到。
一句话选型:服务器单向推数据 → SSE;客户端服务器都要随时发 → WebSocket。
八、从 SSE 到 WebSocket:完整的技术栈位置
把几节课的知识串起来,看看这些通信方式在 AI 应用里的位置:
javascript
用户浏览器
│
├── SSE(单向)← LLM 流式输出(ChatGPT 打字机效果)
│
├── WebSocket(双向)← 实时聊天 / 弹幕 / 协作
│
└── HTTP(一问一答)← 普通 API 请求
│
└── 跨域方案
├── Nginx 反向代理(生产)
├── CORS(标准)
├── JSONP(老式)
├── Vite + MockJS(开发)
└── postMessage(窗口间)
readme 的跨域清单 + 上节课的 SSE + 今天的 WebSocket------前端实时通信的地图拼齐了。
九、websocket 库 vs 浏览器原生
最后留意一个细节------server.js 用的是 ws 库(Node.js 第三方库),index.html 用的是浏览器原生 WebSocket。
javascript
Node.js 服务器: const WebSocket = require('ws') ← 第三方库
浏览器客户端: new WebSocket('ws://...') ← 原生支持
浏览器原生支持 WebSocket ------HTML5 规范的一部分,不需要引入任何库。而 Node.js 没有内置 WebSocket 实现,需要第三方库 ws(最流行的实现)。
代码注释也印证了:
"html5 新增的功能。"
WebSocket 是 HTML5 时代的能力------浏览器内置 WebSocket 构造函数,ws:// 协议 URL,onopen / onmessage 等事件,全是标准 API。这也是 WebSocket 普及的根基------客户端零依赖,服务端一个库搞定。
PS:通信方式进化史到此集齐------HTTP 是写信(一问一答),SSE 是电台广播(单向推送),WebSocket 是打电话(双向实时)。下次直播刷弹幕、微信收消息,想想背后那条 ws:// 长连接------一次 101 握手,换来电波永不消逝。