Сетевое программирование си шарп

Сетевое программирование си шарп

Рассмотрим однонаправленную связь между сокетом-клиентом и сокетом-сервером, когда либо сервер посылает данные, а клиент получает, либо, наоборот, клиент отправляет данные, а сервер получает.

Отправка данных клиенту

Сначала рассмотрим ситуацию, когда сервер отправляет данные, а клиент только их получает. Так, определим для сервера следующий код:

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($"Клиенту отправлены данные"); > > catch(Exception ex)

В качестве примера просто отправляем клиенту текущее время в формате 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($"Текущее время: "); > catch (Exception ex)

На стороне клиента мы знаем, что сервер отправляет дату в виде строки, и для ее считывания определяем буфер — массив из 512 байтов. С помощью метода tcpClient.ReceiveAsync() считываем данные из потока, конвертируем байты в строку и выводим ее на консоль.

Читайте также:  Инструментальные средства визуального компонентного программирования

Запустим сервер и клиент. При обращении к серверу клиент получит текущее время:

А консоль сервера отобразит 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(); // определяем буфер для получения данных byte[] responseData = new byte[512]; int bytes = 0; // количество считанных байтов var response = new StringBuilder(); // для склеивания данных в строку // считываем данные do < bytes = await tcpClient.ReceiveAsync(responseData); response.Append(Encoding.UTF8.GetString(responseData, 0, bytes)); >while (bytes > 0); // выводим отправленные клиентом данные Console.WriteLine(response); > > catch(Exception ex)

Здесь с помощью метода tcpClient.ReceiveAsync() считываем данные из потока в массив байтов. В данном случае предполагаем, что данные будут представлять строку. И после получения из байтов строки выводим ее на консоль.

На клиенте определим простейший код для отправки некоторой строки:

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)

В данном случае с помощью метода 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(); // буфер для считывания одного байта 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($»Получено сообщение: «); > > catch(Exception ex)

Допустим, здесь в качестве маркера окончания сообщения будет выступать символ \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)

Хотя это в какой-то степени несколько примитивное упрощение, поскольку в данном случае мы ограничиваемся отправкой однострочного текста. Но в реальности логика определения конечного маркера может быть более сложной, особенно когда маркер представляет не одним байт/символ, а несколько, но общий принип будет тем же.

В данном случае можно оптимизировать программу, вынеся считывание одного байта в отдельный метод расширения для класса 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(); 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($"Получено сообщение: "); > > 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(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($"Размер сообщения: байтов"); Console.WriteLine($"Сообщение: "); > > catch(Exception ex)

В данном случае мы предполагаем, что размер будет представлять значение типа 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)

Здесь происходит обратный процесс. Сначала получаем размер сообщения в массив байтов методом 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(); // буфер для считывания одного байта 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($»Получено сообщение: «); buffer.Clear(); > > > catch(Exception ex)

Итак, здесь наш протокол клиент-серверного взаимодействия состоит из двух правил. Во-первых, каждое отдельное сообщение должно заканчиваться переводом строки \n. Во-вторых, когда клиент хочет завершить взаимодействие и отключиться от сервера, он посылает команду «END». Поэтому в бесконечном цикле обрабатываем все сообщения от клиента, а во вложенном цикле считываем по байту:

Если пришла команда «END», выходит из бесконечного цикла:

var message = Encoding.UTF8.GetString(buffer.ToArray()); // если прислан маркер окончания взаимодействия, // выходим из цикла и завершаем взаимодействие с клиентом if (message == "END") break; Console.WriteLine($"Получено сообщение: "); 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)

Здесь каждое отправляемое сообщение оканчивается терминальным символом \n, кроме того, последнее сообщение представляет команду окончания взаимодействия «END». В итоге при подключении этого клиента сервер оторазит на консоли все присланные сообщения, кроме команды «END», которая завершит взаимодействие клиента и сервера:

Сервер запущен. Ожидание подключений. Получено сообщение: Hello METANIT.COM Получено сообщение: Hello Tcplistener Получено сообщение: Bye METANIT.COM

Источник

Сетевое программирование в .NET

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

Интернет-приложения

Интернет-приложения можно разделить на два типа: клиентские приложения, запрашивающие сведения, и серверные приложения, которые отвечают на информационные запросы от клиентов. Классическим интернет-серверным приложением является Интернет, где пользователи используют браузеры для доступа к документам и другим данным, хранящимся на веб-серверах по всему миру.

Приложения не ограничиваются только одной из этих ролей; Например, знакомый сервер приложений среднего уровня отвечает на запросы клиентов, запрашивая данные с другого сервера. В этом случае он выступает как сервер, так и клиент.

Клиентское приложение запрашивает, определяя запрошенный интернет-ресурс и протокол связи, используемый для запроса и ответа. При необходимости клиент также предоставляет любые дополнительные данные, необходимые для выполнения запроса, например расположение прокси-сервера или сведения для проверки подлинности (имя пользователя, пароль и т. д.). Как только запрос сформирован, его можно отправить на сервер.

Определение ресурсов

.NET использует универсальный код ресурса (URI) для идентификации запрошенного интернет-ресурса и протокола связи. Универсальный код ресурса (URI) состоит по крайней мере из трех и, возможно, четырех фрагментов: идентификатор схемы, который идентифицирует протокол связи для запроса и ответа; идентификатор сервера, который состоит из имени узла системы доменных имен (DNS) или TCP-адреса, который уникально идентифицирует сервер в Интернете; идентификатор пути, который находит запрошенные сведения на сервере; и необязательная строка запроса, которая передает сведения от клиента серверу.

Тип System.Uri используется в качестве представления универсального идентификатора ресурса (URI) и легкого доступа к частям URI. Чтобы создать Uri экземпляр, можно передать ему строку:

const string uriString = "https://learn.microsoft.com/en-us/dotnet/path?key=value#bookmark"; Uri canonicalUri = new(uriString); Console.WriteLine(canonicalUri.Host); Console.WriteLine(canonicalUri.PathAndQuery); Console.WriteLine(canonicalUri.Fragment); // Sample output: // learn.microsoft.com // /en-us/dotnet/path?key=value // #bookmark 

Класс Uri автоматически выполняет проверку и канонизацию для каждого RCF 3986. Эти правила проверки и канонизации используются, чтобы убедиться, что URI имеет правильный формат и что URI находится в канонической форме.

См. также раздел

Источник

Оцените статью