【问题标题】:How does a TCP packet arrive when using the Socket api in C#在 C# 中使用 Socket api 时 TCP 数据包如何到达
【发布时间】:2017-01-31 22:35:16
【问题描述】:

我一直在阅读有关 TCP 数据包以及它们在航行期间如何多次拆分的信息。我假设我必须在用于实际网络流量的缓冲区的顶部实现某种缓冲区,以便存储每个ReceiveAsync(),直到有足够的数据可用于解析消息。顺便说一句,我正在通过 TCP 发送长度前缀、protobuf 序列化的消息。

然后我读到较低层(以太网?IP?)实际上会透明地重新组装数据包。

我的问题是,在 C# 中,我能保证通过 TCP 接收完整的“消息”吗?换句话说,如果我发送 32 个字节,我是否一定会在“一次性”中收到这 32 个字节(一次调用ReceiveAsync())?还是我必须“存储”每次接收,直到接收到的字节数等于长度前缀?

另外,我能否在一次调用 ReceiveAsync() 时收到更多条消息?说一个“protobuf 消息”是 32 个字节。我送了2个。我是否可以“一次性”接收 48 个字节,然后再接收 16 个字节?

我知道这个问题在谷歌上很容易出现,但我永远无法判断它是否在正确的上下文中(谈论实际的 TCP 协议,或者 C# 如何将网络流量暴露给程序员)。

谢谢。

【问题讨论】:

  • 虽然 TCP 数据在传输过程中可以被拆分并采用不同的路由,但可以说这对应用程序是 100% 透明的。您需要做的就是安全地接收它,因为它是为您重新组装的。这不是 UDP 的情况
  • @MickyD 虽然您的陈述是正确的,但我认为您错过了问题的重点。他询问的是应用程序在任何给定时刻可用的数据量。
  • @JonathonReinhart 这就是为什么这是评论而不是答案

标签: c# sockets networking tcp


【解决方案1】:

TCP 是一个流协议 - 它传输字节流。就这样。绝对没有暗示消息框架/分组。事实上,在使用 TCP 套接字编写代码时,您甚至应该忘记以太网数据包或 IP 数据报。

您可能会发现自己有 1 个字节可用,或 10,000 个字节可供读取。 (同步)Berkeley 套接字 API 的美妙之处在于,作为应用程序程序员,您无需担心这一点。由于您使用的是以长度为前缀的消息格式(干得好!)只需 recv() 与您期望的一样多的字节。如果有 更多 字节可用,而不是应用程序请求,内核将保留其余的缓冲,直到下一次调用。如果可用的字节数少于所需的,则线程将阻塞或调用将指示接收到的字节数较少。在这种情况下,您可以简单地再次休眠,直到数据可用。

异步 ​​API 的问题在于它需要应用程序自己跟踪更多的状态。甚至这个Microsoft example of Asynchronous Client Sockets 也比它需要的复杂得多。使用异步 API,您仍然可以控制从内核请求的数据量,但是当触发异步回调时,您需要知道要请求的下一个数据量。

请注意,4.5 中的 C# async/await 使异步处理更容易,因为您可以以同步方式进行。看看this answer作者在哪里cmets:

Socket.ReceiveAsync 是一个奇怪的。它与 .net4.5 中的 async/await 功能无关。它被设计为一种替代套接字 API,它不会像 BeginReceive/EndReceive 那样严重消耗内存,并且只需要在最核心的服务器应用程序中使用。

【讨论】:

  • 感谢您的回复。所以这意味着如果我使用 new(er) SocketAsyncEventArgs,我将不得不实现自己的消息框架(就像你提到的 berkeley sockets api)?
  • 或多或少,是的。如果您不需要大量的并行连接,我会首先编写您的代码以使用同步 API,并在必要时创建一个新线程来处理每个连接。然后考虑使用 .NET 中最新的异步支持以更少的线程处理更多的连接。
  • 当你说“严重庞大”时,我们在说什么?我正在制作一个(非常基本的)mmo 游戏服务器,所以想想 10k + 连接,每秒都发送 2 或 3 个位置更新以及任何其他信息。在你告诉我这不可能之前,这只是为了教育目的,而不是为了实际生产:)
【解决方案2】:

我的问题是,在 C# 中,我能保证通过 TCP 接收完整的“消息”吗?

没有。您不会收到完整的消息。单次发送不会导致单次接收。您必须在接收方继续阅读,直到您收到所需的一切。

看这里的例子,它将读取的数据保存在缓冲区中,并不断检查是否还有更多数据要读取:

private static void ReceiveCallback(IAsyncResult ar)
{
    try
    {
        // Retrieve the state object and the client socket 
        // from the asynchronous state object.
        StateObject state = (StateObject)ar.AsyncState;
        Socket client = state.workSocket;
        // Read data from the remote device.
        int bytesRead = client.EndReceive(ar);
        if (bytesRead > 0)
        {
            // There might be more data, so store the data received so far.
            state.sb.Append(Encoding.ASCII.GetString(state.buffer, 0, bytesRead));
            //  Get the rest of the data.
            client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0,
                new AsyncCallback(ReceiveCallback), state);
        }
        else
        {
            // All the data has arrived; put it in response.
            if (state.sb.Length > 1)
            {
                response = state.sb.ToString();
            }
            // Signal that all bytes have been received.
            receiveDone.Set();
        }
    }
    catch (Exception e)
    {
        Console.WriteLine(e.ToString());
    }
}

有关详细信息,请参阅 this MSDN 文章和 this 文章。第二个链接更详细,它也有示例代码。

【讨论】:

  • 可能也不会收到完整的消息,并且单次发送不一定对应于单次接收。
【解决方案3】:

TCP 是一种基于流的八位字节协议。因此,从应用程序的角度来看,您只能读取或写入 bytes 到流中。

我一直在阅读有关 TCP 数据包以及它们如何在航行中多次拆分的信息。

TCP 数据包是一个网络实现细节。它们用于提高效率(一次发送一个字节会非常低效)。数据包碎片是在设备驱动程序/硬件级别完成的,并且永远不会暴露给应用程序。应用程序永远不知道“数据包”是什么或它的边界在哪里。

我假设我必须在用于实际网络流量的缓冲区之上实现某种缓冲区,以便存储每个 ReceiveAsync(),直到有足够的数据可用于解析消息。

是的。因为“消息”不是 TCP 的概念。这纯粹是一个应用概念。 大多数应用程序协议确实定义了一种“消息”,因为它更容易推理。

然而,一些应用程序协议并没有定义“消息”的概念;他们将 TCP 流视为实际流,而不是消息序列。

为了支持这两种应用协议,TCP/IP API必须是基于流的。

顺便说一句,我正在通过 TCP 发送带有长度前缀、protobuf 序列化的消息。

这很好。长度前缀比 IMO 更容易处理。

我的问题是,在 C# 中,我能保证通过 TCP 接收完整的“消息”吗?

没有。

或者我是否必须“存储”每次接收,直到接收到的字节数等于长度前缀?另外,我可以在一次 ReceiveAsync() 调用中收到多条消息吗?

是的,是的。

更有趣:

  • 您只能获取部分长度前缀(假设是多字节长度前缀)。
  • 您可以一次收到任意数量的消息。
  • 您的缓冲区可以包含消息的一部分,或消息长度前缀的一部分。
  • 下一次读取可能无法完成当前消息,甚至是当前消息的长度前缀。

有关详细信息,请参阅我的TCP/IP .NET FAQ,尤其是message framing 和一些example code for length-prefixed messages 的部分。

我强烈建议在生产中仅使用异步 API;每个连接有两个线程的同步替代方案会对可伸缩性产生负面影响。

哦,我也总是建议尽可能使用 SignalR。原始 TCP/IP 套接字编程总是很复杂。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-03-27
    • 1970-01-01
    • 1970-01-01
    • 2018-09-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多