102 votes

Pourquoi l'envoi via un UdpClient fait-il échouer la réception ultérieure ?

J'essaie de créer un serveur UDP qui peut envoyer des messages à tous les clients qui lui en envoient. La situation réelle est un peu plus complexe, mais le plus simple est de l'imaginer comme un serveur de chat : tous ceux qui ont envoyé un message auparavant reçoivent tous les messages envoyés par d'autres clients.

Tout cela se fait par l'intermédiaire de UdpClient dans des processus distincts. (Toutes les connexions réseau se font au sein du même système, donc je n'ai pas penser le manque de fiabilité de l'UDP est un problème ici).

Le code du serveur est une boucle comme celle-ci (le code complet est présenté plus loin) :

var udpClient = new UdpClient(9001);
while (true)
{
    var packet = await udpClient.ReceiveAsync();
    // Send to clients who've previously sent messages here
}

Le code du client est également simple - encore une fois, il est légèrement abrégé, mais le code complet sera présenté plus tard :

var client = new UdpClient();
client.Connect("127.0.0.1", 9001);
await client.SendAsync(Encoding.UTF8.GetBytes("Hello"));
await Task.Delay(TimeSpan.FromSeconds(15));
await client.SendAsync(Encoding.UTF8.GetBytes("Goodbye"));
client.Close();

Tout se passe bien jusqu'à ce que l'un des clients ferme son UdpClient (ou que le processus se termine).

La prochaine fois qu'un autre client enverra un message, le serveur essaiera de le propager au client d'origine, désormais fermé. Le serveur SendAsync n'échoue pas - mais ensuite, lorsque le serveur revient à l'appel de ReceiveAsync , que échoue avec une exception, et je n'ai pas trouvé le moyen de la récupérer.

Si je n'envoie jamais de message au client déconnecté, je ne vois jamais le problème. Sur cette base, j'ai également créé un repro "échoue immédiatement" qui envoie simplement un message à un point de terminaison supposé ne pas être à l'écoute, puis tente de le recevoir. Cela échoue avec la même exception.

Exception :

System.Net.Sockets.SocketException (10054): An existing connection was forcibly closed by the remote host.
   at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.CreateException(SocketError error, Boolean forAsyncThrow)
   at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.ReceiveFromAsync(Socket socket, CancellationToken cancellationToken)
   at System.Net.Sockets.Socket.ReceiveFromAsync(Memory`1 buffer, SocketFlags socketFlags, EndPoint remoteEndPoint, CancellationToken cancellationToken)
   at System.Net.Sockets.Socket.ReceiveFromAsync(ArraySegment`1 buffer, SocketFlags socketFlags, EndPoint remoteEndPoint)
   at System.Net.Sockets.UdpClient.ReceiveAsync()
   at Program.<Main>$(String[] args) in [...]

L'environnement :

  • Windows 11, x64
  • NET 6

S'agit-il d'un comportement attendu ? Est-ce que j'utilise UdpClient incorrectement du côté du serveur ? Je veux bien que les clients ne reçoivent pas de messages une fois qu'ils ont fermé leur UdpClient (c'est normal), et dans l'application "complète", je mettrai de l'ordre dans mon état interne pour garder une trace des clients "actifs" (qui ont envoyé des paquets récemment), mais je ne veux pas qu'un client ferme un fichier UdpClient pour faire tomber tout le serveur. Exécutez le serveur dans une console et le client dans une autre. Une fois que le client a terminé, lancez-le à nouveau (de manière à ce qu'il essaie d'envoyer des données au client original, qui n'existe plus).

Le modèle de projet par défaut de l'application console .NET 6 convient à tous les projets.

Code de reproduction

L'exemple le plus simple est présenté en premier, mais si vous souhaitez exécuter un serveur et un client, ils sont affichés ensuite.

Serveur délibérément cassé (échec immédiat)

En partant du principe que c'est bien l'envoi qui est à l'origine du problème, il est facile de le reproduire en quelques lignes :

using System.Net;
using System.Net.Sockets;

var badEndpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 12346);
var udpClient = new UdpClient(12345);
await udpClient.SendAsync(new byte[10], badEndpoint);
await udpClient.ReceiveAsync();

Serveur

using System.Net;
using System.Net.Sockets;
using System.Text;

var udpClient = new UdpClient(9001);
var endpoints = new HashSet<IPEndPoint>();
try
{
    while (true)
    {
        Log($"{DateTime.UtcNow:HH:mm:ss.fff}: Waiting to receive packet");
        var packet = await udpClient.ReceiveAsync();
        var buffer = packet.Buffer;
        var clientEndpoint = packet.RemoteEndPoint;
        endpoints.Add(clientEndpoint);
        Log($"Received {buffer.Length} bytes from {clientEndpoint}: {Encoding.UTF8.GetString(buffer)}");
        foreach (var otherEndpoint in endpoints)
        {
            if (!otherEndpoint.Equals(clientEndpoint))
            {
                await udpClient.SendAsync(buffer, otherEndpoint);
            }
        }
    }
}
catch (Exception e)
{
    Log($"Failed: {e}");
}

void Log(string message) =>
    Console.WriteLine($"{DateTime.UtcNow:HH:mm:ss.fff}: {message}");

Client

(J'avais auparavant une boucle qui recevait les paquets envoyés par le serveur, mais cela ne semble pas faire de différence, et je l'ai donc supprimée pour des raisons de simplicité).

using System.Net.Sockets;
using System.Text;

Guid clientId = Guid.NewGuid();
var client = new UdpClient();
Log("Connecting UdpClient");
client.Connect("127.0.0.1", 9001);
await client.SendAsync(Encoding.UTF8.GetBytes($"Hello from {clientId}"));
await Task.Delay(TimeSpan.FromSeconds(15));
await client.SendAsync(Encoding.UTF8.GetBytes($"Goodbye from {clientId}"));
client.Close();
Log("UdpClient closed");

void Log(string message) =>
    Console.WriteLine($"{DateTime.UtcNow:HH:mm:ss.fff}: {message}");

141voto

Kazar Points 16014

Comme vous le savez peut-être, si un hôte reçoit un paquet pour un port UDP qui n'est pas actuellement lié, il peut renvoie un message ICMP "Port inaccessible". Le fait qu'il le fasse ou non dépend du pare-feu, des paramètres privés/publics, etc. Sur localhost, cependant, il renverra presque toujours ce paquet.

Dans votre code serveur, vous appelez SendAsync aux anciens clients, ce qui provoque ces messages "port inaccessible".

Maintenant, sous Windows (et uniquement sous Windows), par défaut Ainsi, la prochaine fois que vous tenterez de recevoir un message sur la socket, une exception sera levée car la socket a été fermée par le système d'exploitation.

Évidemment, cela cause un mal de tête dans la configuration de socket multi-client, mono-serveur que vous avez ici, mais heureusement il y a un correctif :

Vous devez utiliser l'outil peu utilisé qu'est le SIO_UDP_CONNRESET Code de contrôle Winsock, qui désactive ce comportement intégré de fermeture automatique de la socket.

Je ne crois pas que ce code ioctl soit disponible dans la base de données dotnet. IoControlCodes mais vous pouvez le définir vous-même. Si vous placez le code suivant en tête de votre repro de serveur, l'erreur ne sera plus levée.

const uint IOC_IN = 0x80000000U;
const uint IOC_VENDOR = 0x18000000U;

/// <summary>
/// Controls whether UDP PORT_UNREACHABLE messages are reported. 
/// </summary>
const int SIO_UDP_CONNRESET = unchecked((int)(IOC_IN | IOC_VENDOR | 12));

var udpClient = new UdpClient(9001);
udpClient.Client.IOControl(SIO_UDP_CONNRESET, new byte[] { 0x00 }, null);

Notez que ce code ioctl est o pris en charge sous Windows (XP et versions ultérieures), mais pas sous Linux, puisqu'il est fourni par les extensions Winsock. Bien entendu, comme le comportement décrit n'est que le comportement par défaut sous Windows, cette omission ne constitue pas une perte majeure. Si vous essayez de créer une bibliothèque multiplateforme, vous devriez la considérer comme du code spécifique à Windows.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X