web-dev-qa-db-fra.com

Un client TCP c # peut-il recevoir et envoyer en continu / consécutivement sans sommeil?

Il s'agit dans une certaine mesure d'une question sur les "bases du TCP", mais en même temps je n'ai pas encore trouvé de réponse convaincante ailleurs et je crois avoir une bonne/bonne compréhension des bases du TCP. Je ne sais pas si la combinaison de questions (ou la seule question et pendant que j'y suis, la demande de confirmation de quelques points) est contraire aux règles. J'espère pas.

J'essaie d'écrire une implémentation C # d'un client TCP, qui communique avec une application existante contenant un serveur TCP (je n'ai pas accès à son code, donc pas de WCF). Comment puis-je me connecter, envoyer et recevoir au besoin au fur et à mesure que de nouvelles informations entrent ou sortent, et finalement se déconnecter. En utilisant le code MSDN suivant comme exemple où elles sont répertoriées " Envoyer "et" recevoir "des méthodes asynchrones (ou simplement TcpClient), et en ignorant la connexion et la déconnexion comme triviale, comment puis-je mieux vérifier en permanence les nouveaux paquets reçus et en même temps envoyer en cas de besoin?

J'ai d'abord utilisé TCPClient et GetStream (), et le code msdn semble toujours nécessiter la boucle et le sommeil décrits dans un peu (compteur intuitivement), où j'exécute la méthode de réception dans une boucle dans un thread séparé avec un sommeil (10) millisecondes et envoyez le thread principal (ou troisième) selon vos besoins. Cela me permet d'envoyer correctement et la méthode de réception interroge efficacement à intervalles réguliers pour trouver de nouveaux paquets. Les paquets reçus sont ensuite ajoutés à une file d'attente.

Est-ce vraiment la meilleure solution? Ne devrait-il pas y avoir un équivalent d'événement DataAvailable (ou quelque chose qui me manque dans le code msdn) qui nous permette de recevoir quand, et seulement quand, de nouvelles données sont disponibles?

Après coup, j'ai remarqué que la prise pouvait être coupée de l'autre côté sans que le client ne s'en rende compte jusqu'au prochain envoi bâclé. Pour clarifier alors, le client est obligé d'envoyer des keepalives réguliers (et recevoir n'est pas suffisant, seulement envoyer) pour déterminer si le socket est toujours vivant. Et la fréquence de la keepalive détermine combien de temps je saurai que ce lien est en panne. Est-ce exact? J'ai essayé Poll, socket.connected, etc. pour découvrir pourquoi chacun n'aide pas.

Enfin, pour confirmer (je crois que non mais bon pour vous assurer), dans le scénario ci-dessus d'envoi à la demande et de réception si tcpclient.DataAvailable toutes les dix secondes, peut-il y avoir une perte de données si l'envoi et la réception en même temps? Si en même temps je reçois, j'essaie d'envoyer un échouera, écrasera l'autre ou tout autre comportement indésirable?

34
commentator8

Il n'y a rien de mal nécessairement à regrouper les questions, mais cela rend la réponse à la question plus difficile ... :)

L'article MSDN que vous avez lié montre comment effectuer une communication unique TCP, c'est-à-dire un envoi et une réception. Vous remarquerez également qu'il utilise le Socket directement où la plupart des gens, y compris moi-même, proposeront d'utiliser la classe TcpClient à la place. Vous pouvez toujours obtenir le Socket sous-jacent via le Client propriété si vous avez besoin de configurer un certain socket par exemple (par exemple, SetSocketOption() ).

L'autre aspect de l'exemple à noter est que bien qu'il utilise des threads pour exécuter les délégués AsyncCallback pour les deux BeginSend() et - BeginReceive() , il s'agit essentiellement d'un exemple monothread en raison de la façon dont les objets ManualResetEvent sont utilisés. Pour des échanges répétés entre un client et un serveur, ce n'est pas ce que vous voulez.

Très bien, vous voulez donc utiliser TcpClient. La connexion au serveur (par exemple TcpListener ) devrait être simple - utilisez Connect() si vous souhaitez une opération de blocage ou BeginConnect() si vous souhaitez une opération non bloquante. Une fois la connexion établie, utilisez la méthode GetStream() pour obtenir l'objet NetworkStream à utiliser pour la lecture et l'écriture. Utilisez les opérations Read() / Write() pour bloquer les E/S et BeginRead() / BeginWrite() opérations pour les E/S non bloquantes. Notez que les BeginRead() et BeginWrite() utilisent le même mécanisme AsyncCallback que celui utilisé par les méthodes BeginReceive() et BeginSend() des Socket classe.

Une des choses clés à noter à ce stade est ce petit texte de présentation dans la documentation MSDN pour NetworkStream:

Les opérations de lecture et d'écriture peuvent être effectuées simultanément sur une instance de la classe NetworkStream sans nécessiter de synchronisation. Tant qu'il y aura un thread unique pour les opérations d'écriture et un thread unique pour les opérations de lecture , il n'y aura aucune interférence croisée entre les threads de lecture et d'écriture et aucune synchronisation n'est requise.

En bref, comme vous prévoyez de lire et d'écrire à partir de la même instance TcpClient, vous aurez besoin de deux threads pour ce faire. L'utilisation de threads séparés garantira qu'aucune donnée ne sera perdue lors de la réception de données en même temps que quelqu'un essaie d'envoyer. La façon dont j'ai abordé cela dans mes projets est de créer un objet de niveau supérieur, par exemple Client, qui enveloppe le TcpClient et son NetworkStream sous-jacent. Cette classe crée et gère également deux Thread objets, en passant l'objet NetworkStream à chacun pendant la construction. Le premier thread est le thread Sender. Quiconque souhaite envoyer des données le fait via une méthode publique SendData() sur le Client, qui achemine les données vers le Sender pour transmission. Le deuxième thread est le thread Receiver. Ce fil publie toutes les données reçues aux parties intéressées via un événement public exposé par le Client. Cela ressemble à ceci:

Client.cs

public sealed partial class Client : IDisposable
{
    // Called by producers to send data over the socket.
    public void SendData(byte[] data)
    {
        _sender.SendData(data);
    }

    // Consumers register to receive data.
    public event EventHandler<DataReceivedEventArgs> DataReceived;

    public Client()
    {
        _client = new TcpClient(...);
        _stream = _client.GetStream();

        _receiver = new Receiver(_stream);
        _sender   = new Sender(_stream);

        _receiver.DataReceived += OnDataReceived;
    }

    private void OnDataReceived(object sender, DataReceivedEventArgs e)
    {
        var handler = DataReceived;
        if (handler != null) DataReceived(this, e);  // re-raise event
    }

    private TcpClient     _client;
    private NetworkStream _stream;
    private Receiver      _receiver;
    private Sender        _sender;
}


Client.Receiver.cs

private sealed partial class Client
{
    private sealed class Receiver
    {
        internal event EventHandler<DataReceivedEventArgs> DataReceived;

        internal Receiver(NetworkStream stream)
        {
            _stream = stream;
            _thread = new Thread(Run);
            _thread.Start();
        }

        private void Run()
        {
            // main thread loop for receiving data...
        }

        private NetworkStream _stream;
        private Thread        _thread;
    }
}


Client.Sender.cs

private sealed partial class Client
{
    private sealed class Sender
    {
        internal void SendData(byte[] data)
        {
            // transition the data to the thread and send it...
        }

        internal Sender(NetworkStream stream)
        {
            _stream = stream;
            _thread = new Thread(Run);
            _thread.Start();
        }

        private void Run()
        {
            // main thread loop for sending data...
        }

        private NetworkStream _stream;
        private Thread        _thread;
    }
}

Notez qu'il s'agit de trois fichiers .cs distincts mais définissez différents aspects de la même classe Client. J'utilise l'astuce Visual Studio décrite ici pour imbriquer les fichiers Receiver et Sender respectifs sous le fichier Client. En un mot, c'est comme ça que je le fais.

Concernant la question NetworkStream.DataAvailable / Thread.Sleep() . Je conviendrais qu'un événement serait Nice, mais vous pouvez y parvenir efficacement en utilisant la méthode Read() en combinaison avec un infini ReadTimeout . Cela n'aura aucun impact négatif sur le reste de votre application (par exemple, l'interface utilisateur) car elle s'exécute dans son propre thread. Cependant, cela complique la fermeture du thread (par exemple, lorsque l'application se ferme), donc vous voudrez probablement utiliser quelque chose de plus raisonnable, disons 10 millisecondes. Mais ensuite, vous revenez au sondage, ce que nous essayons d'éviter en premier lieu. Voici comment je le fais, avec des commentaires pour explication:

private sealed class Receiver
{
    private void Run()
    {
        try
        {
            // ShutdownEvent is a ManualResetEvent signaled by
            // Client when its time to close the socket.
            while (!ShutdownEvent.WaitOne(0))
            {
                try
                {
                    // We could use the ReadTimeout property and let Read()
                    // block.  However, if no data is received prior to the
                    // timeout period expiring, an IOException occurs.
                    // While this can be handled, it leads to problems when
                    // debugging if we are wanting to break when exceptions
                    // are thrown (unless we explicitly ignore IOException,
                    // which I always forget to do).
                    if (!_stream.DataAvailable)
                    {
                        // Give up the remaining time slice.
                        Thread.Sleep(1);
                    }
                    else if (_stream.Read(_data, 0, _data.Length) > 0)
                    {
                        // Raise the DataReceived event w/ data...
                    }
                    else
                    {
                        // The connection has closed gracefully, so stop the
                        // thread.
                        ShutdownEvent.Set();
                    }
                }
                catch (IOException ex)
                {
                    // Handle the exception...
                }
            }
        }
        catch (Exception ex)
        {
            // Handle the exception...
        }
        finally
        {
            _stream.Close();
        }
    }
}

En ce qui concerne les "keepalives", il n'y a malheureusement pas de moyen de contourner le problème de savoir quand l'autre côté a quitté la connexion en silence, sauf pour essayer d'envoyer des données. Dans mon cas, puisque je contrôle à la fois les côtés d'envoi et de réception, j'ai ajouté un petit message KeepAlive (8 octets) à mon protocole. Il est envoyé toutes les cinq secondes des deux côtés de la connexion TCP sauf si d'autres données sont déjà envoyées.

Je pense que j'ai abordé toutes les facettes que vous avez abordées. J'espère que ça t'as aidé.

82
Matt Davis