腾讯云图数据可视化支持 Pull 和 Push 两种方式动态更新大屏。
Pull 的方式有:
表格文件
数据库
腾讯云监控
API
Push 的方式有:
WebSocket
Push 的方式是实时的,如果您需要实时更新大屏,可以选择将数据源设置为 WebSocket。下面将介绍 WebSocket 的使用。
快速搭建 WebSocket 服务端
以 NodeJs 举例,使用到了 ws 这个 WebSocket 库,同时配合 HTTP 框架 express 进行使用。使用 NodeJs 运行以下代码,便启动了一个 WebSocket。访问地址是:
ws://127.0.0.1:3000
,代码如下:const http = require('http')const WebSocket = require('ws')const express = require('express')const app = express()const server = http.createServer(app)const wss = new WebSocket.Server({server})wss.on('connection', (ws) => {console.log('client connected')})server.listen(3000)
使用 WebSocket 更新大屏图表
在大屏中添加一个柱状图,然后选择数据源为 WebSocket,设置 WebSocket 的 URL 地址。
单击数据示例,即可看到 WebSocket 服务器刷新此图标组件的消息格式。
格式示例如下:
复制并修改“WebSocket 发送数据格式示例”中的内容,然后使用示例中的代码实现一个消息发送接口。当浏览器访问
http://127.0.0.1:3000/refresh-chart
时,将会向 WebSocket 客户端发送数据刷新组件。const BAR_DATA_MAX = 120function randomNumber(max) {return parseInt(Math.random() * max, 10)}// 实现接口 http://127.0.0.1:3000/refresh-chart// 访问这个接口,将会向 WebSocket 客户端发送刷新组件的消息app.get('/refresh-chart', (req, res, next) => {wss.clients.forEach(ws => {ws.send(JSON.stringify({version: 1,action: 'UpdateComponentData',// body 是数组,这里可以传入多个图表组件的数据body: [{id: 'component/ChartColumnBasic_1_0_0_2_1583478813519',data: [{x: '一月',y: randomNumber(BAR_DATA_MAX)},{x: '二月',y: randomNumber(BAR_DATA_MAX)},{x: '三月',y: randomNumber(BAR_DATA_MAX)},{x: '四月',y: randomNumber(BAR_DATA_MAX)},{x: '五月',y: randomNumber(BAR_DATA_MAX)}]}]}))})res.json({code: 0,msg: 'ok'})})
浏览器每访问一次
http://127.0.0.1:3000/refresh-chart
便会实时刷新柱状图。因为消息中的 body 是数组,因此可以同时发送多个图表组件的数据,刷新多个图表组件。注意
大屏中只用设置一个图表组件的 WebSocket 数据源,便可以控制大屏中所有的图表组件。图表组件的 ID 可以右键编辑界面中的图表组件,单击菜单中的复制 ID 获取。
使用 WebSocket 控制联动
WebSocket 也可以用来控制联动,只需要发送更新全局变量的消息。
如上图,这里需要更改全局变量 tabValue 的值为 tab2,向客户端发送以下消息即可(同样,这里可以传入多个字段):
{"version": 1,"action": "UpdateGlobalField","body": {"tabValue": "tab2"}}
WebSocket 服务端实现访问密钥鉴权
WebSocket 服务端搭建完成后,此时,服务是暴露在公网的,可能被任何人连接。云图提供了访问密钥功能,在 WebSocket 连接建立后会发送带签名的 Connect 消息。
如果在一定时间之内没有收到 Connect 消息或收到 Connect 消息的签名不正确,即认为连接的客户端不合法。
以下面的 SecretId、SecretKey 为例:
SecretId:zUYUtjPu2Kob9jarBhTGxrbxxxxxxxxxxxxxx
SecretKey:xrck1Mgi0IxVjS08B3xxxxxxxxxxxxxx
当访问大屏,大屏连接服务端成功后,服务端将收到带签名的 Connect 消息:
{"version": 1,"from": "tcv-editor","timestamp": 1583487814678,"clientId": "980b05e0-ca11-4536-91dd-3795c5b11b88","action": "Connect","body": {"TcvSignature": "vIwmPQ3yUD8WsXKOA/ABq1jl/iyHVyZnVoWF561hjVU=","TcvSecretId": "zUYUtjPu2Kob9jarBhTGxrbxxxxxxxxxxxxxx","TcvTimestamp": 1583487814,"TcvNonce": 357963}}
服务端使用记录下来的 secretKey 与传入的参数计算签名,将计算结果与接收到的签名做对比,判断是否相同,相同则为合法。NodeJs 计算签名方法:
function isSignatureOK(body) {const secretKey = 'xrck1Mgi0IxVjS08B3xxxxxxxxxxxxxx'const receivedSignature = body.TcvSignature// TcvSignature 不参与签名delete body.TcvSignatureconst params = Object.entries(body)// 升序排列字段params.sort(([key1], [key2]) => {if (key1 > key2) {return 1}if (key1 < key2) {return -1}return 0})// 生成签名字符串const signStr = params.map(kv => kv.join('=')).join('&')console.log(signStr)// 计算签名const signature = crypto.createHmac('sha256', secretKey).update(signStr).digest().toString('base64')console.log(`signature=${signature}, receivedSignature=${receivedSignature}`)// 比较签名结果是否相同return signature === receivedSignature}
WebSocket 服务端实现心跳保活
当大屏 WebSocket 客户端和服务端连接后,客户端和服务端的连接稳定性面临很多问题:
无线网络信号突然变差
网络发生切换
路由器断网
网线断了
而服务端和客户端都不知道连接变慢或已经的断开状态。此时可勾选需要心跳包。
当大屏客户端没有收到服务器的消息时,将每隔设定时间发起 Ping 消息,服务端收到后需要响应 Pong 消息以完成心跳检测。
如果大屏客户端在发送 Ping 消息10秒后没有收到服务回应的 Pong 消息,便认为网络不通,将尝试进行重连。
Ping 消息:
{"version": 1,"from": "tcv-editor","timestamp": 1583490098004,"clientId": "5ca85aad-102a-4468-95fe-e608b5b46b36","action": "Ping"}
Pong 消息:
{"version": 1,"action": "Pong"}
服务端添加定时器,如果超过31秒(上图心跳包 Ping 间隔时间)+ 10秒(Ping 消息在网络上传递的最大时间)的时间没有收到消息,则认为客户端已经断开连接,将主动断开该客户端的连接。
完整服务端代码示例
NodeJs 完整示例代码(支持 node 8 及以上版本运行):
const http = require('http')const WebSocket = require('ws')const express = require('express')const crypto = require('crypto')const BAR_DATA_MAX = 120function randomNumber(max) {return parseInt(Math.random() * max, 10)}function isSignatureOK(body) {const secretKey = 'xrck1Mgi0IxVjS08B3xxxxxxxxxxxxxx'const receivedSignature = body.TcvSignature// TcvSignature 不参与签名delete body.TcvSignatureconst params = Object.entries(body)// 升序排列字段params.sort(([key1], [key2]) => {if (key1 > key2) {return 1}if (key1 < key2) {return -1}return 0})// 生成签名字符串const signStr = params.map(kv => kv.join('=')).join('&')console.log(signStr)// 计算签名const signature = crypto.createHmac('sha256', secretKey).update(signStr).digest().toString('base64')console.log(`signature=${signature}, receivedSignature=${receivedSignature}`)// 比较签名结果是否相同return signature === receivedSignature}const app = express()const server = http.createServer(app)const wss = new WebSocket.Server({server})wss.on('connection', (ws) => {console.log('client connected')let heartbeatTimerconst heartbeat = () => {clearTimeout(heartbeatTimer)heartbeatTimer = setTimeout(() => {ws.terminate()}, (31 + 10) * 1000)}// 连接一建立则设置心跳检测heartbeat()// 10秒内没有收到 Connect 消息,强制关闭连接const connectTimer = setTimeout(() => {if (!ws.receivedConnectMsg) {ws.terminate()}}, 10 * 1000)ws.on('message', (msg) => {console.log('received msg', msg)// 收到消息,则更新心跳计时器,因为如果没有消息,将会在设定时间内收到心跳包heartbeat()const data = JSON.parse(msg)// 在没有收到 Connect 消息之前,丢弃任何消息if (!ws.receivedConnectMsg & data.action !== 'Connect') {return}// 处理来自客户端的消息switch (data.action) {case 'Connect': {ws.receivedConnectMsg = trueclearTimeout(connectTimer)// 签名校验失败,断开连接if (!isSignatureOK(data.body)) {ws.terminate()}break}// 来自客户端的心跳包 Ping 消息,回应 Pong 消息case 'Ping': {ws.send(JSON.stringify({version: 1,action: 'Pong'}))break}default:break}})})// 更新大屏联动变量app.get('/change-tab', (req, res, next) => {wss.clients.forEach(ws => {ws.send(JSON.stringify({version: 1,action: 'UpdateGlobalField',body: {tabValue: 'tab2'}}))})res.json({code: 0,msg: 'ok'})})// 更新大屏图表app.get('/refresh-chart', (req, res, next) => {wss.clients.forEach(ws => {ws.send(JSON.stringify({version: 1,action: 'UpdateComponentData',body: [{id: 'component/ChartColumnBasic_1_0_0_2_1583478813519',data: [{x: '一月',y: randomNumber(BAR_DATA_MAX)},{x: '二月',y: randomNumber(BAR_DATA_MAX)},{x: '三月',y: randomNumber(BAR_DATA_MAX)},{x: '四月',y: randomNumber(BAR_DATA_MAX)},{x: '五月',y: randomNumber(BAR_DATA_MAX)}]}]}))})res.json({code: 0,msg: 'ok'})})server.listen(3000)