C# 异步 TCP 套接字:处理缓冲区大小和大量传输

发布于 2024-10-31 12:54:39 字数 776 浏览 0 评论 0原文

当使用阻塞 TCP 套接字时,我不必指定缓冲区大小。例如:

using (var client = new TcpClient())
{
    client.Connect(ServerIp, ServerPort);

    using (reader = new BinaryReader(client.GetStream()))
    using (writer = new BinaryWriter(client.GetStream()))
    {
        var byteCount = reader.ReadInt32();
        reader.ReadBytes(byteCount);
    }
}

注意远程主机如何发送任意数量的字节。

但是,当使用异步 TCP 套接字时,我需要创建一个缓冲区,从而硬编码最大大小:

 var buffer = new byte[BufferSize];
 socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, callback, null);

我可以简单地将缓冲区大小设置为 1024 字节。如果我只需要接收小块数据,那就可以了。但是如果我需要接收 10 MB 的序列化对象怎么办?我可以将缓冲区大小设置为 10*1024*1024...但是只要应用程序正在运行,这就会浪费恒定的 10 MB RAM。这很愚蠢。

所以,我的问题是:如何使用异步 TCP 套接字有效地接收大块数据?

When using a blocking TCP socket, I don't have to specify a buffer size. For example:

using (var client = new TcpClient())
{
    client.Connect(ServerIp, ServerPort);

    using (reader = new BinaryReader(client.GetStream()))
    using (writer = new BinaryWriter(client.GetStream()))
    {
        var byteCount = reader.ReadInt32();
        reader.ReadBytes(byteCount);
    }
}

Notice how the remote host could have sent any number of bytes.

However, when using async TCP sockets, I need to create a buffer and thus hardcode a maximum size:

 var buffer = new byte[BufferSize];
 socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, callback, null);

I could simply set the buffer size to, say, 1024 bytes. That'll work if I only need to receive small chunks of data. But what if I need to receive a 10 MB serialized object? I could set the buffer size to 10*1024*1024... but that would waste a constant 10 MB of RAM for as long as the application is running. This is silly.

So, my question is: How can I efficiently receive big chunks of data using async TCP sockets?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

窝囊感情。 2024-11-07 12:54:39

两个示例并不等效 - 您的阻塞代码假设远程端发送了后续数据的 32 位长度。如果相同的协议对异步有效 - 只需读取该长度(是否阻塞),然后分配缓冲区并启动异步 IO。

编辑0:

让我还补充一点,分配用户输入的缓冲区,尤其是网络输入的缓冲区大小是灾难的收据。一个明显的问题是当客户端请求巨大的缓冲区并保留时,会发生拒绝服务攻击它 - 说发送数据非常慢 - 并阻止其他分配和/或减慢整个系统。

这里的常识是一次接受固定数量的数据并进行解析。这当然会影响您的应用程序级协议设计。

Two examples are not equivalent - your blocking code assumes the remote end sends the 32-bit length of the data to follow. If the same protocol is valid for the async - just read that length (blocking or not) and then allocate the buffer and initiate the asynchronous IO.

Edit 0:

Let me also add that allocating buffers of user-entered, and especially of network-input, size is a receipt for disaster. An obvious problem is a denial-of-service attack when client requests a huge buffer and holds on to it - say sends data very slowly - and prevents other allocations and/or slows the whole system.

Common wisdom here is accepting a fixed amount of data at a time and parsing as you go. That of course affects your application-level protocol design.

眼趣 2024-11-07 12:54:39

已编辑


经过长时间的分析,我发现解决这个问题的最佳方法如下:

  • 首先,您需要设置缓冲区大小以便从服务器/客户端接收数据。< /p>

  • 其次,您需要找到该连接的上传/下载速度。

  • 第三,根据要发送或接收的包的大小,计算连接超时应该持续多少秒。

设置缓冲区大小


缓冲区大小可以通过两种方式设置:任意设置或客观设置。如果要接收的信息是基于文本的,它并不大并且不需要字符比较,那么任意预设的缓冲区大小是最佳的。如果要接收的信息需要逐字符处理和/或很大,则目标缓冲区大小是最佳选择。

// In this example I used a Socket wrapped inside a NetworkStream for simplicity
// stability, and asynchronous operability purposes.

// This can be done by doing this:
//
//  For server:
//
//  Socket server= new Socket();
//  server.ReceiveBufferSize = 18000;
//  IPEndPoint iPEndPoint = new IPEndPoint(IPAddress.Any, port);
//  server.Bind(iPEndPoint);
//  server.Listen(3000);
// 
// 
//  NetworkStream ns = new NetworkStream(server);

//  For client:
//  
//  Socket client= new Socket();
//  client.Connect("127.0.0.1", 80);
//  
//  NetworkStream ns = new NetworkStream(client);

// In order to set an objective buffer size based on a file's size in order not to
// receive null characters as extra characters because the buffer is bigger than
// the file's size, or a corrupted file because the buffer is smaller than
// the file's size.

//   The TCP protocol follows the Sys, Ack and Syn-Ack paradigm,  
//   so within a TCP connection if the client or server began the 
//   connection by sending a message, the next message within its
//   connection must be read, and if the client or server began 
//   the connection by receiving a message, the next message must
//   be sent.

// [SENDER]

byte[] file = new byte[18032];

byte[] file_length = Encoding.UTF8.GetBytes(file.Length.ToString());

await Sender.WriteAsync(file_length, 0, file_length.Length);

byte[] receiver_response = new byte[1800];

await Sender.ReadAsync(receiver_response, 0, receiver_response.Length);

await Sender.WriteAsync(file, 0, file.Length);

// [SENDER]

// [RECEIVER]

byte[] file_length = new byte[1800];

await Receiver.ReadAsync(file_length, 0, file_length.Length);

byte[] encoded_response = Encoding.UTF8.GetBytes("OK");

await Receiver.WriteAsync(encoded_response, 0, encoded_response.Length);

byte[] file = new byte[Convert.ToInt32(Encoding.UTF8.GetString(file_length))];

await Receiver.ReadAsync(file, 0, file.Length);

// [RECEIVER]

用于接收有效负载长度的缓冲区使用任意缓冲区大小。将要发送的有效负载的长度转换为字符串,然后将字符串转换为 UTF-8 编码的字节数组。然后,接收到的有效负载长度被转换回字符串格式,然后转换为整数,以便设置将接收有效负载的缓冲区的长度。将长度转换为字符串,然后转换为int,然后转换为byte[],以避免由于与有效负载长度相关的信息被破坏而导致数据损坏。不会被发送到与信息大小相同的缓冲区中。当接收方将byte[]内容转换为字符串,然后转换为int时,多余的字符将被删除,信息将保持不变。

获取连接的上传/下载速度并计算Socket接收和发送缓冲区大小


  • 首先,创建一个类,负责计算每个连接的缓冲区大小。
// In this example I used a Socket wrapped inside a NetworkStream for simplicity
// stability, and asynchronous operability purposes.

// This can be done by doing this:
//
//  For server:
//
//  Socket server= new Socket();
//  server.ReceiveBufferSize = 18000;
//  IPEndPoint iPEndPoint = new IPEndPoint(IPAddress.Any, port);
//  server.Bind(iPEndPoint);
//  server.Listen(3000);
// 
//  NetworkStream ns = new NetworkStream(server);

//  For client:
//  
//  Socket client= new Socket();
//  client.Connect("127.0.0.1", 80);
//
//  NetworkStream ns = new NetworkStream(client);

    class Internet_Speed_Checker
    {
        public async Task<bool>> Optimum_Buffer_Size(System.Net.Sockets.NetworkStream socket)
        {
             System.Diagnostics.Stopwatch latency_counter = new System.Diagnostics.Stopwatch();

             byte[] test_payload = new byte[2048];

             //   The TCP protocol follows the Sys, Ack and Syn-Ack paradigm,  
             //   so within a TCP connection if the client or server began the 
             //   connection by sending a message, the next message within its
             //   connection must be read, and if the client or server began 
             //   the connection by receiving a message, the next message must
             //   be sent.
             //
             //   In order to test the connection, the client and server must 
             //   send and receive a package of the same size. If the client 
             //   or server began the connection by sending a message, the 
             //   client or server must do the this connection test by 
             //   initiating a write-read sequence, else it must do this
             //   connection test initiating a read-write sequence.

             latency_counter .Start();

             await client_secure_network_stream.ReadAsync(test_payload, 0, test_payload.Length);

             
             
             await client_secure_network_stream.WriteAsync(test_payload, 0, test_payload.Length);

             latency_counter .Stop();
             
             int bytes_per_second = (int)(test_payload.Length * (1000 / latency_time_counter.Elapsed.TotalMilliseconds));

             int optimal_connection_timeout = (Convert.ToInt32(payload_length) / download_bytes_per_second) * 1000 + 1000;

             double optimal_buffer_size_double = ((download_bytes_per_second / 125000) * (latency_time_counter.Elapsed.TotalMilliseconds / 1000)) * 1048576;

             int optimal_buffer_size = (int)download_optimal_buffer_size_double + 1024;
             
             // If you want to upload data to the client/server -->  client.SendBufferSize = optimal_buffer_size;
             // client.SendTimeout = optimal_connection_timeout;
             
             // If you want to download data from the client/server -->  client.ReceiveBufferSize = optimal_buffer_size;
             // client.ReceiveTimeout = optimal_connection_timeout;
        }
    }

上述方法是确保客户端缓冲区和服务器缓冲区之间传输的数据使用适当的套接字缓冲区大小和套接字连接超时,以避免数据损坏和碎片。当通过具有异步读/写操作的套接字发送数据时,要发送的信息的长度将以数据包为单位进行分段。数据包大小有一个默认值,但它没有涵盖连接的上传/下载速度变化的事实。为了避免数据损坏和最佳的连接下载/上传速度,必须根据连接速度设置数据包大小。在上述示例中,我还展示了如何计算与连接速度相关的超时。上传/下载的数据包大小可以分别使用 socket.ReceiveBufferSize = ... / socket.SendBufferSize = ... 设置。

有关所用方程和原理的更多信息,请查看:

https://www. baeldung.com/cs/calculate-internet-speed-ping

https://docs.oracle.com/cd/E36784_01/html/E37476/gnkor.html#: ~:text=您%20可以%20计算%20%20正确的%20%20连接%20延迟的值%20

EDITED


The best approach for this problem found by me, after a long analysis was the following:

  • First, you need to set the buffer size in order to receive data from the server/client.

  • Second, you need to find the upload/download speed for that connection.

  • Third, you need to calculate how many seconds should the connection timeout last in accordance with the size of package to be sent or received.

Set the buffer size


The buffer size can be set in two ways, arbitrary or objectively. If the information to be received is text based, it is not large and it does not require character comparison, than an arbitrary pre-set buffer size is optimal. If the information to be received needs to be processed character by character, and/or large, an objective buffer size is optimal choice

// In this example I used a Socket wrapped inside a NetworkStream for simplicity
// stability, and asynchronous operability purposes.

// This can be done by doing this:
//
//  For server:
//
//  Socket server= new Socket();
//  server.ReceiveBufferSize = 18000;
//  IPEndPoint iPEndPoint = new IPEndPoint(IPAddress.Any, port);
//  server.Bind(iPEndPoint);
//  server.Listen(3000);
// 
// 
//  NetworkStream ns = new NetworkStream(server);

//  For client:
//  
//  Socket client= new Socket();
//  client.Connect("127.0.0.1", 80);
//  
//  NetworkStream ns = new NetworkStream(client);

// In order to set an objective buffer size based on a file's size in order not to
// receive null characters as extra characters because the buffer is bigger than
// the file's size, or a corrupted file because the buffer is smaller than
// the file's size.

//   The TCP protocol follows the Sys, Ack and Syn-Ack paradigm,  
//   so within a TCP connection if the client or server began the 
//   connection by sending a message, the next message within its
//   connection must be read, and if the client or server began 
//   the connection by receiving a message, the next message must
//   be sent.

// [SENDER]

byte[] file = new byte[18032];

byte[] file_length = Encoding.UTF8.GetBytes(file.Length.ToString());

await Sender.WriteAsync(file_length, 0, file_length.Length);

byte[] receiver_response = new byte[1800];

await Sender.ReadAsync(receiver_response, 0, receiver_response.Length);

await Sender.WriteAsync(file, 0, file.Length);

// [SENDER]

// [RECEIVER]

byte[] file_length = new byte[1800];

await Receiver.ReadAsync(file_length, 0, file_length.Length);

byte[] encoded_response = Encoding.UTF8.GetBytes("OK");

await Receiver.WriteAsync(encoded_response, 0, encoded_response.Length);

byte[] file = new byte[Convert.ToInt32(Encoding.UTF8.GetString(file_length))];

await Receiver.ReadAsync(file, 0, file.Length);

// [RECEIVER]

The buffers that are used to receive the payload length are using an arbitrary buffer size. The length of the payload to be sent is converted to string and then the string is converted in a UTF-8 encoded byte array. The received length of the payload is then converted back into a string format and then converted to an integer in order to set the length of the buffer that will receive the payload. The length is converted to string, then to int and then to byte[], in order to avoid data corruption due to the fact that the information related to the payload length will not be sent into a buffer that has the same size as the information. When the receiver will convert the byte[] content to a string and then to an int, the extra characters will be removed and the information will remain the same.

Get the upload/download speed of the connection and calculate the Socket receive and send buffer size


  • First, Make a class that is responsible for calculating the buffer size for each connection.
// In this example I used a Socket wrapped inside a NetworkStream for simplicity
// stability, and asynchronous operability purposes.

// This can be done by doing this:
//
//  For server:
//
//  Socket server= new Socket();
//  server.ReceiveBufferSize = 18000;
//  IPEndPoint iPEndPoint = new IPEndPoint(IPAddress.Any, port);
//  server.Bind(iPEndPoint);
//  server.Listen(3000);
// 
//  NetworkStream ns = new NetworkStream(server);

//  For client:
//  
//  Socket client= new Socket();
//  client.Connect("127.0.0.1", 80);
//
//  NetworkStream ns = new NetworkStream(client);

    class Internet_Speed_Checker
    {
        public async Task<bool>> Optimum_Buffer_Size(System.Net.Sockets.NetworkStream socket)
        {
             System.Diagnostics.Stopwatch latency_counter = new System.Diagnostics.Stopwatch();

             byte[] test_payload = new byte[2048];

             //   The TCP protocol follows the Sys, Ack and Syn-Ack paradigm,  
             //   so within a TCP connection if the client or server began the 
             //   connection by sending a message, the next message within its
             //   connection must be read, and if the client or server began 
             //   the connection by receiving a message, the next message must
             //   be sent.
             //
             //   In order to test the connection, the client and server must 
             //   send and receive a package of the same size. If the client 
             //   or server began the connection by sending a message, the 
             //   client or server must do the this connection test by 
             //   initiating a write-read sequence, else it must do this
             //   connection test initiating a read-write sequence.

             latency_counter .Start();

             await client_secure_network_stream.ReadAsync(test_payload, 0, test_payload.Length);

             
             
             await client_secure_network_stream.WriteAsync(test_payload, 0, test_payload.Length);

             latency_counter .Stop();
             
             int bytes_per_second = (int)(test_payload.Length * (1000 / latency_time_counter.Elapsed.TotalMilliseconds));

             int optimal_connection_timeout = (Convert.ToInt32(payload_length) / download_bytes_per_second) * 1000 + 1000;

             double optimal_buffer_size_double = ((download_bytes_per_second / 125000) * (latency_time_counter.Elapsed.TotalMilliseconds / 1000)) * 1048576;

             int optimal_buffer_size = (int)download_optimal_buffer_size_double + 1024;
             
             // If you want to upload data to the client/server -->  client.SendBufferSize = optimal_buffer_size;
             // client.SendTimeout = optimal_connection_timeout;
             
             // If you want to download data from the client/server -->  client.ReceiveBufferSize = optimal_buffer_size;
             // client.ReceiveTimeout = optimal_connection_timeout;
        }
    }

The aforementioned method is ensuring that the data transmitted between the client buffer and server buffer uses an appropriate socket buffer size and socket connection timeout in order to avoid data corruption and fragmentation. When the data is sent through a socket with an async Read/Write operation, the length of the information to be sent will be segmented in packets. The packet size has a default value but it does not cover the fact that the upload/download speed of the connection is varying. In order to avoid data corruption and an optimal download/upload speed of the connection, the packet size must be set in accordance with the speed of the connection. In the aforementioned example I also showcased also the how to calculate the timeout in relation with the connection speed. The packet size for upload/download can be set by using the socket.ReceiveBufferSize = ... / socket.SendBufferSize = ... respectively.

For more information related to the equations and principles used check:

https://www.baeldung.com/cs/calculate-internet-speed-ping

https://docs.oracle.com/cd/E36784_01/html/E37476/gnkor.html#:~:text=You%20can%20calculate%20the%20correct,value%20of%20the%20connection%20latency.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文