WebSocket 数据源设置

最近更新时间:2023-01-09 14:48:02

我的收藏
腾讯云图数据可视化支持 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 = 120
function 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.TcvSignature
const 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 = 120
function randomNumber(max) {
return parseInt(Math.random() * max, 10)
}
function isSignatureOK(body) {
const secretKey = 'xrck1Mgi0IxVjS08B3xxxxxxxxxxxxxx'
const receivedSignature = body.TcvSignature
// TcvSignature 不参与签名
delete body.TcvSignature
const 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 heartbeatTimer
const 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 = true
clearTimeout(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)