做前端开发的同学,几乎都踩过跨域的坑:开发环境配 Vite proxy,生产环境上 Nginx 反向代理。但当我们把视线从「普通接口请求」转向「实时通信」时,WebSocket 又成了很多人的知识盲区:
- Socket 和 WebSocket 是一回事吗?
- WebSocket 握手到底发生了什么?
- WebSocket 也有跨域问题吗?
- 为什么 LLM 流式输出都用 SSE,不用 WebSocket?
本文就顺着「跨域 → HTTP 通信局限 → WebSocket 原理 → 实战搭建 → 常见误区」的路径,一次性把这些知识点讲透,附带完整可运行代码。
一、先聊透跨域:本质是浏览器的安全限制
跨域的根源是浏览器的同源策略 :协议、域名、端口三者任一不同,浏览器就会拦截响应。但注意:跨域限制是浏览器的行为,服务端之间不存在跨域。
所有跨域方案的核心思路都一致:让浏览器和同源的服务通信,真正的跨域请求交给服务端去转发。
1.1 生产环境:Nginx 反向代理
这是生产环境最标准的方案:
- 前端打包后的
index.html和静态资源由 Nginx 托管 - 前端发起
/api/xxx请求,打到当前域名的 Nginx - Nginx 根据路径规则,把请求转发到真实的后端服务
- 后端返回结果,Nginx 再原样返回给浏览器
浏览器全程只和当前域名交互,自然不会触发跨域。
1.2 开发环境:Vite 内置代理
开发阶段的 server.proxy 和 Nginx 反向代理是完全相同的逻辑:
- 前端代码依旧写
/api/xxx,浏览器请求localhost:5173/api/xxx - Vite 的 Node 开发服务器收到请求,匹配到代理规则
- Vite 服务端向真实后端发起转发请求,拿到结果后返回给浏览器
误区纠正:Vite 代理不会修改前端代码里的请求地址,浏览器 Network 面板看到的永远是本地地址,转发动作发生在 Node 服务层。
二、从 HTTP 到 WebSocket:为什么需要全双工通信
2.1 HTTP 的天然局限
HTTP 是「一问一答」的短连接协议:客户端主动发请求,服务端被动响应,请求结束连接就断开。
在聊天、实时大屏、股票行情等场景下,HTTP 只能靠「轮询」模拟实时性,频繁建立断开连接,效率极低。我们需要一种建立一次连接、双方随时可以互发消息的通信方式,这就是 WebSocket。
2.2 Socket 与 WebSocket 的区别
很多人会把两者混为一谈,其实是完全不同层面的概念:
- Socket(套接字) :操作系统层面的编程接口,是对 TCP/UDP 传输层的抽象,属于底层能力,浏览器无法直接使用原生 TCP Socket。
- WebSocket :运行在 TCP 之上的应用层协议,专为浏览器设计,遵循标准的握手流程,浏览器内置了原生 API。
可以简单理解为:WebSocket 是 Socket 在 Web 场景下的标准化实现。
2.3 WebSocket 连接建立的完整流程
WebSocket 连接不是凭空建立的,它复用 HTTP 通道完成握手,再切换协议。整个过程分两步:
-
握手阶段(HTTP 协议) 客户端发起普通 HTTP 请求,请求头携带特殊标识:
makefileUpgrade: websocket Connection: Upgrade Sec-WebSocket-Key: xxxxxxxx向服务端申请:把 HTTP 协议升级为 WebSocket 协议。
-
协议切换(101 状态码) 服务端校验通过后,返回
101 Switching Protocols响应。 至此握手完成,TCP 长连接保持,双方进入全双工通信模式,可以随时互发数据。
这里顺便补充 HTTP 状态码的通用分类:
- 1xx:协议处理中,未完成
- 2xx:请求成功
- 3xx:重定向
- 4xx:客户端错误
- 5xx:服务端错误
101 就是典型的 1xx 状态,表示协议正在切换。
三、实战:基于 ws 库搭建完整 WebSocket 通信
3.1 关于 ws 库
ws 是 Node.js 生态最主流、最轻量的 WebSocket 实现,严格遵循 RFC 6455 原生协议,性能极高,Socket.IO 底层也基于它。
注意:
ws是 Node 端专用库 ,浏览器端不需要引入,直接使用内置的原生WebSocketAPI 即可。
安装:
css
npm i ws
3.2 服务端完整代码(Node.js + ws)
javascript
const WebSocket = require('ws');
const http = require('http');
// 1. 创建基础 HTTP 服务(WebSocket 握手依赖 HTTP)
const server = http.createServer((req, res) => {
res.writeHead(200, {
'Content-Type': 'text/plain'
});
res.end('WebSocket Server Running!!!');
});
// 2. 挂载 WebSocket 服务,指定 /ws 路径
const wss = new WebSocket.Server({ server, path: '/ws' });
// 3. 监听新客户端连接
wss.on('connection', (ws) => {
console.log('Client connected');
// 监听客户端发来的消息
ws.on('message', (message) => {
console.log(`Received messages: ${message}`);
// 向客户端回发消息
ws.send(`Server received: ${message}`);
});
});
// 4. 启动服务监听 3000 端口
server.listen(3000, () => {
console.log('listening on http://localhost:3000');
});
代码说明:
- WebSocket 必须依托 HTTP 服务完成握手,所以先创建 HTTP 服务器。
path: '/ws'表示只有请求/ws路径时,才会进入 WebSocket 逻辑。- 命名规范:服务端实例通常命名为
wss,回调中的ws代表单个客户端连接,避免作用域混淆。
3.3 前端完整代码(浏览器原生 API)
xml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket Client</title>
</head>
<body>
<h1>WebSocket Client</h1>
<script>
// 发起 WebSocket 连接,立刻开始握手
const ws = new WebSocket('ws://localhost:3000/ws');
// 握手成功、连接就绪后触发
ws.onopen = () => {
console.log('Connected to server');
// ✅ 必须在 onopen 之后调用 send,连接未就绪时发送会失败
ws.send('Hello from client!');
};
// 收到服务端消息时触发
ws.onmessage = (event) => {
console.log(`Received message: ${event.data}`);
};
</script>
</body>
</html>
关键注意点 : new WebSocket() 只是发起握手,此时连接并未建立。绝对不能在实例化后立刻调用 send() ,必须等待 onopen 事件触发,否则消息会丢失或报错。
四、高频误区与深度思考
4.1 WebSocket 有跨域问题吗?
答案:没有。
同源策略是浏览器针对 HTTP/HTTPS 协议的安全限制,WebSocket 协议本身不受同源策略约束。前端页面可以直接连接任意域名的 WebSocket 服务端,不需要代理,也不会被浏览器拦截。
这是很多初学者容易混淆的点:跨域不是所有网络请求都有,它只针对 HTTP 场景。
4.2 为什么 LLM 流式输出不用 WebSocket?
这是个非常经典的问题。WebSocket 是全双工,看似更适合流式传输,但工业界几乎都采用 HTTP 流式(SSE 或 chunked),原因很现实:
- 场景不匹配:LLM 流式输出本质是「单次请求 → 流式响应」,是单向的,不需要双向通信。
- 开销更低:HTTP 流式不需要维持长连接,服务端资源占用更少,网关、CDN、负载均衡的兼容性更好。
- 架构更简单:可以直接复用现有 HTTP 接口体系,鉴权、限流、日志都可以沿用原有方案,不需要额外维护 WebSocket 服务。
WebSocket 的真正优势在于双向、高频、实时的交互场景,比如聊天室、协同文档、在线游戏,这些才是它的主场。技术选型没有银弹,合适的才是最好的。
五、总结
- 跨域的本质是浏览器同源策略,Nginx 反向代理和 Vite 开发代理的核心逻辑都是「服务端转发」。
- Socket 是底层套接字接口,WebSocket 是基于 TCP 的浏览器应用层协议,通过 HTTP 握手升级。
ws是 Node 端的标准 WebSocket 库,浏览器直接使用原生 API 即可。- WebSocket 不受同源策略限制,不存在跨域问题。
- 单向流式输出优先选 HTTP/SSE,双向实时通信才选 WebSocket