Jon*_*eet 106 c# udp 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
}
Run Code Online (Sandbox Code Playgroud)
客户端代码也很简单 - 同样,这稍微缩写了,但稍后是完整的代码:
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();
Run Code Online (Sandbox Code Playgroud)
这一切都工作正常,直到其中一个客户端关闭它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 [...]
Run Code Online (Sandbox Code Playgroud)
环境:
这是预期的行为吗?我UdpClient
在服务器端使用不正确吗?我对客户端在关闭后没有收到消息UdpClient
(这是预期的)感到满意,并且在“完整”应用程序中,我将整理我的内部状态以跟踪“活动”客户端(已发送数据包的客户端)最近),但我不希望一个客户端关闭 aUdpClient
来关闭整个服务器。在一个控制台中运行服务器,在另一个控制台中运行客户端。一旦客户端完成一次,再次运行它(以便它尝试发送到现已失效的原始客户端)。
默认 .NET 6 控制台应用程序项目模板适用于所有项目。
重现代码
首先是最简单的示例 - 但如果您想运行服务器和客户端,它们会在后面显示。
故意破坏服务器(立即失败)
基于确实是发送导致问题的假设,只需几行即可轻松重现:
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 [...]
Run Code Online (Sandbox Code Playgroud)
服务器
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();
Run Code Online (Sandbox Code Playgroud)
客户
(我之前有一个循环实际上接收服务器发送的数据包,但这似乎没有任何区别,因此为了简单起见,我将其删除。)
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}");
Run Code Online (Sandbox Code Playgroud)
Kaz*_*zar 145
您可能知道,如果主机收到当前未绑定的 UDP 端口的数据包,它可能会发回 ICMP“端口不可达”消息。它是否这样做取决于防火墙、私有/公共设置等。但是,在本地主机上,它几乎总是会发回此数据包。
在您的服务器代码中,您正在调用SendAsync
旧客户端,这会提示这些“端口无法访问”消息。
现在,在 Windows 上(并且仅在 Windows 上),默认情况下,收到的 ICMP 端口不可达消息将关闭发送该消息的 UDP 套接字;因此,下次尝试在套接字上接收时,它将抛出异常,因为套接字已被操作系统关闭。
显然,这会导致此处的多客户端、单服务器套接字设置令人头痛,但幸运的是有一个修复:
您需要利用不经常需要的SIO_UDP_CONNRESET Winsock 控制代码,它会关闭自动关闭套接字的内置行为。
我不相信这个 ioctl 代码在 dotnetIoControlCodes
类型中可用,但您可以自己定义它。如果将以下代码放在服务器重现的顶部,则不再引发错误。
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);
Run Code Online (Sandbox Code Playgroud)
请注意,此 ioctl 代码仅在 Windows(XP 及更高版本)上受支持,在 Linux 上不受支持,因为它是由 Winsock 扩展提供的。当然,由于所描述的行为只是 Windows 上的默认行为,因此这种遗漏并不是重大损失。如果您尝试创建跨平台库,则应将其作为特定于 Windows 的代码进行封锁。