为何使用UdpClient发送后会导致后续接收操作失败?
我正在实现一个UDP聊天服务器,功能是向所有曾经发送过消息的客户端转发消息——所有发过消息的客户端都会收到其他客户端的消息。所有操作都通过UdpClient在独立进程中完成(所有网络连接都在同一系统内,所以我认为UDP的不可靠性不会造成影响)。
服务器核心代码大致如下:
var udpClient = new UdpClient(9001); while (true) { var packet = await udpClient.ReceiveAsync(); // 在此处向之前发送过消息的客户端转发消息 }
客户端简化代码:
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(Memory1 buffer, SocketFlags socketFlags, EndPoint remoteEndPoint, CancellationToken cancellationToken) at System.Net.Sockets.Socket.ReceiveFromAsync(ArraySegment1 buffer, SocketFlags socketFlags, EndPoint remoteEndPoint)
at System.Net.Sockets.UdpClient.ReceiveAsync()
at Program.$(String[] args) in [...]
环境
- Windows 11, x64
- .NET 6
核心疑问
这是预期行为吗?我在服务器端对UdpClient的使用是否有误?我能接受客户端关闭UdpClient后无法接收消息(这是正常情况),在完整应用中我会整理内部状态以跟踪“活跃”客户端(近期发送过数据包的客户端),但不希望单个客户端关闭UdpClient导致整个服务器崩溃。
运行方式:在一个控制台启动服务器,另一个控制台启动客户端。客户端运行结束后,再次启动客户端(此时服务器会尝试向已失效的原客户端发送消息)。所有项目均可使用默认的.NET 6控制台应用项目模板。
复现代码
先展示最简单的示例,之后是可运行的服务器和客户端代码。
主动触发失败的服务器(即时失败)
基于“发送操作导致问题”的假设,仅需几行代码即可复现:
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}");
内容的提问来源于stack exchange,提问作者Jon Skeet

