为什么Socket.BeginReceive会丢失UDP数据包?

7
以下代码等待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,但问题依然存在。
另一个困扰我的问题是使用Sockets时接收缓冲区的全局性质,如果我没有快速处理传入的数据包,缓冲区会被覆盖。目前还不知道该怎么做,但我愿意听取建议。
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数据乱序或丢失的内容。

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


“‘A second issue that bothers me is the global nature of the receive buffer when using Sockets’这句话是什么意思?”它指的是每个套接字都有一个接收缓冲区,没有任何全局性质。” - user207421
我不确定如果我不能快速处理传入的数据包,缓冲区是否会被覆盖。它不会被覆盖。如果缓冲区已满,传入的数据包将被丢弃 - user207421
@EJP,那个评论适用于我的原始代码。考虑到我使用缓冲区的方式,当我读取数据时,下一批数据可能会覆盖它。新代码没有这个问题,因为在我返回监听更多数据之前,它会对缓冲区(约200字节)进行块复制。 - Dan C
“RaiseStatus”方法非常耗费资源,您不应该为每个接收的数据包都调用它。 - David Schwartz
5个回答

4

UDP可以随时丢弃数据包。在这种情况下,您可以尝试在接收器上设置更大的套接字接收缓冲区以减轻问题。


问题在于,根据Wireshark的显示,所有数据包都已经传输到了远程计算机,因此我不认为它们是由于UDP而被丢弃的。此外,每次调用EndReceive都会获取500个字节,当我查看缓冲区的内容时,只有10000个字节中的500个字节被填充,因此我不认为它会溢出。您是否指的是另一个缓冲区? - Dan C
UDP不会停留在远程计算机,它包括远程计算机中的UDP栈,套接字接收缓冲区也是其中的一部分。如果那里没有空间,数据包会被丢弃。因此,请将其足够大,或使您的接收代码足够快,或使您的发送代码足够慢。即使这样,您仍然可能会遇到丢包情况。因此,您的应用程序必须能够应对此类情况。如果您需要可靠性,请使用TCP。 - user207421
我上面的示例代码在约1秒钟内发送了500000字节。你是说我传递给BeginReceive / BeginReceiveFrom的缓冲区必须这么大吗?这似乎相当过度,尤其是每次我调用EndReceive / EndReceiveFrom时,该缓冲区的其余部分都似乎未使用。我应该初始化不同的缓冲区吗? - Dan C
我在谈论套接字接收缓冲区。这是内核中的数据结构,您可以通过API控制其大小。在Windows中,默认情况下它为8k,荒谬地小。 - user207421
谢谢,那就是我在最后一次更新中找到并增加到500000的缓冲区。 - Dan C

1

抱歉,我不理解你的代码。为什么要在循环中包装异步方法?你应该先了解异步处理。

UDP只保证完整的消息被接收,其他什么都不保证。消息可能会丢失或以错误的顺序到达。您需要应用自己的算法来处理它。例如,有一个叫做Selective Repeat的算法。

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

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


感谢您的建议。它在循环中是因为我想读取多个数据包。当异步事件发生时,它会发出信号以继续等待处理,并在处理当前数据包时开始检查下一个数据包。这是我见过的两种有效利用处理器的技术之一(另一种选择是在回调本身中链接调用BeginReceive)。此外,我尝试只使用BeginReceiveFrom/EndReceiveFrom替换我的代码,但仍然遇到了同样的问题。您有任何示例代码建议吗? - Dan C
我还应该补充一点,即问题发生在回调测试代码被削减到最小的情况下,仅仅是对计数器进行递增处理。 - Dan C

1

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


谢谢,请查看我的上面的回复,因为根据Wireshark的显示,我似乎没有丢包。我知道UDP不可靠,但是当其他UDP测试应用程序正常工作时,在本地区域网络上丢失80%的数据包告诉我我的代码存在问题。 - Dan C
@DanC:如果没有缓冲区等待接收数据包,则接收驱动程序会将其丢弃。也就是说,它们已经到达网络接口卡,但未到达您的应用程序。 - Richard
是的,正如jgauffin上面指出的那样,您不应立即处理消息。如果您需要即时处理消息,请将它们传递给其他线程。最后,在特定情况下可能会出现一些BeginReceive问题,但我不了解此场景中UDP操作的具体情况 - 我们在MsgConnect的UDP传输中使用同步操作。 - Eugene Mayevski 'Callback

0

尝试一个更简单的方法。让接收器在一个单独的线程中运行,伪代码如下:

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???

谢谢,我会记住这点,当我真正开始处理缓冲区时会用到的(尽管我更可能使用信号而不是睡眠,因为数据可以随时到达)。然而,上面的示例只是简单地计数接收到的数据包,所以处理时间应该不是一个因素,对吗? - Dan C
请注意,我正在使用接收的阻塞版本,该版本会阻止线程,除非有数据。当有数据时,它只是将其放入另一个线程处理的队列中。处理数据的线程必须有一种在没有要处理的数据时放弃控制的方法。多年前,我编写了一个UDP应用程序,使用所描述的方法测试带宽。我在测试100Mbps FDX交换机的过程中没有遇到问题,可以以线速(200 Mbps)进行测试。 - dbasnett

0
如其他人已经指出,UDP 不是一种可靠的传输机制。因此,即使 Wireshark 显示数据包已发送,也不意味着数据包已在目标处接收到。接收主机上的 TCP/IP 堆栈仍然可以丢弃数据报。

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

本地计算机\IPv4\数据报已接收但被丢弃

或者

本地计算机\IPv6\数据报已接收但被丢弃

如果您正在使用 IPv6 协议。

另外,您可以尝试减少发送数据报的速率,并查看是否降低了丢弃率。


我在发送方和接收方都运行了Wireshark。它显示所有数据包都能够通过旧版本的线路传输。根据System.Net.NetworkInformation.IPv4InterfaceStatistics,两端都没有丢失任何数据包。不确定Wireshark如何获取其数据或在哪个层面上获取,但我的结论是,我的代码以某种方式无法快速从网络中提取数据(即使我在一个相当紧密的循环中)。增加Socket.ReceiveBufferSize似乎给了我更多时间来确保我处理了所有数据包。减少发送速率也改善了情况。 - Dan C

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接