สร้าง Web App แบบ Real-Time ด้วย ASP.NET MVC 4, SignalR และ RabbitMQ… Action!

เกริ่นนำ

บล็อคนี้เป็นการนำเสนอ web application สำหรับ monitor machine อะไรก็ได้ แบบ real-time ที่สามารถสื่อสารแบบ Messaging ด้วยโปรโตคอล AMQP  ตัวอย่างสำหรับ monitor machine  เป็น window PC โดยสื่อกลางสื่อสารบน Message Broker เป็น  RabbitMQ แล้วขึ้นแสดงผลแบบ real-time ด้วย ASP.NET MVC 4 โดยใช้ SignalR หน้าตาของ app จะแสดงแบบนี้

chavp-monitor-web

จากภาพด้านบนแสดงเวลาของเครื่อง, CPU Usage แสดงเป็น graph ของเครื่องที่ชื่อว่า CHAVP-PC

Software Architecture แสดงตามรูปข้างล่างนี้ครับ

architecture-chavp-monitor

จากภาพ architecture แสดง actor เป็น admin เข้ามาดูผ่าน browser ที่เป็น Chrome หรือ IE หรือ FireFox ที่ประมวลผลสร้าง HTML จาก web sever ChavpMonitorWeb โดย web server นี้จะค่อยรับสัญญานที่ช่อง(channel) machine.info ที่ผูกกับ exchange machine

จากภาพ architect จะเห็นว่าเครื่องที่เราต้องการ monitor ได้แก่ PC1, PC2 และ PC3 จะคอย push ข้อมูลอันได้แก่ Machine Time, CPU Usages และ ชื่อเครื่องไปยัง exchange  machine อยู่เสมอ

เอาละเรามาเริ่มต้นเตรียมความพร้อมกันก่อน

เครื่องมือพัฒนา

1. เครื่อง PC ที่มี platform เป็น Windows XP หรือ 7 ก็ได้

2. คุณต้องติดตั้ง Visual Studio 2012 ก่อนนะ

3. load plugin SignalR สำหรับ Visual Studio 2012 ได้ที่ http://www.asp.net/signalr

เตรียม Infrastucture RabbitMQ

1. Download RabbitMQ ตัวอย่างนี้ผมต้องการติดตั้งบน Windows โดย Download ได้ที่ http://www.rabbitmq.com/install-windows.html

2. Download Erlang เพื่อใช้ run RabbitMQ Service download ได้ที่ http://www.erlang.org/download.html โดยตัวอย่างของผมเลือก download เป็น Windows Binary File

3. ติดตั้ง Eralang ก่อน แล้วก็ติดตั้ง RabbitMQ ต่อ

4. เพื่อให้ง่ายในการจัดการ RabbitMQ Service ให้ enable Management Plugin ให้ทำตามขั้นตอนจากที่นี่ http://www.rabbitmq.com/management.html หรือทำตามผมก็ได้

เปิดใช้งาน Management Plugin ของ RabbitMQ

1. หลังจากที่ติดตั้ง RabbitMQ ไว้ที่เครื่องเราเรียบร้อยแล้ว ให้ไปที่ menu -> Start -> All Programs -> RabbitMQ Server -> RabbitMQ Command Prompt เลือกตามภาพข้างล่างนี้

start-rabbitmq-server-command

2. เมื่อแสดงหน้าจอ Command Prompt หรือจอดำๆขึ้นมาก ให้พิมพ์คำสั่ง

rabbitmq-plugins enable rabbitmq_management

เป็นคำสั่งเพื่อเปิดใช้งาน plugin rabbitmq_management หลังจากนั้นให้ restart RabbitMQ Service ครับโดยไปที่ Start เลือก Click ขวาที่ Computer เลือก Manage แล้วไปที่ Services and Application เลือก Services ดูที่หน้า Services เลือก RabbitMQ แล้ว click ขวาเลือก restart ดูภาพ

restart-rabbitmq-service

3. หลังจากเสร็จสิ้นขั้นตอนติดตั้ง RabbitMQ manager แล้วให้เข้าไปจัดการ queue ของเราได้เลยที่ http://localhost:15672 ให้พิมพ์ guest เป็น username และ password

ถึงตรงนี้เรามากำหนดทรัพยากรบน RabbitMQ เตรียมไว้สำหรับ application ของเรากันก่อน

1. สร้าง Virtual Hosts ชื่อว่า machine ทำตามภาพเลยครับ(ตรงนี้คุณจะใช้ default ก็ได้ แต่สำหรับตัวอย่าง ผมสร้าง  Virtual Hosts แยกไว้)

add-VM-machine

2. สร้าง user หนึ่งคนชื่อ machine-1 ใส่ password 123456789 สำหรับเข้าถึง RabbitMQ ทำตามภาพข้างล้างนี้เลย

add-user-machine-1

3. กำหนด permission สำหรับ เข้าถึง Virtual Hosts machine ให้ user guest และ machine-1 ตามภาพครับ

set-permission-1

set-permission-guest

set-permission-machine-1

เสร้จแล้ว สำหรับ infrastructure RabbitMQ Broker Message ของเราครับ ลำดับต่อไปก็เริ่มสร้าง application กันต่อเลย

ให้ clone หรือ download source code แล้วทดลอง run ดูก่อนก็ได้นะที่ https://github.com/chavp/ChavpMonitor  ถ้าไม่เปิด source code ดูก่อน… ก็ตามผมมาเลย

พัฒนา Domain Model ก่อน

1. เปิด VS 2012 สร้าง projects เลือกเป็นชนิด Class Library ให้ชื่อว่า Chavp.Monitors

2. เพิ่ม class MachineInfo เพื่อใช้บรรจุข้อมูล Machine Name, Machine Time และ CPU Usage แล้วเพิ่ม method ให้มันซะหน่อยเพื่อ update ข้อมูลของเครื่องที่มัน run อยู่ ตาม code ข้างล่างนี้

using System;
using System.Diagnostics;

namespace Chavp.Monitors
{
    public class MachineInfo
    {
        public string MachineName { get; set; }
        public DateTime MachineTime { get; set; }
        public float CpuUsage { get; set; }

        PerformanceCounter _cpuCounter;
        public MachineInfo()
        {
            MachineName = Environment.MachineName;

            _cpuCounter = new PerformanceCounter();
            _cpuCounter.CategoryName = "Processor";
            _cpuCounter.CounterName = "% Processor Time";
            _cpuCounter.InstanceName = "_Total";
        }

        public void Update()
        {
            MachineTime = DateTime.Now;
            CpuUsage = _cpuCounter.NextValue();
        }
    }
}

พัฒนา Machine Monitor ต่อ

1. เพิ่ม projects เข้ามาอีก เป็นชนิด Console Application ครับ ตั้งชื่อว่า ChavpMonitorMachine

2. ให้ Add Reference… โดย click ขวาที่ Reference เลือก Solution และ Chavp.Monitors projects ที่ได้สร้างไว้แล้วตอนแรกเข้ามา

3. สำหรับ Console Monitor นี้ผมต้องการให้มัน run schedule ทุกๆวินาทีเพื่อ ทำงาน(job) push ข้อมูล MachineInfo ไปยัง exchange machine ผมจึงใช้ library Quartz.Net มา schedule job นี้ ติดตั้ง Quartz.NET ด้วย NuGet โดยเลือกไปที่ TOOLS ของ VS2012 แล้วเลือก Library Package Manager > Package Manager Console แล้วพิมพ์คำสั่งข้างล่างนี้ลงไปครับ

PM> Install-Package Quartz

4. สำหรับการติดต่อกับ RabbitMQ เพื่อให้ง่ายผมใช้ EasyNetQ ก็ติดตั้งกันก่อน Package Manager Console เหมือนเดิมครับ ใส่คำสั่ง

PM> Install-Package EasyNetQ

5. เพิ่ม class MachineMonitorJob เข้าไป เพิ่มหน้าที่ทำงาน update ข้อมูล MachineIn แล้ว push ข้อมูลไปยัง exchange machine ตาม code ข้างล่างนี้

using System;

namespace ChavpMonitorMachine
{
    using Chavp.Monitors;
    using EasyNetQ;
    using EasyNetQ.Topology;
    using Quartz;

    [DisallowConcurrentExecution]
    public class MachineMonitorJob : IJob
    {
        public void Execute(IJobExecutionContext context)
        {

            var username = context.MergedJobDataMap["username"] as string;
            IExchange exchange = context.MergedJobDataMap["IExchange"] as IExchange;
            IAdvancedPublishChannel boxPublishChannel = context.MergedJobDataMap["IAdvancedPublishChannel"] as IAdvancedPublishChannel;
            MachineInfo machineInfo = context.MergedJobDataMap["MachineInfo"] as MachineInfo;

            machineInfo.Update();

            Console.WriteLine("Execute: " + DateTime.Now + ", CpuUsage: " + machineInfo.CpuUsage);

            var message = new Message<MachineInfo>(machineInfo);
            message.Properties.UserId = username;
            boxPublishChannel.Publish<MachineInfo>(exchange, "info", message);

        }
    }
}

6. ตัว Main program ของ console ตาม code ข้างล่างนี้เลยครับ

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ChavpMonitorMachine
{
    using Chavp.Monitors;
    using EasyNetQ;
    using EasyNetQ.Topology;
    using Quartz;
    using Quartz.Impl;

    class Program
    {
        static void Main(string[] args)
        {
            string rabbitMQBrokerHost = "localhost";
            string virtualHost = "machine";
            string username = "machine-1";
            string password = "123456789";

            string connectionString = string.Format(
                "host={0};virtualHost={1};username={2};password={3}",
                rabbitMQBrokerHost, virtualHost, username, password);

            using (IAdvancedBus bus = RabbitHutch.CreateBus(connectionString).Advanced)
            {
                IExchange exchange = bus.ExchangeDeclare("machine", EasyNetQ.Topology.ExchangeType.Fanout);
                IAdvancedPublishChannel boxPublishChannel = bus.OpenPublishChannel();
                MachineInfo machineInfo = new MachineInfo();

                var jobDataMap = new JobDataMap();
                jobDataMap.Add("username", username);
                jobDataMap.Add("IAdvancedBus", bus);
                jobDataMap.Add("IExchange", exchange);
                jobDataMap.Add("IAdvancedPublishChannel", boxPublishChannel);
                jobDataMap.Add("MachineInfo", machineInfo);

                ISchedulerFactory sf = new StdSchedulerFactory();
                IScheduler sched = sf.GetScheduler();

                DateTimeOffset runTime = DateBuilder.EvenMinuteDate(DateTimeOffset.UtcNow);

                IJobDetail job = JobBuilder.Create<MachineMonitorJob>()
                    .UsingJobData(jobDataMap)
                    .Build();

                ITrigger trigger = TriggerBuilder.Create()
                    .WithIdentity("trigger1", "group1")
                    .StartNow()
                    .WithSimpleSchedule(
                    x => x.WithInterval(TimeSpan.FromMilliseconds(1000)).RepeatForever())
                    .Build();
                sched.ScheduleJob(job, trigger);
                sched.Start();

                Console.WriteLine("Press any key to quit.");
                Console.ReadLine();

                sched.Shutdown(true);
            }

        }
    }
}

พัฒนา Web Application สำหรับ Monitor จบ

1. ที่ Solutin Explorer ไปที่หัว Solution ของเรา click ขวาเลือก New Projects เลือกชนิด เป็น ASP.NET MVC 4 Web Application กด OK

2. ให้ Add Reference… โดย click ขวาที่ Reference เลือก Solution และ Chavp.Monitors projects ที่ได้สร้างไว้แล้วตอนแรกเข้ามา

3. ติดตั้ง EasyNetQ เพื่อให้ Web Application สามารถ pull MachineInfo จาก queue machine-info ที่จะ bind ไว้กับ exchange machine โดยใช้ Console Package Manager ทำเหมือนขั้นตอน สร้าง Machine Monitor ครับ

4. หลังจากที่เราได้ติดตั้ง plugin ASP.NET SignalR สำหรับ VS2012 เรียบร้อยแล้วตั้งแต่ตอนต้น เมื่อ click ขวาที่ projects ChavpMonitorWeb แล้วเลือก Add -> New Items… ที่ menu Web จะมี item SignalR Hub Class โผลออกมาให้เลือก เลือกซะแล้วตั้งชื่อว่า MachineHub.cs ตามภาพนี้ครับ

add-machine-hub

4. ไม่ต้องเพิ่ม code อะไรเข้าไปใน class MachineHub ครับ กรณีนี้เราไม่ต้องการข้อมูลจาก client ที่ push มายัง server แต่ต้องการ push ข้อมูลจาก web server เพื่อ update ไปยัง browser client ตะหากละ ให้มุ่งตรงไปที่ file Global.asax.cs แล้วเติม code  เพื่อ initialize RabbitMQ เพื่อให้ web server สามารถ pull สัญญาณจาก ChavpMonitorMachine ที่ปล่อยมาในสาย exchange machine แล้วดำเนินการโดย push ข้อมูลที่ update นี้แล้วไปยัง client ที่เป็น browser เพื่อแสดงสู่สายตา admin ได้นั่นเองครับ ดู code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using Microsoft.AspNet.SignalR;

namespace ChavpMonitorWeb
{
    using Chavp.Monitors;
    using EasyNetQ;

    // Note: For instructions on enabling IIS6 or IIS7 classic mode, 
    public class MvcApplication : System.Web.HttpApplication
    {
        string rabbitMQBrokerHost = "localhost";
        string virtualHost = "machine";
        string username = "guest";
        string password = "guest";

        IAdvancedBus _bus;
        private IHubContext _hubContext;

        protected void Application_Start()
        {
            RouteTable.Routes.MapHubs();
            AreaRegistration.RegisterAllAreas();

            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            string connectionString = string.Format(
                "host={0};virtualHost={1};username={2};password={3}",
                rabbitMQBrokerHost, virtualHost, username, password);

            _bus = RabbitHutch.CreateBus(connectionString).Advanced;

            var exchange = _bus.ExchangeDeclare("machine", EasyNetQ.Topology.ExchangeType.Fanout);

            var queue = _bus.QueueDeclare(
                "machine.info",
                durable: false,
                exclusive: false,
                autoDelete: true);

            _bus.Bind(exchange, queue, "info");

            _hubContext = GlobalHost.ConnectionManager.GetHubContext<MachineHub>();

            _bus.Consume<MachineInfo>(queue, (body, info) => Task.Factory.StartNew(() =>
            {
                var machine = body.Body;

                _hubContext.Clients.All.update(machine);
            }));
        }

        protected void Application_End()
        {
            if (_bus != null)
            {
                _bus.Dispose();
            }
        }
    }
}

5. ในส่วนของการ แสดงผล หรือ View ผมเพิ่ม HomeController แล้วเพิ่ม View Index เข้ามา และเพิ่ม plugin jQuery Flot เพื่อช่วยแสดงผลเป็น chart แบบ real-time ด้วย ให้ download Flot มาก่อนได้ที่ http://www.flotcharts.org/ ได้มาแล้วแตก zip วางไว้ที่ Scripts/flot ครับ แล้วเขียน code ส่วนแสดงผล ตามข้างล่างนี้ลงไปได้เลย

@{
    // Views/Home/Index.cshtml
    ViewBag.Title = "Index";
}

<div>
    <strong>Machine Time: </strong><span id="machine-time"></span>
    <strong>Machine Name: </strong><span id="machine-name"></span>
    <strong>CPU Usage: </strong><span id="cpu-usage"></span> %
</div>
<div id="content">
    <div class="info-container">
        <div id="placeholder" class="info-placeholder"></div>
    </div>
</div>

@section scripts{
<script src="@Url.Content("~/Scripts/jquery.signalR-1.0.0.js")" type="text/javascript"></script>
<script src="~/signalr/hubs" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/flot/jquery.flot.js")" type="text/javascript"></script>

<script type="text/javascript">
    $(function () {
        var cpuUsages = [], totalPoints = 100, updateInterval = 1000, data = [];

        function getData() {
            if (data.length > 0)
                data = data.slice(1);
            if (cpuUsages.length > totalPoints)
                cpuUsages = cpuUsages.slice(1);
            while (data.length < totalPoints) {

                var current = cpuUsages.length > 0 ? cpuUsages[cpuUsages.length - 1] : 0;

                data.push(current);
            }

            var res = [];
            for (var i = 0; i < data.length; ++i) {
                res.push([i, data[i]])
            }

            return res;
        }

        function update() {

            plot.setData([getData()]);
            plot.draw();
            setTimeout(update, updateInterval);
        }

        var plot = $.plot("#placeholder", [getData()], {
            series: {
                shadowSize: 0	// Drawing is faster without shadows
            },
            yaxis: {
                min: 0,
                max: 100
            },
            xaxis: {
                show: false
            }
        });

        var index = 0;
        // Declare a proxy to reference the hub. 
        var machine = $.connection.machineHub;

        machine.client.update = function (info) {
            $('#machine-time').text(info.MachineTime);
            $('#machine-name').text(info.MachineName);
            $('#cpu-usage').text(info.CpuUsage);
            cpuUsages.push(info.CpuUsage);
        };

        $.connection.hub.start().done(function () {

        });

        update();

    });
</script>
}

เสร็จแล้วครับระบบสำหรับ web application monitor machine แบบ real-time ทดลอง ดูครับเปิด web application ตัวอย่างที่ผม run เป็น Chrome แล้ว run ChavpMonitorMachine.exe ที่เครื่องไว้ด้วย ผมคาดหวังว่า การแสดงผลของคุณจะแสดงผลคล้ายๆผมแบบนี้

sit-1

บล็อคนี้ค่อนข้างยากเพราะเป็นการบูรณาการหลายเทคนิคเพื่อมาสร้าง software application เล็กๆข้างต้นนี้ได้ มีปัญหาใดๆ email หรือ facebook มาถาม หรือแสดงความคิดเห็น แลกเปลี่ยนความรู้ได้นะยินดีอย่างยิ่งครับ

ขอบคุณครับ

#:P

แหล่งทรัพยากรอ้างอิง

Advertisements

#asp-net-mvc-4, #easynetq, #flotchart, #quartz-net, #rabbitmq, #real-time, #signalr