[C#]WebSocketを試してみる。お手軽シンプルブロードキャストエコーサーバー作成の巻。

ブロードキャストエコーサーバー
WebSocketのテスト用。
送信時にちょっと工夫。ユーザーに送信する際にParallel.ForEach()を使用して並列化してみた。これを使うと複数ユーザーに送信する処理を並列化できる。例えば大きいデータなどを分割して送りたい処理などに有効。但し、リソースの消費量が大きくなるので注意が必要。Parallel.ForEach()はマルチコアCPUをフル活用するからパソコンのファンがブーンって鳴る。

並列化の効用。
複数の画像を各クライアントにプッシュ配信するサーバーの例。合計1.77MBの画像データを8KBパケットに分割して送信するとします。実行画面では1フォルダにつき1クライアントが動作しています。

以下、シンプルブロードキャストサーバーのソース。
サーバーソース

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

using SuperSocket.Common;
using SuperSocket.SocketBase;
using SuperSocket.SocketBase.Config;
using SuperSocket.SocketEngine;
using SuperWebSocket;

namespace WsEchoServer
{
    class Program
    {
        static void Main(string[] args)
        {
            /// WebSocket初期化
            var server = new SuperWebSocket.WebSocketServer();
            var rootConfig = new SuperSocket.SocketBase.Config.RootConfig();
            var serverConfig = new SuperSocket.SocketBase.Config.ServerConfig()
            {
                Port = 12345,
                Ip = "Any",
                MaxConnectionNumber = 100,       /// 最大ユーザセッション数
                Mode = SocketMode.Sync,
                Name = "Echo Server"
            };

            /// クライアントセッションを格納
            var clinet_sessions = new List<WebSocketSession>();

            /// セッションの接続
            server.NewSessionConnected += (s) =>
                {
                    Console.WriteLine("{0}:New Session:{1}", DateTime.Now.ToString(), s.RemoteEndPoint.Address.ToString());

                    /// 新クライアントとして格納
                    clinet_sessions.Add(s);
                };

            /// セッションのクローズ
            server.SessionClosed += (s, r) =>
                {
                    Console.WriteLine("{0}:Session Close:{1}", DateTime.Now.ToString(), s.RemoteEndPoint.Address.ToString());

                    /// 該当クライアントの除外
                    clinet_sessions.Remove(s);
                };

            /// データ受信(バイナリ)
            server.NewDataReceived += (s, d) =>
                {
                    Console.WriteLine("{0}:Binary Received:{1}", DateTime.Now.ToString(), s.RemoteEndPoint.Address.ToString());
                    Console.WriteLine("Length={0}", d.Length);
                    
                    /// 全ユーザに送信
                    Parallel.ForEach(clinet_sessions, p => p.SendResponse(d));
                };

            /// データ受信(文字列)
            server.NewMessageReceived += (s, m) =>
                {
                    Console.WriteLine("{0}:String Received:{1}",DateTime.Now.ToString(), s.RemoteEndPoint.Address.ToString());
                    Console.WriteLine("Message={0}", m);

                    /// 全ユーザに送信
                    Parallel.ForEach(clinet_sessions, p => p.SendResponse(m));
                };


            /// サーバセットアップ
            server.Setup(rootConfig, serverConfig, SocketServerFactory.Instance);
            /// サーバ起動
            server.Start();

            Console.WriteLine("{0}:Server start.",DateTime.Now.ToString());
        }
    }
}

クライアントにはWebSocket4Netを使用。
これまでクライアントにはWCF WebSocketsを使っていたけど、SuperWebSocketsに含まれていたWebSocket4Netを使用してみる。
ほぼWCF WebSocketsと使用感は変わらない。

WebSocket4Net
http://websocket4net.codeplex.com/

クライアントソース

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

using WebSocket4Net;

namespace WsClient
{
    class Program
    {
        static void Main(string[] args)
        {
            var ws = new WebSocket("ws://192.168.1.2:12345");

            /// 文字列受信
            ws.MessageReceived += (s, e) =>
                {
                    Console.WriteLine("{0}:String Received:{1}", DateTime.Now.ToString(), e.Message);
                };

            /// バイナリ受信
            ws.DataReceived +=(s,e)=>
                {
                    Console.WriteLine("{0}:Binary Received Length:{1}", DateTime.Now.ToString(), e.Data.Length);
                };

            /// サーバ接続完了
            ws.Opened += (s, e) =>
            {
                Console.WriteLine("{0}:Server connected.", DateTime.Now.ToString());
            };

            /// サーバ接続開始
            ws.Open();

            /// 送受信ループ
            while (true)
            {
                var str = Console.ReadLine();
                if (str == "END") break;

                if (ws.State == WebSocketState.Open)
                {
                    ws.Send(str);
                }
                else
                {
                    Console.WriteLine("{0}:wait...", DateTime.Now.ToString());
                }
            }


            /// ソケットを閉じる
            ws.Close();

        }
    }
}

実行画面

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

[C/C++]libwebsocketsを試してみる。その1。

C言語実装のWebSocketsライブラリ libwebsocketsを試す。
導入メモ。

(更新)2013/2/21での最新版に更新しました。

・実行環境
Ubuntu 12.04 LTS
gcc version 4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5)
autoconf (GNU Autoconf) 2.68
テストブラウザ Google Chrome v24 for win

・libwebsocketsのダウンロード
http://git.warmcat.com/cgi-bin/cgit/libwebsockets/

現時点での最新版Downloadの libwebsockets-master.tar.gz をダウンロードし、解凍する。

・autogen.shの実行

$./autogen.sh

・configureの実行

$ ./configure

・make & make installの実行

$ make
$ sudo make install

・テスト
test-serverというフォルダがあるので、この中のサーバーを実行します。

$ ./libwebsockets-test-server
lwsts[7584]: libwebsockets test server - (C) Copyright 2010-2013 Andy Green <andy@warmcat.com> - licensed under LGPL2.1
lwsts[7584]: Initial logging level 7
lwsts[7584]: Library version: 1.2 
lwsts[7584]:  Started with daemon pid 0
lwsts[7584]:  static allocation: 5460 + (12 x 1024 fds) = 17748 bytes
lwsts[7584]:  canonical_hostname = ubuntu
lwsts[7584]:  Compiled without SSL support
lwsts[7584]:  per-conn mem: 172 + 1328 headers + protocol rx buf
lwsts[7584]:  Listening on port 7681

ブラウザ(Chrome)でアクセスしてみます。
この環境の場合、http://192.168.1.22:7681 のようにhttpでアクセスします。
ウィンドウを2つ開き動作を確認します。下部のキャンバスエリアをなぞると、他のウィンドウと同期することがわかります。
Img20130221213716

[C#]WebSocketを試してみる。C#チャットサーバ作成の巻。

とうとうできたWebSocketのC#サーバ♪
WCFのWebSocketではサーバ側プログラムがどうにもこうにも動作できないでいたので、別のコンポーネントを試してみた。

SuperWebSocket, a .NET WebSocket Server
http://superwebsocket.codeplex.com/

こちらの方のサイトをちょっと参考。感謝感謝。
つくるよ
http://blog.livedoor.jp/tukuruyo/tag/SuperWebSocket

簡単な動作テストを行った所、動作が確認できたので、感動。でも、SuperWebSocketの機能をどうやって使用していいのかよくわかりません。全部英語だし。検索してみても使っている人はまだあまりいなそう。

Pythonのサーバをコーディングし直し。
以前作成したチャットソフトのサーバ側プログラムをフルスクラッチで作成。今までVMでPythonやっていたけど、VM起動する必要なくなった。

◇サーバプログラム・・・C#, SuperWebSocket
できること。
・Pythonで作ったサーバプログラムと同等の機能。
・ソケットのデータフォーマットにはMessagePackを使用。
・なんか、動いているのがわかるようなビューを作成。

◇クライアントプログラム・・・C#, WCF WebSockets
以前作成したチャットソフトと同じもの。

クライアントプログラム コーディング
サーバプログラムを作成するにあたり、共通部分についてはクラスライブラリ化した。なので、改修箇所はその部分のみ。WebSocketは規格なので、ほかにいじるところなし。

サーバプログラム コーディング
キモな部分だけ抽出。

ユーザソケットのセッション管理について
Pythonでコーディングした時、結構ややこしい処理になっていたところをちょと反省。ちゃんとデータクラスを作成して管理することに。
基本的に下記のSessionInfoクラスのリストをグローバルに置いて、各処理がユーザセッションにアクセスできるようにした。
WebSocketSession というのがユーザソケットのインスタンスになる。

        /// <summary>
        /// ユーザセッション管理クラス
        /// </summary>
        public class SessionInfo
        {
            /// <summary>
            /// WebSocketSession
            /// </summary>
            public WebSocketSession Session { get; set; }

            /// <summary>
            /// User GUID
            /// </summary>
            public string Guid { get; set; }

            /// <summary>
            /// UserData
            /// </summary>
            public Interfaces.UserInfo UserInfo { get; set; }

            public SessionInfo(WebSocketSession session, string guid, Interfaces.UserInfo userinfo)
            {
                Session = session;
                Guid = guid;
                UserInfo = userinfo;
            }
        }
        /// <summary>
        /// ユーザセッション管理
        /// </summary>
        private List<SessionInfo> _sessions = new List<SessionInfo>();

サーバ起動処理

                /// WebSocket初期化
                _server = new SuperWebSocket.WebSocketServer();
                var rootConfig = new SuperSocket.SocketBase.Config.RootConfig();
                var serverConfig = new SuperSocket.SocketBase.Config.ServerConfig()
                {
                    Port = int.Parse(textBoxPort.Text),
                    Ip = "Any",
                    MaxConnectionNumber = 100,       /// 最大ユーザセッション数
                    Mode = SocketMode.Sync,
                    Name = "Chat Server"
                };

                _server.NewSessionConnected += HNewSessionConnected;
                _server.SessionClosed += HCloseSession;
                _server.NewDataReceived += HReceiveMessage;
                _server.Setup(rootConfig, serverConfig,SocketServerFactory.Instance);
                /// サーバ起動
                _server.Start();

だいぶイミフwだけど、なかなかシンプル。
なんたらConfigに関してはほぼわからない。MaxConnectionNumberは試してみたらちゃんと同時接続数であることを確認できた。あとのパラメータはあとで調べる。

以下の部分がユーザ接続の新規接続、クローズ、受信部分のイベント。NewDataReceivedはバイナリデータ受信時、文字列はNewMessageReceivedになる。このチャットソフトはMessagePackを使用しているのでバイナリしか扱わない。

_server.NewSessionConnected += HNewSessionConnected;
_server.SessionClosed += HCloseSession;
_server.NewDataReceived += HReceiveMessage;

新規接続(NewSessionConnected)
今回のチャットソフトはソケットのセッション確立後、ユーザからログイン情報(GUID、名前、アイコン)を送信してくるので、今回ほぼ未使用。

ユーザセッションクローズ(SessionClosed)
クローズされたSessionを受け取ることができるので、自分で管理している情報にアクセスし、全ユーザに抜けたユーザ情報を送信できるような仕組みにした。

HCloseSession


        /// <summary>
        /// セッションクローズ要求
        /// </summary>
        /// <param name="session"></param>
        /// <param name="e"></param>
        private void HCloseSession(SuperWebSocket.WebSocketSession session, SuperSocket.SocketBase.CloseReason e)
        {
            Trace.WriteLine("HReceiveMessage");

            /// 該当ユーザのログアウトを知らせる
            RemoveSession(session);
        }

RemoveSession関数


        /// <summary>
        /// セッションのログアウト処理
        /// </summary>
        /// <param name="session"></param>
        private void RemoveSession(SuperWebSocket.WebSocketSession session)
        {
            /// 接続中のユーザ情報を検索
            var sessionInfo = _sessions.Where(p => p.Session == session);
            if (sessionInfo.Any())
            {
                var delSession = sessionInfo.First();

                /// 抜けたユーザ以外にユーザが抜けたことを知らせる。
                var sendIf = new Interfaces.CmdInfo();
                sendIf._1CMDNO = (int)Interfaces.COMMAND.CMDLOGOUT;
                sendIf._2GUID = delSession.Guid;
                sendIf._3TIME = DateTime.Now.ToString();
                SendMessage(session, sendIf, SENDTO.OTHER);

                AddLog(LOGCMD.SYSTEM, "ADMIN", delSession.Guid + " IS LOGOUT");

                _sessions.Remove(sessionInfo.First());

            }
            SetUserCount();
        }

ソケット受信処理
例によってbyte[]をMessagePackでクラスに変換するので、解析超楽。もう僕の中でパターン化しつつある。

HReceiveMessage

        /// <summary>
        /// 受信要求
        /// </summary>
        /// <param name="session"></param>
        /// <param name="message"></param>
        private void HReceiveMessage(SuperWebSocket.WebSocketSession session, byte[] message)
        {
            Trace.WriteLine("HReceiveMessage");

            /// コマンド解析
            var rcvIf = message.MessagePack2Object<Interfaces.CmdInfo>();
            
            /// 送信要求を行う処理を格納
            var sendIfs = new List<Action>();
            /// ブロードキャスト(全てのユーザ)に送信する必要あるならフラグ
            var bcastFlag = false;
            /// 送信要求パケット作成
            var sendIf = new Interfaces.CmdInfo();
            sendIf._1CMDNO = rcvIf._1CMDNO;
            sendIf._2GUID = rcvIf._2GUID;
            sendIf._3TIME = DateTime.Now.ToString();

            AddLog(LOGCMD.RECEIVE, sendIf._2GUID, Enum.GetName(typeof(Interfaces.COMMAND), (Interfaces.COMMAND)rcvIf._1CMDNO));

            switch ((Interfaces.COMMAND)rcvIf._1CMDNO)
            {
                /// ユーザログイン要求
                case Interfaces.COMMAND.CMDLOGIN:
                    {
                        /// 受信パケットからユーザデータ(Interfaces.UserInfo)を取り出す。
                        var newsession = new SessionInfo(session, rcvIf._2GUID, rcvIf._4SENDDATA.MessagePack2Object<Interfaces.UserInfo>());
                        _sessions.Add(newsession);

                        AddLog(LOGCMD.SYSTEM, "ADMIN", newsession.Guid + " IS LOGIN");

                        /// 新規ユーザに現在ログイン中のすべてのユーザ情報を送信
                        Action alluserinfo = new Action(() =>
                            {
                                var sendCmdIf = new Interfaces.CmdInfo();
                                sendCmdIf._1CMDNO = (int)Interfaces.COMMAND.CMDALLUSERINFO;
                                sendCmdIf._2GUID = "ADMIN";
                                sendCmdIf._3TIME = DateTime.Now.ToString();
                                var infos = new List<Interfaces.UserInfo>();
                                _sessions.ForEach(p => infos.Add(p.UserInfo));
                                sendCmdIf._4SENDDATA = CommonLibrary.Extensions.ExtensionsClass.Object2MessagePack1Serializer(infos.ToArray());
                                SendMessage(session, sendCmdIf, SENDTO.ONE);
                            });

                        /// 送信処理に追加
                        sendIfs.Add(alluserinfo);
                        /// 全員に知らせる
                        bcastFlag = true;
                    }
                    break;
                /// チャットメッセージ受信
                case Interfaces.COMMAND.CMDSENDSTRING:

                    var rcvmsg = rcvIf._4SENDDATA.MessagePack2Object<Interfaces.MessageInfo>();
                    
                    /// 送信先が空なら宛先全員
                    if (rcvmsg._1TOGUID == "")
                    {
                        bcastFlag = true;
                    }
                    /// 宛先が特定ユーザ
                    else
                    {
                        /// 接続中ユーザへのメッセージ送信処理
                        var toUserInfo = _sessions.Where(p => p.Guid == rcvmsg._1TOGUID);
                        if (toUserInfo.Any())
                        {
                            Action sendMsg = new Action(() =>
                                {
                                    sendIf._4SENDDATA = rcvIf._4SENDDATA;

                                    /// 送信先->送信元ユーザ
                                    SendMessage(session, sendIf, SENDTO.ONE);
                                    /// 送信先->特定ユーザ
                                    SendMessage(toUserInfo.First().Session, sendIf, SENDTO.ONE);
                                });
                            sendIfs.Add(sendMsg);
                        }

                        /// 他の人には送らない
                        bcastFlag = false;
                    }
                    break;
                /// その他のコマンド
                default:
                    {
                        /// ブロードキャスト
                        bcastFlag = true;
                    }
                    break;
            }

            /// 全員宛のパケット作成
            if (bcastFlag)
            {
                var sendAll = new Action(() =>
                    {
                        sendIf._4SENDDATA = rcvIf._4SENDDATA;
                        SendMessage(session, sendIf, SENDTO.ALL);   /// 送信先=全員
                    });
                sendIfs.Add(sendAll);
            }

            /// 送信処理
            sendIfs.ForEach(p => p.DynamicInvoke());

            SetUserCount();
        }

これでもPythonのサーバと同じ処理のつもり。。。WebSocketsはどんなタイミングでも送信できるから楽だなぁ。
SendMessage関数がデータをパックして送信する処理になる。

SendMessage関数

        /// <summary>
        /// 送信処理
        /// </summary>
        /// <param name="session"></param>
        /// <param name="cmdif"></param>
        /// <param name="sendto"></param>
        /// <param name="delSession"></param>
        private void SendMessage(SuperWebSocket.WebSocketSession session, Interfaces.CmdInfo cmdif,SENDTO sendto,bool delSession = true)
        {
            /// 異常セッション回収用
            var delsession = new List<SuperWebSocket.WebSocketSession>();

            /// ログ文字生成
            var str = string.Format("{0}  FROM {1}  SENDTO {2}",
                Enum.GetName(typeof(Interfaces.COMMAND),(Interfaces.COMMAND)cmdif._1CMDNO),
                cmdif._2GUID,
                Enum.GetName(typeof(SENDTO),sendto));

            AddLog(LOGCMD.SEND, "ADMIN", str);
                
            /// 各セッション宛に送信処理を行う
            for (int i = 0; i < _sessions.Count(); i++)
            {
                var current = _sessions[i];

                /// 送信先指定==自分自身,current != 自分以外 OR
                /// 送信先指定==自分以外,current == 自分  の場合は抜ける
                if ( ((sendto == SENDTO.ONE) && (current.Session != session)) ||
                     ((sendto == SENDTO.OTHER) && (current.Session == session))
                    )
                {
                    continue;
                }

                try
                {
                    /// これが送信処理
                    current.Session.SendResponse(cmdif.Object2MessagePack());
                }
                catch
                {
                    /// 異常セッション回収
                    delsession.Add(current.Session);
                }

                /// 送信先指定==自分の場合は処理終了
                if (sendto == SENDTO.ONE) break;
            }

            /// 異常終了セッションをログアウト通知
            if (delsession.Any())
            {
                for (int i = 0; i < delsession.Count; i++)
                {
                    try
                    {
                        RemoveSession(delsession[i]);
                    }
                    catch { }
                }
            }
        }

送信処理は特定ユーザのみにデータを送信する場合があるので、ちょっとわかりづらいコードになってしまったなう。SuperWebSocket.WebSocketSessionクラスのSendResponse()が送信処理本体で、byte[]とstringの2種類がある。

実行画面

まとめ
さくっとできたところはよかっと思います。

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

[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()

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

[C#,Python]WebSocketを試してみる。その5。

前回作成したチャットソフトに個別送信機能を追加してみる。

やりたいこと。
・ユーザーは名前を付けられる。
・ユーザーは自分の発言に色を使用出来る。
・現在ログインしているユーザー一覧を表示する。なお、ユーザーの増減が生じた場合は自動で更新されること。※ただし異常終了は除く。
・クライアントが異常終了しても、サーバーが落ちないこと。
・MessagePackを通信フォーマットに使用したい。
・ユーザーはアイコンを設定できる。
・シェイクを送信できる。
・「他のユーザが入力中」とか表示できる。
・特定のユーザだけにメッセージを送信できる。←NEW

チャットメッセージクラスを変更

        /// <summary>
        /// チャットメッセージ
        /// </summary>
        public class MessageInfo : ExtensionsClass.ExtensionsClass.IMessage
        {
            public string _1TOGUID { get; set; }
            public int _2COLOR { get; set; }
            public string _3MESSAGESTRING { get; set; }
        }

_1TOGUID変数を追加して、送信先のGUIDを指定できるようにします。尚、送信先が全員の場合は空で送るようにします。

C#チャットメッセージ送信部分を変更
SendMessage関数内でチャットメッセージの送信データ作成時に送信先のGUIDを指定するようにします。

                /// チャットメッセージ送信
                case Interfaces.COMMAND.CMDSENDSTRING:
                    {
                        var data = new Interfaces.MessageInfo();
                        data._1TOGUID = "";
                        if (cBoxToUser.Text != "全員")
                        {
                            var sendUser = _userList.Where(p => p.Value._2NAME == cBoxToUser.Text);
                            if (sendUser.Any())
                            {
                                data._1TOGUID = sendUser.First().Key;
                            }
                            else
                            {
                                MessageBox.Show("送信先エラーです。");
                                cBoxToUser.SelectedIndex = 0;
                                return;
                            }
                        }
                        data._2COLOR = pictureBoxStringColor.BackColor.ToArgb();
                        data._3MESSAGESTRING = textBoxMessage.Text;

                        cmdif._4SENDDATA = data.Object2MessagePack();
                    }
                    break;

cBoxToUserとしてユーザ名が入ったコンボボックスコントロールを追加します。送信時にこのコンボボックスに選択されている名前が送信先になります。デフォルトは”全員”とします。
_userListはGUIDとInterfaces.UserInfoデータクラスのDictionaryなので、送信先の名前からGUIDを特定して_1TOGUIDに指定しています。
ちなみに、名前がかぶった場合の処理が考えてありません。。。

Python側受信処理変更
connection_handle()のメッセージ受信ループの中でチャットメッセージコマンド受信の時の処理を追加します。

    while True:
        msg = ws.receive()
        
        print "Receive Message"
        
        if msg is None:
           break
        
        msgpack_msg = msgpack.unpackb(msg)
        
        cmd = msgpack_msg[0]
        guid = msgpack_msg[1]
        timestr = getTimeStr()
        
        print "\tcmd = " + str(cmd)
        print "\tmsg len = " + str(len(msg))        
        
        sendmsg = []
        sendmsg.append(cmd)
        sendmsg.append(guid)
        sendmsg.append(timestr)
        sendmsg.append(msgpack_msg[3])
        sendmsg_pack = msgpack.packb( sendmsg )
        
        if cmd == CMDLOGIN:
            _userInfo[ id(ws) ] = msgpack_msg[1]
            _userData[ id(ws) ] = msgpack.unpackb( msgpack_msg[3] )
            sendAllUserInfo(ws, SENDONE)
        
        if cmd == CMDMESSAGESTRING:
            data = msgpack.unpackb( msgpack_msg[3] )
            
            touser = wsIdToWs(guidToWsId( data[0] ))
            if touser != None:
                sendMessage(ws, str2bytearray(sendmsg_pack), SENDONE)
                sendMessage(touser, str2bytearray(sendmsg_pack), SENDONE)
            elif len(data[0]) == 0:
                sendMessage(ws, str2bytearray(sendmsg_pack), SENDALL)   
        else:
            sendMessage(ws, str2bytearray(sendmsg_pack), SENDALL)   
    

ヘッダー部分のコマンド番号を解析し、CMDMESSAGESTRING(0x12)の処理を追加します。データ部分をさらに分解し、配列の1番目のGUIDを解析して該当のユーザを特定します。そして、送信ユーザと該当のユーザのみにチャットメッセージを送信します。
guidToWsId()はGUIDからWebSocketのインスタンスIDを特定し、wsIdToWs()はWebSocketのインスタンスIDからWebSocketのインスタンスを返します。

C#メッセージ受信処理変更
RecvMessage()のチャットメッセージ受信処理を変更します。

                /// チャットメッセージ受信
                case Interfaces.COMMAND.CMDSENDSTRING:
                    {
                        var msg = cmdif._4SENDDATA.MessagePack2Object<Interfaces.MessageInfo>();
                        var userData = _userList[ cmdif._2GUID];

                        CtrlAction = new Action(() =>
                            {
                                AddChatMessage(userData.GetIconImage(), userData._2NAME,
                                    msg._3MESSAGESTRING, Color.FromArgb(msg._2COLOR), cmdif._3TIME,msg._1TOGUID);
                            });
                    }
                    break; 

実際にはAddChatMessage関数に引数toguidを追加し、GUIDにより該当ユーザの名前を画面に表示させるようにします。

        private void AddChatMessage(Image icon, string name, string msg, Color msgcolor, string time, string toguid = "")
        {
            var str = time + " :";
            if (toguid != "")
            {
                str += _userList[toguid]._2NAME;
            }
            else
            {
                str += "全員";
            }

            DataGridViewRow dgvrow = new DataGridViewRow();
            
            dgvChatView.Rows.Add(icon, name, str);
            dgvChatView.Rows.Add(null, msg, null);
            dgvChatView.Rows[dgvChatView.Rows.Count - 2].Height = 25;
            dgvChatView.Rows[dgvChatView.Rows.Count - 1].Cells[1].Style.ForeColor = msgcolor;
            
            if (toguid != "")
            {
                dgvChatView.Rows[dgvChatView.Rows.Count - 2].DefaultCellStyle.BackColor = Color.Beige;
                dgvChatView.Rows[dgvChatView.Rows.Count - 1].DefaultCellStyle.BackColor = Color.Beige;
            }
            dgvChatView.FirstDisplayedScrollingRowIndex = dgvChatView.Rows.Count-1;
        }

特定ユーザからのメッセージは背景色を変えています。

実行画面
Img20130123233114

まとめ
今回の機能追加は、サーバ・クライアント間の処理をヘッダー部、データ部に分離してあることによりコアな処理に変更を入れる必要は生じなかったものの、元々拡張性を考えて作っていた割りには、汚いソースとなってしまいました。送信先情報はヘッダー部に入れるべきだったなぁ。サーバ側の元々のサンプルが全てのメッセージをブロードキャストするイメージだったので、個別送信機能は後付になっちゃたなぁ。
やっぱ、設計って大事だなー。

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

[C#,Python]WebSocketを試してみる。その4。

チャットソフトGUI編。

WebSocketのインタフェース部分が出来上がれば、あとはGUIをコツコツ組み立てるだけです。

やりたいこと。
・ユーザーは名前を付けられる。
・ユーザーは自分の発言に色を使用出来る。
・現在ログインしているユーザー一覧を表示する。なお、ユーザーの増減が生じた場合は自動で更新されること。※ただし異常終了は除く。
・クライアントが異常終了しても、サーバーが落ちないこと。
・MessagePackを通信フォーマットに使用したい。
・ユーザーはアイコンを設定できる。
・シェイクを送信できる。
・「他のユーザが入力中」とか表示できる。 ←NEW

画面
Img20130118221221

実行画面

シェイク!!

あー、シェイクうぜっ!シェイクうぜっ!

DataGirdViewのセル結合とかちょっとぐぐたすをイメージしたりとかして、大変だったけど、アイコンイメージが48X48くらいじゃないとうまく収まらないというww
発言とかコピーできないとか、まぁ、いろいろあるけど、面白かったからいいやヽ(^o^)丿

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

[C#,Python]WebSocketを試してみる。その3。

チャットソフト続き。

MessagePackデータの送受信部
その2の記事でデータモデルを作成しましたが、デタラメ書きました。
MessagePackのシリアライズはシリアライズ可能な型であれば、C#であろうが、Pythonであろうが構わないので、なんでもかんでもbyte[]にする必要はなかったのであります。

次のデータクラスのやりとりを考えます。

        /// <summary>
        /// 送信データヘッダー部
        /// </summary>
        public class CmdInfo : ExtensionsClass.ExtensionsClass.IMessage
        {
            /// <summary>
            /// MessagePackにpackするときに、変数の並びが辞書順に並び替えられるらしいので、変数名に注意する。
            /// </summary>
            public int _1CMDNO { get; set; }
            public string _2GUID { get; set; }
            public string _3TIME { get; set; }
            public byte[] _4SENDDATA { get; set; }
        }
        /// <summary>
        /// チャットメッセージ
        /// </summary>
        public class MessageInfo : ExtensionsClass.ExtensionsClass.IMessage
        {
            public int _1COLOR { get; set; }
            public string _2MESSAGESTRING { get; set; }
        }

※尚、C#のMessagePack実装ライブラリはシリアライズ処理時にクラスのプロパティ名を辞書順に並び替えてシリアライズしているようなので、辞書順のプロパティ名にしてあります。
※object <-> MessagePackの拡張メソッドをExtensionsClass.ExtensionsClass.IMessageインタフェースに適用しています。
C#送信部分ソース

            /// ヘッダーデータの生成
            var cmdif = new Interfaces.CmdInfo();
            cmdif._1CMDNO = (int)cmd;  /// cmd = 0x16とします。
            cmdif._2GUID = _guid.ToString();
            cmdif._3TIME = "";
                    
            var data = new Interfaces.MessageInfo();
            data._1COLOR = pictureBoxStringColor.BackColor.ToArgb();
            data._2MESSAGESTRING = textBoxMessage.Text;
            cmdif._4SENDDATA = data.Object2MessagePack();    /// byte[]メンバに自分で定義したクラスのMessagePackデータを載せる

            /// 送信データの作成
            var msgpackData = cmdif.Object2MessagePack();    /// 最終的にはbyte[]を生成させる

            /// ソケット送信
            _ws.SendMessage(msgpackData);

C#側送信データは各処理に応じてCmdInfo データクラスの_4SENDDATAに自前データのバイト配列を入れ子にしてサーバに送信します。
これにより、_4SENDDATAには色々な構造のデータを載せることが可能になります。

Python受信ソース

msg = ws.receive()
        
print "Receive Message"
        
if msg is None:
   break
        
msgpack_msg = msgpack.unpackb(msg)
        
cmd = msgpack_msg[0]
guid = msgpack_msg[1]
        
print "\tcmd = " + str(cmd)
print "\tmsg len = " + str(len(msg))

Python側受信部では受信時はbytearray型になり、MessagePackでデシリアライズするとstr型の配列として扱われます。
strにはunicode形式で入っています(確か)。このサーバは通信データの中身はコマンド番号ぐらいしか解析しないので良いのですが、str型のデータを操作する場合はちょっとめんどっちぃです。

Python側送信ソース

        sendmsg = []
        sendmsg.append(cmd)
        sendmsg.append(guid)
        sendmsg.append(timestr)
        sendmsg.append(msgpack_msg[3])
        sendmsg_pack = msgpack.packb( sendmsg )
        
        sendMessage(ws, str2bytearray(sendmsg_pack), SENDALL)

str2bytearray変換関数

def str2bytearray(strData):
    barray = bytearray()
    for x in strData:
        barray.append(x)
    return barray

sendMessage関数

def sendMessage(currentWs, msg, target, delclient = True):
    
    print "Send Message"
    
    delSockets = []
    
    i = 0
    memlen = len(_clientSockets)
    
    print "\tsend msg len  =" + str(len(msg))
    print "\tsend user len =" + str(memlen)
    
    while i < memlen:
        ws = _clientSockets[i]
        current = currentWs == ws
        
        if current == True and target == SENDOTHER:
            pass
        elif current == False and target == SENDONE:
            pass
        else:
            try:
                ws.send(msg)
            except:
                print "\tsend except"
                delSockets.append(ws)
            finally:
                if target == SENDONE:
                    break
        i += 1
    
    if delclient == False:
        return
    
    for x in delSockets:
        if _userInfo.has_key( id(x) ):
        
            sendmsg = []
            sendmsg.append(CMDLOGOUT)
            sendmsg.append( _userInfo[id(x)] )
            sendmsg.append( getTimeStr() )
            sendmsg_pack = msgpack.packb( sendmsg )
            sendMessage(x,str2bytearray(sendmsg_pack), SENDOTHER, False)
            
        remove_member(x)

ここでの送信処理は受信したメッセージに時間情報(getTimeStr()は時間情報の文字列を返す)を付加し、各クライアントに送信しています。
重要なポイントはmsgpack.packb()はstr型を返しますが、ソケット送信時はバイナリ(bytearray()型)でないと、クライアントが文字列としてデータを受信してしまうので、送信時はbytearray()にデータを変換してあげます。

C#側受信ソース

            /// ソケット受信時操作
            byte[] rcvData = e.BinaryData;    // ソケット受信データ

            /// コマンドヘッダー部取得
            var cmdif = rcvData.MessagePack2Object<Interfaces.CmdInfo>();

            string rcvMsg =
                string.Format("RcvCmd[{0}],GUID[{1}],TIME[{2}]",
                    Enum.GetName(typeof(Interfaces.COMMAND), cmdif._1CMDNO),
                    cmdif._2GUID,
                    cmdif._3TIME);
            
            Trace.WriteLine(rcvMsg);

          /// データ部分解
          var user = cmdif._4SENDDATA.MessagePack2Object<Interfaces.UserInfo>();

C#でのMessagePackデータの受信は拡張メソッドにより、簡単に解析可能な形になります。

その4へ続く。

[C#,Python]WebSocketを試してみる。その2。

その1のチャットソフトを拡張してみる。

やりたいこと。
・ユーザーは名前を付けられる。
・ユーザーは自分の発言に色を使用出来る。
・現在ログインしているユーザー一覧を表示する。なお、ユーザーの増減が生じた場合は自動で更新されること。※ただし異常終了は除く。
・クライアントが異常終了しても、サーバーが落ちないこと。
・MessagePackを通信フォーマットに使用したい。
・ユーザーはアイコンを設定できる。 ← NEW!
・シェイクを送信できる。 ← NEW!

パケットフォーマットに本格的にMessagePackを使用する。
C#でのMessagePackの使用は、C#用のMessagePack実装ライブラリが自前クラスをシリアライズしてくれるが、Python版はまだ自前クラスのシリアライズは対応していない。そこで、サーバー(Python)側でも認識可能なデータにしてあげることで、MessagePackを効率的に使用することができる

データモデル

PacketFormat
{
    int CMDNO;   /// コマンド番号
    byte[] DATA; /// データ部
}

MessagePackではすべてbyte[]になるので、実際には次のような構造になります。

PacketFormat
{
    byte[] CMDNO;
    byte[] DATA;
}

クライアント側(C#)の送信部分

    int cmd = 0x10;    /// Login
    Message msg = new Message();
    
    msg.USERID = 1010;           /// User ID
    msg.NAME = "むんむんむらむら"; /// User Name
    msg.ICON = imagedata;        /// ICON Data

    // 送信データの作成
    // データモデルに沿った形にする
    var sendData = new List<byte[]>();
    sendData.add( cmd.Object2MessagePack() );
    sendData.add( msg.Object2MessagePack() );

    // WebSocketで送信
    ws.SendMessage( sendData.ToArray().Object2MessagePack() );

サーバー側(Python)の受信部分

import msgpack

msg = ws.receive()

msgpack_msg = msgpack.unpackb( msg )
cmd = msgpack.unpackb( msgpack_msg[0] )

if cmd == 0x10:
    print "Login"

ちょっとサーバー側の実装に合わせたパケットフォーマットになるけど、先頭のCMDNOにより処理の振り分けが可能になり、データ部分のunpackはそれぞれの処理に任せることができるので、コーディングがスマートになる。C,C++とかだとvoid*で受けたデータとかをヘッダー部分だけ読み取って処理を振り分けたりするので、それと同じ。

→その3へ。

[C#,Python]WebSocketを試してみる。その1。

WebSocketを試してみる。その1。

ちょっと、WebSocketを試してみたくてしばらく試行錯誤してみた。
とりあえず、言語にはこだわるつもりはなかったのだけれども、サーバーはPython,クライアントはC#で記述。
Pythonはしばらくぶり、かつ初心者なので思い出しながら、簡単なチャットソフトを作成してみる。WebSocketは規格化されているので、言語が違ったって特に問題ない。ただ、普通のソケットプログラミングとは全くの別物なのでそこは注意。

やりたいこと。
・ユーザーは名前を付けられる。
・ユーザーは自分の発言に色を使用出来る。
・現在ログインしているユーザー一覧を表示する。なお、ユーザーの増減が生じた場合は自動で更新されること。※ただし異常終了は除く。
・クライアントが異常終了しても、サーバーが落ちないこと。
・MessagePackを通信フォーマットに使用したい。

C#版のWebSockets
C#ではHTML5Labsで公開されているWCF WebSocketsを使用。

HTML5 LABS – Download

こちらの方のサイトを参考に、同じサンプルプログラムを作成する。
present .NET で WebSocket 使うなら WCF WebSockets で FA

サーバープログラム
さて、C#のWebSocketsサーバーはサンプル通りにやっているのだけれど、クライアント(C#)がサーバー(C#)に接続後、SendMessage()を呼ぶとサーバー側でソケットが閉じられてしまうという事案が発生。
ちょっと何言っているのかわかんなーい状態。ちょうどOSをクリーンインストール後に試したりしたけど、やっぱり同じ現象。ちーん。

そこで、サーバー側はPythonで実装した。

Python版WebSocketsの実装はgeventwebsocketを使用。例のごとく、先駆者様のサイトを参考にやってみると、うまくいったぁ。

へきょのーと geventでWebSocketを使ってみる

サーバーソース

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

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


websocketCons = []

userinfo = {}

def send_userinfo():
    sendstr = "UserInfo:"
    
    for x in userinfo:
        sendstr += userinfo[x] + ","

    i = 0
    memlen = len(websocketCons)
    while i < memlen:
        try:
            print "send str=" + sendstr
            websocketCons[i].send(sendstr)
        except:
            print "remove_member ex1"
        i += 1

def remove_member(ws):
    
    try:
        websocketCons.remove(ws)
        del userinfo[id(ws)]
    except:
        print "remove_member ex2"
    
    print "remove memlen = " + str(len(websocketCons))
    
    send_userinfo()
    

def connection_handle(wenv):
    ws = wenv['wsgi.websocket']
    websocketCons.append(ws)
    print "add memlen = " + str(len(websocketCons))
    while True:
        msg = ws.receive()
        if msg is None:
           break
        
        if isinstance(msg,unicode):
            
            if msg[:5] == "Login":
                print "New>" + msg
                sp = msg.split(':')
                userinfo[id(ws)] = sp[1] + ";" + sp[2]
                send_userinfo()
                
                
        i = 0
        delsocket = []
        memlen = len(websocketCons)
        while i < memlen:
           try:
                websocketCons[i].send(msg)
           except:
                delsocket.append(websocketCons[i])
              
           i += 1
        for x in delsocket:
            remove_member(x)

    remove_member(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 == "/chat":
        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()

htmlの動作は割愛。
動作としてはサーバーに新しいユーザーがログインした際に、ユーザーが送信してくるIDとWebSocketsのインスタンスIDを関連付けて(dictで)覚えておき、ユーザーの増減が発生する度に全ユーザーにユーザー情報を送信するようにした。ユーザーが”Login:”を先頭で送ってきた文字列の場合のみユーザー情報を取得するようにしていて、チャットで使用されるデータはバイナリなので、基本、そのままブロードキャストするイメージ。
ユーザーがcloseをしないで異常終了するとそのソケットに対しての操作は無効になってしまう。その際のcloseイベントをどこに記述していいのかわからないので、サーバー側ではメッセージをユーザに送信するイベントが発生した際に、無効なソケットを収集したあとでremoveするという処理を記述。ちなみに、異常終了を起こしたソケットに対しての例外処理は各ユーザー毎のインスタンス分発生することになる。for文を使用しないでインデックス(変数i)によるループを使用しているのは、途中で例外が発生した場合に最後の要素まで処理を実行させるようにするため。
しかし、もうちょっとなんとかならなかったのだろうか、このソース。

クライアント
画面
Img20130109231137
ソース

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

using System.ServiceModel.WebSockets;
using System.Diagnostics;

using ExtensionsClass;

namespace ChatForm
{
    public partial class Form1 : Form
    {
        WebSocket _ws;

        Guid _guid;

        bool _userClose = false;

        private Dictionary<string, string> _userInfo = new Dictionary<string, string>();

        public Form1(int no)
        {
            InitializeComponent();

            _guid = Guid.NewGuid();

            dataGridView1.Rows.CollectionChanged += (s,e)=>
                dataGridView1.FirstDisplayedScrollingRowIndex = dataGridView1.Rows.Count - 1;

            TbxName.Text = TbxName.Text + no.ToString();
        }

        private void BtnLogin_Click(object sender, EventArgs e)
        {
            WebSocketsInit();

            
        }

        private void WebSocketsInit()
        {
            _ws = new WebSocket();
            _ws.Url = TbxServerAddr.Text;

            _ws.OnData += (s, e) =>
                {
                    if (e.BinaryData == null)
                    {
                        var str = e.TextData;

                        Action UpdateNameList = () =>
                            {
                                listBox1.Items.Clear();
                                foreach (string name in _userInfo.Values)
                                {
                                    listBox1.Items.Add(name);
                                }
                            };

                        if (str.IndexOf("UserInfo:") == 0)
                        {
                            var sp = str.Replace("UserInfo:","").Split(',');

                            _userInfo.Clear();
                            foreach (var name in sp.Where(p=>p.Length != 0))
                            {
                                var n = name.Split(';');
                                _userInfo.Add(n[0], n[1]);
                            }

                            UpdateNameList();
                        }
                    }
                    else
                    {
                        Trace.WriteLine("Rev Bindata");

                        var data = e.BinaryData.MessagePack2Object<Message>();

                        dataGridView1.Rows.Add(data.MESSAGESTRING);
                        dataGridView1.Rows[dataGridView1.Rows.Count - 2].Cells[0].Style.ForeColor = Color.FromArgb(data.STRINGCOLOR);
                    }
                };

            _ws.Open();

            _ws.OnOpen += (s, e) =>
                {
                    Trace.WriteLine("OnOpen");
                    _ws.SendMessage("Login:" + _guid.ToString() + ":" + TbxName.Text);
                };

            _ws.OnClose += (s, e) =>
                {
                    Trace.WriteLine("Socket is close");
                    if (!_userClose)
                    {
                        MessageBox.Show("サーバーエラーが発生しました");
                    }
                };

            this.FormClosed += (s, e) =>
                {
                    if (_ws.ReadyState != WebSocketState.Closed)
                    {
                        _ws.Close();
                    }
                };
        }
        private int _msgNo = 0;

        private void TbxSend_Click(object sender, EventArgs e)
        {
            var str = TbxName.Text + ":" + (_msgNo++).ToString() + ":" + TbxMessage.Text;

            var data = new Message();

            data.GUID = _guid.ToString();
            data.MESSAGESTRING = str;
            data.STRINGCOLOR = pictureBox1.BackColor.ToArgb();

            _ws.SendMessage(data.Object2MessagePack());

        }

        private void BtnLogout_Click(object sender, EventArgs e)
        {
            _ws.Close();
            _userClose = true;
        }

        private void pictureBox1_Click(object sender, EventArgs e)
        {
            var colordig = new ColorDialog();

            colordig.ShowDialog();
            
            pictureBox1.BackColor = colordig.Color;
        }

    }
}

using ExtensionsClassは以前作成したMessagePack用の拡張メソッドを使用。
サーバーへの接続確認はWebSocketクラスのReadyStateを参照することで判定可能だが、接続までの間ポーリングするよりOnOpen()を使用したほうが効率が良い。

            _ws.OnOpen += (s, e) =>
                {
                    Trace.WriteLine("OnOpen");
                    _ws.SendMessage("Login:" + _guid.ToString() + ":" + TbxName.Text);
                };

メッセージ送受信用クラス
チャットのデータはMessageクラスをMessagePackでバイナリ化して送信する。

    public class Message
    {
        /// <summary>
        /// ユーザ識別ID
        /// </summary>
        public string GUID { get; set; }

        /// <summary>
        /// チャットデータ
        /// </summary>
        public string MESSAGESTRING { get; set; }

        /// <summary>
        /// 文字色
        /// </summary>
        public int STRINGCOLOR { get; set; }
    }

メッセージ送信時のMessagePackを使用したバイナリ化。
バイト配列の作成とか、わざわざコーディングしないで送信!!

            var str = TbxName.Text + ":" + (_msgNo++).ToString() + ":" + TbxMessage.Text;

            var data = new Message();

            data.GUID = _guid.ToString();
            data.MESSAGESTRING = str;
            data.STRINGCOLOR = pictureBox1.BackColor.ToArgb();

            _ws.SendMessage(data.Object2MessagePack());

バイナリメッセージ受信時のMessagePackを使用したバイナリデータからMessageクラスへの変換。
受信メッセージ(バイト配列)をMessageクラスへの変換も楽々。あー、これがしたかったんだよなー。

                    if (e.BinaryData == null)
                    {
                        /// (略)
                    }
                    else
                    {
                        Trace.WriteLine("Rev Bindata");

                        var data = e.BinaryData.MessagePack2Object<Message>();

                        dataGridView1.Rows.Add(data.MESSAGESTRING);
                        dataGridView1.Rows[dataGridView1.Rows.Count - 2].Cells[0].Style.ForeColor = Color.FromArgb(data.STRINGCOLOR);
                    }

実行画面(クライアント)

実行画面(サーバログ)

user@ubuntu:python server.py
add memlen = 1
New>Login:9e8daa1c-f4c5-4a3a-93cb-5fed991b845f:名無しさん0
send str=UserInfo:9e8daa1c-f4c5-4a3a-93cb-5fed991b845f;名無しさん0,
add memlen = 2
New>Login:b78c2e4d-107c-43f3-a68b-9a2a65dea8cf:名無しさん1
send str=UserInfo:b78c2e4d-107c-43f3-a68b-9a2a65dea8cf;名無しさん1,9e8daa1c-f4c5-4a3a-93cb-5fed991b845f;名無しさん0,
send str=UserInfo:b78c2e4d-107c-43f3-a68b-9a2a65dea8cf;名無しさん1,9e8daa1c-f4c5-4a3a-93cb-5fed991b845f;名無しさん0,
add memlen = 3
New>Login:66401636-cfae-49d6-bb3f-146688fe4542:名無しさん2
send str=UserInfo:b78c2e4d-107c-43f3-a68b-9a2a65dea8cf;名無しさん1,66401636-cfae-49d6-bb3f-146688fe4542;名無しさん2,9e8daa1c-f4c5-4a3a-93cb-5fed991b845f;名無し さん0,
send str=UserInfo:b78c2e4d-107c-43f3-a68b-9a2a65dea8cf;名無しさん1,66401636-cfae-49d6-bb3f-146688fe4542;名無しさん2,9e8daa1c-f4c5-4a3a-93cb-5fed991b845f;名無し さん0,
send str=UserInfo:b78c2e4d-107c-43f3-a68b-9a2a65dea8cf;名無しさん1,66401636-cfae-49d6-bb3f-146688fe4542;名無しさん2,9e8daa1c-f4c5-4a3a-93cb-5fed991b845f;名無し さん0,
add memlen = 4
New>Login:d88afe91-17f0-4a84-bc9a-79f1966b319c:名無しさん3
send str=UserInfo:b78c2e4d-107c-43f3-a68b-9a2a65dea8cf;名無しさん1,66401636-cfae-49d6-bb3f-146688fe4542;名無しさん2,9e8daa1c-f4c5-4a3a-93cb-5fed991b845f;名無し さん0,d88afe91-17f0-4a84-bc9a-79f1966b319c;名無しさん3,
send str=UserInfo:b78c2e4d-107c-43f3-a68b-9a2a65dea8cf;名無しさん1,66401636-cfae-49d6-bb3f-146688fe4542;名無しさん2,9e8daa1c-f4c5-4a3a-93cb-5fed991b845f;名無し さん0,d88afe91-17f0-4a84-bc9a-79f1966b319c;名無しさん3,
send str=UserInfo:b78c2e4d-107c-43f3-a68b-9a2a65dea8cf;名無しさん1,66401636-cfae-49d6-bb3f-146688fe4542;名無しさん2,9e8daa1c-f4c5-4a3a-93cb-5fed991b845f;名無し さん0,d88afe91-17f0-4a84-bc9a-79f1966b319c;名無しさん3,
send str=UserInfo:b78c2e4d-107c-43f3-a68b-9a2a65dea8cf;名無しさん1,66401636-cfae-49d6-bb3f-146688fe4542;名無しさん2,9e8daa1c-f4c5-4a3a-93cb-5fed991b845f;名無し さん0,d88afe91-17f0-4a84-bc9a-79f1966b319c;名無しさん3,
remove memlen = 3
send str=UserInfo:b78c2e4d-107c-43f3-a68b-9a2a65dea8cf;名無しさん1,66401636-cfae-49d6-bb3f-146688fe4542;名無しさん2,9e8daa1c-f4c5-4a3a-93cb-5fed991b845f;名無し さん0,
send str=UserInfo:b78c2e4d-107c-43f3-a68b-9a2a65dea8cf;名無しさん1,66401636-cfae-49d6-bb3f-146688fe4542;名無しさん2,9e8daa1c-f4c5-4a3a-93cb-5fed991b845f;名無し さん0,
send str=UserInfo:b78c2e4d-107c-43f3-a68b-9a2a65dea8cf;名無しさん1,66401636-cfae-49d6-bb3f-146688fe4542;名無しさん2,9e8daa1c-f4c5-4a3a-93cb-5fed991b845f;名無し さん0,
192.168.1.7 - - [2013-01-08 18:40:58] "GET /chat HTTP/1.1" 101 - -
remove memlen = 2
send str=UserInfo:66401636-cfae-49d6-bb3f-146688fe4542;名無しさん2,9e8daa1c-f4c5-4a3a-93cb-5fed991b845f;名無しさん0,
send str=UserInfo:66401636-cfae-49d6-bb3f-146688fe4542;名無しさん2,9e8daa1c-f4c5-4a3a-93cb-5fed991b845f;名無しさん0,
192.168.1.7 - - [2013-01-08 18:41:00] "GET /chat HTTP/1.1" 101 - -

まとめ
geventwebsocketを使用した今回のサーバーはユーザからデータを受信するとそのデータをすべてのユーザに送信する仕組みなので、データの中身の処理はすべてクライアント側に記述できるという点で楽だった(C#だから)。また、通信フォーマットにMessagePackを使用できたのも面白かった。これでファイルや画像などのデータも楽にできる。
今回はちょっと試してみるというところで、なかなかおもしろかったので、あとで負荷分散とかいろいろ試してみたい。

今回作成したソースはこちら。
※ System.ServiceModel.WebSocketsとMessagePackの参照が必要。