【问题标题】:Why is Socket.BeginReceive losing packets from UDP?为什么 Socket.BeginReceive 会丢失来自 UDP 的数据包?
【发布时间】:2011-04-17 03:00:38
【问题描述】:

以下代码通过 UDP 等待数据。我有一个测试功能,可以发送 1000 个数据包(数据报?),每个数据包 500 个字节。每次我运行测试函数时,接收者只得到前几十个数据包,但丢弃了其余的数据包。我使用 Wireshark 查看了传入的网络数据,我看到所有 1000 个数据包实际上都已收到,但只是不要让它成为可能的应用程序代码。

这里是一些相关的 VB.NET 3.5 代码:

Private _UdbBuffer As Byte()
Private _ReceiveSocket As Socket
Private _NumReceived As Integer = 0
Private _StopWaitHandle As AutoResetEvent

Private Sub UdpListen()
    _StopWaitHandle = New AutoResetEvent(False)
    _UdpEndPoint = New Net.IPEndPoint(Net.IPAddress.Any, UDP_PORT_NUM)

    _ReceiveSocket = New Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
    _ReceiveSocket.Bind(_UdpEndPoint)

    ReDim _UdbBuffer(10000)

    While Not _StopRequested
        Dim ir As IAsyncResult = _ReceiveSocket.BeginReceive(_UdbBuffer, 0, 10000, SocketFlags.None, AddressOf UdpReceive, Nothing)

        If Not _StopRequested Then
            Dim waitHandles() As WaitHandle = {_StopWaitHandle, ir.AsyncWaitHandle}
            If (WaitHandle.WaitAny(waitHandles) = 0) Then
                Exit While
            End If
        End If
    End While

    _ReceiveSocket.Close()
End Sub

Private Sub UdpReceive(ByVal ar As IAsyncResult)
    Dim len As Integer
    If ar.IsCompleted Then
        len = _ReceiveSocket.EndReceive(ar)
        Threading.Interlocked.Increment(_NumReceived)
        RaiseStatus("Got " & _NumReceived & " packets")
    End If
End Sub

我发送数据如下(暂时不关心数据包内容):

For i as UShort = 0 to 999
   Dim b(500) as Byte
   _UdpClient.Send(b, b.Length)       
Next

如果我在每次调用 Send 后添加一个小延迟,就会有更多的数据包通过;但是,由于 Wireshark 说无论如何它们都已收到,看来问题出在我的接收代码中。我应该提到 UdpListen 正在一个单独的线程上运行。

知道我为什么要丢包吗?我也试过 UdpClient.BeginReceive/EndReceive 但有同样的问题。

困扰我的第二个问题是使用套接字时接收缓冲区的全局性质,我不确定我是否没有足够快地处理传入的数据包,以至于缓冲区将被覆盖。目前还不确定该怎么做,但我愿意接受建议。

9 月 26 日:更新


根据对这篇文章和其他帖子的回复提出的各种相互矛盾的建议,我对我的代码进行了一些更改。感谢所有提出各种意见的人;我现在将所有数据包从拨号到快速以太网。正如你所看到的,这是我的代码有问题,而不是 UDP 丢弃数据包的事实(事实上,自从我修复以来,我没有看到超过一小部分数据包被丢弃或乱序)。

区别:

1) 将 BeginReceive()/EndReceive() 替换为 BeginReceiveFrom()/EndReceiveFrom()。不过,这本身并没有明显的影响。

2) 链接 BeginReceiveFrom() 调用,而不是等待异步句柄设置。不确定这里是否有任何好处。

3) 将 Socket.ReceiveBufferSize 显式设置为 500000,这足以以快速以太网速度传输 1 秒的数据。原来这是一个与传递给 BeginReceiveFrom() 的缓冲区不同的缓冲区。这有最大的好处。

4) 我还修改了我的发送例程,在发送一定数量的字节以根据预期带宽进行节流后等待几毫秒。这对我接收代码有很大的好处,即使 Wireshark 说即使没有这种延迟,我的所有数据仍然可以通过。

我最终没有使用单独的处理线程,因为据我了解,对 BeginReceiveFrom 的每次调用都会在新的工作线程上调用我的回调。这意味着我可以同时运行多个回调。这也意味着,一旦我调用 BeginReceiveFrom,我就有时间做我的事情(只要我不花太长时间并耗尽可用的工作线程)。

Private Sub StartUdpListen()
    _UdpEndPoint = New Net.IPEndPoint(Net.IPAddress.Any, UDP_PORT_NUM)
    _ReceiveSocket = New Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
    _ReceiveSocket.ReceiveBufferSize = 500000
    _ReceiveSocket.Bind(_UdpEndPoint)

    ReDim _Buffer(50000)

    _ReceiveSocket.BeginReceiveFrom(_Buffer, 0, _Buffer.Length, SocketFlags.None, _UdpEndPoint, AddressOf UdpReceive, Nothing)

End Sub

Private Sub UdpReceive(ByVal ar As IAsyncResult)
    Dim len As Integer = _ReceiveSocket.EndReceiveFrom(ar, _UdpEndPoint)
    Threading.Interlocked.Increment(udpreceived)

    Dim receiveBytes As Byte()
    ReDim receiveBytes(len - 1)
    System.Buffer.BlockCopy(_Buffer, 0, receiveBytes, 0, len)

    _ReceiveSocket.BeginReceiveFrom(_Buffer, 0, _UdbBuffer.Length, SocketFlags.None, _UdpEndPoint, AddressOf UdpReceive, Nothing)

    //' At this point, do what we need to do with the data in the receiveBytes buffer
    Trace.WriteLine("count=" & udpreceived)
End Sub

上面没有显示的是错误处理和处理 UDP 数据乱序或丢失。

我认为这可以解决我的问题,但如果有人仍然认为上述内容有任何问题(或者我可以做得更好),我很想听听。

【问题讨论】:

  • “困扰我的第二个问题是使用套接字时接收缓冲区的全局性质”是什么意思?每个插座有一个。没有什么全球性的。
  • “我不确定我是否没有足够快地处理传入的数据包,以至于缓冲区将被覆盖”。它不会被覆盖。如果缓冲区已满,传入的数据包将被丢弃
  • @EJP,该评论适用于我的原始代码。考虑到我是如何使用缓冲区的,当我阅读它时,它可能会被下一组数据覆盖。新代码没有这个问题,因为它会在我回去监听更多数据之前对缓冲区(大约 200 个字节)进行块复制。
  • “RaiseStatus”方法非常繁重。您不想为收到的每个数据包调用它。

标签: .net vb.net sockets network-programming udp


【解决方案1】:

UDP 可以随时丢弃数据包。在这种情况下,您可以尝试在接收器处设置一个更大的套接字接收缓冲区来缓解。

【讨论】:

  • 问题在于,根据 Wireshark 的说法,所有数据包都到达了远程计算机,所以我认为它们实际上并没有被 UDP 丢弃。此外,对 EndReceive 的每次调用都会获得 500 个字节,当我查看缓冲区的内容时,10000 个字节中只有 500 个被填充,所以我认为它不会溢出。您指的是不同的缓冲区吗?
  • UDP 不会在远程计算机上停止。它包括远程计算机 in 的 UDP 堆栈,套接字接收缓冲区是其中的一部分。如果没有空间,则丢弃数据包。所以让它足够大,或者让你的接收代码足够快,或者让你的发送代码足够慢。即使那样,您仍然可以丢弃数据包。所以你的应用程序只需要应付。如果您想要可靠性,请使用 TCP。
  • 我上面的示例代码在大约 1 秒内发送了 500000 个字节。你是说我传递给 BeginReceive/BeginReceiveFrom 的缓冲区必须那么大吗?似乎相当过分,特别是因为每次我调用 EndReceive/EndReceiveFrom 时,缓冲区的其余部分似乎都没有使用。我应该初始化不同的缓冲区吗?
  • 我说的是套接字接收缓冲区。这是内核中的一种数据结构,您可以通过 API 控制其大小。默认情况下它在 Windows 中是 8k,小得离谱。
  • 谢谢,这是我最终在我的最新更新中找到并增加到 500000 的缓冲区。
【解决方案2】:

对不起。我不明白你的代码。为什么要将异步方法包装在一个循环中?您应该从阅读异步处理开始。

UDP 只保证接收到完整的消息,没有别的。消息可能被丢弃或以错误的顺序出现。您需要应用自己的算法来处理它。例如,有一个叫Selective Repeat

接下来,如果您希望在短时间内收到大量消息,则不应在收到新消息之前处理每条消息。相反,将每条传入消息排入队列并有一个单独的线程负责处理。

第三:Udp 消息应该使用 BeginReceiveFrom/EndReceiveFrom 进行异步处理或使用 ReceiveFrom 进行同步处理。

【讨论】:

  • 感谢您的建议。它处于一个循环中,因为我想读取多个数据包。当异步事件发生时,它会发出等待句柄继续的信号,并且我在处理当前数据包时开始检查下一个数据包。这是我见过的高效使用处理器的两种技术之一(另一种选择是将调用 BeginReceive 链接到回调本身)。我也尝试用 BeginReceiveFrom/EndReceiveFrom 替换我的代码,但仍然遇到同样的问题。您有任何示例代码建议吗?
  • 我还应该补充一点,回调测试代码减少到无非是增加一个计数器,因此处理非常少。
【解决方案3】:

如上所述,UDP 不是可靠的协议。它是一种无连接协议,与 TCP 相比,它对 IP 数据包的开销要少得多。 UDP 对于许多功能(包括广播和多播消息)都非常有用,但它不能用于可靠的消息传递。如果缓冲区过载,网络驱动程序只会丢弃数据报。如果您需要可靠传递的基于消息的通信,或者您希望发送许多消息,您可以查看我们的MsgConnect 产品(提供免费开源版本),它通过套接字和 UDP 提供基于消息的数据传输.

【讨论】:

  • 谢谢,请看我上面的回复,因为在我看来,我没有根据 Wireshark 丢弃数据包。我知道 UDP 不可靠,但是当其他 UDP 测试应用程序正常工作时,在局域网上丢弃 80% 的数据包告诉我我的代码有问题。
  • @DanC:如果没有缓冲区等待接收数据包,接收驱动程序将丢弃数据包。 IE。他们到达了 NIC,但没有到达您的应用程序。
  • 是的,正如上面 jgauffin 所指出的,您不应该立即处理消息。如果您需要即时处理消息,请将它们传递给其他线程。最后,特别是 BeginReceive 可能存在一些问题,但我不知道这种情况下 UDP 操作的细节 - 我们在 MsgConnect 的 UDP 传输中使用同步操作
【解决方案4】:

尝试更简单的方法。让接收器在一个单独的线程中运行,在伪代码中看起来像这样:

do while socket_is_open???

  buffer as byte()

  socket receive buffer 'use the blocking version

  add buffer to queue of byte()

loop

在另一个线程循环中判断队列大小是否已经收到数据包。如果您收到了数据包,则处理它们,否则休眠。

do

if queue count > 0

  do 

    rcvdata = queue dequeue

    process the rcvdata

  loop while queue count > 0

else

  sleep

end if

loop while socket_is_open???

【讨论】:

  • 谢谢,当我真正开始对缓冲区做一些事情时,我会记住这一点(尽管我更可能使用信号而不是睡眠,因为数据可以随时进入)。但是,我上面的示例实际上只是计算接收到的数据包,因此处理时间不应该是一个因素,不是吗?
  • 请注意,我正在使用接收的阻塞版本,除非有数据,否则它会阻塞该线程。当有数据时,它只是将其放入另一个线程处理的队列中。当没有数据要处理时,处理数据的线程必须有办法放弃控制。几年前,我编写了一个 UDP 应用程序,使用所描述的方法测试带宽。我以线速 (200 Mbps) 测试 100Mbps FDX 交换机没有问题。
【解决方案5】:

正如其他人已经说过的,UDP 不是一种有保证的传递机制。因此,即使wireshark 向您显示数据包已发送,但这并不意味着数据包已在目的地收到。接收主机上的 TCP/IP 堆栈仍然可以丢弃数据报。

您可以通过监视 perfmon.exe 中的以下性能计数器来确认这种情况。

本地计算机\IPv4\收到的数据报 丢弃

本地计算机\IPv6\收到的数据报 丢弃

如果您使用的是 IPv6 协议。

您也可以尝试降低发送数据报的速率,看看是否会降低丢弃率。

【讨论】:

  • 我在发送方和接收方都运行了 Wireshark。它显示即使使用旧版本,所有数据包都可以通过网络传输。根据 System.Net.NetworkInformation.IPv4InterfaceStatistics,两端都没有丢包。不确定 Wireshark 如何或在哪一层获取数据,但我的结论是,不知何故,我的代码从网络中提取的速度不够快(即使我处于一个非常紧凑的循环中)。增加 Socket.ReceiveBufferSize 似乎给了我更多的时间来确保我处理了所有的数据包。降低发送速率也有所改善。
猜你喜欢
  • 2015-06-30
  • 2017-04-04
  • 2013-10-17
  • 2018-06-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多