Рассмотрим однонаправленную связь между клиентом TcpClient и сервером TcpListener, когда либо сервер посылает данные, а клиент получает, либо, наоборот, клиент отправляет данные, а сервер получает. По сути здесь рассмотриваются те же примеры, что и в случае с сокетами в прошлой статье, только с использованием TcpListener и TcpClient.
Сначала рассмотрим ситуацию, когда сервер отправляет данные, а клиент только их получает. Так, определим для сервера следующий код:
using System.Net; using System.Net.Sockets; using System.Text; var tcpListener = new TcpListener(IPAddress.Any, 8888); try { tcpListener.Start(); // запускаем сервер Console.WriteLine("Сервер запущен. Ожидание подключений... "); while (true) { // получаем подключение в виде TcpClient using var tcpClient = await tcpListener.AcceptTcpClientAsync(); // получаем объект NetworkStream для взаимодействия с клиентом var stream = tcpClient.GetStream(); // определяем данные для отправки - отправляем текущее время byte[] data = Encoding.UTF8.GetBytes(DateTime.Now.ToLongTimeString()); // отправляем данные await stream.WriteAsync(data); Console.WriteLine($"Клиенту {tcpClient.Client.RemoteEndPoint} отправлены данные"); } } finally { tcpListener.Stop(); }
В качестве примера просто отправляем клиенту текущее время в формате hh:mm:ss. Для этого конвертируем строку в массив байтов и отправляем их с помощью метода stream.WriteAsync(). А на консоль сервера выводим диагностическое сообщение.
На стороне клиента определим следующий код:
using System.Net.Sockets; using System.Text; using TcpClient tcpClient = new TcpClient(); await tcpClient.ConnectAsync("127.0.0.1", 8888); // буфер для считывания данных byte[] data = new byte[512]; // получаем NetworkStream для взаимодействия с сервером var stream = tcpClient.GetStream(); // получаем данные из потока int bytes = await stream.ReadAsync(data); // получаем отправленное время string time = Encoding.UTF8.GetString(data, 0, bytes); Console.WriteLine($"Текущее время: {time}");
На стороне клиента мы знаем, что сервер отправляет дату в виде строки, и для ее считывания определяем буфер - массив из 512 байтов. С помощью метода stream.ReadAsync()
считываем данные из потока, конвертируем байты в строку и выводим ее на консоль.
Запустим сервер и клиент. При обращении к серверу клиент получит текущее время:
Текущее время: 20:16:49
А консоль сервера отобразит ip-адрес клиента:
Сервер запущен. Ожидание подключений... Клиенту 127.0.0.1:65060 отправлены данные
Теперь рассмотрим другой вид однонаправленной связи между клиентом и серверов, когда, наоборот, клиент отправляет данные, а сервер просто получает. Так, определим для сервера следующий код:
using System.Net; using System.Net.Sockets; using System.Text; var tcpListener = new TcpListener(IPAddress.Any, 8888); try { tcpListener.Start(); // запускаем сервер Console.WriteLine("Сервер запущен. Ожидание подключений... "); while (true) { // получаем подключение в виде TcpClient using var tcpClient = await tcpListener.AcceptTcpClientAsync(); // получаем объект NetworkStream для взаимодействия с клиентом var stream = tcpClient.GetStream(); // определяем буфер для получения данных byte[] responseData = new byte[1024]; int bytes = 0; // количество считанных байтов var response = new StringBuilder(); // для склеивания данных в строку // считываем данные do { bytes = await stream.ReadAsync(responseData); response.Append(Encoding.UTF8.GetString(responseData, 0, bytes)); } while(bytes > 0); // выводим отправленные клиентом данные Console.WriteLine(response); } } catch (Exception ex) { Console.WriteLine(ex.Message); } finally { tcpListener.Stop(); // останавливаем сервер }
Здесь получаем у tcpClient поток NetworkStream и используем его для считывания данных от клиента. В частности, с помощью метода stream.ReadAsync()
считываем данные из потока в массив байтов. В данном случае предполагаем, что данные будут представлять строку.
И после получения из байтов строки выводим ее на консоль.
На клиенте определим простейший код для отправки некоторой строки:
using System.Net.Sockets; using System.Text; using TcpClient tcpClient = new TcpClient(); await tcpClient.ConnectAsync("127.0.0.1", 8888); // сообщение для отправки var message = "Hello METANIT.COM"; // считыванием строку в массив байт byte[] requestData = Encoding.UTF8.GetBytes(message); // получаем NetworkStream для взаимодействия с сервером var stream = tcpClient.GetStream(); // отправляем данные await stream.WriteAsync(requestData); Console.WriteLine("Сообщение отправлено");
В данном случае с помощью метода stream.WriteAsync()
отправляем данные серверу. В качестве данных выступает простая строка.
Запустим сервер и клиент. После успешного подключения и отправки данных на консоли клиента мы увидим соответствующее сообщение:
Сообщение отправлено
А сервер получит от клиента сообщение и отобразит его на консоли:
Сервер запущен. Ожидание подключений... Hello METANIT.COM
Одна из наиболее сложных частей в клиент-серверном взаимодействии в tcp - это получение данных. И здесь нет однозначного решения, но есть ряд стратегий:
Использование буфера фиксированной длины, когда мы точно знаем, какой именно объем данных будет послан
Отправка в ответе информации о размере ответа, получив которую, нам будет проще считать нужное количество байтов
Использование маркера окончания ответа, получив который, мы завершим считывание данных
Использование любой из этих стратегий подразумевает определение пусть и примитивного но протокола взаимодействия между клиентом и сервером, когда клиент и сервер в соответствии с едиными правилами формируют, отправляют и извлекают данные из потока.
Версия с фиксированным буфером довольно очевидная, поэтому рассмотрим две остальных стратегии.
Определим следующий код сервера:
using System.Net; using System.Net.Sockets; using System.Text; var tcpListener = new TcpListener(IPAddress.Any, 8888); try { tcpListener.Start(); // запускаем сервер Console.WriteLine("Сервер запущен. Ожидание подключений... "); while (true) { // получаем подключение в виде TcpClient using var tcpClient = await tcpListener.AcceptTcpClientAsync(); // получаем объект NetworkStream для взаимодействия с клиентом var stream = tcpClient.GetStream(); // буфер для входящих данных var buffer = new List<byte>(); int bytesRead = '\n'; // считываем данные до конечного символа while((bytesRead = stream.ReadByte())!='\n') { // добавляем в буфер buffer.Add((byte)bytesRead); } var message = Encoding.UTF8.GetString(buffer.ToArray()); Console.WriteLine($"Получено сообщение: {message}"); } } finally { tcpListener.Stop(); // останавливаем сервер }
Здесь с помощью метода stream.ReadByte()
считываем каждый байт из потока. Результатом метода является представление байта в виде int. Допустим, здесь
в качестве маркера окончания сообщения будет выступать символ \n или перевод строки, который представляет значение 10. И если встретился перевод строки, то заканчиваем чтение
данных и конвертируем полученные данные в строку.
В этом случае клиент должен будет добавлять в конец сообщения символ \n:
using System.Net.Sockets; using System.Text; using TcpClient tcpClient = new TcpClient(); await tcpClient.ConnectAsync("127.0.0.1", 8888); // сообщение для отправки // сообщение завершается конечным символом - \n, // который символизирует окончание сообщения var message = "Hello METANIT.COM\n"; // получаем NetworkStream для взаимодействия с сервером var stream = tcpClient.GetStream(); // считыванием строку в массив байт byte[] requestData = Encoding.UTF8.GetBytes(message); // отправляем данные await stream.WriteAsync(requestData); Console.WriteLine("Сообщение отправлено");
Хотя это в какой-то степени несколько примитивное упрощение, поскольку в данном случае мы ограничиваемся отправкой однострочного текста. Но в реальности логика определения конечного маркера может быть более сложной, особенно когда маркер представляет не одним байт/символ, а несколько, но общий принип будет тем же.
Определим следующий сервер:
using System.Net; using System.Net.Sockets; using System.Text; var tcpListener = new TcpListener(IPAddress.Any, 8888); try { tcpListener.Start(); // запускаем сервер Console.WriteLine("Сервер запущен. Ожидание подключений... "); while (true) { // получаем подключение в виде TcpClient using var tcpClient = await tcpListener.AcceptTcpClientAsync(); // получаем объект NetworkStream для взаимодействия с клиентом var stream = tcpClient.GetStream(); // буфер для считывания размера данных byte[] sizeBuffer = new byte[4]; // сначала считываем размер данных await stream.ReadExactlyAsync(sizeBuffer, 0, sizeBuffer.Length); // узнаем размер и создаем соответствующий буфер int size = BitConverter.ToInt32(sizeBuffer, 0); // создаем соответствующий буфер byte[] data = new byte[size]; // считываем собственно данные int bytes = await stream.ReadAsync(data); var message = Encoding.UTF8.GetString(data, 0, bytes); Console.WriteLine($"Размер сообщения: {size} байтов"); Console.WriteLine($"Сообщение: {message}"); } } finally { tcpListener.Stop(); // останавливаем сервер }
В данном случае мы предполагаем, что размер будет представлять значение типа int - то есть значение в 4 байта. Соответственно для считывания размера создаем буфер из 4 байт и считываем его с помощью
метода stream.ReadExactlyAsync
, который считывает определенное количество байт из потока (в данном случае 4 байта):
await stream.ReadExactlyAsync(sizeBuffer, 0, sizeBuffer.Length);
Считав размер, мы можем конвертировать его в число int с помощью статического метода BitConverter.ToInt32()
, определить буфер соответствующей длины и считать в него данные.
На стороне клиента определим следующий код:
using System.Net.Sockets; using System.Text; using TcpClient tcpClient = new TcpClient(); await tcpClient.ConnectAsync("127.0.0.1", 8888); // сообщение для отправки var message = "Hello METANIT.COM"; // получаем NetworkStream для взаимодействия с сервером var stream = tcpClient.GetStream(); // считыванием строку в массив байт byte[] data = Encoding.UTF8.GetBytes(message); // определяем размер данных byte[] size = BitConverter.GetBytes(data.Length); // отправляем размер данных await stream.WriteAsync(size); // отправляем данные await stream.WriteAsync(data); Console.WriteLine("Сообщение отправлено");
Здесь происходит обратный процесс. Сначала получаем размер сообщения в массив байтов методом BitConverter.GetBytes()
:
byte[] size = BitConverter.GetBytes(data.Length);
Отправляем размер в виде четырех байтов и затем отправляем сами данные:
await stream.WriteAsync(size); await stream.WriteAsync(data);
В итоге после отправки клиентом данных консоль сервера отобразит размер данных и сами данные:
Сервер запущен. Ожидание подключений... Размер сообщения: 17 байтов Сообщение: Hello METANIT.COM
Выше клиент отправлял, а сервер получал только одно сообщение. Но что, если отправляется множество сообщений, которые сервер должен получать по отдельности. В этом случае нам опять нужно определить какой-нибудь протокол, в соответствии с которым сервер может определить, когда завершается одной сообщение, а когда клиент вообще завершает взавимодействие. Рассмотрим небольшой пример. Пусть сервер будет иметь следующий код:
using System.Net; using System.Net.Sockets; using System.Text; var tcpListener = new TcpListener(IPAddress.Any, 8888); try { tcpListener.Start(); Console.WriteLine("Сервер запущен. Ожидание подключений... "); while (true) { // получаем подключение в виде TcpClient using var tcpClient = await tcpListener.AcceptTcpClientAsync(); // получаем объект NetworkStream для взаимодействия с клиентом var stream = tcpClient.GetStream(); // буфер для входящих данных var buffer = new List<byte> (); int bytesRead = 10; while(true) { // считываем данные до конечного символа while ((bytesRead = stream.ReadByte()) != '\n') { // добавляем в буфер buffer.Add((byte)bytesRead); } var message = Encoding.UTF8.GetString(buffer.ToArray()); // если прислан маркер окончания взаимодействия, // выходим из цикла и завершаем взаимодействие с клиентом if (message == "END") break; Console.WriteLine($"Получено сообщение: {message}"); buffer.Clear(); } } } finally { tcpListener.Stop(); }
Итак, здесь наш протокол клиент-серверного взаимодействия состоит из двух правил. Во-первых, каждое отдельное сообщение должно заканчиваться переводом строки \n. Во-вторых, когда клиент хочет завершить взаимодействие и отключиться от сервера, он посылает команду "END". Поэтому в бесконечном цикле обрабатываем все сообщения от клиента, а во вложенном цикле считываем по байту:
while(true) { // считываем данные до конечного символа while ((bytesRead = stream.ReadByte()) != '\n') { // добавляем в буфер buffer.Add((byte)bytesRead); }
Если пришла команда "END", выходит из бесконечного цикла:
var message = Encoding.UTF8.GetString(buffer.ToArray()); // если прислан маркер окончания взаимодействия, // выходим из цикла и завершаем взаимодействие с клиентом if (message == "END") break; Console.WriteLine($"Получено сообщение: {message}"); buffer.Clear(); }
Для работы с этим сервером определим следующий клиент:
using System.Net.Sockets; using System.Text; using TcpClient tcpClient = new TcpClient(); await tcpClient.ConnectAsync("127.0.0.1", 8888); // сообщения для отправки // сообщение завершается конечным символом - \n, // который символизирует окончание сообщения var messages = new string[] { "Hello METANIT.COM\n", "Hello Tcplistener\n", "Bye METANIT.COM\n", "END\n" }; // получаем NetworkStream для взаимодействия с сервером var stream = tcpClient.GetStream(); foreach (var message in messages) { // считыванием строку в массив байт byte[] data = Encoding.UTF8.GetBytes(message); // отправляем данные await stream.WriteAsync(data); } Console.WriteLine("Все сообщения отправлены");
Здесь каждое отправляемое сообщение оканчивается терминальным символом \n, кроме того, последнее сообщение представляет команду окончания взаимодействия "END". В итоге при подключении этого клиента сервер оторазит на консоли все присланные сообщения, кроме команды "END", которая завершит взаимодействие клиента и сервера:
Сервер запущен. Ожидание подключений... Получено сообщение: Hello METANIT.COM Получено сообщение: Hello Tcplistener Получено сообщение: Bye METANIT.COM