Среди главных проблем с которой можно столкнуться при обмене данными по сети, это длительное ожидание ответа сервера или «зависание» серверной части в бесконечном цикле при ожидании запроса от клиента.
В этой статье мы будем решать вторую проблему путём превращения «обычного» сервера, который был написан в одной из предыдущих статей [1] в асинхронный.
Сразу отметим, что здесь мы не будем затрагивать вопросы касающиеся непосредственно обмена данными по сети. Они подробно описаны в [1] и в рамках наших сегодняшних доработок мы их практически не коснёмся. Поэтому, если вы не знакомы с основными принципами работы с TCP в C#, рекомендуется вначале прочитать статью [1] (ссылка дана в конце).
Доработаем алгоритм серверной части из [1].
Добавим поле, отвечающее за «включение» и «выключение» серверной части (рекомендуется по умолчанию присваивать ему значение true).
private bool run = true;
Для того чтобы «включить» или «выключить» работу серверной части будем присваивать этому полю значение true или false соответственно.
Далее нам предстоит самое сложное. Заменить метод AcceptTcpClient класса TcpListener на метод этого же класса под названием AcceptTcpClientAsync и доработать алгоритм серверной части для корректной обработки значения поля run.
Понять, что сделано и, главное, с какой целью в данном случае проще всего на конкретном примере.
Ниже приведён уже доработанный пример серверной части из [1]. Внесённые изменения и их смысл поясняются в комментариях к коду.
private async Task ListenAsync() { IPAddress localAddr = IPAddress.Parse("127.0.0.1"); int port = 8888; TcpListener server = new TcpListener(localAddr, port); server.Start(); // Выполняем цикл только, если серверная часть «включена» while (run) { try { // Асинхронное подключение клиента TcpClient client = await server.AcceptTcpClientAsync(); NetworkStream stream = client.GetStream(); // Обмен данными только, если серверная часть «включена». try { // Читаем данные if (stream.CanRead && run) { byte[] myReadBuffer = new byte[1024]; StringBuilder myCompleteMessage = new StringBuilder(); int numberOfBytesRead = 0; do { numberOfBytesRead = stream.Read(myReadBuffer, 0, myReadBuffer.Length); myCompleteMessage.AppendFormat("{0}", Encoding.UTF8.GetString(myReadBuffer, 0, numberOfBytesRead)); } while (stream.DataAvailable); Byte[] responseData = Encoding.UTF8.GetBytes("УСПЕШНО!"); stream.Write(responseData, 0, responseData.Length); } } finally { stream.Close(); client.Close(); } } catch { server.Stop(); break; } } // Если серверная часть «выключена», обязательно останавливаем прослушивание порта. // Иначе потом серверная часть не «включится». if (!run) { server.Stop(); } }
Если сравнить с исходным вариантом, то изменений в коде не так уж и много, но в то же время работа алгоритма и приложения, где он используется, меняется кардинально. Теперь последнее не будет «зависать» после запуска бесконечного цикла. Более того, бесконечный цикл по своей сути перестал быть бесконечным. Работой алгоритма теперь можно управлять.
«Включение» серверной части можно выполнить следующим образом:
run = true; ListenAsync();
Или так (альтернативный вариант для C# 7.0 и выше):
run = true; _ = ListenAsync();
«Отключение»:
run = false;
Также стоит отметить, что в целом материал данной статьи применим для C# 5.0 (.NET Framework 4.5) и выше. В предыдущих версиях (4.0 и ниже) класс Task, а также операторы async/await отсутствуют. Для этих версий необходимо использовать низкоуровневую реализацию на основе Thread. Последняя в данной статье не рассматривается по причине глубокого морального устаревания данного подхода.
Источники
- Обмен данными по сети в C# (протокол TCP)