在不分配新缓冲区的情况下接收 UDP 消息

Receive UDP messages without allocating new buffer

在尝试使用 UdpClient.Receive 时,我注意到它只是 returns 一个字节数组。这是否为每个数据包分配一个新缓冲区?据我所知,为每个接收到的数据包分配一个新的字节数组不利于性能和内存使用。为了在使用 Tcp 时解决同样的问题,我使用 ArrayPool 来租用缓冲区。但是我如何使用 Udp 做到这一点?

我想过使用与 Tcp 相同的方法,只使用一个套接字,但 udp 不是基于流的,因此这种方法没有什么意义(我不知道一个数据包有多大,我不能连续像在 Tcp 中一样从流中读取)。

TL;DR:

如何在接收 UDP 数据包时节省性能,同时仍然始终接收完整数据包?

When trying to use UdpClient.Receive I noticed that it just returns a byte array. Does this allocate a new buffer for every packet?

通常是的。

在内部,UdpClient 使用 fixed-sized byte[65536] 数组来读取每个数据包。如果接收到的数据包恰好是那个大小,则内部缓冲区是 returned as-is。但是,如果接收到的数据包小于该大小(通常是这种情况),那么它会分配一个新的 byte[] 实际数据包大小,将内部缓冲区的数据复制到该新数组中,然后 return就这样了。

I don't know how big one packet will be

可以在 Winsock API 中确定下一个数据包的实际大小,而无需从套接字队列中提取数据包。 Winsock 的 recvfrom() 函数有一个用于该目的的 MSG_PEEK 标志。

UdpClient.Receive() 不支持该标志,但是 UdpClient 的基础 Socket 在其 ReceiveFrom() 方法中支持,例如:

byte[] ignore = new byte[0]; // the buffer parameter of ReceivFrom() can't be null!
EndPoint ep;

int size = UdpClient.Client.ReceiveFrom(ignore, SocketFlags.Peek, ref ep);
// obtain a buffer of sufficient size ... 
UdpClient.Client.ReceiveFrom(buffer, size, SocketFlags.None, ref ep);

尽管 Socket.ReceiveFrom() 确实允许 0 长度的 byte[] 数组(它会将 NULL 指针和 0 缓冲区长度传递给 Winsock 的 recvfrom()),但我不确定是 Socket.ReceiveFrom() 是否实际上 return 下一个数据包大小,或者如果您尝试使用小于 byte[] 的数组查看队列,它是否会抛出 SocketException实际数据包(因为我不确定 recvfrom() 是否在使用 MSG_PEEK 标志时报告 WSAEMSGSIZE 错误)。

为避免此潜在问题,请继续阅读...

How do I save performance when receiving UDP packets while still always receiving the full packet?

我建议您只使用单个 64K byte[] 数组(就像 UdpClient 在内部所做的那样),而不是尝试合并数组,因为 UDP 数据包永远不会超过该大小。直接用UdpClient.Client.ReceiveFrom()代替UdpClient.Receive()就可以了,eg:

byte[] myBuffer = new byte[65536];
EndPoint ep;

...

int received = UdpClient.Client.ReceiveFrom(myBuffer, ref ep);
// use myBuffer up to received bytes ...
// use (IPEndPoint)ep if needed ...

... 

这样,您可以 re-use 单个 byte[] 数组进行多次读取(就像 UdpClient 在内部所做的那样)。