【问题标题】:Reusing Streams in a UDP Server在 UDP 服务器中重用流
【发布时间】:2016-03-13 05:12:55
【问题描述】:

我正在编写一个 UDP 服务器,并希望减少每个传入和传出数据包的分配数量。我想做的一件事是重新使用我分配的 Streams 来读取和写入我的数据报包。

我有这种方法,它在接收数据包时使用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);
}

如果 I Seek 回到流的开头,重复使用相同的 BinaryReader 和底层 MemoryStream 是否安全?在做了一些阅读之后,似乎 Socket 不会同时读取或写入数据包,这将要求我每次调用 EndReceiveFrom 都使用 Stream。听起来您确实可以同时执行每个操作,例如读取 + 写入,但不能同时执行读取或并发写入。因此,我认为我可以重复使用阅读器,但我不确定是否可以。为此,我必须给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 而不必为每条消息都实例化一个新消息?如果我每秒发送数千条消息可能会导致一些内存压力,不是吗?

【问题讨论】:

  • 使用 MemoryStream,如果您想保留它,您将需要重用底层字节数组。除非您指定一个“固定”长度的 MemoryStream(通过在 ctor 上传递字节数组),否则它可以重新分配它的缓冲区……所以在水中存在危险。

标签: c# sockets udp memorystream binarywriter


【解决方案1】:

我在使用流、阅读器和网络内容时遇到了相关问题,因此选择了一种不同的方法。我研究了BinaryReaderBinaryWriterBitConverter 的实现,并编写了扩展方法来直接在底层字节[] 缓冲区上读写数据。温和的 PITA,但我不再有流和读者或作家。

例如,这里是一个int-writing扩展方法:

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

返回可能看起来很笨拙,但所有扩展方法都返回我“移动”到字节数组中的字节数......所以我可以始终知道我在哪里;伪流,如果你愿意的话。此外,编写代码并不总是知道它在写什么。每种简单类型都有相同的覆盖,一个用于字符串,一个用于 byte[]。

我的更大的担忧是在客户端,我有一个更受限制的操作环境......并且我维护一个长寿命的读取缓冲区。 UdpClient.Receive 方法为每次读取生成新的字节数组,这简直要了我的命。 GC 发疯了……这当然对 UDP 流式传输具有很大的破坏性 :-) 所以,我找到了 ReceiveReceiveAsync 实现,并发现我可以控制底层的 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 使用单个缓冲区来读取,而我只是在发送端使用普通的Socket

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-28
    • 2011-01-19
    • 2015-06-11
    • 2013-10-13
    • 2015-11-12
    相关资源
    最近更新 更多