По какому принципу создавать 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 шт):
Вам нужен протокол верхнего уровня, который бы индентифицировал запросы и ответы, чтобы можно было соотносить ответ с запросом.
Протокол HTTP обеспечивает такую функцию.
В простейшем случае, к массиву передаваемых байтов нужно добавить номер запроса, который генерируется клиентом последовательно. Числа типа Int в качестве номера запроса будет вполне достаточно. Диапазона хватит для индентификации огромного количества запросов на интервале времени задержки ответа.
Самый простейший способ - начинить протокол идентификацией запросов. Самый простейший способ - ввести инкрементирующий счетчик запросов, значение которого передается в запросе. А ответ содержит то же значение. Так им образов ы избежите
- Выполнение одного и того же запроса дважды или (если надо) просто отошлете тот же ответ что отсылался раньше (если семантика того требует).
- Отсутствие путаницы в том что же это за сообщение пришло.
- Бонус: корректная обработка ошибок и отслеживание неудачных запросов - нам не ответили в течение 20 секунд, а другие сообщения идут? что-то пошло не так.
Сообщения не обязательно руками заворачивать в "пользовательском" коде. Это и медленно и особенно некрасиво в случае использования интерпретирующих языков, да и нет гарантии что оно развернуто будет так же на другой стороне. Можно применить что-нибудь вроде Google Protobuf.
Дальше есть проблема насколько своевременно нужны ответы на запросы, в ряде случаев приходится отказываться от TCP-обмена просто потому что буферизацией сложнее управлять.
Если протокол поменять нет возможности, то используйте пул коннектов. Например откройте 4 соединения и передавайте их разным таскам по очереди. После использования возвращайте в пул.