質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

89.86%

C# TCP通信の複数クライアント

解決済

回答 2

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 5,001

START_

score 25

現在Visualstudioにてwindowsフォームアプリケーションでチャットアプリを作成しています。
下記は1対1の通信のサーバ側のコードです。
そこから複数クライアントに対応したいと思っています。
イメージでは接続できたクライアントをスレッドに渡し、待ち受け側はそのままループで頭に戻り受付再開するのかと思い、取得したポートナンバを参照渡し(ref)で渡そうと考えたのですがうまくいきません。
詳細->うまくいかない内容ですが、
非同期メソッドにて参照渡しができない点。ですのでいきなり行き詰りました。。

なにか良い方法はないでしょうか?
よろしくお願いします!

  /// <summary>
        /// 待ち受けボタンクリック処理
        /// </summary>
        /// <param name="sender">オブジェクト</param>
        /// <param name="e">イベント</param>
        private void WaitServerBtn_Click(object sender, EventArgs e)
        {
                try
                {
                    if (status != SeverStatus.IDLE)
                    {
                        StopSever();
                    }
                    else
                    {
                        string ipAddress = this.getIpAdd1.IpAddress;
                        string portString = this.myPortEdit.Text;
                        port = Convert.ToInt32(portString);

                        StartServer(port);
                    }
                }
                catch (Exception exept)
                {
                    MessageBox.Show(exept.Message);
                }
        }
/// <summary>
        /// サーバー側スレッド処理
        /// </summary>
        /// <param name="PortnumSV">ポート番号</param>
        void StartServer(int PortnumSV)
        {
            Task.Factory.StartNew(() =>
             {
                 RunRecvMessageAsync(PortnumSV);
             });
        }
 /// <summary>
        /// 非同期受信メッセージ処理
        /// </summary>
        public async void RunRecvMessageAsync(int PortnumSV)
        {
            status = SeverStatus.CONNECT_INIT;

            // クライアント接続&ネットストリームの初期化 
            TcpClient client = null;
            NetworkStream stream = null;

            try
            {
                IPAddress localAddr = IPAddress.Parse("127.0.0.1");     // IPアドレスの設定*今回はローカルしか使わないので直値

                // サーバーを開始
                server = new TcpListener(localAddr, PortnumSV);
                server.Start();

                Byte[] bytes = new Byte[17];

                //待機中の間ループして探し続ける
                while (true)
                {
                    switch (status)
                    {
                        case SeverStatus.CONNECT_INIT:
                            SafeSetLable(Properties.Resources.ConnectWait);     //接続待機中のラベル表示
                            this.WaitServerBtn.Invoke(new Action(() =>
                            {
                                this.WaitServerBtn.Text = Properties.Resources.Cutting; //待ち受けボタンを切断に変更
                            }));

                            status = SeverStatus.CONNECT_ACCEPT_WAIT;
                            break;

                        // ステータス = 待ち受けの時
                        case SeverStatus.CONNECT_ACCEPT_WAIT:
                            if (server.Pending())
                            {
                                client = server.AcceptTcpClient();              // クライアント接続待ち   //クライアント接続前にサーバ切断するとerr!
                                stream = client.GetStream();                    // ストリーム取得
                                SafeSetLable(Properties.Resources.Connected);   // 接続されましたのラベル表示

                                status = SeverStatus.CONNECTED;                 //ステータス = 接続中
                            }
                            break;

                        // ステータス = 接続中の時
                        case SeverStatus.CONNECTED:
                            // メッセージを受信 
                            // 読み込むデータが存在する?
                            if (stream.DataAvailable)
                            {
                                // データを読み込む
                                int readlen = stream.Read(bytes, 0, bytes.Length);
                                if (readlen != 0)
                                {
                                    String data = System.Text.Encoding.UTF8.GetString(bytes, 0, readlen);
                                    // UIスレッド以外から呼び出された時のためにinvokeする
                                    this.ChatText.Invoke(new Action(() =>
                                    {
                                        this.ChatText.Text += (Properties.Resources.Partner + ":" + data + "\r\n");
                                    }));
                                }
                            }
                            // クライアントから切断された?
                            if (client.Client.Poll(1000, SelectMode.SelectRead) && (client.Client.Available == 0))
                            {
                                stream.Dispose();   // ストリームの開放
                                stream = null;
                                client.Close();     // クライアントとの接続終了
                                client = null;

                                status = SeverStatus.CONNECT_ACCEPT_WAIT;
                            }
                            break;
                        // ステータス = アイドルの時
                        case SeverStatus.IDLE:
                        default:
                            break;
                    }   // ループ終わり

                    // 続きの実行は適当な空いているスレッドに割り当てるようにする
                    await Task.Delay(1).ConfigureAwait(false);
                }
            }
            catch (SocketException socketEx)
            {
                // MessageBox.Show(except.Message);
                if (socketEx.ErrorCode == WSAEINTR)
                {
                    MessageBox.Show("クライアント側で通信が切断されました。\n");
                }

            }
            finally
            {
                if (stream != null)
                {
                    stream.Dispose();
                    stream = null;
                }
                if (client != null)
                {
                    client.Close();
                    client = null;
                }
                if (server != null)
                {
                    server.Stop();
                    server = null;
                    SafeSetLable("切断されました");
                    this.WaitServerBtn.Invoke(new Action(() =>
                    {
                        this.WaitServerBtn.Text = "待ち受け";
                        this.WaitServerBtn.Enabled = true;
                    }));
                }
                status = SeverStatus.IDLE;      // ステータス = アイドル
            }
        }
        /// <summary>
        /// サーバー側スレッド処理
        /// </summary>
        /// <param name="PortnumSV">ポート番号</param>
        void StartServer(int PortnumSV)
        {
            Task.Factory.StartNew(() =>
             {
                 RunRecvMessageAsync(PortnumSV);
             });
        }
        private async void StopSever()
        {
            if (status != SeverStatus.IDLE)
            {
                this.WaitServerBtn.Enabled = false;
                server.Stop();

                while (status != SeverStatus.IDLE)
                {
                    await Task.Delay(100);        //Task.waitで待つとデッドロックする
                }
            }
        }
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • y_waiwai

    2018/04/10 11:25

    うまくいかないとは、なにがどう、うまくいかないんでしょうか

    キャンセル

回答 2

checkベストアンサー

+2

◇ベストカレントプラティクス
WebSocketで作成されるのが一番だと思います。

◇以下は処理案です。
1,あまりコードをよく見ていませんが、server.AcceptTcpClientの戻り値をスレッドプールで処理するのも一つの手ですが、クライアントのメッセージを受信部分は非同期処理のserver.BeginAcceptTcpClientを使うのも一つの手かと。
2,RunRecvMessageAsync内で、 server = new TcpListener(localAddr, PortnumSV);を行うのではなく。StartServerStopSeverで行ってください。

3,TcpListenerを使っても最終的には独自の通信プロトコルを作成する必要があるため、TcpListenerではなくHttpListenerを使うと、プロトコルのデバック作業が楽になります。
動作確認してませんが、サンプルソースです雰囲気を感じ取ってくださいな。

private HttpListener server = new HttpListener();
private readonly object locker = new object();
public Action<string> OnLogWrite;// ログ出力用
public void Start()
{
    lock (this.locker)
    {
        this.server.Prefixes.Add(ConfigurationManager.AppSettings["prefix"]);
        this.server.Start();
        this.server.BeginGetContext(this.OnRequested, this.server);
    }
}
public void Stop()
{
    lock (this.locker)
    {
        this.server.Close();
        this.server = new HttpListener();
    }
}
public void OnRequested(IAsyncResult res){
    var listener = res.AsyncState as HttpListener;
    if (!listener.IsListening)
    {
       // 受信開始→終了でOnRequestedイベントが発火するため、受信待機状態でない時はSkip
       return;
    }
    var context = listener.EndGetContext(res);

    listener.BeginGetContext(this.OnRequested, listener);
  // 以下はやりたいこと。
}


4.RunRecvMessageAsyncメソッド内で、変数:statusをクライアントの接続の度に変更しているのも複数接続で、破綻しているのではないでしょうか。
5,画面やログの書き出しはactionを定義すると便利です。スレッドセーフを意識してくださいな。

◇参考情報

  1. TcpListener#BeginAcceptTcpClient
  2. HttpListener クラス

コンパイル通してませんが、TcpListener の場合はこのような形です。

private TcpListener server = new TcpListener();
private readonly object locker = new object();
public Action<string> OnLogWrite;// ログ出力用
public void Start()
{
    lock (this.locker)
    {
        // ここらへんでlistenとbindが必要なはず。
        // start
        this.server.Start();
        this.server.BeginAcceptTcpClient(this.OnRequested, this.server);
    }
}
public void Stop()
{
    lock (this.locker)
    {
        this.server.Close();
        this.server = new TcpListener();
    }
}
public void OnRequested(IAsyncResult res){
    var listener = res.AsyncState as TcpListener;
    var client = listener.EndAcceptTcpClient(res);

    listener.BeginAcceptTcpClient(this.OnRequested, listener);
    Task.Factory.StartNew(() =>
    {
       //これ以降は各クライアントで行いたい処理。
        client.Client.RemoteEndPoint;
        var stream = client.GetStream();
    });
}

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/04/10 14:33 編集

    >_Takuyaさんへ
    WINDOWSフォームアプリでもサーバー起動時に管理者権限が必要ですが、可能です。
    最終的に貴方がやりたいことによるのですが、チャットを自力作成したいのなら、WebSocketのライブラリを使うのが一番手間がかからないと思います。
    TCPListenerは単純な送信には便利なのですが、チャットルームを作るに当たり、例えば誰がこのチャットの発言しているのかの情報を送信する必要がありますが、クライアントから送られてきたチャットの文字と区別する必要があります。

    そのために通信プロトコル HTTPをご存知でしょうか?HTTPの場合はGET URL HTTP 1.0 の文字列をサーバーに送信してます。例えば文字列の先頭に発言者のIDを付けるなどをして貴方が全部定義する必要があります。そして、この通信プロトコルをデバックする必要があります。HTTPを使うとブラウザの開発者ツールや他のHTTP Clientをデバック時に使えるので、開発負荷が軽減されるのです。

    キャンセル

  • 2018/04/10 16:12

    >umyu 様
    細かいところまでわかりやすく教えていただきありがとうございます!!
    WebSocket 調べながらやっていくとうまくいきました!
    これから教えていただいたTCPListenerやHttpListenerもチャレンジしようと思います。
    ほんとに助かりました。
    また機会がありましたらよろしくお願いいたします!!

    キャンセル

  • 2018/04/10 16:49

    >_Takuyaさんへ
    解決してよかったです。

    キャンセル

+1

非同期メソッドにて参照渡しができない点。ですのでいきなり行き詰りました。。 

というのであるなら、ThreadLocal を使うとか。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/04/10 16:02

    コメントありがとうございます!
    y_waiwai 様
    ThreadLocal 使ってみるとうまくいきました。
    いつも助けていただいてありがとうございます!!

    キャンセル

  • 2018/04/10 16:07

    ああ、これはあくまで別解としてみてください。

    他の回答のほうがまっとうな対処だと思いますよw

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 89.86%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる