别把 WebSocket 当成一门新协议学:搞懂"借 HTTP 握手",双端 Demo 和跨域就都通了
想让服务器主动推消息?先会遇到三个选择:轮询、SSE、WebSocket。轮询全是无效请求;SSE 只能服务器到浏览器单向输出(AI 流式回答够用);要做聊天、在线状态这种双向实时,答案才是 WebSocket。本文的判断是:WebSocket 的关键不是背 API,而是理解它"借一次 HTTP 握手(101)升级为双工长连接"这一个动作 ------理解了它,为什么能复用 8080 端口、为什么能跨域,全部顺理成章。材料来自一份学习笔记与最小双端 Demo,代码静态整理、运行未验证,依赖需 npm i ws。
先看握手:一个 HTTP 请求的"变身"
笔记里记的两步是全部核心:先由 http 连接找到 web server,再靠 101 Switching Protocols 升级协议(readme.md L53-56)。握手请求只比普通请求多几个头:
http
GET /chat HTTP/1.1
Upgrade: websocket // 我想升级协议
Connection: Upgrade // 别当普通请求处理
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== // RFC 6455 示例值
背景是 HTTP 的单向性:一问一答,服务器一般不能主动发数据(readme.md L37-39)。而 WebSocket 是 HTTP 之外的双工协议,两端平等、随时互发,QQ/微信式实时聊天靠的就是它(readme.md L41-44)。
服务端:一台 HTTP 服务器,两种协议
demo/server.js(CommonJS)完整骨架:
js
const WebSocket = require('ws'); // 第三方 WebSocket 库
const http = require('http'); // Node 内置 http 模块
const server = http.createServer((req, res) => {
res.writeHead(200, {'Content-Type': 'text/plain'}); // 状态码 + 响应头
res.end('WebSocket Server Running'); // 响应体 + 结束
});
const wss = new WebSocket.Server({ server, path: '/ws' });
wss.on('connection', (ws) => {
console.log('Client connected');
ws.on('message', (msg) => {
console.log(`Received: ${msg}`) // msg 直接是消息内容(Buffer)
})
ws.send('你好') // 服务器→客户端
});
server.listen(8080, () => {
console.log('WebSocket Server Running on port 8080')
})
这份代码里最值钱的三个设计:
- 复用 + 分流 :
WebSocket.Server({ server, path: '/ws' })借用上面那台 HTTP 服务器收握手请求,只接管/ws路径------普通请求走 createServer,升级请求归 wss,一个 8080 端口两种协议。 - 嵌套的作用域原因 :
ws是"这个客户端"的专属连接对象,只在 connection 回调里存在;所以 message 监听必须嵌套在里面。先知道谁来了,才能监听他说话。 - createServer 不等于开门 :
server.listen(8080)之后端口才监听;随后进程不退出,进入事件等待------服务器 99% 的时间在等事件。
客户端:异步握手决定代码顺序
demo/index.html 脚本部分:
js
const ws = new WebSocket('ws://localhost:8080/ws') // 连接服务器
ws.onopen = () => {
console.log('连接成功')
ws.send('hellow from client') // 客户端→服务器
}
ws.onmessage = (e) => {
console.log(`Received: ${e.data}`)
}
ws.onerror = (e) => { console.log(e) }
ws.onclose = () => { console.log('disconnect from server') }
材料里标注的一个细节值得单独说(index.html L20 注释):浏览器 onmessage 收到的必须是 MessageEvent 对象 ,消息在 .data;而 ws 库的 message 直接给内容(Buffer),模板字符串会隐式 toString。两端参数结构不同,是接口设计差异,不是写错。
一次完整往返:onopen 发 hellow from client → 服务端打印 → 服务端回 你好 → onmessage 打印。双端事件对应:
| 浏览器端 | 服务器端 | 触发时机 |
|---|---|---|
onopen |
connection |
握手完成(同时触发) |
ws.send() |
message 事件 |
对端发送时 |
onmessage |
ws.send() |
对端发送时 |
ws.close() |
close 事件 |
一端挂断 |
跨域:同源策略管不到 WebSocket 的握手
同源策略的本质:跨域请求其实已经发出,是浏览器拦截了响应读取;服务器之间通信不受限。笔记原文(readme.md L64-67):同源策略会拦截跨域请求,但不限制 WebSocket 的跨域通信------不过 WebSocket 主要用于实时交流,不会被拿来当常规跨域手段。
由此整理跨域方案全景(nginx 部分摘自 readme.md L3-18:80 端口返回 index.html,拦截 /api 转发 3001,浏览器全程同源):
| 方案 | 思路 | 场景 |
|---|---|---|
| nginx 反向代理 | 让请求不跨域 | 生产部署 |
| vite proxy | 同上,开发服务器兼任 nginx | 本地开发 |
| CORS | 后端声明放行;非简单请求先发 OPTIONS 预检 | 前后端分离主流 |
| JSONP | 钻 <script> 不受限的空子,只支持 GET |
已过时,答原理 |
| WebSocket | 握手不受同源策略限制 | 实时聊天、推送 |
| postMessage | 窗口/iframe 间通信 | 嵌第三方页面 |
顺手记一组状态码(readme.md L57-61):1xx 通信中(101 升级)、2xx 成功、3xx 重定向、4xx 前端错、5xx 后端错。
收藏:跑通前的自检清单
-
npm i ws已执行(demo 未附带依赖清单) -
server.listen(8080)存在,回调打印启动日志 - 客户端 URL 路径与
path: '/ws'一致 -
send放在onopen内 - 浏览器取
e.data,服务端直接用msg - 想双向对话:服务端 message 回调里补
ws.send(...)
留一个可立即执行的检查
可迁移的判断:WebSocket 不是平行于 HTTP 的另一套世界,而是 HTTP 连接的一次"升级",并且升级请求可以和普通 HTTP 共用同一台服务器、按路径分流 ------这也是它天然跨域的根源(同源策略只拦 HTTP 响应读取)。现在就做一件事:打开浏览器 DevTools 的 Network 面板,筛选 WS 类型刷新页面,你会亲眼看到那条 101 的握手记录;若没看到,按上面清单检查路径是否为 /ws。下一步实验方向:给 demo 加"群聊广播",体会 connection 里每个 ws 的专属语义。
标签:WebSocket、Node.js、前端、面试