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

文章来源声明: 原文作者:YIAN; 来源站点:掘金; 原文链接:https://juejin.cn/post/7690775846017630234; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

把常被割裂的知识点串成一条线:跨域转发、WebSocket 握手、ws 实战与 SSE 选型,代码可直接运行,适合想系统补齐实时通信认知的前后端开发者。

做前端开发的同学,几乎都踩过跨域的坑:开发环境配 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 请求,请求头携带特殊标识:

    <span>Upgrade: websocket</span>
    <span>Connection: Upgrade</span>
    <span>Sec-WebSocket-Key: xxxxxxxx</span>
    
    

    向服务端申请:把 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 即可。

安装:

npm <span>i</span> ws

3.2 服务端完整代码(Node.js + ws)

<span>const</span> <span>WebSocket</span> = <span>require</span>(<span>'ws'</span>);
<span>const</span> http = <span>require</span>(<span>'http'</span>);

<span>// 1. 创建基础 HTTP 服务(WebSocket 握手依赖 HTTP)</span>
<span>const</span> server = http.<span>createServer</span>(<span>(<span>req, res</span>) =></span> {
    res.<span>writeHead</span>(<span>200</span>, {
        <span>'Content-Type'</span>: <span>'text/plain'</span>
    });
    res.<span>end</span>(<span>'WebSocket Server Running!!!'</span>);
});

<span>// 2. 挂载 WebSocket 服务,指定 /ws 路径</span>
<span>const</span> wss = <span>new</span> <span>WebSocket</span>.<span>Server</span>({ server, <span>path</span>: <span>'/ws'</span> });

<span>// 3. 监听新客户端连接</span>
wss.<span>on</span>(<span>'connection'</span>, <span>(<span>ws</span>) =></span> {
    <span>console</span>.<span>log</span>(<span>'Client connected'</span>);

    <span>// 监听客户端发来的消息</span>
    ws.<span>on</span>(<span>'message'</span>, <span>(<span>message</span>) =></span> {
        <span>console</span>.<span>log</span>(<span>`Received messages: <span>${message}</span>`</span>);
        <span>// 向客户端回发消息</span>
        ws.<span>send</span>(<span>`Server received: <span>${message}</span>`</span>);
    });
});

<span>// 4. 启动服务监听 3000 端口</span>
server.<span>listen</span>(<span>3000</span>, <span>() =></span> {
    <span>console</span>.<span>log</span>(<span>'listening on http://localhost:3000'</span>);
});

代码说明:

  • WebSocket 必须依托 HTTP 服务完成握手,所以先创建 HTTP 服务器。
  • path: '/ws' 表示只有请求 /ws 路径时,才会进入 WebSocket 逻辑。
  • 命名规范:服务端实例通常命名为 wss,回调中的 ws 代表单个客户端连接,避免作用域混淆。

3.3 前端完整代码(浏览器原生 API)

<span>html</span>></span>
<span><<span>html</span> <span>lang</span>=<span>"en"</span>></span>
<span><<span>head</span>></span>
    <span><<span>meta</span> <span>charset</span>=<span>"UTF-8"</span>></span>
    <span><<span>meta</span> <span>name</span>=<span>"viewport"</span> <span>content</span>=<span>"width=device-width, initial-scale=1.0"</span>></span>
    <span><<span>title</span>></span>WebSocket Client<span></<span>title</span>></span>
<span></<span>head</span>></span>
<span><<span>body</span>></span>
    <span><<span>h1</span>></span>WebSocket Client<span></<span>h1</span>></span>
    <span><<span>script</span>></span><span>
        <span>// 发起 WebSocket 连接,立刻开始握手</span>
        <span>const</span> ws = <span>new</span> <span>WebSocket</span>(<span>'ws://localhost:3000/ws'</span>);

        <span>// 握手成功、连接就绪后触发</span>
        ws.<span>onopen</span> = <span>() =></span> {
            <span>console</span>.<span>log</span>(<span>'Connected to server'</span>);
            <span>// ✅ 必须在 onopen 之后调用 send,连接未就绪时发送会失败</span>
            ws.<span>send</span>(<span>'Hello from client!'</span>);
        };

        <span>// 收到服务端消息时触发</span>
        ws.<span>onmessage</span> = <span>(<span>event</span>) =></span> {
            <span>console</span>.<span>log</span>(<span>`Received message: <span>${event.data}</span>`</span>);
        };
    </span><span></<span>script</span>></span>
<span></<span>body</span>></span>
<span></<span>html</span>></span>

关键注意点: 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