Рассмотрим однонаправленную связь между сокетом-клиентом и сокетом-сервером, когда либо сервер посылает данные, а клиент получает, либо, наоборот, клиент отправляет данные, а сервер получает.
Сначала рассмотрим ситуацию, когда сервер отправляет данные, а клиент только их получает. Так, определим для сервера следующий код:
using System.Net; using System.Net.Sockets; using System.Text; IPEndPoint ipPoint = new IPEndPoint(IPAddress.Any, 8888); using Socket tcpListener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try { tcpListener.Bind(ipPoint); tcpListener.Listen(); // запускаем сервер Console.WriteLine("Сервер запущен. Ожидание подключений... "); while (true) { // получаем входящее подключение using var tcpClient = await tcpListener.AcceptAsync(); // определяем данные для отправки - текущее время byte[] data = Encoding.UTF8.GetBytes(DateTime.Now.ToLongTimeString()); // отправляем данные await tcpClient.SendAsync(data); Console.WriteLine($"Клиенту {tcpClient.RemoteEndPoint} отправлены данные"); } } catch(Exception ex) { Console.WriteLine(ex.Message); }
В качестве примера просто отправляем клиенту текущее время в формате hh:mm:ss. Для этого после получения подключения конвертируем строку в массив байтов и отправляем их с помощью метода tcpClient.SendAsync()
.
А на консоль сервера выводим диагностическое сообщение.
На стороне клиента определим следующий код:
using System.Net.Sockets; using System.Text; using var tcpClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try { await tcpClient.ConnectAsync("127.0.0.1", 8888); // буфер для считывания данных byte[] data = new byte[512]; // получаем данные из потока int bytes = await tcpClient.ReceiveAsync(data); // получаем отправленное время string time = Encoding.UTF8.GetString(data, 0, bytes); Console.WriteLine($"Текущее время: {time}"); } catch (Exception ex) { Console.WriteLine(ex.Message); }
На стороне клиента мы знаем, что сервер отправляет дату в виде строки, и для ее считывания определяем буфер - массив из 512 байтов. С помощью метода tcpClient.ReceiveAsync()
считываем данные из потока, конвертируем байты в строку и выводим ее на консоль.
Запустим сервер и клиент. При обращении к серверу клиент получит текущее время:
Текущее время: 20:16:49
А консоль сервера отобразит ip-адрес клиента:
Сервер запущен. Ожидание подключений... Клиенту 127.0.0.1:65060 отправлены данные
Теперь рассмотрим другой вид однонаправленной связи между клиентом и серверов, когда, наоборот, клиент отправляет данные, а сервер просто получает. Так, определим для сервера следующий код:
using System.Net; using System.Net.Sockets; using System.Text; IPEndPoint ipPoint = new IPEndPoint(IPAddress.Any, 8888); using Socket tcpListener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try { tcpListener.Bind(ipPoint); tcpListener.Listen(); // запускаем сервер Console.WriteLine("Сервер запущен. Ожидание подключений... "); while (true) { // получаем подключение в виде TcpClient using var tcpClient = await tcpListener.AcceptAsync(); // определяем буфер для получения данных List<byte> response = []; byte[] buffer = new byte[512]; int bytes = 0; // количество считанных байтов // считываем данные do { bytes = await tcpClient.ReceiveAsync(buffer); // добавляем полученные байты в список response.AddRange(buffer.Take(bytes)); } while (bytes > 0); // выводим отправленные клиентом данные var responseText = Encoding.UTF8.GetString(response.ToArray()); Console.WriteLine(responseText); } } catch(Exception ex) { Console.WriteLine(ex.Message); }
Здесь с помощью метода tcpClient.ReceiveAsync()
считываем данные из потока в массив байтов. Затем добавляем этот массив в список response, который хранит все полученные байты.
В данном случае предполагаем, что данные будут представлять строку.
И после получения из байтов строки выводим ее на консоль.
На клиенте определим простейший код для отправки некоторой строки:
using System.Net.Sockets; using System.Text; using var tcpClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try { await tcpClient.ConnectAsync("127.0.0.1", 8888); // сообщение для отправки var message = "Hello METANIT.COM"; // считыванием строку в массив байт byte[] requestData = Encoding.UTF8.GetBytes(message); // отправляем данные await tcpClient.SendAsync(requestData); Console.WriteLine("Сообщение отправлено"); } catch (Exception ex) { Console.WriteLine(ex.Message); }
В данном случае с помощью метода tcpClient.SendAsync()
отправляем данные серверу. В качестве данных выступает простая строка.
Запустим сервер и клиент. После успешного подключения и отправки данных на консоли клиента мы увидим соответствующее сообщение:
Сообщение отправлено
А сервер получит от клиента сообщение и отобразит его на консоли:
Сервер запущен. Ожидание подключений... Hello METANIT.COM
Одна из наиболее сложных частей в клиент-серверном взаимодействии в tcp - это отправка и получение данных. И здесь нет однозначного решения, как сделать все максимально оптимально, но есть ряд стратегий:
Использование буфера фиксированной длины, когда мы точно знаем, какой именно объем данных будет послан
Отправка в ответе информации о размере ответа, получив которую, нам будет проще считать нужное количество байтов
Использование маркера окончания ответа, получив который, мы завершим считывание данных
Использование любой из этих стратегий подразумевает определение пусть и примитивного но протокола взаимодействия между клиентом и сервером, когда клиент и сервер в соответствии с едиными правилами формируют, отправляют и извлекают данные из потока.
Версия с фиксированным буфером довольно очевидная, поэтому рассмотрим две остальных стратегии.
Определим следующий код сервера:
using System.Net; using System.Net.Sockets; using System.Text; using Socket tcpListener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try { tcpListener.Bind(new IPEndPoint(IPAddress.Any, 8888)); tcpListener.Listen(); // запускаем сервер Console.WriteLine("Сервер запущен. Ожидание подключений... "); while (true) { // получаем подключение в виде TcpClient using var tcpClient = await tcpListener.AcceptAsync(); // буфер для накопления входящих данных var buffer = new List<byte>(); // буфер для считывания одного байта var bytesRead = new byte[1]; // считываем данные до конечного символа while (true) { var count = await tcpClient.ReceiveAsync(bytesRead); // смотрим, если считанный байт представляет конечный символ, выходим if (count == 0 || bytesRead[0] == '\n') break; // иначе добавляем в буфер buffer.Add(bytesRead[0]); } var message = Encoding.UTF8.GetString(buffer.ToArray()); Console.WriteLine($"Получено сообщение: {message}"); } } catch(Exception ex) { Console.WriteLine(ex.Message); }
Допустим, здесь в качестве маркера окончания сообщения будет выступать символ \n или перевод строки, который представляет значение 10. Чтобы отследить конечный символ, считываем посимвольно. Для считывания определяем массив из одного байта. Считываем в него по байту и проверяем его значение. И если встретился перевод строки, то заканчиваем чтение данных и конвертируем полученные данные в строку.
В этом случае клиент должен будет добавлять в конец сообщения символ \n:
using System.Net.Sockets; using System.Text; using var tcpClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try { await tcpClient.ConnectAsync("127.0.0.1", 8888); // сообщение для отправки // сообщение завершается конечным символом - \n, // который символизирует окончание сообщения var message = "Hello METANIT.COM\n"; // считыванием строку в массив байт byte[] requestData = Encoding.UTF8.GetBytes(message); // отправляем данные await tcpClient.SendAsync(requestData); Console.WriteLine("Сообщение отправлено"); } catch (Exception ex) { Console.WriteLine(ex.Message); }
Хотя это в какой-то степени несколько примитивное упрощение, поскольку в данном случае мы ограничиваемся отправкой однострочного текста. Но в реальности логика определения конечного маркера может быть более сложной, особенно когда маркер представляет не одним байт/символ, а несколько, но общий принип будет тем же.
В данном случае можно оптимизировать программу, вынеся считывание одного байта в отдельный метод расширения для класса Socket:
using System.Net; using System.Net.Sockets; using System.Text; using Socket tcpListener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try { tcpListener.Bind(new IPEndPoint(IPAddress.Any, 8888)); tcpListener.Listen(); // запускаем сервер Console.WriteLine("Сервер запущен. Ожидание подключений... "); while (true) { // получаем подключение в виде TcpClient using var tcpClient = await tcpListener.AcceptAsync(); // буфер для входящих данных var buffer = new List<byte>(); int bytesRead = '\n'; // считываем данные до конечного символа while (true) { bytesRead = tcpClient.ReadByte(); if(bytesRead== '\n' || bytesRead==0) break; // добавляем в буфер buffer.Add((byte)bytesRead); } var message = Encoding.UTF8.GetString(buffer.ToArray()); Console.WriteLine($"Получено сообщение: {message}"); } } catch(Exception ex) { Console.WriteLine(ex.Message); } public static class SocketExtension { public static int ReadByte(this Socket socket) { byte b = 0; var buffer = new Span<byte>(ref b); var count= socket.Receive(buffer); return count== 0 ? -1 : b; } }
Определим следующий сервер:
using System.Net; using System.Net.Sockets; using System.Text; using Socket tcpListener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try { tcpListener.Bind(new IPEndPoint(IPAddress.Any, 8888)); tcpListener.Listen(); // запускаем сервер Console.WriteLine("Сервер запущен. Ожидание подключений... "); while (true) { // получаем подключение в виде TcpClient using var tcpClient = await tcpListener.AcceptAsync(); // буфер для считывания размера данных byte[] sizeBuffer = new byte[4]; // сначала считываем размер данных await tcpClient.ReceiveAsync(sizeBuffer); // узнаем размер и создаем соответствующий буфер int size = BitConverter.ToInt32(sizeBuffer, 0); // создаем соответствующий буфер byte[] data = new byte[size]; // считываем собственно данные int bytes = await tcpClient.ReceiveAsync(data); var message = Encoding.UTF8.GetString(data, 0, bytes); Console.WriteLine($"Размер сообщения: {size} байтов"); Console.WriteLine($"Сообщение: {message}"); } } catch(Exception ex) { Console.WriteLine(ex.Message); }
В данном случае мы предполагаем, что размер будет представлять значение типа int - то есть значение в 4 байта. Соответственно для
считывания размера создаем буфер из 4 байт и считываем в него первую часть данных. Считав размер, мы можем конвертировать его в число int с
помощью статического метода BitConverter.ToInt32()
, определить буфер соответствующей длины и считать в него данные.
byte[] sizeBuffer = new byte[4]; await tcpClient.ReceiveAsync(sizeBuffer); int size = BitConverter.ToInt32(sizeBuffer, 0); byte[] data = new byte[size]; int bytes = await tcpClient.ReceiveAsync(data);
На стороне клиента определим следующий код:
using System.Net.Sockets; using System.Text; using var tcpClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try { await tcpClient.ConnectAsync("127.0.0.1", 8888); // сообщение для отправки var message = "Hello METANIT.COM"; // считыванием строку в массив байт byte[] data = Encoding.UTF8.GetBytes(message); // определяем размер данных byte[] size = BitConverter.GetBytes(data.Length); // отправляем размер данных await tcpClient.SendAsync(size); // отправляем данные await tcpClient.SendAsync(data); Console.WriteLine("Сообщение отправлено"); } catch (Exception ex) { Console.WriteLine(ex.Message); }
Здесь происходит обратный процесс. Сначала получаем размер сообщения в массив байтов методом BitConverter.GetBytes()
:
byte[] size = BitConverter.GetBytes(data.Length);
Отправляем размер в виде четырех байтов и затем отправляем сами данные:
await tcpClient.SendAsync(size); await tcpClient.SendAsync(data);
В итоге после отправки клиентом данных консоль сервера отобразит размер данных и сами данные:
Сервер запущен. Ожидание подключений... Размер сообщения: 17 байтов Сообщение: Hello METANIT.COM
Выше клиент отправлял, а сервер получал только одно сообщение. Но что, если отправляется множество сообщений, которые сервер должен получать по отдельности. В этом случае нам опять нужно определить какой-нибудь протокол, в соответствии с которым сервер может определить, когда завершается одной сообщение, а когда клиент вообще завершает взавимодействие. Рассмотрим небольшой пример. Пусть сервер будет иметь следующий код:
using System.Net; using System.Net.Sockets; using System.Text; using Socket tcpListener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try { tcpListener.Bind(new IPEndPoint(IPAddress.Any, 8888)); tcpListener.Listen(); // запускаем сервер Console.WriteLine("Сервер запущен. Ожидание подключений... "); while (true) { // получаем подключение в виде TcpClient using var tcpClient = await tcpListener.AcceptAsync(); // буфер для накопления входящих данных var buffer = new List<byte>(); // буфер для считывания одного байта var bytesRead = new byte[1]; while (true) { // считываем данные до конечного символа while (true) { var count = await tcpClient.ReceiveAsync(bytesRead); // смотрим, если считанный байт представляет конечный символ, выходим if (count == 0 || bytesRead[0] == '\n') break; // иначе добавляем в буфер buffer.Add(bytesRead[0]); } var message = Encoding.UTF8.GetString(buffer.ToArray()); // если прислан маркер окончания взаимодействия, // выходим из цикла и завершаем взаимодействие с клиентом if (message == "END") break; Console.WriteLine($"Получено сообщение: {message}"); buffer.Clear(); } } } catch(Exception ex) { Console.WriteLine(ex.Message); }
Итак, здесь наш протокол клиент-серверного взаимодействия состоит из двух правил. Во-первых, каждое отдельное сообщение должно заканчиваться переводом строки \n. Во-вторых, когда клиент хочет завершить взаимодействие и отключиться от сервера, он посылает команду "END". Поэтому в бесконечном цикле обрабатываем все сообщения от клиента, а во вложенном цикле считываем по байту:
while (true) { while (true) { var count = await tcpClient.ReceiveAsync(bytesRead); if (count == 0 || bytesRead[0] == '\n') break; buffer.Add(bytesRead[0]); }
Если пришла команда "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 var tcpClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try { 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" }; foreach (var message in messages) { // считыванием строку в массив байт byte[] data = Encoding.UTF8.GetBytes(message); // отправляем данные await tcpClient.SendAsync(data); } Console.WriteLine("Все сообщения отправлены"); } catch (Exception ex) { Console.WriteLine(ex.Message); }
Здесь каждое отправляемое сообщение оканчивается терминальным символом \n, кроме того, последнее сообщение представляет команду окончания взаимодействия "END". В итоге при подключении этого клиента сервер оторазит на консоли все присланные сообщения, кроме команды "END", которая завершит взаимодействие клиента и сервера:
Сервер запущен. Ожидание подключений... Получено сообщение: Hello METANIT.COM Получено сообщение: Hello Tcplistener Получено сообщение: Bye METANIT.COM