Un client TCP c # peut-il recevoir et envoyer de manière continue / consécutive sans sumil?

C’est un peu une question de base sur les bases du TCP, mais en même temps, je n’ai pas encore trouvé de réponse convaincante ailleurs et je pense bien comprendre les bases du TCP. Je ne suis pas sûr si la combinaison de questions (ou la seule question et tant 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 access à son code, donc pas de WCF). Comment puis-je me connecter, envoyer et recevoir au besoin lorsque de nouvelles informations entrent ou sortent, et finalement me déconnecter. En utilisant le code MSDN suivant comme exemple où ils répertorient les méthodes asynchrones “Envoyer” et “Recevoir” (ou simplement TcpClient), et en ignorant la connexion et la déconnexion de manière sortingviale, comment puis-je vérifier en permanence les nouveaux paquets reçus et à la même heure envoyer en cas de besoin?

J’ai initialement utilisé TCPClient et GetStream (), et le code msdn semble toujours nécessiter la boucle et le sumil décrits dans un bit (compteur intuitivement), où je lance la méthode receive dans une boucle dans un thread séparé avec un sumil (10) millisecondes , et Envoyez le fil principal (ou le troisième) au besoin. 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 queue.

Est-ce vraiment la meilleure solution? Ne devrait-il pas y avoir un événement équivalent à DataAvailable (ou quelque chose qui me manque dans le code msdn) nous permettant 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 n’en soit averti avant le prochain envoi bâclé. Pour clarifier les choses, le client est obligé d’envoyer des informations persistantes (et recevoir ne suffit pas, il suffit d’envoyer) pour déterminer si le socket est toujours actif. Et la fréquence du Keepalive détermine combien de temps je saurai que ce lien est en panne. Est-ce exact? J’ai seulement essayé Poll, socket.connected, etc. pour découvrir pourquoi chacun ne les aidait pas.

Enfin, pour confirmer (je ne crois pas que ce soit bon mais pour en être sûr), 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 s’il est envoyé et reçu simultanément? Si en même temps je reçois, j’essaie et j’envoie un échec, écrasons l’autre ou tout autre comportement indésirable de ce type?

Il n’y a pas forcément de mal à regrouper les questions ensemble, mais cela rend la réponse à la question plus ardue … 🙂

L’article MSDN que vous avez lié indique comment effectuer une communication TCP simple, c’est-à-dire envoyer et recevoir. Vous remarquerez également qu’il utilise directement la classe Socket plupart des gens, y compris moi-même, suggérant d’utiliser TcpClient classe TcpClient . Vous pouvez toujours obtenir le Socket sous-jacent via la propriété Client si vous devez configurer un certain socket par exemple (par exemple, SetSocketOption() ).

L’autre aspect de l’exemple à noter est qu’il utilise des threads pour exécuter les delegates AsyncCallback pour BeginSend() et BeginReceive() , mais qu’il s’agit essentiellement d’un exemple à thread unique en raison de la façon dont les objects ManualResetEvent sont utilisés. Pour un échange répété entre un client et un serveur, ce n’est pas ce que vous voulez.

Bon, vous voulez donc utiliser TcpClient . La connexion au serveur (par exemple, TcpListener ) doit ê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’object NetworkStream à utiliser pour la lecture et l’écriture. Utilisez les opérations Read() / Write() pour bloquer les E / S et les opérations BeginRead() / BeginWrite() pour les E / S non bloquantes. Notez que BeginRead() et BeginWrite() utilisent le même mécanisme AsyncCallback employé par les BeginReceive() et BeginSend() de la classe Socket .

Un des éléments clés à noter à ce stade est ce petit texte de synthèse 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 existe un seul thread pour les opérations d’écriture et un seul thread pour les opérations de lecture , il n’y aura pas d’interférence croisée entre les threads de lecture et d’écriture et aucune synchronisation n’est requirejse.

En bref, comme vous envisagez de lire et d’écrire à partir de la même instance de TcpClient , vous aurez besoin de deux threads pour le faire. L’utilisation de threads distincts garantira qu’aucune donnée n’est perdue lors de la réception de données au même moment où quelqu’un essaie d’envoyer. Dans mes projets, j’ai abordé cette question en créant un object de niveau supérieur, par exemple Client , qui TcpClient et son TcpClient NetworkStream sous-jacent. Cette classe crée et gère également deux objects Thread , en leur transmettant l’object NetworkStream lors de la construction. Le premier thread est le thread de l’ Sender . Toute personne souhaitant envoyer des données le fait via une SendData() publique SendData() sur le Client , qui achemine les données vers l’ Sender vue de leur 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 à quelque chose comme ça:

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 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 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éfinissent différents aspects de la même classe de Client . J’utilise l’astuce de Visual Studio décrite ici pour imbriquer les fichiers Receiver et Sender respectifs dans 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 bien, mais vous pouvez y parvenir efficacement en utilisant la méthode Read() en combinaison avec un ReadTimeout infini. Cela n’aura aucun impact négatif sur le rest de votre application (par exemple, l’interface utilisateur) car elle s’exécute dans son propre thread. Toutefois, cela complique l’arrêt du thread (par exemple, lorsque l’application ferme), vous voudrez donc probablement utiliser quelque chose de plus raisonnable, par exemple 10 millisecondes. Mais ensuite, vous êtes de retour aux sondages, ce que nous essayons d’éviter en premier lieu. Voici comment je le fais, avec 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’ya malheureusement pas de solution de contourner le problème consistant à savoir quand l’autre partie est sortie de 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 émetteur et récepteur, j’ai ajouté un minuscule message KeepAlive (8 octets) à mon protocole. Ceci 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 avoir abordé toutes les facettes que vous avez abordées. J’espère que ça t’as aidé.