从跨域代理到 WebSocket:梳理前端实时通信的底层逻辑与完整实践

做前端开发的同学,几乎都踩过跨域的坑:开发环境配 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 通道完成握手,再切换协议。整个过程分两步:

  1. 握手阶段(HTTP 协议) 客户端发起普通 HTTP 请求,请求头携带特殊标识:

    makefile 复制代码
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: xxxxxxxx

    向服务端申请:把 HTTP 协议升级为 WebSocket 协议。

  2. 协议切换(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 端专用库 ,浏览器端不需要引入,直接使用内置的原生 WebSocket API 即可。

安装:

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),原因很现实:

  1. 场景不匹配:LLM 流式输出本质是「单次请求 → 流式响应」,是单向的,不需要双向通信。
  2. 开销更低:HTTP 流式不需要维持长连接,服务端资源占用更少,网关、CDN、负载均衡的兼容性更好。
  3. 架构更简单:可以直接复用现有 HTTP 接口体系,鉴权、限流、日志都可以沿用原有方案,不需要额外维护 WebSocket 服务。

WebSocket 的真正优势在于双向、高频、实时的交互场景,比如聊天室、协同文档、在线游戏,这些才是它的主场。技术选型没有银弹,合适的才是最好的。


五、总结

  1. 跨域的本质是浏览器同源策略,Nginx 反向代理和 Vite 开发代理的核心逻辑都是「服务端转发」。
  2. Socket 是底层套接字接口,WebSocket 是基于 TCP 的浏览器应用层协议,通过 HTTP 握手升级。
  3. ws 是 Node 端的标准 WebSocket 库,浏览器直接使用原生 API 即可。
  4. WebSocket 不受同源策略限制,不存在跨域问题。
  5. 单向流式输出优先选 HTTP/SSE,双向实时通信才选 WebSocket
相关推荐
一曲终散1 小时前
创建 SVG 图标预览页面:从零实现到解决 CORS 问题
前端
静默回滚1 小时前
iPhone照片电脑上打不开:HEIC解码从原理到WASM
前端
dsyyyyy11012 小时前
Vue 3 Watch 监视完全指南
前端·javascript·vue.js
航飞光电市场经理2 小时前
人员定位系统“全栈自研”技术解析:从射频前端到定位引擎的架构拆解
前端·架构
Apifox2 小时前
Apifox 9 月更新|CLI 能力升级、GitLab 私有化部署接入与产品体验优化
前端·后端·测试
deli0072 小时前
随机乱跳为什么能画出完美三角形:混沌游戏分形实验室
前端
Gizzap_Tech2 小时前
国内官网增加英文版:URL、语言切换与 hreflang 怎么配置?
前端
剪刀石头布啊2 小时前
泛洪DFS、BFS
前端
qetfw2 小时前
Windows Server AD CS:根 CA、Web 证书模板与域内自动注册
前端·windows·windows-server