[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種類がある。

実行画面

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

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

コメントを残す

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

CAPTCHA


This blog is kept spam free by WP-SpamFree.