在 UDP 服务器中重用流
Reusing Streams in a UDP Server
我正在编写一个 UDP 服务器,我想减少每个传入和传出数据报包的分配数量。我想做的一件事是重新使用我分配的流来读取和写入我的数据报包。
我有这个方法,它在接收数据包时使用 BinaryReader
的实例将缓冲区读取到实例对象上。
// Simplified version of the receive data method.
private void ReceiveClientData(IAsyncResult result)
{
int receivedData = socket.EndReceiveFrom(result, ref endPoint);
if (receivedData == 0)
{
this.ListenForData(socket);
return;
}
// Read the header in from the buffer first so we know what kind of message and how to route.
this.binaryReader.BaseStream.SetLength(receivedData);
this.binaryReader.BaseStream.Seek (0, SeekOrigin.Begin);
// Can't reset the MemoryStream buffer to the new packet buffer.
// How does this get consumed by the stream without allocating a new MemoryStream(buffer)?
byte[] buffer = (byte[])result.AsyncState;
// Deserialize the bytes into the header and datagram.
IClientHeader header = new ClientHeader();
header.GetMessageFromBytes(this.binaryReader);
IClientDatagram datagram = this.datagramFactory.CreateDatagramFromClientHeader(header);
datagram.GetMessageFromBytes(this.binaryReader);
this.ListenForData(socket);
}
重新使用相同的 BinaryReader
和基础 MemoryStream
是否安全,前提是我要返回到流的开头?在做了一些阅读之后,Socket 似乎不会同时读取或写入数据包,这需要我在每次调用 EndReceiveFrom
时使用 Stream
。听起来您可以并发执行每个操作,例如读取 + 写入,但不能同时执行读取或并发写入。因此,我认为我可以重新使用 reader 但我不确定是否可以。为了这样做,我必须给 MemoryStream
一个新的缓冲区来读取。你能做到吗?还是我需要为从套接字接收到的每个新数据包创建一个新的 MemoryBuffer
?
我想采用同样的方法将数据包发送回客户端。为此,我尝试将 MemoryStream
的长度设置为 0 并返回到开头。如 another answer here 中所述,这似乎并没有清除缓冲区。我仍然在下面的 t
中看到旧的缓冲区数据。
public void SendMessage(IServerDatagram message)
{
this.binaryStream.Seek(0, SeekOrigin.Begin);
this.binaryStream.SetLength (0);
// Contains old buffer data.
var t = this.binaryStream.GetBuffer ();
message.Serialize (this.binaryWriter);
// Contains a mixture of new bytes + old bytes.
byte[] data = this.binaryStream.GetBuffer();
this.binaryWriter.Flush ();
// Send the datagram packet.
this.udpServerSocket.SendTo(data, this.connectedClients.FirstOrDefault().Key);
}
我事先不知道缓冲区需要多大,所以我无法 SetLength
调整新缓冲区的大小。有没有其他方法可以重用 Stream 而不必为每条消息实例化一个新 Stream ?如果我每秒发送数千条消息,这可能会导致一些内存压力,不是吗?
我在使用流、阅读器和网络时遇到了相关问题 - 并选择了一种稍微不同的方法。我研究了 BinaryReader
、BinaryWriter
和 BitConverter
实现并编写了扩展方法以直接在底层 byte[] 缓冲区上读写数据。温和的 PITA,但我不再有流和读者或作家。
这里以int写扩展为例:
[System.Security.SecuritySafeCritical]
public unsafe static int Write( this byte[ ] array, int value, int offset = 0 )
{
if ( offset + sizeof( int ) > array.Length ) throw new IndexOutOfRangeException( );
fixed ( byte* b = array )
{
*( ( int* ) ( b + offset ) ) = value;
}
return sizeof( int );
}
return 可能看起来很傻,但是所有扩展方法 return 字节数我已经 "moved" 到字节数组中......所以我可以始终知道在哪里我在;伪流,如果你愿意的话。此外,编写代码并不总是知道它在写什么。每种简单类型都有相同的覆盖,一个用于字符串,一个用于 byte[].
我的 更大的担忧是在客户端,我有一个更受限制的操作环境……并且我维护了一个长寿命的读取缓冲区。 UdpClient.Receive
方法为每次读取生成新的字节数组,这简直要了我的命。 GC 发疯了……这当然对 UDP 流式传输非常具有破坏性 :-) 所以,我找到了 Receive
和 ReceiveAsync
实现,并发现我可以控制底层 socked 需要的内容吃饱了又做了我自己的 RecieveBroadcastToBuffer
扩展方法:
const int MaxUdpSize = 0x0000ffff;
const int AnyPort = 0;
static EndPoint anyV4Endpoint = new IPEndPoint( IPAddress.Any, AnyPort );
static EndPoint anyV6Endpoint = new IPEndPoint( IPAddress.IPv6Any, AnyPort );
/// <summary>Receives a UDP datagram into the specified buffer at the specified offset</summary>
/// <returns>The length of the received data</returns>
public static int ReceiveBroadcastToBuffer( this UdpClient client, byte[ ] buffer, int offset = 0 )
{
int received;
var socket = client.Client;
if ( socket.AddressFamily == AddressFamily.InterNetwork )
{
received = socket.ReceiveFrom( buffer, offset, MaxUdpSize, SocketFlags.None, ref anyV4Endpoint );
}
else
{
received = socket.ReceiveFrom( buffer, offset, MaxUdpSize, SocketFlags.None, ref anyV6Endpoint );
}
return received;
}
这 几乎 正是 UdpClient.Receive
所做的...除了我管理缓冲区。我发现 UdpClient
使用单个缓冲区来读取,而我只在发送端使用普通的 ol' Socket
。
我正在编写一个 UDP 服务器,我想减少每个传入和传出数据报包的分配数量。我想做的一件事是重新使用我分配的流来读取和写入我的数据报包。
我有这个方法,它在接收数据包时使用 BinaryReader
的实例将缓冲区读取到实例对象上。
// Simplified version of the receive data method.
private void ReceiveClientData(IAsyncResult result)
{
int receivedData = socket.EndReceiveFrom(result, ref endPoint);
if (receivedData == 0)
{
this.ListenForData(socket);
return;
}
// Read the header in from the buffer first so we know what kind of message and how to route.
this.binaryReader.BaseStream.SetLength(receivedData);
this.binaryReader.BaseStream.Seek (0, SeekOrigin.Begin);
// Can't reset the MemoryStream buffer to the new packet buffer.
// How does this get consumed by the stream without allocating a new MemoryStream(buffer)?
byte[] buffer = (byte[])result.AsyncState;
// Deserialize the bytes into the header and datagram.
IClientHeader header = new ClientHeader();
header.GetMessageFromBytes(this.binaryReader);
IClientDatagram datagram = this.datagramFactory.CreateDatagramFromClientHeader(header);
datagram.GetMessageFromBytes(this.binaryReader);
this.ListenForData(socket);
}
重新使用相同的 BinaryReader
和基础 MemoryStream
是否安全,前提是我要返回到流的开头?在做了一些阅读之后,Socket 似乎不会同时读取或写入数据包,这需要我在每次调用 EndReceiveFrom
时使用 Stream
。听起来您可以并发执行每个操作,例如读取 + 写入,但不能同时执行读取或并发写入。因此,我认为我可以重新使用 reader 但我不确定是否可以。为了这样做,我必须给 MemoryStream
一个新的缓冲区来读取。你能做到吗?还是我需要为从套接字接收到的每个新数据包创建一个新的 MemoryBuffer
?
我想采用同样的方法将数据包发送回客户端。为此,我尝试将 MemoryStream
的长度设置为 0 并返回到开头。如 another answer here 中所述,这似乎并没有清除缓冲区。我仍然在下面的 t
中看到旧的缓冲区数据。
public void SendMessage(IServerDatagram message)
{
this.binaryStream.Seek(0, SeekOrigin.Begin);
this.binaryStream.SetLength (0);
// Contains old buffer data.
var t = this.binaryStream.GetBuffer ();
message.Serialize (this.binaryWriter);
// Contains a mixture of new bytes + old bytes.
byte[] data = this.binaryStream.GetBuffer();
this.binaryWriter.Flush ();
// Send the datagram packet.
this.udpServerSocket.SendTo(data, this.connectedClients.FirstOrDefault().Key);
}
我事先不知道缓冲区需要多大,所以我无法 SetLength
调整新缓冲区的大小。有没有其他方法可以重用 Stream 而不必为每条消息实例化一个新 Stream ?如果我每秒发送数千条消息,这可能会导致一些内存压力,不是吗?
我在使用流、阅读器和网络时遇到了相关问题 - 并选择了一种稍微不同的方法。我研究了 BinaryReader
、BinaryWriter
和 BitConverter
实现并编写了扩展方法以直接在底层 byte[] 缓冲区上读写数据。温和的 PITA,但我不再有流和读者或作家。
这里以int写扩展为例:
[System.Security.SecuritySafeCritical]
public unsafe static int Write( this byte[ ] array, int value, int offset = 0 )
{
if ( offset + sizeof( int ) > array.Length ) throw new IndexOutOfRangeException( );
fixed ( byte* b = array )
{
*( ( int* ) ( b + offset ) ) = value;
}
return sizeof( int );
}
return 可能看起来很傻,但是所有扩展方法 return 字节数我已经 "moved" 到字节数组中......所以我可以始终知道在哪里我在;伪流,如果你愿意的话。此外,编写代码并不总是知道它在写什么。每种简单类型都有相同的覆盖,一个用于字符串,一个用于 byte[].
我的 更大的担忧是在客户端,我有一个更受限制的操作环境……并且我维护了一个长寿命的读取缓冲区。 UdpClient.Receive
方法为每次读取生成新的字节数组,这简直要了我的命。 GC 发疯了……这当然对 UDP 流式传输非常具有破坏性 :-) 所以,我找到了 Receive
和 ReceiveAsync
实现,并发现我可以控制底层 socked 需要的内容吃饱了又做了我自己的 RecieveBroadcastToBuffer
扩展方法:
const int MaxUdpSize = 0x0000ffff;
const int AnyPort = 0;
static EndPoint anyV4Endpoint = new IPEndPoint( IPAddress.Any, AnyPort );
static EndPoint anyV6Endpoint = new IPEndPoint( IPAddress.IPv6Any, AnyPort );
/// <summary>Receives a UDP datagram into the specified buffer at the specified offset</summary>
/// <returns>The length of the received data</returns>
public static int ReceiveBroadcastToBuffer( this UdpClient client, byte[ ] buffer, int offset = 0 )
{
int received;
var socket = client.Client;
if ( socket.AddressFamily == AddressFamily.InterNetwork )
{
received = socket.ReceiveFrom( buffer, offset, MaxUdpSize, SocketFlags.None, ref anyV4Endpoint );
}
else
{
received = socket.ReceiveFrom( buffer, offset, MaxUdpSize, SocketFlags.None, ref anyV6Endpoint );
}
return received;
}
这 几乎 正是 UdpClient.Receive
所做的...除了我管理缓冲区。我发现 UdpClient
使用单个缓冲区来读取,而我只在发送端使用普通的 ol' Socket
。