[C#,Python]WebSocketを試してみる。同期するUIを作ってみた編。

WebSocketで実現する同期するUI。group1とgroup2のコントロールの値を同期させてみる。

なんか思いつきで作ってみた。
サーバはPython,画面はC#。
クラスライブラリ化したので部品として使用したい場合は、さくっと使用出来る。

staticクラスを使用してWebSocketの接続部を作成。ソケットの部分はシングルトンイメージ。
WsExtensionsクラス

    /// <summary>
    /// Contolの拡張メソッドと静的変数の定義
    /// </summary>
    public static class WsExtensions
    {
        /// <summary>
        /// WebSocket接続用
        /// </summary>
        private static WebSocket _ws = null;

        /// <summary>
        /// WebSocketでの受信時イベントハンドラ
        /// </summary>
        public static EventHandler OnData;
        
        /// <summary>
        /// 受信イベント処理
        /// </summary>
        /// <param name="ctrl"></param>
        /// <param name="e"></param>
        private static void OnRcvHandler(Control ctrl, WebSocketEventArgs e)
        {
            if (OnData != null)
            {
                OnData(ctrl, e);
            }
        }

        /// <summary>
        /// コントロールに紐つけるIDをセットする
        /// </summary>
        /// <param name="ctrl"></param>
        /// <param name="name"></param>
        public static void WSSetUIName(Control ctrl, string name)
        {
            ctrl.Tag = name;
        }

        /// <summary>
        /// WebSocket初期化
        /// </summary>
        /// <param name="ctrl"></param>
        /// <param name="wsuri"></param>
        /// <param name="uiname"></param>
        public static void WSConnect(System.Windows.Forms.Control ctrl, string wsuri, string uiname)
        {
            /// 対象のControl(パラメータ ctrl)とIDをセットする
            WSSetUIName(ctrl, uiname);

            /// WebSocket初期化
            if (_ws == null)
            {
                _ws = new WebSocket(wsuri);
                _ws.Open();
                _ws.OnData += (s, e) =>
                    {
                        OnRcvHandler(ctrl, e);
                    };
            }
            ctrl.Disposed += (s, e) =>
                {
                    if ((_ws != null) && (_ws.ReadyState != WebSocketState.Closed))
                    {
                        _ws.Close();
                    }
                };

        }

        /// <summary>
        /// WebSocket送信処理
        /// </summary>
        /// <param name="ctrl"></param>
        /// <param name="value"></param>
        public static void SendMessage(Control ctrl, byte[] value)
        {
            /// IDとデータをパックして送信
            var uimessage = new UIMessage();
            uimessage._0UIName = (string)ctrl.Tag;
            uimessage._1Value = value;

            _ws.SendMessage(uimessage.Object2MessagePack());
        }

        /// <summary>
        /// object -> MessagePack(byte[])
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="obj"></param>
        /// <returns></returns>
        public static byte[] Object2MessagePack<T>(this object obj)
        {
            var serializer = MessagePackSerializer.Create<T>();
            var ms = new System.IO.MemoryStream();
            serializer.Pack(ms, (T)obj);
            var ret = ms.ToArray();
            ms.Close();
            return ret;
        }

        public static byte[] Object2MessagePack(this object obj)
        {
            var serializer = MessagePackSerializer.Create<object>();
            var ms = new System.IO.MemoryStream();
            serializer.Pack(ms, (object)obj);
            var ret = ms.ToArray();
            ms.Close();
            return ret;
        }

        /// <summary>
        /// MessagePack(byte[]) -> object
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="bytes"></param>
        /// <returns></returns>
        public static object MessagePack2Object<T>(this byte[] bytes)
        {
            var serialize = MessagePackSerializer.Create<T>();
            var ms = new System.IO.MemoryStream(bytes);
            var ret = serialize.Unpack(ms);
            ms.Close();
            return ret;
        }
   
        /// <summary>
        /// 送信用データクラス
        /// </summary>
        public class UIMessage
        {
            /// <summary>
            /// 対象となるControlに割り振ったIDを指定する
            /// </summary>
            public string _0UIName { get; set; }

            /// <summary>
            /// 値
            /// </summary>
            public byte[] _1Value { get; set; }
        }
     }

UIにより値を格納している方法が違うので、元のコントロールクラス(System.Windows.Form)を継承した各コントロールクラスを作成。そして、値が変更になった場合にデータをソケットで送信し、データが受信された場合にはコントロールの値を変更する。

UIクラス

    public class UI
    {
        /// <summary>
        /// TextBox
        /// </summary>
        public class wsTextBox : System.Windows.Forms.TextBox
        {
            public wsTextBox()
            {

                WsExtensions.OnData += (s,e)=>
                    {
                        var rcvData = (WebSocketEventArgs)e;
                        var uiMes = (WsExtensions.UIMessage)rcvData.BinaryData.MessagePack2Object<WsExtensions.UIMessage>();
                        
                        if (uiMes._0UIName != this.Tag.ToString()) return;
                        if (!this.Focused)
                        {
                            this.Text = (string)uiMes._1Value.MessagePack2Object<string>();
                        }
                    };

                this.TextChanged += (s, e) =>
                    {
                        if (this.Focused) WsExtensions.SendMessage(this,this.Text.Object2MessagePack());
                    };
            }
        }

        /// <summary>
        /// HScrollBar
        /// </summary>
        public class wsHScrollBar : System.Windows.Forms.HScrollBar
        {
            public wsHScrollBar()
            {
                bool flg = false;

                WsExtensions.OnData += (s, e) =>
                {
                    var rcvData = (WebSocketEventArgs)e;
                    var uiMes = (WsExtensions.UIMessage)rcvData.BinaryData.MessagePack2Object<WsExtensions.UIMessage>();

                    if (uiMes._0UIName != this.Tag.ToString()) return;

                    if (!flg)
                    {
                        this.Value = (int)uiMes._1Value.MessagePack2Object<int>();
                    }
                };

                this.MouseHover += (s, e) => flg = true;
                this.MouseLeave += (s, e) => flg = false;

                this.ValueChanged += (s, e) =>
                {
                    if (flg) WsExtensions.SendMessage(this, this.Value.Object2MessagePack());
                };
            }
        }

        /// <summary>
        /// VScrollBar
        /// </summary>
        public class wsVScrollBar : System.Windows.Forms.VScrollBar
        {
            public wsVScrollBar()
            {
                bool flg = false;

                WsExtensions.OnData += (s, e) =>
                {
                    var rcvData = (WebSocketEventArgs)e;
                    var uiMes = (WsExtensions.UIMessage)rcvData.BinaryData.MessagePack2Object<WsExtensions.UIMessage>();

                    if (uiMes._0UIName != this.Tag.ToString()) return;

                    if (!flg)
                    {
                        this.Value = (int)uiMes._1Value.MessagePack2Object<int>();
                    }
                };

                this.MouseHover += (s, e) => flg = true;
                this.MouseLeave += (s, e) => flg = false;

                this.ValueChanged += (s, e) =>
                {
                    if (flg) WsExtensions.SendMessage(this, this.Value.Object2MessagePack());
                };
            }
        }

        /// <summary>
        /// Label(受信専用)
        /// </summary>
        public class wsOptInLabel : System.Windows.Forms.Label
        {
            public wsOptInLabel()
            {
                WsExtensions.OnData += (s, e) =>
                {
                    var rcvData = (WebSocketEventArgs)e;
                    var uiMes = (WsExtensions.UIMessage)rcvData.BinaryData.MessagePack2Object<WsExtensions.UIMessage>();

                    if (uiMes._0UIName != this.Tag.ToString()) return;

                    this.Text = (string)uiMes._1Value.MessagePack2Object<string>();
                };
            }
        }

        /// <summary>
        /// CheckBox
        /// </summary>
        public class wsCheckBox : System.Windows.Forms.CheckBox
        {
            public wsCheckBox()
            {
                WsExtensions.OnData += (s, e) =>
                {
                    var rcvData = (WebSocketEventArgs)e;
                    var uiMes = (WsExtensions.UIMessage)rcvData.BinaryData.MessagePack2Object<WsExtensions.UIMessage>();

                    if (uiMes._0UIName != this.Tag.ToString()) return;

                    this.Checked = (bool)uiMes._1Value.MessagePack2Object<bool>();
                };

                this.CheckedChanged += (s, e) =>
                {
                    WsExtensions.SendMessage(this, this.Checked.Object2MessagePack());
                };
            }
        }

        /// <summary>
        /// RadioButton
        /// </summary>
        public class wsRadioButton : System.Windows.Forms.RadioButton
        {
            public wsRadioButton()
            {
                WsExtensions.OnData += (s, e) =>
                {
                    var rcvData = (WebSocketEventArgs)e;
                    var uiMes = (WsExtensions.UIMessage)rcvData.BinaryData.MessagePack2Object<WsExtensions.UIMessage>();

                    if (uiMes._0UIName != this.Tag.ToString()) return;

                    this.Checked = (bool)uiMes._1Value.MessagePack2Object<bool>();
                };

                this.CheckedChanged += (s, e) =>
                {
                    WsExtensions.SendMessage(this, this.Checked.Object2MessagePack());
                };
            }
        }
    }

同期用のGUIコントロール(WebSocketUILib.UI)はすべてSystem.Windows.Formsからの派生型。
デザイナで配置したあと、親FormのDesigner.csでInitializeComponent()と変数定義を書き換える。

        /// <summary>
        /// デザイナー サポートに必要なメソッドです。このメソッドの内容を
        /// コード エディターで変更しないでください。
        /// </summary>
        private void InitializeComponent()
        {
            this.textBox1 = new WebSocketUILib.UI.wsTextBox();
            this.textBox2 = new WebSocketUILib.UI.wsTextBox();
            this.groupBox1 = new System.Windows.Forms.GroupBox();
            this.checkBox2 = new WebSocketUILib.UI.wsCheckBox();
            this.checkBox1 = new WebSocketUILib.UI.wsCheckBox();
            this.groupBox2 = new System.Windows.Forms.GroupBox();
            this.radioButton2 = new WebSocketUILib.UI.wsRadioButton();
            this.radioButton1 = new WebSocketUILib.UI.wsRadioButton();
            this.vScrollBar1 = new WebSocketUILib.UI.wsVScrollBar();
            this.label2 = new WebSocketUILib.UI.wsOptInLabel();
            this.label1 = new WebSocketUILib.UI.wsOptInLabel();
            this.hScrollBar1 = new WebSocketUILib.UI.wsHScrollBar();
            this.groupBox1.SuspendLayout();
            this.groupBox2.SuspendLayout();
            this.SuspendLayout();

            /// 省略
        }

        /// 変数定義の書き換え
        private WebSocketUILib.UI.wsTextBox textBox1;
        private WebSocketUILib.UI.wsTextBox textBox2;
        private System.Windows.Forms.GroupBox groupBox1;
        private WebSocketUILib.UI.wsCheckBox checkBox2;
        private WebSocketUILib.UI.wsCheckBox checkBox1;
        private System.Windows.Forms.GroupBox groupBox2;
        private WebSocketUILib.UI.wsRadioButton radioButton2;
        private WebSocketUILib.UI.wsRadioButton radioButton1;
        private WebSocketUILib.UI.wsVScrollBar vScrollBar1;
        private WebSocketUILib.UI.wsOptInLabel label2;
        private WebSocketUILib.UI.wsOptInLabel label1;
        private WebSocketUILib.UI.wsHScrollBar hScrollBar1;
    }

親Formのコンストラクタで同期対象コントロールのWebSocketを初期化する。全部のUIで初期化しているけど、実際には1接続しか確立しないようになっている。
親Formのコンストラクタ

        public Form1()
        {
            InitializeComponent();

            // group1
            WsExtensions.WSConnect(textBox1,"ws://192.168.1.22:12345/ui", "Text1");        /// -> label1と同期
            WsExtensions.WSConnect(textBox2,"ws://192.168.1.22:12345/ui", "Text2");        /// -> label2と同期
            WsExtensions.WSConnect(hScrollBar1,"ws://192.168.1.22:12345/ui", "BarValue");  /// -> vScrollBar1と同期
            hScrollBar1.Minimum = 0;
            hScrollBar1.Maximum = 100;
            WsExtensions.WSConnect(checkBox1,"ws://192.168.1.22:12345/ui", "Check1");      /// -> radioButton1と同期
            WsExtensions.WSConnect(checkBox2,"ws://192.168.1.22:12345/ui", "Check2");      /// -> radioButton2と同期

            // group2
            WsExtensions.WSConnect(label1,"ws://192.168.1.22:12345/ui", "Text1");          /// -> textBox1と同期
            WsExtensions.WSConnect(label2,"ws://192.168.1.22:12345/ui", "Text2");          /// -> textBox2と同期
            WsExtensions.WSConnect(vScrollBar1,"ws://192.168.1.22:12345/ui", "BarValue");  /// -> hScrollBar1と同期
            vScrollBar1.Minimum = 0;
            vScrollBar1.Maximum = 100;
            WsExtensions.WSConnect(radioButton1,"ws://192.168.1.22:12345/ui", "Check1");   /// -> checkBox1と同期
            WsExtensions.WSConnect(radioButton2,"ws://192.168.1.22:12345/ui", "Check2");   /// -> checkBox2と同期
        }

ポイントはWSConnectで渡している2つめのパラメータ。これを定義することで、同じIDが定義された他のコントロールともデータの共有ができる。
UIクラスの各コントロールソース内で、受信データとして送られてくるIDと自分自身の値(ControlクラスのTagプロパティ)を比較し、受信すべきデータであるか判定している。C#は多重継承ができないのでControlクラスがもっているTag(object型)は助かった。
今回は値だけだったけど、他のプロパティを変更するコードを書けばもっと面白くなるかもしれない。テキストボックスやスライドバーなどは他の言語でもよくある型なので、連携させるのはあっさりできるかもしれない。

ちなみに、以下サーバソース。サーバはgeventwebsocketのブロードキャストサーバのほぼまんま。
ui_server.py

#!/usr/bin/env python2.6
# -*- coding:utf-8 -*-

import os
import geventwebsocket
import msgpack
from gevent import pywsgi
from geventwebsocket.handler import WebSocketHandler


websocketCons = []

def connection_handle(wenv):
    ws = wenv['wsgi.websocket']
    wskey = wenv['HTTP_SEC_WEBSOCKET_KEY']
    websocketCons.append(ws)
    print "add memlen = " + str(len(websocketCons))
    while True:
        msg = ws.receive()
        if msg is None:
           break
        
        i = 0
        while i < len(websocketCons):
           try:
                websocketCons[i].send(msg)
           except:
                websocketCons.remove(websocketCons[i])
           i += 1
    print "remove"
    websocketCons.remove(ws)


def websocket_app(environ, response):
    path = environ["PATH_INFO"]
    res_ConText = [("Content-Type", "text/html")]
 
    if path == "/":
        response("200 OK", res_ConText)
        return open('./index.html').read()
    elif path == "/ui":
        connection_handle(environ)
    else:
        response("404 Not Found",res_ConText)
        return "=======   Not Found!   ======="

if __name__ == "__main__":
   server = pywsgi.WSGIServer(('192.168.1.22', 12345), websocket_app, handler_class=WebSocketHandler)
   server.serve_forever()

今回作成したソースはこちら

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA


This blog is kept spam free by WP-SpamFree.