为什么通过 UdpClient 发送会导致后续接收失败?

我试图创建一个 UDP 服务器,可以发送消息给所有客户端发送消息给它。实际情况稍微复杂一些,但是最简单的想象是把它想象成一个聊天服务器: 在此之前发送过消息的每个人都会收到其他客户机发送的所有消息。

所有这些都是通过 UdpClient在不同的进程中完成的。(所有的网络连接都在同一个系统中,所以我没有 好好想想,UDP 的不可靠性在这里是一个问题。)

服务器代码是这样一个循环(稍后是完整代码) :

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

客户机代码也很简单——再次声明,这段代码略微缩写,但稍后将提供完整的代码:

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

在其中一个客户端关闭其 UdpClient(或进程退出)之前,这一切都工作正常。

下一次另一个客户机发送消息时,服务器将尝试将该消息传播到现已关闭的原始客户机。对此的 SendAsync调用不会失败——但是当服务器循环回到 ReceiveAsync时,那个异常失败,我还没有找到恢复的方法。

如果我从来没有向客户端发送断开连接的消息,我就不会看到问题。在此基础上,我还创建了一个“立即失败”的复制,只是发送到一个端点假定没有监听,然后尝试接收。同样的异常也会导致失败。

例外:

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 [...]

环境:

  • 视窗11,x64
  • .NET 6

这是预料之中的行为吗?我是否在服务器端错误地使用了 UdpClient?我可以接受客户端在关闭 UdpClient后没有收到消息(这是可以预料的) ,在“完整”应用程序中,我会整理我的内部状态,以跟踪“活动”客户端(最近发送了数据包) ,但我不希望一个客户端关闭 UdpClient导致整个服务器瘫痪。在一个控制台中运行服务器,在另一个控制台中运行客户机。一旦客户端完成了一次,再次运行它(以便它尝试发送到现在已经不存在的原始客户端)。

默认的.NET6控制台应用程序项目模板适用于所有项目。

复制代码

最简单的例子首先出现——但是如果您想运行一个服务器和客户机,它们会在之后显示。

故意破坏服务器(立即失败)

基于这样一种假设,即问题确实是由发送方造成的,因此只需要几行代码就可以很容易地重现这一点:

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

服务器

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

客户

(我以前有一个循环实际上接收服务器发送的数据包,但这似乎没有任何区别,所以为了简单起见,我删除了它。)

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}");
5516 次浏览

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.