web项目轮询实践

? 近期在实现即时通信时使用了2种不同的方式:一种是基于定时器实现的轮询(在项目内部与后端协作), 一种是基于websocket的双向通信(项目与第三方集成协作)。接下来会介绍这2种方式不同的即时通信方式的基本使用、优缺点。
第一种:Ajax轮询
轮询

  • 需求场景:需实时刷新展示的面板数据。
  • 实现过程:客户端主动向服务器发出请求,等待一段固定的时间(通常使用 JavaScript 的 setInterval 函数),然后再次发出请求
    setInterval(function () { fetch('get-csrf') .then((res) => res.json()) .then((res) => { console.log(res); }); }, 5 * 1000);

  • 优点:实现简单,不需要任何服务器端的特定功能,只需客户端就能处理。并且所有的浏览器上都支持,良好的错误处理系统,超时管理
  • 缺点:
    • 服务器和网络资源浪费: 链接多数是无效重复的
    • 响应数据有延时:setInterval的时间间隔设置越长,服务器上的新数据就需要越多的时间才能到达客户端,不具有可伸缩性
    • 响应的结果没有顺序:因为是异步请求,当发送的请求没有返回结果的时候,后面的请求又被发送,而此时如果后面的请求比前面的请求要先返回结果,那么当前面的请求返回结果数据时已经是过时无效的数据
  • 问题场景: 每隔5s 定时请求接口 vs 接口返回后再间隔5s时间获取
    • 请求响应后间隔5s执行,可以保证响应的顺序性,但同时需要考虑,当请求没有响应时,后续的操作不会执行的问题。
    • 可添加超时响应的处理,超过一定的时间没有响应则取消请求。这个超时的时间同时也得考虑其他请求时间过长的接口,且没有实时刷新的部分功能,防止超时取消后无数据展示。
    • fetch 请求的实现超时取消请求的方式,可以通过timeout+abort方式来实现。Promise.race([fetch(),timout])的方式也可以实现超时的功能
    // 接口返回后再间隔5s时间获取 function getToken() { fetch('get-csrf') .then((res) => res.json()) .then((res) => { console.log(res); setTimeout(() => { getToken(); }, 5 * 1000); }); } getToken();

长轮询
  • 实现过程:客户端向服务器端发送请求接口,然后等待服务器端响应。服务器端需要实现特定功能来允许请求被挂起,只要一有事件发生,服务器端就会在挂起的请求中发送响应并关闭该请求,客户端就会使用这一响应并打开一个新的到服务器端的长生存期的Ajax请求。
  • 相比于上述的客户端主动轮询的方式,需服务器端有特殊的功能来临时挂起连接。在项目实现过程中,对于前端人员,采用了第一种方式实现更快捷及具有可操作性
    • 客户端代码示例:
      function getToken() { fetch('get-csrf') .then((res) => res.json()) .then((res) => { console.log(res); }); } getToken(); /** 在这种长轮询方式下,客户端是在 XMLHttpRequest 的 readystate 为 4(即数据传输结束)时调用回调函数,进行信息处理。当 readystate 为 4 时,数据传输结束,连接已经关闭 **/

其他轮询方式
  • script标签的长轮询 & Iframe的流:
    • 实现:script标签附加到页面上以让脚本执行。服务器端则会挂起连接直到有事件发生,接着把脚本内容发送回浏览器,然后重新打开另一个script标签来获取下一个事件。 (script 标签的src 或 iframe 的src指向服务器地址)
    • 没有方法可用来实现可靠的错误处理或是跟踪连接的状态, 而其具有跨域功能,也有更多实现方式如cors
第二种:websocket
  • 第一种轮询实现的方式,是基于HTTP方式来实现,HTTP的连接具有被动性(需一端主动发起)、无状态、单向、非持久化的特点。同时轮询实现的数据延时(时间间隔)、请求重复浪费等缺点。
  • 而对于项目场景如: 频繁的请求更新数据:控台操作2d地图时,需将地图视野变化(平移缩放旋转等)即时传递给第三方集成应用的3d地图,并同步作出响应,这种场景对于操作的实时性要求更高且响应频繁。多用户通信,由于集成web页面的方式,通过chromium 加载各个不同的页面,这些页面相当于独立的浏览器页面,各个页面之间需要通信、页面与第三方集成应用间的也需要通信 ,所以综合考虑,websocket具有的双向实时通信能力能更好的满足业务选择。
websocket连接
  • WebSocket的连接,先进行TCP的三次握手后,再依赖 HTTP 协议进行一次握手 ,握手成功后,数据就直接从 TCP 通道传输,与 HTTP 无关了。
  • 客户端:当客户端连接服务端的时候,会向服务端发送一个类似下面的http报文,升级协议
    GET ws://localhost:8080/socket.io/?UserGroup=toyGroup&EIO=3&transport=websocket HTTP/1.1 Host: http://localhost:8080 Connection: Upgrade Upgrade: websocket Origin: http://localhost:8000 Sec-WebSocket-Version: 13 Sec-WebSocket-Key: V1yj21hlXCrSK2HDuJsD9A== Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

    • Connection: Upgrade:表示要升级协议
    • Upgrade: websocket:它的作用是告诉服务端需要将通信协议切换到websocket
    • Sec-WebSocket-Version: 13:表示websocket的版本。如果服务端不支持该版本,需要返回一个Sec-WebSocket-Versionheader,里面包含服务端支持的版本号。
    • Sec-WebSocket-Key:与后面服务端响应首部的Sec-WebSocket-Accept是配套的,提供基本的防护,比如恶意的连接,或者无意的连接。
  • 服务端:如果服务端支持websocket协议,那么它就会将通信协议切换到websocket,同时发给客户端类似于以下的一个响应报文头
    HTTP/1.1 101 Switching Protocols Date: Mon, 20 Dec 2021 07:14:21 GMT Connection: upgrade Upgrade: websocket Sec-WebSocket-Accept: 8L5tW9vVu5z9vfRMglkhare9o58=

    • 返回的状态码为101,表示同意客户端协议转换请求,并将它转换为websocket协议。
    • Sec-WebSocket-Accept根据客户端请求首部的Sec-WebSocket-Key计算出来。
websocket使用
  • 服务端实现:在nodejs, 使用ws模块来实现
    const WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 8080 }); wss.on('connection', function connection(ws) { console.log('服务端连接'); ws.on('message', function (message) { // msessage 默认是Buffer console.log('服务端端接收:', message.toString()); }); ws.send('world', { binary: false }); });

  • 客户端初始化websocket实例
    import React, { useState, useEffect } from 'react'; import styles from './index.less'; interface SocketDemoProps {}const SocketDemo: React.FC = () => { const [msg, setMsg] = useState([]); useEffect(() => { var ws = new WebSocket('ws://localhost:8080'); ws.onopen = function () { msg.push('客户端连接:成功'); setMsg([...msg]); ws.send('hello'); }; ws.onmessage = function (e) { msg.push('客户端接收消息:' + e.data); setMsg([...msg]); }; }, []); return ({msg.map((item) => ( {item} ))}); }; export default SocketDemo;

  • 【web项目轮询实践】客户端输出
    客户端连接:成功 客户端接收消息:world

  • 服务端输出
    服务端连接服务端端接收: // 默认是Buffer , 可用 toString转为相应的字符串服务端连接服务端端接收: hello

websocket的Opcode ? opcode中定义了帧的类型:
? 持续帧:
0:继续前一帧;表示和前一个帧的类型完全一致的

非控制帧:主要用来传输数据的
1:文本帧(UTF8) 2: 二进制帧 3-7: 为非控制保留帧

控制帧
8:关闭帧:当关闭ws链接的时候就会有关闭帧 9:心跳帧ping A:心跳帧pong B-F:为控制保留帧

websocket服务搭建: socket.io ? node.js提供了高效的服务端运行环境,但是由于浏览器端对HTML5的支持不一,为了兼容所有浏览器,提供卓越的实时的用户体验,并且为程序员提供客户端与服务端一致的编程体验,于是socket.io诞生。
? Socket.IO 封装了 Websocket、基于 Node 的 JavaScript 框架,包含 client 的 JavaScript 和 server 的 Node。其屏蔽了所有底层细节,让顶层调用非常简单。
? 另外,Socket.IO 还有一个非常重要的好处。其不仅支持 WebSocket,还支持许多种轮询机制以及其他实时通信方式,并封装了通用的接口。这些方式包含 Adobe Flash Socket、Ajax 长轮询、Ajax multipart streaming 、持久 Iframe、JSONP 轮询等。换句话说,当Socket.IO 检测到当前环境不支持 WebSocket 时,能够自动地选择最佳的方式来实现网络的实时通信
const socket = io("127.0.0.1:8080", { transports: ["websocket", "polling"]// 注意:transports属性可直接为websocket,不设置时默认采用polling方式 }); socket.on("connect_error", () => { // revert to classic upgrade socket.io.opts.transports = ["polling", "websocket"]; });

提供的特性
  1. 可靠性: 连接依然可以建立即使应用环境存在: 代理或者负载均衡器 个人防火墙或者反病毒软件
  2. 支持自动连接: 除非特别指定,否则一个断开的客户端会一直重连服务器直到服务器恢复可用状态。重连设置
    import { io } from "socket.io-client"; const socket = io({ reconnection: false// 自动重连设置为false之后,需手动设置重连 }); const tryReconnect = () => { setTimeout(() => { socket.io.open((err) => { if (err) { tryReconnect(); } }); }, 2000); }socket.io.on("close", tryReconnect);

  3. 断开连接检测:在http://Engine.io层实现了一个心跳机制,这样允许客户端和服务器知道什么时候其中的一方不能响应。该功能是通过设置在服务端和客户端的定时器实现的,在连接握手的时候,服务器会主动告知客户端心跳的间隔时间以及超时时间。浏览器连接后的默认设置的断开重新时间。chrome的Network的ws面板可查看
    心跳检测设置
    pingInterval: 25000 pingTimeout: 5000

  4. 二进制的支持:任何序列化的数据结构都可以用来发送
  5. 跨浏览器的支持:该库甚至支持到IE8
  6. 支持复用:为了在应用程序中将创建的关注点隔离开来,http://Socket.io允许你创建多个namespace,这些namespace拥有单独的通信通道,但将共享相同的底层连接
  7. 支持Room:在每一个namespace下,你可以定义任意数量的通道,我们称之为"房间",你可以加入或者离开房间,甚至广播消息到指定的房间。
开发版本问题
  • 跨域
    ? socket.io v3与 socket.io v2的一个更改为:socket.io v2 默认支持跨域,socket.io v3 需要手动支持
  • socket.io v3版本客户端连接 socket.io v2版本的服务端?
  • Socket.io v3 服务端连接socket.io v2 客户端
    const io = require("socket.io")({ allowEIO3: true // false by default });

更多信息:https://socket.io/blog/socket...
websocket 开发过程的问题
socket服务端需提供的能力
  • namespace 与 group 分组
  • 状态同步
  • 服务端并发连接数

    推荐阅读