สร้าง Kernel แบบ private Cloud ด้วย RabbitMQ บน .NET กันเถอะ… Action!

เกริ่นนำ

บล็อคนี้ผมตั้งใจนำเสนอการสร้าง application ที่สามารถปรับแต่งขนาดของตัวประมวลผล (processor) แบบตามปริมาณของผู้ใช้ได้ หรือจะเรียกว่าเป็นการสร้าง private cloud แบบง่ายๆไว้ใช้เองก็ว่าได้

แนวคิดของ application นี้ก็คือจะมี application clint ที่เป็น Kernel รับคำสั่ง(command) กับ parameters จากผู้ใช้ แล้วส่งคำสั่งนี้ไปยัง exchange ที่อยู่บน RabbitMQ เจ้า exchange นี้ก็จะ route command ต่อไปยัง queue ซึ่งจะไปถึง PROCESSOR ที่คอยรอรับ request คำสั่งอยู่ที่ queue นี้อยู่แล้ว เมื่อมันได้รับคำสั่งมาก็จะดำเนินการประมวลผล แล้วตอบกลับไปยังผู้ใช้เจ้าของคำสั่งนั้น ดูภาพ software architecture ข้างล่างนี้ครับ

architecture

จากภาพ user-1, user-2 … user-n ส่งคำสั่งด้วย program ChavpKernelCommand เจ้า program ตัวนี้จะดำเนินการ publish คำสั่งไปยัง exchange easy_net_q_rpc ซึ่งจะ route message คำสั่งนี้ต่อไปยัง queue Chavp_Kernel_Commands_Request:Chavp_Kernel_Commands ซึ่งก็จะมี processor รอรับคำสั่งอยู่ที่ queue นี้ processor ก็คือ program ChavpKernelProcessor ที่เปิดอยู่คือ PROCESSOR-1, PROCESSOR-2… PROCESSOR-n ซึ่งเราก็สามารถเปิดประมวลผลตามปริมาณที่เราต้องการเพื่อไว้คอยรอรับคำสั่งบริการเท่าไรก็ได้(on-demand)

สำหรับเครื่องมือพัฒนา และติดตั้ง RabbitMQ ดูได้ที่บล็อค สร้าง Web App แบบ Real-Time ด้วย ASP.NET MVC 4, SignalR และ RabbitMQ… Action!

ถ้าคุณอ่านบล็อคก่อนหน้านี้ของผมที่มีการใช้ RabbitMQ พัฒนา application จนเข้าใจแล้วคุณสามารถ download source code มาดูแล้วทำความเข้าใจได้เลย ที่นี่ครับ ChavpKernel.git

แต่ถ้าไม่ก็ทำตามผมมาเลย

เครื่องมือที่ต้องมีก่อน

1. เครื่องพัฒนาของท่านจะต้องเป็น Windows XP หรือ 7 ครับ

2. ติดตั้ง Visual Studio 2012 ลงเครื่องของท่านเตรียมไว้เลย

3. download RabbitMQ จาก http://www.rabbitmq.com/download.html เลือก download เป็น Windows ครับ

4. downlaod Erlang runtime จาก http://www.erlang.org/download.html เลือก download เป็น Windows Binary File ครับ

5. ติดตั้ง Erlang runtime และ RabbitMQ ตามลำดับกันไป

พร้อมละเครื่อง ต่อไปก็ลงมือสร้าง application กันเลย

สร้าง Domain Model กันก่อน

1. เปิด Visual Studio 2012 ขึ้นมา เลือก New Projects… เลือก Templates > Vistual C# > เลือกแบบเป็น Class Library แล้วตั้งชื่อว่า Chavp.Kernel.Commands

2. ที่ projects นี้ Add > New Item… แล้วค่อยๆ เพิ่ม Class เข้าไป 3 Class ครับคือ Request.cs, Response.cs และ StatisticsRequest.cs ใส่ code ลงไปตามลำดับ ตามข้างล่างนี้

// Request.cs
using System;

namespace Chavp.Kernel.Commands
{
    public struct Request
    {
        public string Name { get; set; }
        public DateTime Date { get; set; }
        public string Command { get; set; }
    }
}

———————————————————-

// Response.cs
using System;

namespace Chavp.Kernel.Commands
{
    public struct Response
    {
        public string Name { get; set; }
        public DateTime Date { get; set; }
        public string Return { get; set; }
    }
}

———————————————————-

// StatisticsRequest.cs

using System.Collections.Generic;

namespace Chavp.Kernel.Commands
{
    public struct StatisticsRequest
    {
        public string Command { get; set; }
        public IList<double> Values { get; set; }
    }
}

———————————————————-

สร้าง Processor application

1. ไปที่ Solution click ขวา เลือก Add > New Projects… แล้วเลือกไปที่ Windows เป็น Console Application ตั้งชื่อว่า ChavpKernelProcessor

2. เพิ่ม package EasyNetQ ด้วย NuGet ไปที่ MENU ด้านบนสุดของ Visual Studio เลือก TOOLS > Library Package Manager เลือก Package Manager Console ให้พิมพ์คำสั่งข้างล่างลงไปเพื่อติดตั้ง

PM> Install-Package EasyNetQ

มีข้อสังเกตุในการติดตั้ง Package Manager Console อยู่อย่างหนึ่งคือก่อนจะ enter คำสั่งลงไปให้เลือก Default project เพื่อติดตั้ง pakchage ให้ถูกต้องก่อน โดยสังเกตุได้จาก menu ที่อยู่ด้านบนของ Package Manager Console ดูจากภาพ ได้เลือก project ChavpKernelProcessor เพื่อติดตั้ง

install-package

สำหรับ EasyNetQ นี้เป็น library ที่ช่วยให้เราเขียน application เพื่อสื่อสารกับ RabbitMQ ได้สะดวกขึ้น อ่านเพิ่มเติมได้ที่ http://easynetq.com/

3. เพิ่ม package Newtonsoft.Json เหมือนกับการติดตั้ง EasyNetQ ครับ โดยพิมพ์คำสั่งด้านล่างนี้ที่ Package Manager Console

PM> Install-Package Newtonsoft.Json

สำหรับเจ้า library Newtonsoft.Json นี้เพิ่มเข้าช่วนเรา convert message จาก json string ไปเป็น object ที่เราต้องการครับ ดูรายละเอียดเพิ่มเติมได้ที่ http://james.newtonking.com/projects/json-net.aspx

4. เพิ่ม reference library Chavp.Kernel.Commands ที่ได้สร้างไว้ตอนแรกเข้ามาที่ project นี้ครับ โดยไปที่ References > Add Reference… จะ Popup หน้าจอ Reference Manager ขึ้นมาที่ menu ด้านซ้าย เลือก Solution > Projects แล้ว check เลือก Chavp.Kernel.Commands เสร็จแล้วกด OK

5. ไปที่ Main method ของ class Program ซึ่งจะอยู่ที่ file Program.cs ครับ เติม code ตามข้างล่างนี้ลงไป

// Program.cs

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

namespace ChavpKernelProcessor
{
    using EasyNetQ;
    using Newtonsoft.Json;
    using Newtonsoft.Json.Linq;
    using Chavp.Kernel.Commands;
    using ChavpKernelProcessor.Properties;

    class Program
    {

        static void Main(string[] args)
        {
            string processorName = "processor-1";

            string rabbitMQBrokerHost = "localhost";
            string virtualHost = "/";
            string username = "guest";
            string password = "guest";

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

            using (IBus bus = RabbitHutch.CreateBus(connectionString))
            {
                bus.RespondAsync<Request, Response>(request =>
                Task.Factory.StartNew(() =>
                {
                    string reqMessage = string.Format(
                        "Request: {0}, From: {1}, Command: {2}",
                        request.Date, request.Name, request.Command);

                    Console.WriteLine(reqMessage);

                    var resp = new Response
                    {
                        Name = processorName,
                        Date = DateTime.Now,
                    };

                    try
                    {
                        var comm = JsonConvert.DeserializeObject<StatisticsRequest>(request.Command);

                        if (comm.Command == "sum")
                        {
                            resp.Return = JsonConvert.SerializeObject(new 
                            {
                                sum = comm.Values.Sum(),
                            });
                        }
                        else if (comm.Command == "average")
                        {
                            resp.Return = JsonConvert.SerializeObject(new
                            {
                                average = comm.Values.Average(),
                            });
                        }
                        else
                        {
                            resp.Return = "unrecognized command " + comm.Command;
                        }

                        string respMessage = string.Format(
                            "Response: {0}, Return: {1}",
                            resp.Date, resp.Name);
                    }
                    catch (Exception ex)
                    {
                        resp.Return = "exception from server: " + ex.Message;
                    }

                    return resp;
                }));

                Console.ReadLine();
            }
        }
    }
}

เสร็จละครับ สำหรับ application ที่เป็น processor ที่จะค่อยรับคำสั่งจาก queue ของ RabbitMQ แล้วดำเนินการประมวลผลและตอบกลับ

ท่านลองสังเกตุที่ผมได้ ไฮไลท์ สีแดงๆไว้ นั่นคือเงื่อนไขที่ผมได้ตรวสอบคำสั่งที่รับเข้ามา ซึ่งผมรับไว้แค่ 2 คำสั่งง่ายๆคือ sum และ average

เอาละต่อไปก็สร้าง application ที่เป็น Kernel สำหรับใส่คำสั่ง เพื่อมา process ที่ processor ที่ได้เตรียมไว้ที่ของเรากันต่อดีกว่า

สร้าง Kernel application

1. ไปที่ Solution click ขวา เลือก Add > New Projects… แล้วเลือกไปที่ Windows เลือกเป็น WPF Application ตั้งชื่อว่า ChavpKernelCommand ตั้งใจเลือกเป็น WPF เพราะจะได้ใส่คำสั่งง่ายๆ

2. ให้ references  library Chavp.Kernel.Commands เหมือนตอน สร้าง processor Application

3. ติดตั้ง package EasyNetQ และ Newtonsoft.Json เหมือนตอน สร้าง processor ครับ แต่ให้เลือก Default project ที่หน้าต่าง Package Manager Console เป็น ChavpKernelCommand

4. เพิ่ม class CommandViewModel แล้วเขียน code ข้างล่างนี้ลงไป

// CommandViewModel.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Input;

namespace ChavpKernelCommand
{
    using Chavp.Kernel.Commands;
    using EasyNetQ;

    public class CommandViewModel: INotifyPropertyChanged
    {
        private string _Command;
        private string _Result;

        public CommandViewModel(IPublishChannel publishChannel)
        {
            RequestCommand = new RequestCommand(this);
            PublishChannel = publishChannel;
        }

        public ICommand RequestCommand { get; set; }
        public IPublishChannel PublishChannel { get; private set; }
        public string Command
        {
            get { return _Command; }
            set
            {
                _Command = value;
                RaisePropertyChanged("Command");
            }
        }
        public string Result
        {
            get { return _Result; }
            set
            {
                _Result = value;
                RaisePropertyChanged("Result");
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        private void RaisePropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

5. เพิ่ม class RequestCommand เพื่อใช้ publish คำสั่งไปยัง exchange ของ RabbitMQ

// RequestCommand.cs

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

namespace ChavpKernelCommand
{
    using Chavp.Kernel.Commands;
    using ChavpKernelCommand.Properties;

    public class RequestCommand : ICommand
    {
        CommandViewModel _vm;
        public RequestCommand(CommandViewModel vm)
        {
            _vm = vm;
        }

        public bool CanExecute(object parameter)
        {
            return true;
        }

        public event EventHandler CanExecuteChanged;

        public void Execute(object parameter)
        {
            var request = new Request
            {
                Name = "user-1",
                Date = DateTime.Now,
                Command = _vm.Command
            };

            string result = string.Format("Request: {0}, Command: {1}n"
                , request.Date.ToString("dd/MM/yyyy HH:mm:ss"), request.Command);

            _vm.PublishChannel.Request<Request, Response>(
                request, response =>
                {
                    result += string.Format(
                        "Response: {0}, From: {1}, Result: {2}n"
                        , response.Date.ToString("dd/MM/yyyy HH:mm:ss"), response.Name, response.Return);

                    _vm.Result += result;
                    _vm.Result += string.Format("Elapsed time: {0}n", response.Date - request.Date);
                    _vm.Result += "-------------------------------------------------------------------n";
                });
        }
    }
}

6. ไปที่ file MainWindow.xaml เพิ่ม XAML ข้างล่างนี้ลงไปครับ

<Window x:Class="ChavpKernelCommand.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Chavp Kernel Command" Height="400" Width="591" ResizeMode="NoResize">
    <Grid>
        <TextBox Text="{Binding Command, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Height="23" TextWrapping="Wrap" VerticalAlignment="Top" Width="514" Margin="63,5,0,0">
            <TextBox.InputBindings>
                <KeyBinding Key="Enter" Command="{Binding RequestCommand}" />
            </TextBox.InputBindings>
        </TextBox>
        <TextBlock HorizontalAlignment="Left" TextWrapping="Wrap" Text="Chavp>" VerticalAlignment="Top" Height="23" Width="58" Margin="5,5,0,0"/>
        <TextBox VerticalScrollBarVisibility="Auto" Text="{Binding Result}" HorizontalAlignment="Left" Height="329" TextWrapping="Wrap" VerticalAlignment="Top" Width="577" Margin="0,33,0,0" />
    </Grid>
</Window>

7. ไปที่ file MainWindow.xaml.cs ซึ่งเป็นส่วน code ของ MainWindow.xaml เติม code ตามข้างล่างนี้ลงไปครับ

// MainWindow.xaml.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace ChavpKernelCommand
{
    using Chavp.Kernel.Commands;
    using ChavpKernelCommand.Properties;
    using EasyNetQ;

    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        IBus _bus;
        IPublishChannel _publishChannel;

        CommandViewModel _vm;
        public MainWindow()
        {
            InitializeComponent();

            string rabbitMQBrokerHost = "localhost";
            string virtualHost = "/";
            string username = "guest";
            string password = "guest";

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

            _bus = RabbitHutch.CreateBus(connectionString);

            _publishChannel = _bus.OpenPublishChannel();

            _vm = new CommandViewModel(_publishChannel);
            _vm.Command = "{ command: "sum", values: [1,2,3,4,5,6,7,8,9] }";

            DataContext = _vm;
        }

        protected override void OnClosed(EventArgs e)
        {
            if (_bus != null)
                _bus.Dispose();

            base.OnClosed(e);
        }
    }
}

จาก code ที่ file MainWindow.xaml.cs ผมกำหนดค่า default คำสั่ง command เป็น sum มี parameter values เท่ากับ 1, 2, 3, 4, 5, 6, 7, 8, 9 เพื่อเอาไว้ทดสอบครับ นั่นหมายความว่า เมื่อผมกด enter คำสั่งนี้ไปแล้วผมจะต้องได้ผลลัพย์กลับมาเป็นผล รวมของเลขชุดดังกล่าว

OK ถึงเวลาที่เราจะมาทดสอบกันแล้วละครับ ก่อนอื่น run ChavpKernelProcessor ขึ้นมารอไว้ก่อนครับ ซึ่งจะเป็น Console Application หลังจากนั้นให้ run ChavpKernelCommand ที่เป็น WPF application ขึ้นมา หรือจะเลือก run แบบ Multiple startup projects โดย click ขวาที่ Solution เลือก Properties เลือก Startup Projects แล้วกำหนดตามภาพข้างล่างนี้ครับ

setup-multiple-startups-app

เมื่อ run 2 application ขึ้นมาแล้ว ให้กด enter เข้าไปที่ textbox คำสั่งของ ChavpKernelCommand ได้เลยครับ ก็จะแสดงผลลัพย์เหมือนผมตามภาพข้างล่างนี้เลย

run-multiapp

ก็จบละครับบล็อคนี้ มีปัญหาอย่างไรท่านสามารถ EMail หรือ Facebook ที่อยู่ในหน้า About เพื่อมาแลกเปลี่ยนความรู้กันได้เลยครับยินดีอย่างมาก และหวังว่าบล็อคนี้จะเป็นแนวทางให้ท่านนำไปประยุกต์ใช้พัฒนา application ของท่านใช้แก้ปัญหาต่างๆบนโลกใบนี้ของเราได้ไม่มากก็น้อยนะครับ

Source Code ของทั้งหมดอยู่ที่นี่ ChavpKernel.git ครับ

ขอบคุณครับ

#:P

Advertisements

#amqp, #cloud, #easynetq, #event-driven, #rabbitmq