C# Async TCP-сокеты: обработка размера буфера и огромные передачи

При использовании блокирующего сокета 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 МБ? Я мог бы установить размер буфера 10*1024*1024... но это потратило бы постоянные 10 МБ ОЗУ на время работы приложения. Это глупо.

Итак, мой вопрос: как я могу эффективно получать большие порции данных с помощью асинхронных TCP-сокетов?

2 ответа

Решение

Два примера не эквивалентны - ваш код блокировки предполагает, что удаленный конец отправляет 32-битную длину данных, чтобы следовать. Если тот же протокол действителен для асинхронного - просто прочитайте эту длину (блокирующую или нет), затем выделите буфер и инициируйте асинхронный ввод-вывод.

Изменить 0:

Позвольте мне также добавить, что выделение буферов размера, введенного пользователем, и особенно сетевого ввода, является квитанцией за катастрофу. Очевидной проблемой является атака типа "отказ в обслуживании", когда клиент запрашивает огромный буфер и удерживает его, скажем, отправляет данные очень медленно, и предотвращает другие распределения и / или замедляет работу всей системы.

Общепринятое мнение здесь - принимать фиксированный объем данных за раз и анализировать их на ходу. Это, конечно, влияет на ваш дизайн протокола уровня приложения.

ОТРЕДАКТИРОВАНО




Лучший подход к этой проблеме, найденный мной после длительного анализа, был следующим:

  • Во-первых, вам нужно установить размер буфера, чтобы получать данные с сервера/клиента.

  • Во-вторых, вам нужно найти скорость загрузки/выгрузки для этого соединения.

  • В-третьих, вам необходимо рассчитать, сколько секунд должно длиться время ожидания соединения в соответствии с размером пакета, который необходимо отправить или получить.

Установить размер буфера


Размер буфера может быть установлен двумя способами, произвольно или объективно. Если принимаемая информация имеет текстовую основу, она невелика и не требует сравнения символов, то оптимальным является произвольный заданный размер буфера. Если получаемая информация должна обрабатываться посимвольно и/или в больших объемах, оптимальным выбором является объективный размер буфера.

      // 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. Полученная длина полезной нагрузки затем преобразуется обратно в формат строки, а затем преобразуется в целое число, чтобы установить длину буфера, который будет получать полезную нагрузку. Длина преобразуется в строку, затем в , а затем в , чтобы избежать повреждения данных из-за того, что информация, относящаяся к длине полезной нагрузки, не будет отправлена ​​в буфер того же размера, что и информация. Когда приемник преобразуетbyte[]содержимое в строку, а затем вint, лишние символы будут удалены, а информация останется прежней.

Получите скорость загрузки/выгрузки соединения и рассчитайте размер буфера приема и отправки сокета.


  • Во-первых, создайте класс, отвечающий за расчет размера буфера для каждого соединения.
      // 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

. _

Другие вопросы по тегам