web-dev-qa-db-ja.com

C#TCP / IPマルチクライアントとの簡単なチャット

私はc#ソケットプログラミングを学んでいます。したがって、私はTCP=チャットをすることを決定しました。基本的な考え方は、クライアントがサーバーにデータを送信し、サーバーがすべてのクライアントにオンラインでブロードキャストすることです(この場合、すべてのクライアントは辞書で)。

接続されているクライアントが1つある場合、期待どおりに機能し、接続されているクライアントが複数ある場合に問題が発生します。

サーバー:

_class Program
{
    static void Main(string[] args)
    {
        Dictionary<int,TcpClient> list_clients = new Dictionary<int,TcpClient> ();

        int count = 1;


        TcpListener ServerSocket = new TcpListener(IPAddress.Any, 5000);
        ServerSocket.Start();

        while (true)
        {
            TcpClient client = ServerSocket.AcceptTcpClient();
            list_clients.Add(count, client);
            Console.WriteLine("Someone connected!!");
            count++;
            Box box = new Box(client, list_clients);

            Thread t = new Thread(handle_clients);
            t.Start(box);
        }

    }

    public static void handle_clients(object o)
    {
        Box box = (Box)o;
        Dictionary<int, TcpClient> list_connections = box.list;

        while (true)
        {
            NetworkStream stream = box.c.GetStream();
            byte[] buffer = new byte[1024];
            int byte_count = stream.Read(buffer, 0, buffer.Length);
            byte[] formated = new Byte[byte_count];
            //handle  the null characteres in the byte array
            Array.Copy(buffer, formated, byte_count);
            string data = Encoding.ASCII.GetString(formated);
            broadcast(list_connections, data);
            Console.WriteLine(data);

        } 
    }

    public static void broadcast(Dictionary<int,TcpClient> conexoes, string data)
    {
        foreach(TcpClient c in conexoes.Values)
        {
            NetworkStream stream = c.GetStream();

            byte[] buffer = Encoding.ASCII.GetBytes(data);
            stream.Write(buffer,0, buffer.Length);
        }
    }

}
class Box
{
    public TcpClient c;
     public Dictionary<int, TcpClient> list;

    public Box(TcpClient c, Dictionary<int, TcpClient> list)
    {
        this.c = c;
        this.list = list;
    }

}
_

このボックスを作成したので、Thread.start()に2つの引数を渡すことができます。

クライアント:

_class Program
{
    static void Main(string[] args)
    {
        IPAddress ip = IPAddress.Parse("127.0.0.1");
        int port = 5000;
        TcpClient client = new TcpClient();
        client.Connect(ip, port);
        Console.WriteLine("client connected!!");
        NetworkStream ns = client.GetStream();

        string s;
        while (true)
        {
             s = Console.ReadLine();
            byte[] buffer = Encoding.ASCII.GetBytes(s);
            ns.Write(buffer, 0, buffer.Length);
            byte[] receivedBytes = new byte[1024];
            int byte_count = ns.Read(receivedBytes, 0, receivedBytes.Length);
            byte[] formated = new byte[byte_count];
            //handle  the null characteres in the byte array
            Array.Copy(receivedBytes, formated, byte_count); 
            string data = Encoding.ASCII.GetString(formated);
            Console.WriteLine(data);
        }
        ns.Close();
        client.Close();
        Console.WriteLine("disconnect from server!!");
        Console.ReadKey();        
    }
}
_
5
wants-to-learn

あなたの質問から、具体的にどのような問題が発生しているかは明らかではありません。ただし、コードを検査すると、2つの重要な問題が明らかになります。

  1. スレッドセーフな方法でディクショナリにアクセスすることはありません。つまり、ディクショナリにアイテムを追加する可能性のあるリスニングスレッドが、クライアントサービングスレッドがディクショナリを調べようとしているときにオブジェクトを操作できます。ただし、追加操作はアトミックではありません。つまり、アイテムの追加中に、辞書が一時的に無効な状態になる可能性があります。これは、同時にそれを読み取ろうとしているクライアントサービングスレッドに問題を引き起こします。
  2. クライアントコードは、ユーザー入力の処理を試み、サーバーからのデータの受信を処理しているのと同じスレッドでサーバーに書き込みます。これにより、少なくともいくつかの問題が発生する可能性があります。
    • 次回ユーザーが入力するまで、別のクライアントからデータを受信することはできません。
    • ユーザーが入力を提供した後でも、1回の読み取り操作で1バイトしか受信しない可能性があるため、以前に送信された完全なメッセージをまだ受信できない場合があります。

これらの2つの問題に対処するコードのバージョンを次に示します。

サーバーコード:

_class Program
{
    static readonly object _lock = new object();
    static readonly Dictionary<int, TcpClient> list_clients = new Dictionary<int, TcpClient>();

    static void Main(string[] args)
    {
        int count = 1;

        TcpListener ServerSocket = new TcpListener(IPAddress.Any, 5000);
        ServerSocket.Start();

        while (true)
        {
            TcpClient client = ServerSocket.AcceptTcpClient();
            lock (_lock) list_clients.Add(count, client);
            Console.WriteLine("Someone connected!!");

            Thread t = new Thread(handle_clients);
            t.Start(count);
            count++;
        }
    }

    public static void handle_clients(object o)
    {
        int id = (int)o;
        TcpClient client;

        lock (_lock) client = list_clients[id];

        while (true)
        {
            NetworkStream stream = client.GetStream();
            byte[] buffer = new byte[1024];
            int byte_count = stream.Read(buffer, 0, buffer.Length);

            if (byte_count == 0)
            {
                break;
            }

            string data = Encoding.ASCII.GetString(buffer, 0, byte_count);
            broadcast(data);
            Console.WriteLine(data);
        }

        lock (_lock) list_clients.Remove(id);
        client.Client.Shutdown(SocketShutdown.Both);
        client.Close();
    }

    public static void broadcast(string data)
    {
        byte[] buffer = Encoding.ASCII.GetBytes(data + Environment.NewLine);

        lock (_lock)
        {
            foreach (TcpClient c in list_clients.Values)
            {
                NetworkStream stream = c.GetStream();

                stream.Write(buffer, 0, buffer.Length);
            }
        }
    }
}
_

クライアントコード:

_class Program
{
    static void Main(string[] args)
    {
        IPAddress ip = IPAddress.Parse("127.0.0.1");
        int port = 5000;
        TcpClient client = new TcpClient();
        client.Connect(ip, port);
        Console.WriteLine("client connected!!");
        NetworkStream ns = client.GetStream();
        Thread thread = new Thread(o => ReceiveData((TcpClient)o));

        thread.Start(client);

        string s;
        while (!string.IsNullOrEmpty((s = Console.ReadLine())))
        {
            byte[] buffer = Encoding.ASCII.GetBytes(s);
            ns.Write(buffer, 0, buffer.Length);
        }

        client.Client.Shutdown(SocketShutdown.Send);
        thread.Join();
        ns.Close();
        client.Close();
        Console.WriteLine("disconnect from server!!");
        Console.ReadKey();
    }

    static void ReceiveData(TcpClient client)
    {
        NetworkStream ns = client.GetStream();
        byte[] receivedBytes = new byte[1024];
        int byte_count;

        while ((byte_count = ns.Read(receivedBytes, 0, receivedBytes.Length)) > 0)
        {
            Console.Write(Encoding.ASCII.GetString(receivedBytes, 0, byte_count));
        }
    }
}
_

ノート:

  • このバージョンは、lockステートメントを使用して、_list_clients_オブジェクトのスレッドによる排他的アクセスを保証します。
  • コレクションの列挙中にクライアントが削除されないこと、および別のスレッドがソケットで送信しようとしているときにクライアントがスレッドによって閉じられないようにするために、メッセージのブロードキャスト全体を通してロックを維持する必要があります。
  • このバージョンでは、Boxオブジェクトは必要ありません。コレクション自体は、実行中のすべてのメソッドからアクセス可能な静的フィールドによって参照され、各クライアントに割り当てられたint値がスレッドパラメータとして渡されるため、スレッドは適切なクライアントオブジェクトを検索できます。
  • サーバーとクライアントの両方が、_0_のバイトカウントで完了する読み取り操作を監視して処理します。これは、リモートエンドポイントの送信が完了したことを示すために使用される標準のソケット信号です。エンドポイントは、Shutdown()メソッドを使用して送信が完了したことを示します。グレースフルクロージャを開始するために、Shutdown()が「送信」理由で呼び出され、エンドポイントが送信を停止したが、引き続き受信することを示します。もう一方のエンドポイントは、最初のエンドポイントへの送信が完了すると、「両方」の理由でShutdown()を呼び出して、送信と受信の両方が完了したことを示すことができます。

コードにはまださまざまな問題があります。上記は最も明白なものだけを扱っており、非常に基本的なサーバー/クライアントアーキテクチャの実際のデモンストレーションの合理的な複製にコードをもたらします。


補遺:

コメントからのフォローアップ質問に対処するためのいくつかの追加メモ:

  • クライアントは、受信側スレッドでThread.Join()を呼び出して(つまり、そのスレッドが終了するのを待ちます)、正常なクローズプロセスを開始した後、シャットダウンによってリモートエンドポイントが応答するまで実際にソケットを閉じないようにします。その終わり。
  • ParameterizedThreadStartデリゲートとしてのo => ReceiveData((TcpClient)o)の使用は、スレッド引数のキャストよりも私が好むイディオムです。これにより、スレッドのエントリポイントを厳密に型指定されたままにすることができます。ただし、そのコードは、私が通常それを書いた方法とまったく同じではありません。そのイディオムを説明する機会をまだ使用しながら、私は元のコードに固執していました。しかし、実際には、パラメーターなしのThreadStartデリゲートを使用してコンストラクターオーバーロードを使用し、ラムダ式に必要なメソッド引数をキャプチャさせます:Thread thread = new Thread(() => ReceiveData(client)); thread.Start();次に、キャストを行う必要はありません(そして引数が値型の場合、ボックス化/ボックス化解除のオーバーヘッドなしで処理されます。通常、このコンテキストでは重要な問題ではありませんが、気分がよくなります:))。
  • これらの手法をWindowsフォームプロジェクトに適用すると、当然ながら複雑さが増します。非UIスレッドで受信する場合(接続ごとの専用スレッド、またはネットワークI/Oのいくつかの非同期APIのいずれかを使用する場合)、UIオブジェクトと対話するときにUIスレッドに戻る必要があります。ここでの解決策は通常と同じです。最も基本的なアプローチは、Control.Invoke()(またはWPFプログラムでDispatcher.Invoke())を使用することです。より洗練された(そしてIMHOより優れた)アプローチは、I/Oにasync/awaitを使用することです。 StreamReaderを使用してデータを受信して​​いる場合、そのオブジェクトにはすでに待機可能なReadLineAsync()および同様のメソッドがあります。 Socketを直接使用する場合は、 Task.FromAsync() メソッドを使用して、BeginReceive()およびEndReceive()メソッドを待ってます。どちらの方法でも、I/Oは非同期で発生しますが、UIオブジェクトに直接アクセスできるUIスレッドで完了が処理されます。 (このアプローチでは、Thread.Join()を使用する代わりに、受信コードを表すタスクを待機して、ソケットが途中で閉じないようにします。)
12
Peter Duniho