如果我们不从TcpClient流中读取,接收到的数据会发生什么

本文关键字:数据 什么 我们 TcpClient 读取 如果 | 更新日期: 2023-09-27 18:22:34

我对tcp流中的一个案例感到好奇。

假设我们创建了一个TcpClient,并向传出流写入一些有意义的请求字符串。例如Http请求。

try
        {
            string requestString = "GET /Api/Test HTTP/1.1 'r'n" +
                                   "Host: 192.168.2.45 'r'n" +
                                   "Connection:close 'r'n'r'n";
            TcpClient client = new TcpClient();
            client.Connect(new IPEndPoint(IPAddress.Parse("192.168.2.45"), 80));
            NetworkStream stream = client.GetStream();
            byte[] reqBuffer = System.Text.Encoding.Default.GetBytes(requestString);
            stream.Write(reqBuffer, 0, reqBuffer.Length);

因此,我们的NIC会立即从目标套接字接收到响应。

以下是我的问题:

  1. 传入的字节存储在哪里?(NIC是否有存储器,或者存储在RAM中?)
  2. 如果我们不从应用程序中读取这些字节,NIC会对它们做什么

读取:

if (stream.CanRead)
{
   bufferInt = stream.Read(buffer, 0, client.ReceiveBufferSize);
}

如果我们不从TcpClient流中读取,接收到的数据会发生什么

与许多其他事情一样,答案在于分层。

所以,让我们从硬件开始:

所有NIC都有一些内部缓冲区。这是组装任何响应的第一个地方,但这也是像TCP这样的东西实际上意义不大的地方;NIC关心的只是它自己的网络协议,例如以太网或PPP。在这个级别上,IP只是一个不区分的有效负载,而IP又以TCP作为有效负载(尽管应该注意的是,分层还远远不够完美:)例如,TCP和IP之间有很多耦合)。

必须先对传入的数据进行解释,然后才能执行任何操作;让我们跳过细节,假设NIC缓冲区现在包含一个漂亮的小TCP/IP数据包。现在,NIC驱动程序开始发挥作用——机器上的每个打开的端口都有一块相关的内存,用于接收数据。基本上,这是您在设置ReceiveBufferSizeSendBufferSize时控制的内容。驱动程序会指示NIC如何处理传入的数据——通常,NIC会使用DMA将数据直接发送到RAM。这是非常快的-现代NIC实际上不需要有大的板载存储芯片;即使对于服务器NIC,其数量通常也在32 MiB左右。

对于您的问题,这两个RAM缓冲区是最重要的——当它们满了时,NIC只会丢弃任何到达的数据包。在具有流量控制的TCP的情况下,它会告诉对方停止传输,谢谢。实际上,这模仿了缓冲流的通常行为——发送者将被阻止,直到可以再次发送另一段数据为止。当这种情况发生时,发送方将重新发送上次未发送的数据。在像UDP这样的协议的情况下,没有流量控制,也没有重新传输,所以你只是不可逆转地丢失了数据(甚至没有告诉你有问题)。

如果您有一个挂起的发送/接收操作(例如NetworkStream.Read),您也将使用自己的缓冲区——这是另一层,但它实际上是最不重要的。这里发生的一切是,当操作系统从NIC驱动程序获得信息时,它会用内部缓冲区的数据填充缓冲区,并向您发出信号。在同步场景中(如您的情况),这只会导致Read调用返回。异步场景有些棘手,因为.NET和操作系统交互以生成回调,但其余部分基本相同。例如,这与从本地硬盘读取文件没有本质区别。

在.NET中需要注意的一件重要事情是,在这些发送/接收中使用的缓冲区在操作期间被固定。这意味着缓冲区被禁止在内存中移动,这会降低垃圾收集器的有效性(阻止正确的堆压缩);如果你有很多长时间运行的操作,你真的希望尽可能多地重用缓冲区——如果你总是为每个操作创建一个新的缓冲区,你可能会面临堆碎片的问题。

  1. 它存储在RAM中,使用的RAM数量为client.ReceiveBufferSize。网卡上还有少量缓冲存储器,这将取决于网卡。

  2. 一旦RAM缓冲区已满,操作系统将停止从网卡中读取,这将导致网卡的缓冲区已装满,并停止确认传入的数据包。这将导致TCP窗口缩小到0,发送器将停止发送,直到收到来自网卡的确认,只有当数据离开网卡的缓冲区时才会发生这种情况。

不是答案,但这里有一个代码片段,让您可以使用ReceiveBufferSize和SendBufferSize属性。

下面发生了什么?

在您按下回车键之前,服务器不会读取传入的缓冲区,当您这样做时,您可以看到客户端发送了额外的缓冲区。

public class Program
{
    public static void Main(string[] args)
    {
        TcpListener server = new TcpListener(IPAddress.Any, 8989);
        server.Start();
        server.BeginAcceptSocket(AcceptSocket, server);
        TcpClient client = new TcpClient();
        client.Connect("127.0.0.1", 8989);
        FileStream dataToSend = File.OpenRead("c:''temp''videoUpload.rar");
        byte[] tempSendBuffer = new byte[4096];
        int numberOfBytesRead = 0;
        while ((numberOfBytesRead = dataToSend.Read(tempSendBuffer, 0, tempSendBuffer.Length)) > 0)
        {
            client.GetStream().Write(tempSendBuffer, 0, numberOfBytesRead);
            Console.WriteLine("{0} bytes sent", numberOfBytesRead);
        }
        Console.ReadLine();
    }
    private static void AcceptSocket(IAsyncResult asyncResult)
    {
        Socket localSocket = (asyncResult.AsyncState as TcpListener).EndAcceptSocket(asyncResult);
        while (true)
        {
            if (localSocket.Available > 0)
            {
                Console.WriteLine("Local Socket has {0} bytes pending to be received. Enter to receive", localSocket.Available);
                Console.ReadLine();
                byte[] tempReadBuffer = new byte[4096];
                int numberOfReceivedBytes = localSocket.Receive(tempReadBuffer);
                Console.WriteLine("{0} bytes RECEIVED", numberOfReceivedBytes);
            }
            Thread.Sleep(500);
        }
    }
}