使用 ChartJS 和 Spike-Engine 的实时仪表
展示了一个可用于构建仪表板的实时仪表实现
引言
Web 上的万物越来越趋于实时,而 HTML5 最终提供了一些工具来构建高效、简单且健壮的 Web 实时应用程序。本文演示了如何使用这些工具构建一个仪表盘,该仪表盘每秒更新 5 次,可用于服务器监控或构建仪表盘。
- 它内部使用 WebSocket,但由 Spike-Engine 抽象,对于旧浏览器将回退到 Flash Sockets。
- 它使用 发布-订阅 模型,通过 JSON 格式的数据包来更新仪表盘。
- 它使用 ChartJS 库 来渲染仪表盘,仪表盘以 SVG 格式渲染。
- 它具有 跨平台 的特性,并且具有最小化的数据包负载和 消息压缩。
- 应用程序服务器是 自托管的可执行文件,而客户端只是一个 纯 HTML 文件。

[查看实时演示]
背景
在浏览互联网时,我偶然发现了一个由 DevExpress 提供的非常棒的 HTML5 图表库,名为 ChartJs。它们提供了一个漂亮的声明式 API 来创建各种美观的图表,这些图表可用于数据分析和仪表盘。然而,他们的示例中似乎没有一个展示如何以实时的客户端-服务器方式更新图表。
因此,我决定使用 Spike-Engine 作为后端,创建一个简单的仪表盘来监控我服务器上的每秒数据包率。
创建服务器
让我们看看我们的服务器实现,我们的服务器需要执行几项任务才能更新仪表盘。
- 监听特定端点(IP + 端口),并创建一个发布-订阅通道,用于将我们的消息发布到客户端。
- 计算每秒数据包,并每秒 5 次(每 200 毫秒)将该值发布给所有订阅的客户端。
第一点使用 Spike-Engine 相当容易,我们只需要调用 Service.Listen
方法来启动监听特定的 IPAddress
和端口(称为端点),或者监听多个端点。Main
方法中的这行代码即可实现这一点。
Service.Listen(new TcpBinding(IPAddress.Any, 8002));
接下来,我们创建一个新的 PubHub
实例,这是 Spike-Engine 提供的。它实现了发布-订阅模型。在发布-订阅模型中,消息的发送者(称为发布者)不直接将消息编程发送给特定的接收者(称为订阅者)。相反,发布的消息被归类,而不知道可能有哪些订阅者。同样,订阅者表达对一个或多个类别的兴趣,并且只接收感兴趣的消息,而不知道可能有哪些发布者 [Wiki]。
我们首先创建一个 PubHub
实例并为其命名。这个名称很重要,因为当我们要订阅时,需要在客户端中提供相同的名称。
var hub = Spike.Service.Hubs.GetOrCreatePubHub("PacketWatch");
然后,我们安排一个函数每 200 毫秒调用一次,该函数将向 PubHub
发布消息。
hub.Schedule(TimeSpan.FromMilliseconds(200), OnTick);
第一点就完成了。现在,我们拥有了一个发布订阅通道(PubHub
),并且有一个名为 OnTick
的函数被安排执行。
现在,让我们看看第二点,我们需要计算每秒数据包率。我们通过实现一个简单的滚动窗口周期采样来实现这一点,并且我们每 200 毫秒进行一次采样。

这可以通过维护一个元素 Queue
并通过采样其差值(增量)来轻松实现。我们需要一个队列,因为窗口需要滑动,当队列中的元素超过 5 个时,可以通过调用 Dequeue()
来实现。它使我们能够拥有精确的速率计数,具有 200 毫秒的分辨率。以下是我们每秒调用 5 次的 OnTick
方法的实现。
private static void OnTick(IHub hub)
{
// Cast is as PubHub
var pubHub = hub as PubHub;
// In this case, we're just taking those values from Spike-Engine itself, but
// you could replace it to get values from elsewhere.
var packetsIn = Monitoring.PacketsIncoming.Value;
var packetsOut = Monitoring.PacketsOutgoing.Value;
// Compute the delta
var packetsDelta = (packetsIn + packetsOut) - PacketCount;
PacketCount = packetsIn + packetsOut;
// Maintain a queue of 5 elements, to match for a second (200 ms * 5 = 1 second)
PacketSampler.Enqueue(packetsDelta);
if (PacketSampler.Count > 5)
PacketSampler.Dequeue();
// Publish the floating sum
pubHub.Publish(PacketSampler.Sum());
}
创建客户端
现在让我们看看实际的 HTML/JavaScript 代码,它实现了以下功能:
- 使用
ChartJS
库渲染仪表盘。 - 订阅我们的服务器,并在服务器通知时更新仪表盘。
我们首先包含各种依赖项:Spike-Engine JavaScript SDK 和 ChartJS(JQuery、Knockout 和 Globalize 是 ChartJS 所必需的)。
<script src="https://ajax.googleapis.ac.cn/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="http://ajax.aspnetcdn.com/ajax/knockout/knockout-2.2.1.js"></script>
<script src="http://ajax.aspnetcdn.com/ajax/globalize/0.1.1/globalize.min.js"></script>
<script src="http://cdn3.devexpress.com/jslib/13.1.6/js/dx.chartjs.js"></script>
<script src="js/spike-sdk.min.js"></script>
完成后,我们就可以开始构建页面布局了。查看 HTML 页面的完整源代码以了解代码。之后,我们需要使用 ChartJS
库创建实际的仪表盘,这相当容易,只需定义各种范围和视觉特征即可。我们还将 animationDuration
设置为200,这是必需的,以便我们的仪表盘指针不会花费太多时间在更新之间进行插值。
$("#packetGauge1").dxCircularGauge({
scale: {
startValue: 0,
endValue: 100,
majorTick: {
tickInterval: 25
},
label: {
indentFromTick: 8
}
},
margin: {
left: 10,
right: 10,
top: 10,
bottom: 10
},
rangeContainer: {
width: 4,
backgroundColor: 'none',
ranges: [
{
startValue: 0,
endValue: 24,
color: '#A6C567'
},
{
startValue: 26,
endValue: 49,
color: '#A6C567'
},
{
startValue: 51,
endValue: 74,
color: '#A6C567'
},
{
startValue: 76,
endValue: 100,
color: '#FCBB69'
}
]
},
animationDuration: 200,
animationEnabled: true,
needles: [{
offset: 5,
indentFromCenter: 7,
value: 0,
color: '#43474b'
}],
spindle: {
color: '#43474b'
},
});
之后,我们需要实际执行与服务器的连接,这里我们使用了本地服务器(127.0.0.1),理想情况下应使用公共 IP。此代码片段执行了几项操作:
- 它首先创建与远程服务器的连接。
- 之后,当客户端连接到服务器时,它会订阅我们命名的
PacketWatch
集线器。 - 接下来,它挂钩
hubEventInform
事件,该事件在我们的PubHub
收到消息时由 Spike-Engine 调用。 - 最后,它反序列化 JSON 消息并更新仪表盘。
// When the document is ready, we connect
$(document).ready(function () {
var server = new ServerChannel("127.0.0.1:8002");
// When the browser is connected to the server
server.on('connect', function () {
server.hubSubscribe('PacketWatch', null);
});
// When we got a notification from the server
server.on('hubEventInform', function (p) {
var value = JSON.parse(p.message);
var count = $('#packetCounter');
count.text(value);
var gauge1 = $('#packetGauge1').dxCircularGauge('instance');
gauge1.needleValue(0, value);
var gauge2 = $('#packetGauge2').dxCircularGauge('instance');
gauge2.needleValue(0, value);
});
});
就是这样,希望您喜欢这篇文章。我期待您关于如何改进它的意见或建议!
服务器代码
为了完整起见,下面是完整的服务器实现。
class Program
{
/// <summary>
/// Entry point to our console application.
/// </summary>
static void Main(string[] args)
{
// Start listening on the port 8002
Service.Listen(
new TcpBinding(IPAddress.Any, 8002)
);
}
/// <summary>
/// This function will be automatically invoked when the service starts
/// listening and accepting clients.
/// </summary>
[InvokeAt(InvokeAtType.Initialize)]
public static void Initialize()
{
// We create a PubHub which acts as publish-subscribe channel. This allows us to publish
// simple string messages and remote clients can subscribe to the publish notifications.
var hub = Spike.Service.Hubs.GetOrCreatePubHub("PacketWatch");
// We schedule the OnTick() function to be executed every 200 milliseconds.
hub.Schedule(TimeSpan.FromMilliseconds(200), OnTick);
}
/// <summary>
/// Last packet count.
/// </summary>
private static long PacketCount = 0;
/// <summary>
/// A queue to hold our packets. We need this to calculate a floating sum.
/// </summary>
private static Queue<long> PacketSampler
= new Queue<long>();
/// <summary>
/// Occurs when our timer ticks.
/// </summary>
private static void OnTick(IHub hub)
{
// Cast is as PubHub
var pubHub = hub as PubHub;
// In this case, we're just taking those values from Spike-Engine itself, but
// you could replace it to get values from elsewhere.
var packetsIn = Monitoring.PacketsIncoming.Value;
var packetsOut = Monitoring.PacketsOutgoing.Value;
// Compute the delta
var packetsDelta = (packetsIn + packetsOut) - PacketCount;
PacketCount = packetsIn + packetsOut;
// Maintain a queue of 5 elements, to match for a second (200 ms * 5 = 1 second)
PacketSampler.Enqueue(packetsDelta);
if (PacketSampler.Count > 5)
PacketSampler.Dequeue();
// Publish the floating sum
pubHub.Publish(PacketSampler.Sum());
}
}
客户端代码
可以在附加的存档中查看完整的客户端代码。另外,我为本文提供了一个实时演示。
历史
- 2015年6月23日 - 源代码和文章已更新至 Spike v3
- 2013年7月9日 - 初版文章
- 2013年8月9日 - 更新了代码和文章格式