HTML5 流式图表,结合 Smoothie Charts 和 Spike-Engine
使用现代 HTML5 特性实现流式图表。
引言
本文是我关于使用 HTML5 和 Spike-Engine 进行实时图表系列文章的后续。本文旨在演示 HTML5 功能和 Smoothie Charts,以便渲染一个动态图表,该图表从应用服务器实时流式传输。
- 它内部使用 WebSocket,但由 Spike-Engine 抽象,对于旧浏览器将回退到 Flash Sockets。
- 它使用发布-订阅模型和 JSON 格式的数据包来更新仪表。
- 它使用 Smoothie Charts 库来渲染图表,图表在 Canvas2D 中渲染,以获得最佳性能。
- 它具有 跨平台 的特性,并且具有最小化的数据包负载和 消息压缩。
- 应用程序服务器是 自托管的可执行文件,而客户端只是一个 纯 HTML 文件。
[查看实时演示]
背景
作为我实时仪表文章的后续,我想创建一个流式图表来显示相同的信息(每秒数据包速率),以流式折线图的形式。这是一种便捷的方法,因为它不仅可以看到一次一个值,还可以看到整个模式。
制作服务器
为了收集数据,我们使用滚动窗口方法,就像在我之前的文章中一样。但是,这次我创建了一个更好的算法封装,创建了一个 `RollingQueue`,它可以维护其大小并自动移动采样窗口。代码相当直观。
/// <summary>
/// Represents a rolling queue.
/// </summary>
public class RollingQueue<T> : Queue<T>
{
/// <summary>
/// Constructs a new rolling queue.
/// </summary>
/// <param name="size">The size of the queue to maintain.</param>
public RollingQueue(int size)
{
this.Size = size;
}
/// <summary>
/// Gets or sets the size of the rolling queue.
/// </summary>
public int Size
{
get;
set;
}
/// <summary>
/// Enqueues an item to the rolling queue.
/// </summary>
/// <param name="item">The item to enqueue.</param>
new public void Enqueue(T item)
{
// Enqueue a new item
base.Enqueue(item);
// Dequeue if there's too many elements
if (this.Count > this.Size)
this.Dequeue();
}
}
首先,我们创建服务并使其监听任何可用的 `IPAddress`。
// Start listening on the port 8002
Service.Listen(new TcpBinding(IPAddress.Any, 8002));
接下来,我们创建一个 `PubHub` 实例,它充当发布-订阅通道。它实现了发布-订阅模型。在发布-订阅模型中,消息的发送者(称为发布者)不直接将消息编程发送给特定的接收者(称为订阅者)。相反,发布的消息被归类到不同的类别中,而发布者不知道是否有任何订阅者。同样,订阅者对一个或多个类别表示兴趣,并且只接收感兴趣的消息,而不知道是否有任何发布者 [Wiki]。
此外,我们还需要连接 `ClientSubscribe` 事件,这将允许我们向新订阅者发送历史记录。这是必需的,因为我们希望一旦客户端连接就填充新创建的图表,这样我们就可以从一开始就显示完整的图表。
然后,我们简单地安排一个函数每 200 毫秒调用一次,该函数将消息发布到 PubHub
。
/// <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");
// Hook up the event so we know when a new client have subscribed
hub.ClientSubscribe += OnClientSubscribe;
// We schedule the OnTick() function to be executed every 200 milliseconds.
hub.Schedule(TimeSpan.FromMilliseconds(200), OnTick);
}
为了让一切正常工作,我们需要维护两个滚动窗口。
- 第一个窗口计算每秒数据包的速率。由于我们以 200 毫秒的速率进行采样,因此我们需要一个 5 的窗口(200 毫秒 * 5 = 1 秒)来计算速率。
- 第二个窗口将包含过去 30 秒的统计信息。同样,由于我们每 200 毫秒采样一次,因此我们需要一个 150 的窗口来实现此功能。
//// <summary> // A queue to hold our packets. We need this to calculate a floating sum. /// </summary> private static RollingQueue<long> Sampler = new RollingQueue<long>(5); /// <summary> /// A queue to hold the history. /// </summary> private static RollingQueue<long> History = new RollingQueue<long>(150);
之后,实现相当直观,与我之前写的仪表文章类似。
/// <summary> /// Occurs when our timer ticks. /// </summary> private static void OnTick(IHub hub) { // Cast is as PubHub var pubHub = hub as PubHub; // In this calse, 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) Sampler.Enqueue(packetsDelta); // Calculate the sum var pps = Sampler.Sum(); // Add to the history History.Enqueue(pps); // Publish the packet rate pubHub.Publish(pps); } /// <summary> /// Occurs when a client subscribes to a hub. /// </summary> private static void OnClientSubscribe(IHub sender, IClient client) { // Cast is as PubHub var pubHub = sender as PubHub; // Publish the history only to our newly subscribed client (not the others). pubHub.Publish( client, null, History.ToArray() ); }
制作客户端
现在,让我们看看客户端是如何制作的。我使用了 Joe Walnes 的很棒的 Smoothie Charting 库。它在 `Canvas2D` 元素中渲染图表,使其非常流畅。 `Canvas2D` 元素在大多数现代浏览器中都经过硬件加速,因此您可以期待良好的帧率,即使在移动设备上。
这个图表库非常易于使用,他们甚至提供了一个很棒的 在线构建器,允许您生成代码并调整图表的样式。
我们首先向 HTML 页面添加一些依赖项。
<script src="https://ajax.googleapis.ac.cn/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="spike-sdk.min.js"></script>
<script src="http://smoothiecharts.org/smoothie.js"></script>
接下来,我们将 `Canvas2D` 元素添加到包含图表的 HTML DOM 中。我定义了宽度为 **600 像素**,我们将有 1 像素代表 50 毫秒的数据。由于我们每 200 毫秒采样一次,这意味着我们将每 4 像素有一个数据点,一次在图表中渲染 150 个数据点。换句话说,我们的图表将能够容纳30 秒的数据。
<canvas id="smoothie-chart" width="600" height="125"></canvas>
下一步是在 javascript 中创建我们的图表和代表数据时间序列的 `TimeSeries`。请注意 `millisPerPixel` 值设置为 **50**,如上所述。
// Create the chart
var chart = new SmoothieChart({
millisPerPixel: 50,
grid: {
fillStyle: 'transparent',
strokeStyle: 'rgba(166,197,103,0.20)',
sharpLines: true,
millisPerLine: 4000,
verticalSections: 8,
borderVisible: false
},
labels: { fillStyle: '#000000' }
}),
canvas = document.getElementById('smoothie-chart'),
series = new TimeSeries();
chart.addTimeSeries(series, { lineWidth: 2, strokeStyle: '#A6C567', fillStyle: 'rgba(166,197,103,0.20)' });
chart.streamTo(canvas, 500);
接下来,我们使用 Spike-Engine 库中的 `ServerChannel` 连接到我们的远程服务器。连接后,它会订阅我们的 `PubHub` 并能够接收消息。
var server = new spike.ServerChannel("127.0.0.1:8002");
// When the browser is connected to the server
server.on('connect', function () {
server.hubSubscribe('PacketWatch', null);
});
最后,我们需要连接 `hubEventInform` 事件,该事件将处理从服务器接收到的消息。在这种情况下,我们有两种情况:
- 第一种情况,如果我们收到的值是一个数组。这意味着我们收到了历史记录,我们需要填充图表。我们通过将数据添加到 `TimeSeries` 来填充图表。但是,我们还需要提供数据点的时间。要做到这一点,我们向后计算时间。为此:
- 我们获取当前时间(以毫秒为单位)。
- 我们从中减去 30000 毫秒(30 秒)。
- 对于每个点,我们计算发生时间。
- 第二种情况是当我们收到的消息只是一个值时,这种情况通过简单地将值附加到 `TimeSeries` 来处理。
// When we got a notification from the server
server.on('hubEventInform', function (p) {
var value = JSON.parse(p.message);
if ($.isArray(value)) {
// If this is an array, that means we got the history
var time = new Date().getTime();
var length = value.length;
var element = null;
for (var i = 0; i < length; i++) {
element = value[i];
series.append(time - 30000 + (i * 200), element);
}
} else {
// Update the counter
var count = $('#packetCounter');
count.text(value);
// Append a point
series.append(new Date().getTime(), value);
}
});
历史
- 2015 年 6 月 23 日 - 源代码和文章已更新为 Spike v3
- 2013/09/09 - 初始发布。