По какому принципу создавать TCP соединения при многопоточных запросах?

В текущем проекте на ASP.NET есть необходимость в общении с неким сервером посредством TCP в формате запрос-ответ. Ниже представлен метод, который отвечает за это:

public async Task<byte[]> GetBytesAsync(List<byte> bodyBytes)
{
    async Task WriteAsyncWithTimeout(NetworkStream stream, byte[] buffer, int offset, int count, CancellationToken cT)
    {
        Task writeTask = stream.WriteAsync(buffer, offset, count, cT);
        await Task.WhenAny(writeTask, Task.Delay(stream.WriteTimeout));
        if (!writeTask.IsCompleted)
            throw new TimeoutException();

        await writeTask;
    }

    async Task<int> ReadAsyncWithTimeout(NetworkStream stream, byte[] buffer, int offset, int count, CancellationToken cT)
    {
        Task<int> readTask = stream.ReadAsync(buffer, offset, count, cT);
        await Task.WhenAny(readTask, Task.Delay(stream.ReadTimeout));
        if (!readTask.IsCompleted)
            throw new TimeoutException();

        return await readTask;
    }

    try
    {
        var bytes = new List<byte>();
        bytes.AddRange(bodyBytes);

        Byte[] bytesArray = bytes.ToArray();

        if (!_client.Connected)
            await _client.ConnectAsync(_ipAddress, 1200, _token);

        NetworkStream stream = _client.GetStream();
        await WriteAsyncWithTimeout(stream, bytesArray, 0, bytesArray.Length, _token);

        var receivedBytes = new byte[SIZE];
        Int32 bytesRead = await ReadAsyncWithTimeout(stream, receivedBytes, 0, receivedBytes.Length, _token);
        return receivedBytes.Take(bytesRead).ToArray();
    }
    catch (Exception e)
    {
        ...
    }
}

Всё было хорошо, когда был один поток, в котором TCP запросы выполнялись синхронно. Однако когда появилась необходимость обрабатывать и другие запросы, которые приходили в других потоках, использование одного и того же TcpClient приводило к тому, что ответы с сервера путались местами. То есть на запрос А приходил ответ с запроса Б и наоборот. Проблема решилась созданием других экземпляров TcpClient (соединений). Теперь же, на каждую определённую задачу приходится создавать TCP клиент (соединение) и выносить их в поле. Но не похоже, что это является нормальной практикой.

P.S. Раннее я всегда использовал HTTP клиент и не приходилось плодить экземпляры HttpClient, в рамках приложения всегда использовался один.


Ответы (3 шт):

Автор решения: rotabor

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

Протокол HTTP обеспечивает такую функцию.

В простейшем случае, к массиву передаваемых байтов нужно добавить номер запроса, который генерируется клиентом последовательно. Числа типа Int в качестве номера запроса будет вполне достаточно. Диапазона хватит для индентификации огромного количества запросов на интервале времени задержки ответа.

→ Ссылка
Автор решения: Swift - Friday Pie

Самый простейший способ - начинить протокол идентификацией запросов. Самый простейший способ - ввести инкрементирующий счетчик запросов, значение которого передается в запросе. А ответ содержит то же значение. Так им образов ы избежите

  1. Выполнение одного и того же запроса дважды или (если надо) просто отошлете тот же ответ что отсылался раньше (если семантика того требует).
  2. Отсутствие путаницы в том что же это за сообщение пришло.
  3. Бонус: корректная обработка ошибок и отслеживание неудачных запросов - нам не ответили в течение 20 секунд, а другие сообщения идут? что-то пошло не так.

Сообщения не обязательно руками заворачивать в "пользовательском" коде. Это и медленно и особенно некрасиво в случае использования интерпретирующих языков, да и нет гарантии что оно развернуто будет так же на другой стороне. Можно применить что-нибудь вроде Google Protobuf.

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

→ Ссылка
Автор решения: eri

Если протокол поменять нет возможности, то используйте пул коннектов. Например откройте 4 соединения и передавайте их разным таскам по очереди. После использования возвращайте в пул.

→ Ссылка