当前位置: 首页 > 知识库问答 >
问题:

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

凤凡
2023-03-14

我正在尝试创建一个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();

这一切都可以正常工作,直到其中一个客户端关闭其Udp客户端(或进程退出)。

下次另一个客户端发送消息时,服务器会尝试将其传播到现在已关闭的原始客户端。对此的SendAsync调用不会失败,但当服务器返回ReceiveAsync时,它会失败并出现异常,我还没有找到恢复的方法。

如果我从未向断开连接的客户端发送消息,我就永远看不到问题所在。基于此,我还创建了一个“立即失败”的repo,它只发送到一个假定没有监听的endpoint,然后尝试接收。这将失败,并出现相同的异常。

例外:

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 来关闭整个服务器。在一个控制台中运行服务器,在另一个控制台中运行客户端。客户端完成一次后,再次运行它(以便它尝试发送到现已失效的原始客户端)。

默认的。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}");

共有1个答案

龙涵蓄
2023-03-14

您可能知道,如果主机接收到当前未绑定的UDP端口的数据包,它可能会发回ICMP“端口不可访问”消息。它是否这样做取决于防火墙、私有/公共设置等。然而,在localhost上,它几乎总是会发送回这个数据包。

在您的服务器代码中,您正在调用SendAsync到旧客户端,这会提示这些“端口不可达”消息。

现在,在Windows(并且仅在Windows)上,默认情况下,收到的ICMP端口不可访问消息将关闭发送该消息的UDP套接字;因此,下次尝试在套接字上接收时,它将抛出异常,因为套接字已被操作系统关闭。

显然,这在您这里的多客户机、单服务器套接字设置中引起了令人头痛的问题,但幸运的是有一个修复方法:

您需要使用不常需要的SIO_UDP_CONNRESETWinsock控制代码,它关闭了自动关闭套接字的内置行为。

我不相信这个ioctl代码在dot net < code > IoControlCodes 类型中可用,但是您可以自己定义它。如果您将以下代码放在服务器报告的顶部,将不再出现错误。

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

请注意,此ioctl代码仅在Windows(XP及更高版本)上受支持,而在Linux上不受支持,因为它是由Winsock扩展提供的。当然,由于所描述的行为只是Windows上的默认行为,所以这种遗漏并不是一个重大损失。如果您试图创建一个跨平台库,您应该将其作为特定于Windows的代码。

 类似资料:
  • 我从C代码中调用Java方法。每次调用时,我调用AttachCurrentThread,调用后,我调用DetachCurrentThread。 这可以很好地工作,但问题是,我看到了由此导致的ECSESSIVE垃圾收集,即几乎每个通过JNI的调用。VisualVM图形上的小集合基本上都是绿色的!从本机代码到Java的调用速率是每秒数百次。在调用过程中,我还可以看到创建了过多的Java线程,如Thre

  • 问题内容: 在我的Ajax代码中,我正在向go lang api发送一个关联数组,但是go lang不会接收任何数组。为什么? 为什么这个Ajax不会将数组发送到Go API?在下面的mvc结构中,我想要接收此数据: 问题答案: 您不能直接将数组从客户端发送到服务器,因为数组定义在两侧可能不相同。 有两种解决方法: 一个。您可以在clinet中将数组转换为json字符串,然后将其作为字符串参数发送

  • 问题内容: 好吧,我试图理解并阅读可能导致它的原因,但我却无法理解: 我的代码中有这个地方: 事实是,当它尝试调用某些方法时,它将引发而不是其他预期的异常(特别是)抛出 。我实际上知道调用了什么方法,所以我直接转到该方法代码,并为应该抛出的行添加了一个块 ,它实际上按预期抛出。然而,当它上升时,以某种方式更改了上面的代码并没有 按预期进行。 是什么原因导致这种行为的?我该如何检查? 问题答案: 通

  • 为什么我在下面的代码段中的X轴上有一个溢出? 在我的网格容器上应用时,就会产生溢出。 null null https://codepen.io/anon/pen/wdjexz?editors=1100

  • 问题内容: 即使模型类中没有验证约束,我也会收到此错误(所有成员变量均已正确设置,但我在创建对象时仍然遇到此异常)。如何调试此错误? 问题答案: 每个都有一个数组。每个显示了您要保留的bean的哪个属性被侵犯。正如@Arthur正确指出的那样,违反Java持久性注释的结果也以s 结尾。 要调试你的问题,我会暂时赶在并打印出每个如下:

  • 问题内容: 输出: 但是当我执行以下操作时: 输出: 为什么扩展列表引用而不是列表扩展? 我发现这是因为我在尝试将listC扩展到listB时试图将listB附加到listA。 假设列表不能直接附加/扩展。解决该问题最常用的表格形式是什么? 问题答案: 修改到位列表,不返回任何内容,从而导致。在第二种情况下,这是一个正在扩展的临时列表,该列表在该行后立即消失,而在第一种情况下,可以通过引用。 在尝