104

I'm trying to create a UDP server which can send messages to all the clients that send messages to it. The real situation is a little more complex, but it's simplest to imagine it as a chat server: everyone who has sent a message before receives all the messages that are sent by other clients.

All of this is done via UdpClient, in separate processes. (All network connections are within the same system though, so I don't think the unreliability of UDP is an issue here.)

The server code is a loop like this (full code later):

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

The client code is simple too - again, this is slightly abbreviated, but full code later:

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();

This all works fine until one of the clients closes its UdpClient (or the process exits).

The next time another client sends a message, the server tries to propagate that to the now-closed original client. The SendAsync call for that doesn't fail - but then when the server loops back to ReceiveAsync, that fails with an exception, and I haven't found a way to recover.

If I never send a message to the client that's disconnected, I never see the problem. Based on that I've also created a "fails immediately" repro which just sends to an endpoint assumed not to be listening, and then tries to receive. This fails with the same 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 [...]

Environment:

  • Windows 11, x64
  • .NET 6

Is this expected behaviour? Am I using UdpClient incorrectly on the server side? I'm fine with clients not receiving messages after they've closed their UdpClient (that's to be expected), and in the "full" application I'll tidy up my internal state to keep track of "active" clients (who have sent packets recently) but I don't want one client closing a UdpClient to bring down the whole server. Run the server in one console, and the client in another. Once the client has finished once, run it again (so that it tries to send to the now-defunct original client).

The default .NET 6 console app project template is fine for all projects.

Repro code

The simplest example comes first - but if you want to run a server and client, they're shown afterwards.

Deliberately broken server (fails immediately)

Based on the assumption that it really is the sending that's causing the problem, this is easy to reproduce in just a few lines:

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();

Server

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

(I previously had a loop actually receiving the packets sent by the server, but that doesn't seem to make any difference, so I've removed it for simplicity.)

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}");
Ofer Zelig
  • 17,068
  • 9
  • 59
  • 93
Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194

2 Answers2

144

As you may be aware, if a host receives a packet for a UDP port that is not currently bound, it may send back an ICMP "Port Unreachable" message. Whether or not it does this is dependent on the firewall, private/public settings, etc. On localhost, however, it will pretty much always send this packet back.

In your server code, you are calling SendAsync to old clients, which prompts these "port unreachable" messages.

Now, on Windows (and only on Windows), by default, a received ICMP Port Unreachable message will close the UDP socket that sent it; hence, the next time you try to receive on the socket, it will throw an exception because the socket has been closed by the OS.

Obviously, this causes a headache in the multi-client, single-server socket set-up you have here, but luckily there is a fix:

You need to utilise the not-often-required SIO_UDP_CONNRESET Winsock control code, which turns off this built-in behaviour of automatically closing the socket.

I don't believe this ioctl code is available in the dotnet IoControlCodes type, but you can define it yourself. If you put the following code at the top of your server repro, the error no longer gets raised.

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);

Note that this ioctl code is only supported on Windows (XP and later), not on Linux, since it is provided by the Winsock extensions. Of course, since the described behavior is only the default behavior on Windows, this omission is not a major loss. If you are attempting to create a cross-platform library, you should cordon this off as Windows-specific code.

Cody Gray - on strike
  • 239,200
  • 50
  • 490
  • 574
Alistair Evans
  • 36,057
  • 7
  • 42
  • 54
  • 22
    Works perfectly - and I'd *never* have worked this out on my own. Thank you so much! – Jon Skeet Nov 05 '22 at 11:56
  • 9
    Note that the IOControl call is not supported on linux, just in case you wanted to make a portable library. – jmik Nov 05 '22 at 12:54
  • 3
    Why the `unchecked` and the uint/int mixing? Is there something wrong with `IOC_IN | IOC_VENDOR | 12U`? – user2357112 Nov 06 '22 at 00:21
  • You get an error because the resulting uint value (2550136844) cannot be converted to an int, but that's what's required by the IOControl call. – Alistair Evans Nov 06 '22 at 10:08
  • You only get an error because you declared the first two constants as `uint` rather than `int` in the first place, you could just as equally do the unchecked cast on the first constant only. I believe you are also technically supposed to do `IOControl(SIO_UDP_CONNRESET, new byte[] { 0x00, 0x00, 0x00, 0x00, }, null)` but byte padding would probably allow it to work either way. – Charlieface Nov 06 '22 at 11:22
  • 4
    Very good explanation except that there is no such "built-in behaviour of automatically closing the socket". The socket is not closed by Windows. There are many causes of a SocketException, not only a closed socket. If .NET closes the socket when it sees this error, that's a .NET bug not a Windows one. – Ben Voigt Nov 07 '22 at 15:44
  • A friend of mine just showed me this post as I am working on UDP and the question and the answer is just worth gold! Thank you both for this information. – Stephan Møller Jan 19 '23 at 21:50
2

As a result of other peoples work inhere, I made a little udpSocket creation code, that will handle the if windows/if not logic incapsulated:

using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;

public static class UdpSocketUtils
{
    public static Socket CreateUdpSocket(IPEndPoint localEndpoint)
    {
        var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
        socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.ReuseAddress, true);
        socket.Bind(localEndpoint);

        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
        {
            // Due to this issue: https://stackoverflow.com/questions/74327225/why-does-sending-via-a-udpclient-cause-subsequent-receiving-to-fail
            // .. the following needs to be done on windows
            const uint IOC_IN = 0x80000000U;
            const uint IOC_VENDOR = 0x18000000U;
            const int SIO_UDP_CONNRESET = unchecked((int)(IOC_IN | IOC_VENDOR | 12));
            socket.IOControl(SIO_UDP_CONNRESET, new byte[] { 0x00 }, null);
        }
        return socket;
    }
}
Stephan Møller
  • 1,247
  • 19
  • 39