数据实时更新的多种实现方式,你会吗?

开发 前端
Web 应用愈发复杂,用户对实时交互体验的要求也越来越高,比如:社媒的即时通讯、大屏的数据更新、实时消息的提醒等,这些都表明实时交互已成高品质应用的必备特性。

一、前言

如今,Web 应用愈发复杂,用户对实时交互体验的要求也越来越高,比如:社媒的即时通讯、大屏的数据更新、实时消息的提醒等,这些都表明实时交互已成高品质应用的必备特性。而作为开发者,我们常面对复杂的开发环境,要应对即时通讯与数据实时更新的问题。那么,该如何精准高效实现这些功能呢?我们将共同探讨下,轮询、Web Socket 、SSE(Server-Sent Events) 三种解决方案,最终根据当下场景选出最优方案,打造更为出色的产品。

二、方案一:轮询(Polling)

1. 短轮询

实现短轮询,我们可以采用定时器的方式来实现,让客户端每隔较短固定时间就向服务端发起请求,无论服务器有无新消息,都会正常给予回应。短轮询的优劣势一目了然。优势在于,容易理解、实现过程简便,同时兼容性又很好,在几乎所有支持 HTTP 协议的浏览器及服务器环境都能很好的运行短轮询。不过,缺点也非常显而易见。当按照很短的固定时间间隔去频繁请求数据,如果此时的数据并未更新,这些请求就成了无效请求,但是每一个无效的请求都得完成 HTTP 建立连接的一系列流程,像三次握手 、 四次挥手 ,这无疑造成了不必要的资源浪费。同时也是由于按固定间隔请求,数据更新也可能会存在延迟的现象,在要求实时性的场景下就不满足了。

图片图片

2. 长轮询

相对于短轮询,长轮询的实现过程会复杂一些。首先,同样是由客户端向服务端发送HTTP请求后,不过与短轮询的差别在于,服务器接到请求后并不即刻返回响应,而是选择将该请求暂时挂起,静静等待数据更新,或是直至达到特定的超时时间(这个超时时间通常比短轮询间隔长)。在等待期间,如果有数据更新了,就会立即将新数据返回给客户端,若一直未等到数据更新,直到达到超时时间,服务器才返回一个空响应或者告知客户端无新数据。客户端收到响应后,无论是否有新数据,都会立即再次发起新的长轮询请求,如此循环往复。

这种长轮询方式相较短轮询存在一定优势,首先减少了无效请求的次数,因为只有在数据更新的时候,服务器才会响应,实时性得到了增强,也同时在一定程度上节省了网络资源。长轮询虽然减少了无效请求,但是长时间挂起的请求仍会占用服务器的内存、线程等关键资源。若同时存在大量长轮询请求,且长时间处于等待数据更新状态,服务器资源可能会被大量消耗,同样可能引发性能问题。

图片图片

由此可见, 无论是运用长轮询还是短轮询策略,在对实时性有着严格要求的场景之下这都不是一个非常好的方案。一方面,频繁的无效请求以及长时间挂起的连接,极易造成服务器资源的浪费。另一方面,由于轮询时间间隔的固定性,一旦设置的不合理,就可能错失数据更新的最佳时机,无法及时将关键信息推送给客户端,导致实时交互出现延迟。

三、方案二:Web Socket

Web Socket 是一种基于单个 TCP 连接的协议,拥有实现 全双工 通信的能力。它让客户端和服务端可以双向、实时传输数据,摆脱了传统 HTTP 请求那种请求 - 响应模式的限制。有了 Web Socket,服务器能随时主动给客户端推送消息,客户端也能马上向服务器发数据,就如同构建起了一条实时双向通道。

图片图片

1.Web Socket通信原理

1)握手阶段

  • 初始请求阶段

a.首先,我们可以从下图实现效果中看出来,起始于客户端向服务端发送了一个HTTP请求,但这个请求和常规的HTTP请求又不太一致,他包含了两个特殊的请求头, Upgrade: websocket  和  Connection: Upgrade。

b.Upgrade: websocket 像是一个信号,它的作用就是用来告知服务器,客户端希望将当前的通信协议从 HTTP升级为Web Socket协议。

c.Connection: Upgrade 是与Upgrade: websocket 进行配合使用的,它的作用是告诉服务器,不要把这个请求当作普通的 HTTP 请求来处理,而是要关注 “Upgrade” 请求头中的内容,按照要求进行协议的升级。

  • 服务器响应阶段
  • 服务器收到客户端的升级请求后,如果支持 Web Socket 协议,就会返回一个响应来完成握手过程。响应状态码通常是 101,表示 协议切换。响应头也会包含和客户端请求对应的 Upgrade 和 Connection 字段,确认连接升级。Sec - WebSocket - Accept ,它是根据客户端请求中的 Sec - WebSocket - Key 生成的,用于安全验证。通过这个握手过程,双方就建立了一个稳定的 Web Socket 连接这样,这个连接是基于 TCP的,为后续的双向通信做好了准备。

2) 数据传输阶段

  • 我们从下图中可以看出,Web socket全双工通道的特点,客户端可以向服务端主动发送消息,服务端也可以推送数据到服务端。具体的数据格式,我们可以通过抓包软件来进行抓包看下。

3) 连接维护和关闭阶段

  • 在整个通信过程中,Web Socket 协议要求客户端和服务器都要维护连接的状态。连接状态包括连接是否打开、正在关闭或者已经关闭等。客户端和服务器通过心跳机制(发送周期性的小数据包来检测对方是否还在线)或者其他自定义的连接检测方法来确保连接的稳定性。
  • 当需要关闭 WebSocket 连接时,无论是客户端还是服务器都可以发起关闭请求。关闭请求也是通过发送一个特定的帧(关闭帧)来实现的。对方在收到关闭帧后,会进行一些必要的清理工作,如释放资源等,然后关闭连接。

2.示例

  • 模拟客户端代码
<div id="app">
  <input v-model="message" placeholder="输入要发送的消息" />
  <button @click="sendMessage">发送消息</button>
  <div>
    <h3>收到的回复记录:</h3>
    <ul>
      <li v-for="(msg, index) in receivedMessages" :key="msg">{{ msg }}</li>
    </ul>
  </div>
</div>

<script>
  const app = new Vue({
    el: "#app",
    data() {
      return {
        message: "",
        receivedMessages: [], // 新增数组用于存储所有收到的消息
        socket: null,
      };
    },
    mounted() {
      // 创建WebSocket连接,这里的地址要和后端服务器监听的地址对应,这里假设后端在本地3000端口
      this.socket = new WebSocket("ws://localhost:3000");
      // 连接成功时触发的事件
      this.socket.addEventListener("open", () => {
        console.log("已连接到WebSocket服务器");
      });
      // 接收服务器发送消息的事件
      this.socket.addEventListener("message", (event) => {
        const receivedMsg = event.data;
        this.receivedMessages.push(receivedMsg); // 将收到的消息添加到数组中
      });
      // 连接关闭时触发的事件
      this.socket.addEventListener("close", () => {
        console.log("与WebSocket服务器的连接已关闭");
      });
    },
    methods: {
      sendMessage() {
        if (this.socket.readyState === WebSocket.OPEN) {
          this.socket.send(this.message);
          this.message = "";
        } else {
          console.log("WebSocket连接未就绪,无法发送消息");
        }
      },
    },
  });
</script>
  • 模拟服务端发送请求

注意:下方代码中,定时器仅用来模拟,在实际业务中应该替换为业务代码

const WebSocket = require('ws');

// 创建WebSocket服务器实例,监听在3000端口,你可以根据需求修改端口号
const wss = new WebSocket.Server({ port: 3000 });

// 用于存储已连接的客户端WebSocket实例,方便后续向所有客户端发送消息等操作
const clients = [];

// 当有客户端连接时触发的事件
wss.on('connection', (ws) => {
  console.log('客户端已连接');
  clients.push(ws);

  // 接收客户端发送的消息
  ws.on('message', (message) => {
    console.log(`收到客户端消息: ${message}`);
    // 这里简单地将收到的消息加上一个后缀后再发回客户端
    const responseMessage = `你发送的消息是:${message}`;
    ws.send(responseMessage);
  });

  // 当客户端关闭连接时触发的事件
  ws.on('close', () => {
    console.log('客户端已断开连接');
    const index = clients.indexOf(ws);
    if (index > -1) {
      clients.splice(index, 1);
    }
  });
});

// 【注意: 模拟一个定时任务,每隔1秒向所有已连接的客户端发送一条消息,你可以编写自己的业务代码】
setInterval(() => {
  const messageToSend = '这是服务端主动发送的消息,当前时间:' + new Date().toLocaleString();
  clients.forEach((client) => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(messageToSend);
    }
  });
}, 1000);

console.log('WebSocket服务器已启动,正在监听3000端口...');
  • 实现效果

图片图片

图片图片

3. 特点

相较于传统的 HTTP 请求,Web Socket 在实时性方面展现出了卓越优势,它打破了以往被动等待响应的模式,服务器得以实时且主动地将数据推送至客户端。在面对高实时性的场景下,大大的提升了用户体验。并且只需建立一次 TCP 连接,后续数据传输高效便捷,既减少了网络带宽的无谓占用,又减轻了服务器频繁处理连接请求的负担。然而,当应用场景出现变化,假设客户端仅仅是获取服务端推送的消息,自身并无向服务端发送信息的需求,此时,我们可以考虑下 SSE(Server-Sent Events)这个方案。

四、方案三:SSE

SSE(Server-Sent Events) 同样具备了服务端主动向客户端推送数据的能力,而无需客户端不断地发起请求。与Web Socket不同的是,SSE 是基于 HTTP 协议的,使用的是单向通信, 而Web Socket是基于TCP协议的,使用的话双向通信。

图片图片

1、SSE通信原理

1) 客户端发起请求

  • 客户端需要发送一个特定的请求告知服务器准备接收事件。在传统的 HTTP 请求 - 响应模式中,一次请求完成后连接通常会关闭,但 SSE 通过在服务器端和客户端设置特定的头部信息,让连接持续开启。例如:设置了Content - Type头部为text/event - stream,并设置Cache - Control为no - cache以及Connection为keep - alive,这样告知客户端,这是一个 SSE 连接,数据会持续推送,并且不需要缓存数据。客户端只需向服务器发送一个普通的 HTTP 请求,指向服务器端提供 SSE 服务的特定端点就行,例如/events,就可以顺利开启SSE连接。

2)服务器响应请求

  • 服务器收到客户端请求后,会维持一个长连接,并且定期向客户端发送事件数据。每个事件都是通过特定的格式(例如 data:\n\n)发送给客户端的。

3) 客户端接收数据

  • 客户端通过 JavaScript 的 EventSource 对象接收从服务器推送过来的数据。这些数据可以是普通文本,也可以是 JSON 格式的对象,取决于服务器如何发送。

4) 事件流的关闭

  • 一旦不再需要推送数据,比如客户端主动关闭连接,又或是中途出现错误,导致无法继续推送时,服务器便会果断关闭连接。

2、示例

  • 服务端,使用express框架为例

注意:下方代码中,定时器仅用来模拟,在实际业务中应该替换为业务代码

const express = require('express');
const app = express();
const port = 3000;

// 设置响应头,表明这是一个SSE流
app.get('/events', (req, res) => {
    res.setHeader('Content-Type','text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');

    // 【注意:模拟定时发送数据(实际应用中需要根据真实业务逻辑触发发送)!!!】
    const interval = setInterval(() => {
        const data = `data: { "time": "${new Date().toLocaleString()}"}\n\n`;
        res.write(data);
    }, 3000);

    // 当客户端关闭连接时,清除定时器
    req.on('close', () => {
        clearInterval(interval);
    });
});

app.listen(port, () => {
    console.log(`服务器运行在 http://localhost:${port}`);
});
  • 模拟客户端代码
mounted() {
  const source = new EventSource('http://localhost:3000/events');
  source.onmessage = (event) => {
      const data = JSON.parse(event.data); // data 就是发送的消息
      this.message = data.message;
  };
  source.onerror = (error) => {
      console.log('SSE连接出错:', error);
  };
}
  • 实现效果

图片图片

图片图片

3.特点

对于SSE来说,优势上在于简单易用,客户端用 EventSource可以接收,服务器端定期发数据即可推送。且单向通信、基于长连接,服务器资源消耗低。不过,SSE 是有局限的。只能够进行单向通信,现代浏览器大多支持,但是IE或者旧版本浏览器不支持。SSE 只支持文本格式的数据流,不支持二进制数据传输。总之,SSE 在合适场景能发挥优势,支撑实时数据推送需求。

五、总结

上述文章中,介绍了轮询、Web Socket、SSE 三种方案。适合的场景也有一些差别,例如:轮询的方式可以在更新频率不高的场景下使用、Web Socket可以在需要双向交互的场景下使用、SSE适用于服务器到客户端的单向数据流的场景下使用。开发者可以根据场景选择最优方案,提升产品实时交互体验。

责任编辑:武晓燕 来源: 大转转FE
相关推荐

2016-05-03 14:02:44

2024-07-03 08:02:19

MySQL数据搜索

2021-08-19 15:36:09

数据备份存储备份策略

2014-01-22 11:22:44

华为HANA一体机FusionCube大数据分析

2025-01-10 09:15:57

2024-02-22 08:31:26

数据恢复工具MySQL回滚SQL

2011-06-22 10:37:08

rsyncinotify

2021-02-26 05:21:56

MySQL数据设计

2021-09-13 07:23:52

Go Set 设计

2017-01-16 14:13:37

分布式数据库

2018-04-03 16:24:34

分布式方式

2023-01-31 08:34:19

2022-07-05 08:05:00

策略模式接口实现类

2024-10-18 08:00:00

SpringBoot框架开发

2020-06-01 15:13:41

腾讯云图数据库

2024-06-06 08:58:08

大数据SQLAPI

2024-06-04 14:10:00

FlinkSQL窗口大数据

2021-03-10 14:04:10

大数据计算技术

2024-09-02 09:31:19

2019-07-05 11:01:59

Google电子商务搜索引擎
点赞
收藏

51CTO技术栈公众号