【问题标题】:C# async Begin Receive Call back method misses some packetsC# async Begin Receive 回调方法丢失了一些数据包
【发布时间】:2013-12-16 11:45:12
【问题描述】:

我从客户端向正在使用 BeginReceive 侦听的服务器发送 2 个连续数据包

服务器总是接收第一个数据包而不是另一个数据包,除非我在调试模式下运行客户端并在另一个之后缓慢发送下一个数据包。

这是我发送函数的一个 sn-p

if (soc.Connected)
{
    byte[] byData = new byte[System.Text.Encoding.ASCII.GetByteCount("Hi")];
    byData = System.Text.Encoding.ASCII.GetBytes("Hi");
    soc.Send(BitConverter.GetBytes(byData.Length));
    soc.Send(byData);
}

这是位于我的服务器内部的回调函数:

 private void Recieve(IAsyncResult iar)
 {
    int j = 0;

    Socket server_conn = (Socket)iar.AsyncState;
    server_conn.EndReceive(iar);

    if (!SocketConnected(server_conn))
    {   
        server_conn.Close();
        return;
    }

    if (g_bmsg.Length != 0)
    {
        logthis(server_conn.RemoteEndPoint.ToString() + ": " + Encoding.ASCII.GetString(g_bmsg, 0, g_bmsg.Length));
    }

    //Find out who sent this
    foreach (ClientData cls in clientlist)
    {
        if (server_conn.RemoteEndPoint == cls.clientsock.RemoteEndPoint)
        {
            j = cls.index;
            break;
        }
    }
     server_conn.BeginReceive(g_bmsg, 0, g_bmsg.Length, SocketFlags.None, new AsyncCallback(Recieve), server_conn);
}

【问题讨论】:

  • g_bmsg 在哪里定义?它是如何分配的?如果您在第一条消息中收到长度,然后在第二条消息中收到数据,会发生什么?任何合理的通信协议实现都应该包括一个 FIFO 缓冲区和一个允许拆分消息的解析器。
  • g_bmsg 定义在 Form1 类 private byte[] g_bmsg = new byte[1024];我没有对我的服务器进行编码,除了阅读下一条消息的长度,因为我只是在测试它。
  • 并且可能作为缓冲区参数传递给Socket.BeginReceive
  • 是的,它在第一次启动 BeginReceive 时通过。我应该为每个客户创建一个单独的缓冲区还是我在这里缺少什么?
  • 不保证SendReceive 调用之间的一一对应关系。由于您似乎正在发送少量数据,因此第一个 Receive 很可能正在用分别发送的两条消息填充缓冲区。

标签: c# sockets asynchronous


【解决方案1】:

在使用 TCP/IP 套接字(或几乎任何通信层)时,您很少能保证您打算发送的整个消息都将出现在一个数据包中。

这意味着您应该始终保留自己的 FIFO 缓冲区,并按顺序对其进行解析。

这是一般的想法:

// fifo queue (for background thread parsing, make sure it's thread safe)
private readonly ConcurrentQueue<byte> _commQueue = new ConcurrentQueue<byte>();

在您的 Receive 方法中,您应该简单地将数据排入 FIFO:

private void Receive(IAsyncResult iar)
{
    Socket server_conn = (Socket)iar.AsyncState;

    // check how many bytes we actually received
    var numBytesReceived = server_conn.EndReceive(iar);

    if (!SocketConnected(server_conn))
    {   
        server_conn.Close();
        return;
    }

    // get the received data from the buffer
    if (numBytesReceived > 0)
    {
        for (int i = 0; i < numBytesReceived; i++)
            _commQueue.Enqueue(g_bmsg[i]);

        // signal the parser to continue parsing        
        NotifyNewDataReceived();
    }

    // continue receiving
    server_conn.BeginReceive(g_bmsg, 0, g_bmsg.Length, SocketFlags.None, new AsyncCallback(Recieve), server_conn);
}

【讨论】:

  • 您可以使用单独的线程进行解析,该线程将使用NotifyNewDataReceived 唤醒(例如,通过AutoResetEvent 或其他方式,检查this thread 以获取多线程生产者/消费者的示例),或者在Receive 方法中立即执行(如果它不是很密集并且不会阻塞套接字太久)。归结为:虽然队列不为空:1. 读取长度,2. 检查是否有足够的数据。 2.a.如果是,则将其出列并处理,2.b 如果不是,请等待其余部分。
  • @InstallGentoo: 尽管如此,我还是建议您使用“cookie”(即已知的字母或数字序列)扩展您的协议,甚至可能在末尾添加一个 CRC,即MSG XXXX (data) CRC,其中@ 987654329@ 是你的 cookie,XXXX 是长度(根据你的代码是 32 位),CRC 是某种校验和。这样你就知道你的消息是完整的并且被正确接收了,如果它以一个cookie开头并且最后有一个正确的crc。如果收到部分内容,则可以跳过字节,直到到达下一个 cookie。
  • 您写道,您必须想办法将长度信息与实际数据分开。您可以像现在一样简单地获取前 4 个字节,但是代码中最轻微的错误将阻止您的接收器再次恢复。诚然,TCP/IP 永远不会传递损坏或乱序的消息,但您的客户端很容易在传输过程中崩溃。 为什么要冒险?这也将允许随时简单地切换到非可靠协议 (UDP)。而 cookie 的重点是让自己在 FIFO 缓冲区的中间跳转并立即看到消息边界。
  • 另外,您还可以免费获得版本控制。你改变了主意并想要使用 8 位长度(出于任何疯狂的原因)?引入一个新的 cookie。您想通过单一渠道进行点对点通信吗?引入一个支持扩展“标头”的新 cookie(例如消息长度、发件人信息、收件人信息、任何其他元数据)。你已经为未来的一系列问题做好了准备。我们公司不得不扩展许多定义不明确的协议并使用各种 hack 来确保向后兼容性,而拥有包装消息是迄今为止被证明很好的一种方法。
  • 这真的很有帮助,让我改变了对我正在考虑实现的一些算法的想法。为了简单起见,我想我将在没有 CRC 检查的情况下实现 MSG XXXX(数据)。非常感谢 Groo。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-05-26
  • 1970-01-01
  • 1970-01-01
  • 2017-05-30
  • 1970-01-01
  • 1970-01-01
  • 2017-07-31
相关资源
最近更新 更多