web-dev-qa-db-fra.com

Quelle est la bonne façon de lire à partir de NetworkStream dans .NET

J'ai eu du mal avec cela et je ne peux pas trouver une raison pour laquelle mon code ne parvient pas à lire correctement à partir d'un serveur TCP que j'ai également écrit. J'utilise le TcpClient classe et sa méthode GetStream() mais quelque chose ne fonctionne pas comme prévu. Soit l'opération se bloque indéfiniment (la dernière opération de lecture ne se termine pas comme prévu), soit les données sont recadrées (pour une raison quelconque, une lecture L'opération renvoie 0 et quitte la boucle, peut-être que le serveur ne répond pas assez rapidement. Il s'agit de trois tentatives d'implémentation de cette fonction:

// this will break from the loop without getting the entire 4804 bytes from the server 
string SendCmd(string cmd, string ip, int port)
{
    var client = new TcpClient(ip, port);
    var data = Encoding.GetEncoding(1252).GetBytes(cmd);
    var stm = client.GetStream();
    stm.Write(data, 0, data.Length);
    byte[] resp = new byte[2048];
    var memStream = new MemoryStream();
    int bytes = stm.Read(resp, 0, resp.Length);
    while (bytes > 0)
    {
        memStream.Write(resp, 0, bytes);
        bytes = 0;
        if (stm.DataAvailable)
            bytes = stm.Read(resp, 0, resp.Length);
    }
    return Encoding.GetEncoding(1252).GetString(memStream.ToArray());
}

// this will block forever. It reads everything but freezes when data is exhausted
string SendCmd(string cmd, string ip, int port)
{
    var client = new TcpClient(ip, port);
    var data = Encoding.GetEncoding(1252).GetBytes(cmd);
    var stm = client.GetStream();
    stm.Write(data, 0, data.Length);
    byte[] resp = new byte[2048];
    var memStream = new MemoryStream();
    int bytes = stm.Read(resp, 0, resp.Length);
    while (bytes > 0)
    {
        memStream.Write(resp, 0, bytes);
        bytes = stm.Read(resp, 0, resp.Length);
    }
    return Encoding.GetEncoding(1252).GetString(memStream.ToArray());
}

// inserting a sleep inside the loop will make everything work perfectly
string SendCmd(string cmd, string ip, int port)
{
    var client = new TcpClient(ip, port);
    var data = Encoding.GetEncoding(1252).GetBytes(cmd);
    var stm = client.GetStream();
    stm.Write(data, 0, data.Length);
    byte[] resp = new byte[2048];
    var memStream = new MemoryStream();
    int bytes = stm.Read(resp, 0, resp.Length);
    while (bytes > 0)
    {
        memStream.Write(resp, 0, bytes);
        Thread.Sleep(20);
        bytes = 0;
        if (stm.DataAvailable)
            bytes = stm.Read(resp, 0, resp.Length);
    }
    return Encoding.GetEncoding(1252).GetString(memStream.ToArray());
}

Le dernier "fonctionne", mais il semble certainement moche de mettre un sommeil codé en dur dans la boucle étant donné que les sockets prennent déjà en charge les délais de lecture! Dois-je configurer une ou plusieurs propriétés sur le TcpClient du NetworkStream? Le problème réside-t-il sur le serveur? Le serveur ne ferme pas les connexions, c'est au client de le faire. Ce qui précède fonctionne également dans le contexte du thread d'interface utilisateur (programme de test), peut-être que cela a quelque chose à voir avec cela ...

Est-ce que quelqu'un sait comment utiliser correctement NetworkStream.Read pour lire les données jusqu'à ce qu'il n'y ait plus de données disponibles? Je suppose que ce que je souhaite, c'est quelque chose comme les anciennes propriétés de délai d'expiration Win32 winsock ... ReadTimeout, etc. Il essaie de lire jusqu'à ce que le délai soit atteint, puis retourne 0 ... Mais il semble parfois pour retourner 0 lorsque les données doivent être disponibles (ou en cours de route. Read peut-il retourner 0 s'il est disponible?) et il se bloque indéfiniment lors de la dernière lecture lorsque les données ne sont pas disponibles ...

Oui, je suis perdu!

22
Loudenvier

La définition de la propriété de socket sous-jacent ReceiveTimeout a fait l'affaire. Vous pouvez y accéder comme ceci: yourTcpClient.Client.ReceiveTimeout. Vous pouvez lire le docs pour plus d'informations.

Désormais, le code ne "dormira" que le temps nécessaire pour que certaines données arrivent dans le socket, ou il déclenchera une exception si aucune donnée n'arrive, au début d'une opération de lecture, pendant plus de 20 ms. Je peux modifier ce délai si nécessaire. Maintenant, je ne paie pas le prix de 20 ms à chaque itération, je ne le paie qu'à la dernière opération de lecture. Étant donné que la longueur du contenu du message dans les premiers octets est lue sur le serveur, je peux l'utiliser pour l'ajuster encore plus et ne pas essayer de lire si toutes les données attendues ont déjà été reçues.

Je trouve l'utilisation de ReceiveTimeout beaucoup plus facile que l'implémentation d'une lecture asynchrone ... Voici le code de travail:

string SendCmd(string cmd, string ip, int port)
{
  var client = new TcpClient(ip, port);
  var data = Encoding.GetEncoding(1252).GetBytes(cmd);
  var stm = client.GetStream();
  stm.Write(data, 0, data.Length);
  byte[] resp = new byte[2048];
  var memStream = new MemoryStream();
  var bytes = 0;
  client.Client.ReceiveTimeout = 20;
  do
  {
      try
      {
          bytes = stm.Read(resp, 0, resp.Length);
          memStream.Write(resp, 0, bytes);
      }
      catch (IOException ex)
      {
          // if the ReceiveTimeout is reached an IOException will be raised...
          // with an InnerException of type SocketException and ErrorCode 10060
          var socketExept = ex.InnerException as SocketException;
          if (socketExept == null || socketExept.ErrorCode != 10060)
              // if it's not the "expected" exception, let's not hide the error
              throw ex;
          // if it is the receive timeout, then reading ended
          bytes = 0;
      }
  } while (bytes > 0);
  return Encoding.GetEncoding(1252).GetString(memStream.ToArray());
}
8
Loudenvier

Le code réseau est notoirement difficile à écrire, à tester et à déboguer.

Vous avez souvent beaucoup de choses à considérer telles que:

  • quel "endian" utiliserez-vous pour les données échangées (Intel x86/x64 est basé sur little-endian) - les systèmes qui utilisent big-endian peuvent toujours lire des données en petit-endian (et vice versa), mais ils doivent réorganiser les données. Lorsque vous documentez votre "protocole", indiquez clairement lequel vous utilisez.

  • y a-t-il des "paramètres" qui ont été définis sur les sockets qui peuvent affecter le comportement du "flux" (par exemple SO_LINGER) - vous devrez peut-être activer ou désactiver certains si votre code est très sensible

  • comment la congestion dans le monde réel qui provoque des retards dans le flux affecte-t-elle votre logique de lecture/écriture

Si le "message" échangé entre un client et un serveur (dans les deux sens) peut varier en taille, alors vous devez souvent utiliser une stratégie pour que ce "message" soit échangé de manière fiable (aka Protocole).

Voici plusieurs façons de gérer l'échange:

  • avoir la taille du message encodée dans un en-tête qui précède les données - cela pourrait simplement être un "nombre" dans les premiers 2/4/8 octets envoyés (en fonction de la taille maximale de votre message), ou pourrait être un "en-tête" plus exotique

  • utiliser un marqueur spécial "fin de message" (sentinelle), avec les données réelles encodées/échappées s'il y a possibilité de confusion de données réelles avec une "fin de marqueur"

  • utiliser un délai d'attente .... c'est-à-dire. une certaine période de réception d'aucun octet signifie qu'il n'y a plus de données pour le message - cependant, cela peut être sujet à des erreurs avec de courts délais d'attente, qui peuvent facilement être atteints sur des flux encombrés.

  • avoir un canal "commande" et "données" sur des "connexions" séparées .... c'est l'approche que le protocole FTP utilise (l'avantage est une séparation claire des données des commandes ... au détriment d'une 2ème connexion)

Chaque approche a ses avantages et ses inconvénients pour la "justesse".

Le code ci-dessous utilise la méthode "timeout", car celle-ci semble être celle que vous souhaitez.

Voir http://msdn.Microsoft.com/en-us/library/bk6w7hs8.aspx . Vous pouvez accéder au NetworkStream sur le TCPClient afin de pouvoir changer le ReadTimeout.

string SendCmd(string cmd, string ip, int port)
{
  var client = new TcpClient(ip, port);
  var data = Encoding.GetEncoding(1252).GetBytes(cmd);
  var stm = client.GetStream();
  // Set a 250 millisecond timeout for reading (instead of Infinite the default)
  stm.ReadTimeout = 250;
  stm.Write(data, 0, data.Length);
  byte[] resp = new byte[2048];
  var memStream = new MemoryStream();
  int bytesread = stm.Read(resp, 0, resp.Length);
  while (bytesread > 0)
  {
      memStream.Write(resp, 0, bytesread);
      bytesread = stm.Read(resp, 0, resp.Length);
  }
  return Encoding.GetEncoding(1252).GetString(memStream.ToArray());
}

En tant que note de bas de page pour d'autres variantes de ce code de réseau d'écriture ... lorsque vous effectuez un Read où vous voulez éviter un "bloc", vous pouvez vérifier le drapeau DataAvailable puis lire UNIQUEMENT ce qui est dans le tampon vérifiant la propriété .Length par exemple stm.Read(resp, 0, stm.Length);

20
Colin Smith

Selon votre condition, Thread.Sleep est parfaitement adapté à l'utilisation, car vous ne savez pas quand les données seront disponibles, vous devrez peut-être attendre qu'elles soient disponibles. J'ai légèrement changé la logique de votre fonction, cela pourrait vous aider un peu plus.

string SendCmd(string cmd, string ip, int port)
{
    var client = new TcpClient(ip, port);
    var data = Encoding.GetEncoding(1252).GetBytes(cmd);
    var stm = client.GetStream();
    stm.Write(data, 0, data.Length);
    byte[] resp = new byte[2048];
    var memStream = new MemoryStream();

    int bytes = 0;

    do
    {
        bytes = 0;
        while (!stm.DataAvailable)
            Thread.Sleep(20); // some delay
        bytes = stm.Read(resp, 0, resp.Length);
        memStream.Write(resp, 0, bytes);
    } 
    while (bytes > 0);

    return Encoding.GetEncoding(1252).GetString(memStream.ToArray());
}

J'espère que cela t'aides!

0
Furqan Safdar