在C#中使用Socket api时,TCP数据包是如何到达的

时间:2017-01-31 00:53:01

标签: c# sockets networking tcp

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

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

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

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

我知道这个问题很容易在google上显示,但我永远不知道它是否在正确的上下文中(谈论实际的TCP协议,或者C#将如何向程序员公开网络流量)。

感谢。

3 个答案:

答案 0 :(得分: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

  

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

答案 1 :(得分:1)

  

我的问题是,在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文章。第二个链接进入更多细节,它还有示例代码。

答案 2 :(得分:0)

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

  

我一直在阅读有关TCP数据包的信息,以及它们在航行期间如何分割它们的次数。

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

  

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

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

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

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

  

BTW,我正在通过TCP发送长度为前缀的protobuf序列化消息。

那很好。与IMO备选方案相比,长度前缀更容易处理。

  

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

没有

  

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

是的,是的。

更有趣:

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

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

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

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