2009-12-13 10 views
2

Un de mes amis est venu me voir avec un problème: lorsque vous utilisez la classe NetworkStream à l'extrémité du serveur de la connexion, si le client se déconnecte, NetworkStream ne parvient pas à le détecter.Détection déconnexion TCP client en utilisant la classe NetworkStream

Minimaliste, son code C# ressemblait à ceci:

List<TcpClient> connections = new List<TcpClient>(); 
TcpListener listener = new TcpListener(7777); 
listener.Start(); 

while(true) 
{ 
    if (listener.Pending()) 
    { 
     connections.Add(listener.AcceptTcpClient()); 
    } 
    TcpClient deadClient = null; 
    foreach (TcpClient client in connections) 
    { 
     if (!client.Connected) 
     { 
      deadClient = client; 
      break; 
     } 
     NetworkStream ns = client.GetStream(); 
     if (ns.DataAvailable) 
     { 
      BinaryFormatter bf = new BinaryFormatter(); 
      object o = bf.Deserialize(ns); 
      ReceiveMyObject(o); 
     } 
    } 
    if (deadClient != null) 
    { 
     deadClient.Close(); 
     connections.Remove(deadClient); 
    } 
    Thread.Sleep(0); 
} 

Le code fonctionne, en ce que les clients peuvent se connecter avec succès et le serveur peut lire les données qui lui sont envoyées. Toutefois, si le client distant appelle tcpClient.Close(), le serveur ne détecte pas la déconnexion - client.Connected reste vrai et ns.DataAvailable est faux.

Une recherche de Stack Overflow a fourni et réponse - puisque Socket.Receive n'est pas appelé, le socket ne détecte pas la déconnexion. C'est suffisant. Nous pouvons travailler autour que:

foreach (TcpClient client in connections) 
{ 
    client.ReceiveTimeout = 0; 
    if (client.Client.Poll(0, SelectMode.SelectRead)) 
    { 
     int bytesPeeked = 0; 
     byte[] buffer = new byte[1]; 
     bytesPeeked = client.Client.Receive(buffer, SocketFlags.Peek); 
     if (bytesPeeked == 0) 
     { 
      deadClient = client; 
      break; 
     } 
     else 
     { 
      NetworkStream ns = client.GetStream(); 
      if (ns.DataAvailable) 
      { 
       BinaryFormatter bf = new BinaryFormatter(); 
       object o = bf.Deserialize(ns); 
       ReceiveMyObject(o); 
      } 
     } 
    } 
} 

(j'ai laissé de côté le code de gestion des exceptions pour la brièveté.)

Ce code fonctionne, cependant, je ne dirais pas cette solution « élégante ». L'autre solution élégante au problème que je connais est de générer un thread par TcpClient, et permettre à l'appel BinaryFormatter.Deserialize (née NetworkStream.Read) de bloquer, ce qui permettrait de détecter la déconnexion correctement. Cependant, cela entraîne la création et la maintenance d'un thread par client.

Je sens que je manque une réponse secrète, impressionnante qui conserverait la clarté du code d'origine, mais il faut éviter l'utilisation de threads supplémentaires pour effectuer les lectures asynchrones. Bien que, peut-être, la classe NetworkStream n'a jamais été conçue pour ce genre d'utilisation. Quelqu'un peut-il nous éclairer?

Mise à jour: voudrais préciser que je suis curieux de voir si le framework .NET a une solution qui couvre cette utilisation de NetworkStream (à savoir l'interrogation et évitant le blocage) - évidemment il peut être fait; le NetworkStream pourrait facilement être enveloppé dans une classe de support qui fournit la fonctionnalité. Il semblait juste étrange que le framework nécessite essentiellement d'utiliser des threads pour éviter de bloquer sur NetworkStream.Read, ou de jeter un coup d'œil sur le socket lui-même pour vérifier les déconnexions - presque comme si c'était un bug. Ou un manque potentiel d'une fonctionnalité. ;)

Répondre

1

Le serveur s'attend-il à recevoir plusieurs objets sur la même connexion? SI donc je ne vois pas comment ce code fonctionnera, car il n'y a pas de délimiteur envoyé qui signifie où le premier objet commence et l'objet suivant se termine.

Si un seul objet est envoyé et la connexion fermée après, le code d'origine fonctionnerait.

Une opération réseau doit être lancée pour déterminer si la connexion est toujours active ou non. Ce que je ferais, c'est qu'au lieu de désérialiser directement à partir du flux réseau, je préférerais tamponner dans un MemoryStream. Cela me permettrait de détecter quand la connexion a été perdue. J'utiliserais également le cadrage de message pour délimiter plusieurs réponses sur le flux.

 MemoryStream ms = new MemoryStream(); 

     NetworkStream ns = client.GetStream(); 
     BinaryReader br = new BinaryReader(ns); 

     // message framing. First, read the #bytes to expect. 
     int objectSize = br.ReadInt32(); 

     if (objectSize == 0) 
       break; // client disconnected 

     byte [] buffer = new byte[objectSize]; 
     int index = 0; 

     int read = ns.Read(buffer, index, Math.Min(objectSize, 1024); 
     while (read > 0) 
     { 
      objectSize -= read; 
      index += read; 
      read = ns.Read(buffer, index, Math.Min(objectSize, 1024); 
     } 

     if (objectSize > 0) 
     { 
      // client aborted connection in the middle of stream; 
      break; 
     } 
     else 
     { 
      BinaryFormatter bf = new BinaryFormatter(); 
      using(MemoryStream ms = new MemoryStream(buffer)) 
      { 
       object o = bf.Deserialize(ns); 
       ReceiveMyObject(o); 
      } 
     } 
+1

Cela permet la détection de déconnexion en interprétant le résultat de la lecture des appels. Cependant, ma curiosité provient de l'absence apparente du cadre d'une «autre» façon de le faire, surtout compte tenu de l'élégance qu'elle confère à l'implémentation originale. En outre, le code d'origine délimitait correctement les objets, de sorte que plusieurs messages pouvaient être envoyés. Je suppose que cela est dû à la façon dont BinaryFormatter sérialise les objets - il sait par nature quand il est "fait". (Cependant, le code que vous avez présenté inclut le besoin d'un délimiteur.) –

+0

Pouvez-vous me dire comment voudriez-vous que cela soit implémenté dans le framework? Le Framework est juste un wrapper au-dessus des sockets, ce qui est un mécanisme de transport. D'autres sémantiques doivent être implémentées par l'application. En outre, BinaryFormatter prend un tampon en entrée pour la désérialisation, vous devez donc lui donner le tampon exact nécessaire pour récupérer l'objet. Sinon, il ne fonctionnera pas comme prévu. – feroze

+0

Vous avez raison - la désérialisation nécessite un tampon exact ou elle va mourir douloureusement, et ce n'est pas la responsabilité de BinaryFormatter de s'assurer que le flux est complet; dans ce cas, ce n'est pas non plus NetworkStream. Je pense que j'y ai peut-être mal pensé et que ce n'est pas la responsabilité du Cadre, mais la mienne. ;) –

1

Oui, mais que faire si vous perdez une connexion avant d'obtenir la taille? à-dire juste avant la ligne suivante:

// message framing. First, read the #bytes to expect. 

int objectSize = br.ReadInt32(); 

ReadInt32() va bloquer le fil indéfiniment.

Questions connexes