クライアント・サーバー型システムの試作(サーバー)

ちょっと思い立ってクライアント・サーバー型システムの骨組みのようなものを作ってみました 😉

プログラムの配布ページの方に zip ファイルのダウンロードページを作ったので、ファイルが欲しい方はそちらからダウンロードして下さい。

プロトコルは次のような感じです。

  Server        client
            ← 接続
[100 Service ready\r\n] →
            ← [HELO\r\n]
[110 Request completed\r\n] →
            ← [GET\r\n]
[120 Send data\r\n]
(データを送信 終端は\x1a\r\n)
            ← [QUIT\r\n]
[110 Request completed\r\n] →
   (接続を閉じる)

・サーバーのその他のレスポンス・コード
[500 Syntax error]
[501 Bad sequence of commands]
[502 Timeout connection closed]

コンソールへの表示内容は、次のようにしています。
サーバー側
server

クライアント側(受信ログには最後に \x1a\r\n が付加されていますが、受信データの表示では削除されています)
client

サーバーは接続待ちがなければ終了するようにしています。ただし、シングル・スレッドで処理しているので、複数の接続要求があると片方の処理が終わり接続が閉じるまで、キューで取り出し待ちになります。これは気が向いたらマルチ・スレッド化するかもしれません(マルチスレッド化したものを書いてみました)。

Main メソッドで接続要求と ‘HELO’ コマンドの処理と transaction メソッドの呼び出しをしています。

// Main
        static void Main(string[] args)
        {
            // 文字コードを UTF-8 とする
            var enc = System.Text.Encoding.UTF8;

            status = Server.serverStatus.None;

            // ローカルアドレスで Listen を開始する
            var host = "localhost";
            int port = 2001;
            var ipAddress = System.Net.Dns.GetHostAddresses(host).First();
            System.Net.Sockets.TcpListener listener = null;
            try
            {
                listener = new System.Net.Sockets.TcpListener(ipAddress, port);
                listener.Start();
                Console.WriteLine("Port[{0}]の Listen を開始しました。", port);

                do
                {
                    // 接続要求があったら受け入れる
                    using (var tcp = listener.AcceptTcpClient())
                    {
                        Console.WriteLine("クライアントが接続しました。");
                        // NetworkStream を取得
                        using (var stream = tcp.GetStream())
                        {
                            try
                            {
                                // Read のタイムアウトに15秒を設定
                                (stream as System.Net.Sockets.NetworkStream).ReadTimeout = 15000;

                                sendData(stream, "100 Service ready\r\n");

                                var rString = receiveData(stream, enc);
                                if (rString == "HELO\r\n")
                                {
                                    status = Server.serverStatus.TRANSACTION;
                                    sendData(stream, "110 Request completed\r\n");
                                    transaction(stream, enc);
                                }
                                else
                                {
                                    sendData(stream, "501 Bad sequence of commands\r\n");
                                }
                            }
                            catch (System.IO.IOException e)
                            {
                                if (e.InnerException.GetType() == typeof(System.Net.Sockets.SocketException) &&
                                    (e.InnerException as System.Net.Sockets.SocketException).ErrorCode == 10060)
                                {   // タイムアウト発生時
                                    sendData(stream, "502 Timeout connection closed\r\n");
                                    Console.WriteLine("タイムアウトが発生したため、クライアントとの接続を切断します。");
                                }
                                else
                                {
                                    throw;
                                }
                            }

                            stream.Close();
                        }

                        tcp.Close();
                        Console.WriteLine("切断しました。");
                    }
                } while (listener.Pending());
            }
            catch (System.Net.Sockets.SocketException e)
            {
                Console.WriteLine("SocketException: {0}", e);
            }
            finally
            {
                listener.Stop();
            }

            Console.ReadLine();
        }

transaction メソッドで操作要求コマンドの振り分けと処理の呼び出しをしています。

// 
        private enum serverStatus
        {
            None,
            TRANSACTION
        }

        private static serverStatus status;

        private static void transaction(System.IO.Stream stream, System.Text.Encoding enc)
        {
            string rString = "";
            try
            {
                do
                {
                    if (status != Server.serverStatus.TRANSACTION)
                    {
                        throw new InvalidOperationException("ステータスが TRANSACTION でないのでコマンドは処理できません。");
                    }

                    rString = receiveData(stream, enc);
                    switch (rString)
                    {
                        case "QUIT\r\n":
                            quit(stream);
                            break;
                        case "GET\r\n":
                            get(stream);
                            break;
                        default:
                            invalidCommand(stream);
                            break;
                    }
                }
                while (rString != "QUIT\r\n");
            }
            catch (ClientCloseException)
            {
            }

            status = Server.serverStatus.None;
        }

        private static void quit(System.IO.Stream stream)
        {
            sendData(stream, "110 Request completed\r\n");
        }

        private static void get(System.IO.Stream stream)
        {
            var data = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\r\n" +
                       "あいうえお\r\nかきくけこ\r\nさしすせそ\r\nたちつてと\r\nなにぬねの\r\n" +
                       "はひふへほ\r\nまみむめも\r\nやゆよ\r\nらりるれろ\r\nわを\r\nん\r\n";

            sendData(stream, "120 Send data\r\n");
            sendData(stream, data + "\x1a\r\n");
        }

        private static void invalidCommand(System.IO.Stream stream)
        {
            sendData(stream, "500 Syntax error\r\n");
        }

そして、受信と送信のメソッド。

//
        private static string receiveData(System.IO.Stream stream, System.Text.Encoding enc)
        {
            // クライアントから送られてきたデータを受信する
            var recieveMessage = "";
            using (var memStream = new System.IO.MemoryStream())
            {
                var rBuff = new byte[256];
                int rSize;
                do
                {
                    // データの一部を受信する
                    rSize = stream.Read(rBuff, 0, rBuff.Length);
                    // rSize が 0 のときにはクライアントが切断したと判断
                    if (rSize == 0)
                    {
                        Console.WriteLine("クライアントが切断しました。");
                        throw new ClientCloseException("ストリームの読み出しの際、クライアントが切断していました。");
                    }
                    // 受信したデータを蓄積する
                    memStream.Write(rBuff, 0, rSize);
                } while ((stream as System.Net.Sockets.NetworkStream).DataAvailable);

                // 受信したデータを文字列に変換
                recieveMessage = enc.GetString(memStream.ToArray());
                memStream.Close();
            }

            Console.WriteLine("[C]" + recieveMessage);
            return recieveMessage;
        }

        private static void sendData(System.IO.Stream stream, string str)
        {
            // 文字列をバイト配列へ
            var data = System.Text.Encoding.UTF8.GetBytes(str);
            stream.Write(data, 0, data.Length);
        }

あと、ClientCloseException クラスです。

//
    public class ClientCloseException : ApplicationException
    {
        public ClientCloseException(string message) : base(message) { }
    }

長くなったので、クライアント側のプログラムは日を改めて投稿します。


クライアント・サーバー型システムの試作(サーバー)」への2件のフィードバック

コメントを残す

メールアドレスが公開されることはありません。