前端如何理解SSE(Server-Sent Events)和WebSocket

2,116 阅读9分钟

SSE:

使用场景

  • 实时更新:如股票行情、聊天应用中的实时消息推送、监控系统的实时数据等。
  • 轻量级推送:与 WebSocket 相比,SSE 的实现更加简单,只需要 HTTP 协议,并且浏览器支持较好。

关键点

  1. 数据格式:SSE 使用纯文本格式,数据通过事件流推送。服务器将不断发送数据流,客户端可以根据事件监听到每条消息。
  2. 单向通信:SSE 是单向的,意味着服务器可以主动向客户端推送数据,而客户端无法通过相同的连接向服务器发送数据(客户端需要发起独立的 HTTP 请求)。
  3. 自动重连:浏览器原生支持自动重新连接功能,当连接中断时,客户端会自动尝试重新建立连接

SSE 的工作原理

  1. 服务器端 通过 HTTP 响应头 Content-Type: text/event-stream 表示这是一个 SSE 连接。
  2. 客户端 可以通过 JavaScript 的 EventSource API 来接收服务端持续推送的事件。

示例:服务端响应(Node.js 示例)

在服务器端,配置响应头并定期推送消息:

// node.js
const http = require('http');

http.createServer((req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive'
  });
  
  // 发送事件
  setInterval(() => {
    res.write(`data: ${new Date().toLocaleTimeString()}\n\n`);
  }, 1000);
}).listen(3000, () => {
  console.log("SSE server is running on port 3000");
});

客户端接收(HTML + JS)

客户端使用 EventSource 来建立 SSE 连接并接收事件:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SSE Example</title>
</head>
<body>
    <h1>Server-Sent Events Example</h1>
    <div id="time"></div>

    <script>
        const eventSource = new EventSource('http://localhost:3000');
        
        eventSource.onmessage = function(event) {
            document.getElementById('time').textContent = event.data;
        };

        eventSource.onerror = function() {
            console.error("Error receiving events.");
        };
    </script>
</body>
</html>

主要事件

  • onmessage:接收服务端发送的消息。
  • onopen:当连接建立时触发。
  • onerror:当出现错误或连接中断时触发。

SSE 与 WebSocket 区别

  • 通信方向:SSE 是服务端到客户端的单向通信,WebSocket 是双向通信。
  • 连接复杂度:SSE 使用 HTTP 协议,较为简单;WebSocket 是独立的协议,复杂性稍高。
  • 浏览器支持:SSE 在大多数现代浏览器中支持得较好,但不支持 IE;WebSocket 也有广泛的支持。
  • 自动重连:SSE 具有内置的自动重连机制,WebSocket 需要手动处理重连逻辑。

SSE 是非常适合用于实时数据推送的轻量级解决方案。

fetchEventSource

fetchEventSource 是一个用于处理 Server-Sent Events (SSE) 的 JavaScript 函数,可以简化接收服务器端流式事件的操作。

fetchEventSource 通常是在实现 SSE 时作为替代 EventSource 使用的库或方法,如一些库(比如 fetch-sse@microsoft/fetch-event-source),为你提供更灵活的 SSE 处理方案。

主要功能

  • 接收流数据fetchEventSourceEventSource 类似,用于处理服务器推送的文本流数据。
  • 自动重连:可以实现自动重连机制,当连接断开时,它会尝试自动重连。
  • 自定义控制:相比原生的 EventSourcefetchEventSource 可以提供更多对请求和响应的控制,如更灵活的请求头、控制自动重连的行为等。

示例:@microsoft/fetch-event-source 使用

安装:

首先通过 npm 安装这个包:

npm install @microsoft/fetch-event-source

基本用法:

下面是一个使用 fetchEventSource 接收 SSE 的示例:

import { fetchEventSource } from '@microsoft/fetch-event-source';

async function connectToSSE() {
  await fetchEventSource('http://localhost:3000/sse', {
    method: 'GET',
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Accept': 'text/event-stream'
    },
    // SSE链接成功时触发
    onopen(response) {
      if (response.ok && response.headers.get('content-type') === 'text/event-stream') {
        console.log("Connection established.");
      } else if (response.status >= 400 && response.status < 500 && response.status !== 429) {
        console.error("Client error, cannot establish connection.", response.status);
      }
    },
    // 当服务端推送新消息时触发。`event.data` 包含服务器发送的数据
    onmessage(event) {
      console.log("New message:", event.data);
      // Process the received event data here
    },
    // 当发生错误或连接失败时触发。
    onerror(err) {
      console.error("Error:", err);
    },
    // 当连接被关闭时触发
    onclose() {
      console.log("Connection closed by the server.");
    }
  });
}

connectToSSE();

自动重连机制:

fetchEventSource 还可以处理自动重连,当连接中断时,它会在合适的时间重新连接。这一点和原生的 EventSource 一样,但更可定制化。

与原生 EventSource 的对比:

  1. 灵活性fetchEventSource 基于 fetch,比原生 EventSource 更灵活,允许你传递自定义请求头、控制自动重连的逻辑。
  2. 更好控制:通过事件钩子(如 onopen, onmessage, onclose 等),开发者可以更灵活地控制流式数据接收过程。
  3. 更好的错误处理:可以对连接状态进行更详细的错误处理,特别是客户端和服务器错误之间的区分。
  4. 适用场景:

  • 长连接数据流:例如接收实时股票数据、消息推送、进度条更新等。
  • 更好的控制流:需要对请求头、重连策略等有更多自定义需求的情况,比 EventSource 提供了更多控制力。

fetchEventSource 是原生 SSE 的一种增强实现,适合需要更多灵活性和自定义行为的场景,尤其是在复杂前端应用中接收服务端推送数据的场景。

WebSocket:

WebSocket 是一种在客户端与服务器之间进行全双工通信协议,允许双方在单个连接上同时发送和接收数据。它设计用于解决传统 HTTP 协议下的双向通信瓶颈,特别是在需要实时数据更新的应用场景中,如聊天应用、股票行情、游戏和通知系统。

WebSocket 的特点:

  1. 全双工通信:允许客户端和服务器在一个连接上同时发送和接收消息,而不像 HTTP 需要轮询或开启多个连接。
  2. 持久化连接:连接一旦建立后,客户端与服务器之间保持连接,无需像 HTTP 那样每次请求都要重新建立连接。
  3. 低开销:WebSocket 头部信息较少,相较于 HTTP 的大量头部数据,每次通信的带宽占用更少,尤其在频繁数据交互时有优势。
  4. 实时性强:WebSocket 提供了低延迟的实时通信能力,特别适合于需要快速更新数据的场景。

WebSocket 的工作原理

1. 建立连接(握手)

WebSocket 是基于 HTTP 的,但在 HTTP 连接建立之后,客户端会发送一个特定的 WebSocket 握手请求,服务器接收并返回相应的握手应答,确认 WebSocket 连接。 握手的过程类似于 HTTP 请求:

  • 客户端发送一个 Upgrade 请求:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
  • 服务器返回一个 Switching Protocols 的响应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

这一步骤完成后,HTTP 协议将被升级为 WebSocket 协议,客户端和服务器可以在同一个连接上进行双向通信。

2. 数据传输

一旦 WebSocket 连接建立,客户端和服务器就可以使用 WebSocket 帧格式传输数据。每个 WebSocket 消息被分成多个帧是 WebSocket 协议中的基本数据单位

WebSocket 的数据传输有以下两种主要格式:

  • 文本帧:可以传递 UTF-8 编码的文本数据。
  • 二进制帧:可以传递二进制数据(如文件、图片等)。

每个 WebSocket 帧的结构如下:

  • FIN(1 位):表示这是不是消息的最后一帧。
  • Opcode(4 位):定义帧的类型(文本帧、二进制帧、关闭帧、Ping/Pong)。
  • Payload Length:数据负载的长度。
  • Payload Data:实际的数据内容。

3. 关闭连接

当通信结束时,任意一方(客户端或服务器)都可以发送一个关闭帧来请求关闭连接。收到关闭帧后,另一方必须响应一个关闭帧,然后断开连接

WebSocket vs HTTP

特性WebSocketHTTP
连接模型全双工连接,持续连接半双工,客户端发起请求,服务器响应后关闭连接
协议层位于 TCP 层之上,协议是独立的基于请求-响应模式的应用层协议
实时性适合实时、低延迟的通信场景需通过轮询或长连接等机制模拟实时性
传输数据允许传输文本和二进制数据主要用于传输文本(如 JSON,HTML,XML 等)
头部开销一次握手,后续数据传输开销很小每个请求/响应都带有完整的头部信息
长连接是的,长时间保持连接默认不支持长连接,需通过 HTTP/2 或长轮询等方式
常见应用场景聊天应用、游戏、实时通知、金融行情等实时应用REST API、资源请求、网页加载等

WebSocket 应用场景

  1. 在线聊天/消息传递: WebSocket 是聊天应用和消息系统的理想选择,因为它支持实时的消息传递,延迟低且不需要轮询。
  2. 实时通知: 像股票行情、体育比赛比分等应用,要求数据能够实时推送给用户,WebSocket 可以保持长连接,服务器可以主动推送数据给客户端。
  3. 多人协作/游戏: 在多人协作应用(如 Google Docs)或多人在线游戏中,WebSocket 可以用于保持多个客户端的同步,支持低延迟的双向通信。
  4. 物联网(IoT) : WebSocket 在 IoT 设备之间进行通信也有广泛应用,它允许设备与服务器之间保持实时的数据传输。
  5. 实时数据流: 比如实时视频流或音频流,WebSocket 提供了数据的低延迟传输方式。

WebSocket API 使用示例

以下是浏览器中使用 WebSocket 的基本流程,主要分为创建连接、发送消息、接收消息、关闭连接

// 创建 WebSocket 连接,`url` 为服务器的 WebSocket 地址。
const socket = new WebSocket('ws://example.com/socket');

// 连接成功时触发,可以在这里发送初始消息。
socket.onopen = function(event) {
  console.log('WebSocket connection opened.');
  // 通过 WebSocket 连接发送消息
  socket.send('Hello Server!');
};

// 接收消息时触发,可以在这里处理服务器发来的数据。
socket.onmessage = function(event) {
  console.log('Received message from server:', event.data);
};

// 连接关闭时触发,可以在这里进行清理操作
socket.onclose = function(event) {
  console.log('WebSocket connection closed.', event);
};

// 发生错误时触发
socket.onerror = function(error) {
  console.log('WebSocket error:', error);
};

WebSocket 服务器端实现

在服务器端,使用 WebSocket 通信也很简单。以下是使用 Node.jsws 库实现 WebSocket 服务器的示例:

  1. 安装 ws
npm install ws
  1. 创建 WebSocket 服务器:
const WebSocket = require('ws');

// 创建 WebSocket 服务器,监听指定端口
const ws = new WebSocket.Server({ port: 8080 });

ws.on('connection', (ws) => {
  console.log('Client connected.');

  // 监听消息,服务器接收到客户端消息时触发
  ws.on('message', (message) => {
    console.log('Received:', message);
    // 回发消息,服务器向客户端发送消息
    ws.send('Message received: ' + message);
  });

  // 连接关闭时
  ws.on('close', () => {
    console.log('Client disconnected.');
  });
});

console.log('WebSocket server started on ws://localhost:8080');

WebSocket 的局限性

  1. 浏览器兼容性:虽然现代浏览器都支持 WebSocket,但一些较旧的浏览器(如 IE 10 以下)可能不支持。
  2. 防火墙和代理:有些防火墙和代理服务器可能不支持 WebSocket,因为 WebSocket 使用的是非标准的 TCP 协议,不是所有的中间设备都能处理。
  3. 复杂性:与 REST API 相比,WebSocket 需要额外的状态管理和连接管理代码,特别是在长连接、断线重连、错误处理等方面。