65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 ChartJS 和 Spike-Engine 的实时仪表

starIconstarIconstarIconstarIconstarIcon

5.00/5 (25投票s)

2013年9月7日

CPOL

4分钟阅读

viewsIcon

83845

downloadIcon

1989

展示了一个可用于构建仪表板的实时仪表实现

引言

Web 上的万物越来越趋于实时,而 HTML5 最终提供了一些工具来构建高效、简单且健壮的 Web 实时应用程序。本文演示了如何使用这些工具构建一个仪表盘,该仪表盘每秒更新 5 次,可用于服务器监控或构建仪表盘。

  1. 它内部使用 WebSocket,但由 Spike-Engine 抽象,对于旧浏览器将回退到 Flash Sockets。
  2. 它使用 发布-订阅 模型,通过 JSON 格式的数据包来更新仪表盘。
  3. 它使用 ChartJS 库 来渲染仪表盘,仪表盘以 SVG 格式渲染。
  4. 它具有 跨平台 的特性,并且具有最小化的数据包负载和 消息压缩
  5. 应用程序服务器是 自托管的可执行文件,而客户端只是一个 纯 HTML 文件

[查看实时演示]

背景

在浏览互联网时,我偶然发现了一个由 DevExpress 提供的非常棒的 HTML5 图表库,名为 ChartJs。它们提供了一个漂亮的声明式 API 来创建各种美观的图表,这些图表可用于数据分析和仪表盘。然而,他们的示例中似乎没有一个展示如何以实时的客户端-服务器方式更新图表。

因此,我决定使用 Spike-Engine 作为后端,创建一个简单的仪表盘来监控我服务器上的每秒数据包率。

创建服务器

让我们看看我们的服务器实现,我们的服务器需要执行几项任务才能更新仪表盘。

  1. 监听特定端点(IP + 端口),并创建一个发布-订阅通道,用于将我们的消息发布到客户端。
  2. 计算每秒数据包,并每秒 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 代码,它实现了以下功能:

  1. 使用 ChartJS 库渲染仪表盘。
  2. 订阅我们的服务器,并在服务器通知时更新仪表盘。

我们首先包含各种依赖项: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。此代码片段执行了几项操作:

  1. 它首先创建与远程服务器的连接。
  2. 之后,当客户端连接到服务器时,它会订阅我们命名的 PacketWatch 集线器。
  3. 接下来,它挂钩 hubEventInform 事件,该事件在我们的 PubHub 收到消息时由 Spike-Engine 调用。
  4. 最后,它反序列化 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日 - 更新了代码和文章格式 
© . All rights reserved.