前端跨域与实时通信全解析:从 SSE 到 WebSocket 实战

0 阅读4分钟

在面试和日常开发中,“跨域”与“实时通信”是绕不开的两座大山。最近在复习相关知识,顺便写了一个 Node.js + 原生前端的 WebSocket 实战 Demo。本文将从跨域方案盘点到 HTTP 协议的限制,再到 WebSocket 双工通信的代码落地,希望能帮你彻底理清这些概念。

一、 跨域方案大盘点

在浏览器中,由于同源策略(协议、域名、端口不同即视为跨域),前端发起请求时经常会遇到跨域拦截。笔记中梳理了以下几种常见的解决思路:

  1. Nginx 反向代理:前端项目部署 index.html,前端发送的请求统一走 /api,通过 Nginx 反向代理到真实的 :3001/ 服务器。这也是生产环境最常用的方案。
  2. Vite + MockJS:利用 Vite 的开发服务器代理,在本地开发时进行接口 Mock。
  3. CORS(跨域资源共享) :服务端设置 Access-Control-Allow-Origin 等响应头,属于真正的跨域资源共享。
  4. JSONP:利用 <script> 标签没有跨域限制的特性,通过 json with padding 实现,但只支持 GET 请求。
  5. postMessage:主要用于页面之间的跨域通信(如 iframe 与父页面)。
  6. WebSocketWebSocket 协议本身不受同源策略限制,原生支持跨域通信。这是因为 HTTP/S 协议受同源策略约束,需要浏览器拦截跨域请求,而 WebSocket 在建立连接时是通过 HTTP 协议升级(101 Switching Protocols)实现的,后续通信不再受同源策略的限制。

二、 HTTP 的局限与实时通信的演进

传统的 HTTP 协议是单向传输的:用户发送请求,服务器反馈完成就断开(短连接)。一般情况下,服务器是不可以主动向用户发送数据的。

为了实现“服务器主动推送”,技术经历了以下演进:

1. SSE(Server-Sent Events)

SSE 是一种单向流式输出技术,服务器可以不断向浏览器推送数据。
核心响应头:

http

Content-Type: text/event-stream;
Cache-control: no-cache;
Connection: keep-alive;

面试思考:既然 WebSocket 是全双工通信,为什么大模型(LLM)的流式输出往往使用 SSE 而不是 WebSocket?
解答:大模型的流式输出通常是“单边生成,单边输出”,SSE 基于 HTTP,实现更轻量,断线重连机制更简单。虽然 WebSocket 也可以做到一边生成一边输出(双向也是可以的),但如果不是为了实时双向交互(如聊天室、直播弹幕),SSE 是更经济的选择。

2. WebSocket(全双工通信)

像 QQ、微信等即时通讯,不再是 HTTP 那种“只有浏览器发数据,服务器才能回”的模式。WebSocket 允许服务器和客户端两边都可以发送数据,地位平等
应用场景包括:在线状态同步、实时聊天、直播弹幕等。

连接建立过程(分两步):

  1. 先通过 HTTP 连接服务器(http://localhost:8080),此时是一次普通的 HTTP 请求。
  2. 发送 101 Status Code(Switching Protocols),协议升级为 WebSocket 协议。基于 HTTP Web Server 的 Socket 服务,双向通信就建立了。

补充:HTTP 状态码 1XX 表示还在通信中(没有完成),2XX 成功,3XX 跳转,4XX 用户错误,5XX 服务器错误。

三、 Node.js WebSocket 服务端实战

下面结合代码,看看如何基于 Node.js 的 ws 库搭建 WebSocket 服务。注意代码中的每一行注释,都是核心知识点。

javascript

// commonjs 老的,esm 新的
// module
// html5 新增的功能
const WebSocket = require('ws');
const http = require('http'); // node 内置的 http 模块

// 先要把 http server 启动
const server = http.createServer((req, res) => {
    res.writeHead(200, {
        'Content-Type': 'text/plain',
    });
    res.end('WebSocket Server Running');
});

// 基于 http server 再搭建 socket 协议
// WebSocket
const wss = new WebSocket.Server({ server, path: '/ws' });

// 监听事件 有人连接
wss.on('connection', (ws) => {
    console.log('Client Connected');
    
    // 监听客户端发来的消息
    ws.on('message', (message) => {
        console.log('收到消息', message);
        // 给客户端发送消息
        ws.send(`Server received: ${message}`);
    });
});

server.listen(8080, () => {
    console.log('WebSocket Server Running on http://localhost:8080');
});

代码解析:这里非常巧妙的一点是,WebSocket.Server 挂载到了原生的 http 实例上({ server, path: '/ws' })。这意味着我们不需要额外占用端口,HTTP 和 WebSocket 共用 8080 端口,通过路径 /ws 进行区分。

四、 前端 WebSocket 客户端实现

前端原生就支持 WebSocket API(HTML5 新增功能)。需要注意的是,连接地址的协议头要从 http:// 改为 ws://,如果是 HTTPS 则对应 wss://

html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>WebSocket Client</h1>
    <script>
        // html5 支持 WebSocket 协议
        // http:// -> ws://
        const ws = new WebSocket('ws://localhost:8080/ws');

        // 连接成功
        ws.onopen = () => {
            console.log('连接成功');
            ws.send('Hello From Client!');
        }

        // 收到服务器消息
        ws.onmessage = (e) => {
            console.log(`收到消息:${e.data}`);
        }

        // 连接错误
        ws.onerror = (e) => {
            console.log('连接错误', e);
        }

        // 连接关闭
        ws.onclose = () => {
            console.log('连接关闭');
        }
    </script>
</body>
</html>

五、 总结

  1. 跨域限制:常规 HTTP 请求受同源策略限制,需要 Nginx、CORS 等手段解决;而 WebSocket 协议不需要遵守同源策略,原生支持跨域通信。
  2. 协议选择:SSE 适合服务器单向推送(如 LLM 流式输出、新闻推送);WebSocket 适合双向高频交互(如聊天室、协同编辑、游戏)。
  3. 连接本质:WebSocket 建立在 HTTP 之上,通过 101 状态码升级协议,底层复用了 HTTP Server。

希望这篇笔记能帮你在面试或实际开发中,更清晰地把握前端网络通信的脉络。如果觉得有帮助,欢迎点赞收藏!