web-dev-qa-db-fra.com

Manière correcte d'arrêter TcpListener

J'utilise actuellement TcpListener pour adresser les connexions entrantes. Chacune d'entre elles se voit attribuer un thread permettant de gérer la communication, puis d'arrêter cette connexion unique. Le code ressemble à ceci:

TcpListener listener = new TcpListener(IPAddress.Any, Port);
System.Console.WriteLine("Server Initialized, listening for incoming connections");
listener.Start();
while (listen)
{
     // Step 0: Client connection
     TcpClient client = listener.AcceptTcpClient();
     Thread clientThread = new Thread(new ParameterizedThreadStart(HandleConnection));
     clientThread.Start(client.GetStream());
     client.Close();
}

La variable listen est un booléen qui est un champ de la classe. Maintenant, quand le programme s'arrête, je veux qu'il arrête d'écouter les clients. Régler listen sur false l'empêchera de prendre plus de connexions, mais comme AcceptTcpClient est un appel bloquant, il prendra au minimum le client suivant et se terminera. Y a-t-il un moyen de le forcer simplement à sortir et à s'arrêter, juste à ce moment-là? Quel est l'effet de l'appel de listener.Stop () lorsque l'autre appel bloquant est en cours d'exécution?

56
Christian P.

Il y a 2 suggestions que je ferais compte tenu du code et de ce que je présume être votre conception. Cependant, je voudrais tout d’abord préciser que vous devez vraiment utiliser des rappels d’E/S non bloquants lorsque vous travaillez avec des E/S telles que des réseaux ou des systèmes de fichiers. C'est beaucoup LOIN plus efficace et votre application fonctionnera beaucoup mieux, bien qu'elle soit plus difficile à programmer. Je couvrirai brièvement une modification de conception suggérée à la fin.

  1. Utilisez Using () {} pour TcpClient
  2. Thread.Abort ()
  3. TcpListener.Pending ()
  4. Réécriture asynchrone

Utilisez Using () {} pour TcpClient

*** Notez que vous devez vraiment inclure votre appel TcpClient dans un bloc using () {} pour vous assurer que les méthodes TcpClient.Dispose () ou TcpClient.Close () sont appelées même en cas d'exception. Alternativement, vous pouvez mettre ceci dans le bloc finally d'un bloc try {} finally {}.

Thread.Abort ()

Je vois que vous pouvez faire 2 choses. 1 est que si vous avez démarré ce thread TcpListener depuis un autre, vous pouvez simplement appeler la méthode d’instance Thread.Abort sur le thread, ce qui entraînera la génération d’une exception threadabortexception dans l’appel bloquant et la progression dans la pile.

TcpListener.Pending ()

La deuxième solution à faible coût consisterait à utiliser la méthode listener.Pending () pour implémenter un modèle d'interrogation. Vous utiliseriez alors un Thread.Sleep pour "attendre" avant de voir si une nouvelle connexion est en attente. Une fois que vous avez une connexion en attente, appelez AcceptTcpClient pour libérer la connexion en attente. Le code ressemblerait à quelque chose comme ça.

while (listen){
     // Step 0: Client connection
     if (!listener.Pending())
     {
          Thread.Sleep(500); // choose a number (in milliseconds) that makes sense
          continue; // skip to next iteration of loop
     }

     TcpClient client = listener.AcceptTcpClient();
     Thread clientThread = new Thread(new ParameterizedThreadStart(HandleConnection));
     clientThread.Start(client.GetStream());
     client.Close();
}

Réécriture Asynchrone

Enfin, je vous recommanderais de passer réellement à une méthodologie non bloquante pour votre application. Sous les couvertures, la structure utilisera les ports d’achèvement d’E/S et d’E/S superposés pour implémenter des E/S non bloquantes à partir de vos appels asynchrones. Ce n'est pas terriblement difficile non plus, cela nécessite simplement de penser votre code un peu différemment.

Fondamentalement, vous commenceriez votre code avec la méthode BeginAcceptTcpClient et gardiez une trace du résultat IAsyncResult que vous avez renvoyé. Vous pointez sur une méthode qui est chargée d’obtenir TcpClient et de le transmettre NOT à un nouveau thread, mais à un thread du ThreadPool.QueueUserWorkerItem afin que vous ne fassiez pas tourner nouveau thread pour chaque demande client (notez que vous devrez peut-être utiliser votre propre pool de threads si vous avez des requêtes particulièrement longues, car le pool de threads est partagé et si vous monopolisez tous les threads, d'autres parties de votre application implémentées par le système risquent de manquer de ressources) . Une fois que la méthode d’écoute a lancé votre nouveau TcpClient à sa propre demande ThreadPool, elle appelle à nouveau BeginAcceptTcpClient et renvoie le délégué à lui-même.

Effectivement, vous ne faites que diviser votre méthode actuelle en 3 méthodes différentes qui seront ensuite appelées par les différentes parties. 1. pour bootstrap tout, 2. pour être la cible à appeler EndAcceptTcpClient, démarrez le TcpClient vers son propre thread, puis appelez-le à nouveau, 3. pour traiter la demande du client et le fermer à la fin.

58
Peter Oehlert

listener.Server.Close() d'un autre thread interrompt l'appel bloquant.

A blocking operation was interrupted by a call to WSACancelBlockingCall
47
zproxy

Les sockets offrent de puissantes fonctionnalités asynchrones. Jetez un oeil à Utilisation d'un socket de serveur asynchrone

Voici quelques notes sur le code.

L'utilisation de threads créés manuellement dans ce cas peut constituer une surcharge. 

Le code ci-dessous est soumis à des conditions de concurrence - TcpClient.Close () ferme le flux réseau que vous obtenez via TcpClient.GetStream (). Pensez à fermer le client là où vous pouvez définitivement dire que ce n’est plus nécessaire.

 clientThread.Start(client.GetStream());
 client.Close();

TcpClient.Stop () ferme le socket sous-jacent. TcpCliet.AcceptTcpClient () utilise la méthode Socket.Accept () sur le socket sous-jacent qui lève SocketException une fois qu'il est fermé. Vous pouvez l'appeler d'un autre thread.

Quoi qu'il en soit, je recommande les sockets asynchrones.

3
Dzmitry Huba

Voir ma réponse ici https://stackoverflow.com/a/17816763/2548170TcpListener.Pending() n'est pas une bonne solution

2
Andriy Vandych

Ne pas utiliser une boucle. Au lieu de cela, appelez BeginAcceptTcpClient () sans boucle. Dans le rappel, lancez simplement un autre appel à BeginAcceptTcpClient (), si votre indicateur d'écoute est toujours défini.

Pour arrêter l’auditeur, puisque vous n’avez pas bloqué, votre code peut simplement appeler Close () dessus.

2
Mike Scott

Pour ajouter encore plus de raisons d'utiliser l'approche asynchrone, je suis à peu près sûr que Thread.Abort ne fonctionnera pas car l'appel est bloqué dans la pile TCP au niveau du système d'exploitation. 

De plus ... si vous appelez BeginAcceptTCPClient dans le rappel pour écouter toutes les connexions sauf la première, veillez à ce que le thread qui a exécuté BeginAccept initial ne se termine pas, sinon l'auditeur sera automatiquement disposé par le framework. Je suppose que c'est une caractéristique, mais dans la pratique, c'est très ennuyant. Ce n'est généralement pas un problème dans les applications de bureau, mais sur le Web, vous pouvez utiliser le pool de threads, car ces threads ne se terminent jamais vraiment.

1
Eric Nicholson

Déjà mentionné ci-dessus, utilisez BeginAcceptTcpClient à la place, il est beaucoup plus facile de gérer de manière asynchrone.

Voici un exemple de code: 

        ServerSocket = new TcpListener(endpoint);
        try
        {
            ServerSocket.Start();
            ServerSocket.BeginAcceptTcpClient(OnClientConnect, null);
            ServerStarted = true;

            Console.WriteLine("Server has successfully started.");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Server was unable to start : {ex.Message}");
            return false;
        }
0
Peter Suwara