web-dev-qa-db-fra.com

Chat simple C # TCP / IP avec plusieurs clients

J'apprends la programmation de socket c #. J'ai donc décidé de faire un TCP chat, l'idée de base est qu'un client envoie des données au serveur, puis le serveur les diffuse en ligne pour tous les clients (dans ce cas, tous les clients sont dans un dictionnaire).

Lorsqu'il y a 1 client connecté, cela fonctionne comme prévu, le problème se produit lorsqu'il y a plus d'un client connecté.

Serveur:

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;
    }

}

J'ai créé cette boîte, donc je peux passer 2 arguments pour la Thread.start().

Client:

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

Il n'est pas clair à partir de votre question quels problèmes spécifiquement que vous rencontrez. Cependant, l'inspection du code révèle deux problèmes importants:

  1. Vous n'accédez pas à votre dictionnaire de manière sécurisée pour les threads, ce qui signifie que le thread d'écoute, qui peut ajouter des éléments au dictionnaire, peut fonctionner sur l'objet en même temps qu'un thread de service client essaie d'examiner le dictionnaire. Mais, l'opération d'ajout n'est pas atomique. Cela signifie qu'au cours de l'ajout d'un élément, le dictionnaire peut être temporairement dans un état non valide. Cela entraînerait des problèmes pour tout thread de service client qui essaie de le lire simultanément.
  2. Votre code client tente de traiter l'entrée utilisateur et écrit sur le serveur dans le même thread qui gère la réception des données du serveur. Cela peut entraîner au moins quelques problèmes:
    • Il n'est pas possible de recevoir des données d'un autre client jusqu'à la prochaine fois que l'utilisateur fournira une entrée.
    • Dans la mesure où vous pouvez recevoir un seul octet en une seule opération de lecture, même après que l'utilisateur a fourni une entrée, vous pouvez toujours ne pas recevoir le message complet qui a été envoyé précédemment.

Voici une version de votre code qui résout ces deux problèmes:

Code serveur:

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);
            }
        }
    }
}

Code client:

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));
        }
    }
}

Remarques:

  • Cette version utilise l'instruction lock pour garantir un accès exclusif par un thread de l'objet list_clients.
  • Le verrou doit être maintenu tout au long de la diffusion des messages, pour garantir qu'aucun client n'est supprimé lors de l'énumération de la collection et qu'aucun client n'est fermé par un thread pendant qu'un autre essaie d'envoyer sur le socket.
  • Dans cette version, il n'y a pas besoin de l'objet Box. La collection elle-même est référencée par un champ statique accessible par toutes les méthodes en cours d'exécution et la valeur int affectée à chaque client est transmise en tant que paramètre de thread, afin que le thread puisse rechercher l'objet client approprié.
  • Le serveur et le client recherchent et gèrent une opération de lecture qui se termine avec un nombre d'octets de 0. Il s'agit du signal de socket standard utilisé pour indiquer que le point de terminaison distant a terminé l'envoi. Un point de terminaison indique que l'envoi est terminé à l'aide de la méthode Shutdown(). Pour lancer la fermeture progressive, Shutdown() est appelée avec la raison "send", indiquant que le point de terminaison a cessé d'envoyer, mais recevra toujours. L'autre point de terminaison, une fois l'envoi envoyé au premier point de terminaison, peut alors appeler Shutdown() avec la raison "les deux" pour indiquer qu'il est effectué à la fois en envoi et en réception.

Il y a encore une variété de problèmes dans le code. Ce qui précède ne concerne que les plus flagrants et apporte le code à un fac-similé raisonnable d'une démonstration fonctionnelle d'une architecture serveur/client très basique.


Addendum:

Quelques notes supplémentaires pour répondre aux questions complémentaires des commentaires:

  • Le client appelle Thread.Join() sur le thread de réception (c'est-à-dire attend que ce thread se termine), pour s'assurer qu'après le démarrage du processus de fermeture en douceur, il ne ferme pas réellement le socket jusqu'à ce que le point de terminaison distant réponde en s'arrêtant sa fin.
  • L'utilisation de o => ReceiveData((TcpClient)o) en tant que délégué ParameterizedThreadStart est un idiome que je préfère à la conversion de l'argument thread. Il permet au point d'entrée du thread de rester fortement typé. Cependant, ce code n'est pas exactement comme je l'aurais écrit habituellement; Je m'en tenais étroitement à votre code d'origine, tout en profitant de l'occasion pour illustrer cet idiome. Mais en réalité, j'utiliserais la surcharge du constructeur en utilisant le délégué sans paramètre ThreadStart et je laisserais simplement l'expression lambda capturer les arguments de méthode nécessaires: Thread thread = new Thread(() => ReceiveData(client)); thread.Start(); Ensuite, aucun transtypage n'a lieu (et si des arguments sont des types de valeur, ils sont traités sans surcharge de boxe/unboxing… ce n'est généralement pas un problème critique dans ce contexte, mais ça me fait quand même du bien :)).
  • L'application de ces techniques à un projet Windows Forms ajoute des complications, sans surprise. Lors de la réception dans un thread non UI (que ce soit un thread dédié par connexion ou en utilisant l'une des plusieurs API asynchrones pour les E/S réseau), vous devrez revenir au thread UI lors de l'interaction avec les objets UI. La solution qui est ici la même que d'habitude: l'approche la plus basique consiste à utiliser Control.Invoke() (ou Dispatcher.Invoke(), dans un programme WPF); une approche plus sophistiquée (et à mon humble avis, supérieure) consiste à utiliser async/await pour les E/S. Si vous utilisez StreamReader pour recevoir des données, cet objet possède déjà une ReadLineAsync() et des méthodes similaires attendues. Si vous utilisez directement Socket, vous pouvez utiliser la méthode Task.FromAsync() pour encapsuler les méthodes BeginReceive() et EndReceive() dans un en attente. Dans les deux cas, le résultat est que même si les E/S se produisent de manière asynchrone, les achèvements sont toujours traités dans le thread d'interface utilisateur où vous pouvez accéder directement à vos objets d'interface utilisateur. (Dans cette approche, vous attendriez la tâche représentant le code de réception, au lieu d'utiliser Thread.Join(), pour vous assurer de ne pas fermer le socket prématurément.)
12
Peter Duniho