🚲 从写信到打电话:WebSocket 双工通信与跨域方案的"通信进化史"

写在前面:上节课讲了 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.jsws 库实现了 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 恰好匹配:

  1. 单向够了------LLM 生成回答不需要客户端随时发消息
  2. 实现简单------任何 HTTP 服务器都能做 SSE,WebSocket 要单独起服务
  3. 自动重连------SSE 断线自动重连,WebSocket 要自己写
  4. 兼容性好------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 握手,换来电波永不消逝。

相关推荐
掘金酱1 小时前
社区排行榜现已上线
前端·人工智能
zzjyr1 小时前
AI 问答的流式响应是怎么实现的?从 fetch 到 SSE 逐字解析
前端·人工智能·ai编程
ikoala1 小时前
DeepSeek 官方仓库惊现 DeepSeek Harness 桌面端!
前端·javascript·后端
liangshanbo12152 小时前
React useEffect 与 useLayoutEffect 面试题整理
前端·javascript·react.js
ID34610744202 小时前
【课程设计】基于Spring Boot+Vue的校园共享无人机服务系统设计与实现-计算机毕设 附源码44219
javascript·vue.js·spring boot·python·node.js·php·课程设计
夜不会漫长2 小时前
C++:类和对象(2)
java·javascript·c++
掰头战士2 小时前
聊聊 Function Calling,你的模型是否答非所问?
前端·typescript·agent
Yeyu3 小时前
车载多 App 同屏渲染(三):RemoteCompose 序列化 UI
前端
weixin199701080163 小时前
[特殊字符]《京东POP二手品类对接:B2C订单模型 vs 二手C2C属性的字段转换难题》(附Python源码)
前端·python·算法