Energy
education

сайт для тех, кто хочет изучать энергетику

3. Структура передачи данных

В современном мире для передачи данных разработан и используется модель Open Systems Interconnection (OSI). Модель определяет сетевые протоколы, распределяя их на 7 логических уровней. Важно отметить, что в любом процессе, управление сетевой передачей переходит от уровня к уровню, последовательно подключая протоколы на каждом из уровней. Модель OSI была изначально придумана как стандартный подход, архитектура или паттерн, который бы описывал сетевое взаимодействие любого сетевого приложения.

Определение функции.
Модель Open Systems Interconnection (OSI).

Подробнее про модель OSI вы можете узнать в соответствующем разделе.

Для передачи данных используются различные протоколы транспортного уровня.

Modbus — коммуникационный протокол, основан на архитектуре ведущий-ведомый (master-slave). Использует для передачи данных интерфейсы RS-485, RS-422, RS-232, а также Ethernet сети TCP/IP (протокол Modbus TCP). Modbus был разработан компанией Modicon для использования в её контроллерах с программируемой логикой. Впервые спецификация протокола была опубликована в 1979 году. Основные достоинства стандарта — открытость и массовость. Промышленностью выпускается очень много типов и моделей датчиков, исполнительных устройств, модулей обработки и нормализации сигналов и др. Практически все промышленные системы контроля и управления имеют программные драйверы для работы с MODBUS-сетями. Подробнее про данный протокол вы можеле прочитать в соответствующем разделе.

Протокол TCP позволяет осуществить соединение одного сокета (IP-адрес + порт) хоста источника с сокетом хоста назначения. Заголовок IP будет содержать информацию, связанную с IP-адресами, а заголовок TCP — информацию о порте. Заголовки TCP перемещаются по сети для установления, поддержки и завершения TCP-соединений, а также передачи данных.

TCP или Transmission Control Protocol, используется как надежный протокол, обеспечивающий взаимодействие через взаимосвязанную сеть компьютеров. TCP проверяет, что данные доставляются по назначению и правильно. TCP — это ориентированный на соединения протокол, предназначенный для обеспечения надежной передачи данных между процессами, выполняемыми или на одном и том же компьютере или на разных компьютерах. Термин "ориентированный на соединения" означает, что два процесса или приложения прежде чем обмениваться какими-либо данными должны установить TCP-соединение. В этом TCP отличается от протокола UDP, являющегося протоколом "без организации соединения", позволяющим выполнять широковещательную передачу данных неопределенному числу клиентов.

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

Определение функции.
Модель OSI для TCP.

Чтобы понять, как работает TCP, вкратце рассмотрим структуру заголовка TCP.

Определение функции.
Заголовок TCP.

В заголовке TCP содержаться следующие поля:

  • Source port (16 бит): порт источника. Порт хоста, от которого исходит запрос.
  • Destination port (16 бит): порт назначения. Порт хоста, куда направляется запрос.
  • Sequence number, SYN (32 бита): порядковый номер. Позволяет контролировать порядок сообщений. Каждая конечная точка (как порт источника, так и порт назначения) будут поддерживать свой уникальный порядковый номер для отправляемых сообщений. При установлении соединения TCP (используется сообщение с установленным флагом SYN) в качестве изначального порядкового номера будет сгенерировано случайное число. Вернее, не совсем случайно сгенерировано, а будет содержать конкретное 32-битное число, то есть в пределах от 0 до 4294967295 (или 2 в 32-ой степени возможных вариантов), которое будет соответствовать времени, прошедшему после перегрузки системы отправителя (из расчета +1 за каждые прошедшие 4 микросекунды), а также увеличенное на 64000 каждый раз при установлении нового соединения. Так как сгенерированное число будет уникальным для периода времени почти в пять часов (если при этом никакие соединения не устанавливались), то такой подход к выбору порядкового номера позволяет избежать случайных коллизий при передаче данных, когда для нескольких пакетов из разных соединений будет совпадать порядковый номер. В дальнейшем, при отправке следующих пакетов, значение порядкового номера будет увеличиваться на +1 для всех пакетов с флагом SYN, пакетов с флагом FIN и для каждого байта отправленных данных. Это позволяет принимающей системе обрабатывать пакеты в правильной последовательности, как они были сформированы при отправлении, а не в том порядке, как они были получены.
  • Acknowledgement number, ACK (32 бита): номер подтверждения. Когда сообщение содержит флаг ACK, то значение в номере подтверждения должно соответствовать следующему порядковому номеру (SYN), которое отправитель сообщения с флагом ACK ожидает получить от передающей системы. Таким образом, отправка одного номера подтверждения способна подтвердить получение всех байтов с информацией, полученных до этого. Более наглядно об использовании порядкового номера и номера подтверждения вы можете посмотреть на этом видео:
  • Data offset (4 бита): длина заголовка, известная также как смещение данных. Содержит размер заголовка TCP, измеряемый в 32-битных сегментах. Минимальный размер заголовка TCP составляет пять 32-битных сегментов (всего 20 байт), а максимальный — пятнадцать 32-битных сегмента (или 60 байт).
  • Reserved (3 бита): зарезервировано. Зарезервировано для будущего использования, пока просто забивается нулями. На данный момент осталось три незадействованных бита, в то время как еще три ранее зарезервированных бита уже используются как флаги.
  • Flags, 9 бит (флаги или управляющие биты):
    • NS (1 бит): одноразовая сумма (Nonce Sum). Используется для улучшения работы механизма явного уведомления о перегрузке (Explicit Congestion Notification, ECN).
    • CWR (1 бит): окно перегрузки уменьшено (Congestion Window Reduced). Данный флаг устанавливается отправителем, чтобы показать, что TCP-фрагмент был получен с установленным полем ECE. Таким образом, это является подтверждением получения пакета данных с флажком ECE от хоста получателя и включением отправителем механизма уменьшения перегрузки (Congestion Control), позволяющим оптимизировать отправку пакетов с данными в перегруженных сетях, избежав серьезных задержек из-за отбрасывания пакетов.
    • ECE (1 бит): ECN-Эхо (ECN-Echo). Выполняет двойственную роль, в зависимости от значения флага SYN. При установленном флаге SYN это указывает на то, что отправитель пакета поддерживает ECN. Если флаг SYN сброшен (SYN=0), а ECE установлен, то это означает, что пакет с установленным флагом CE (Congestion Experienced, Подтвержденная перегрузка) был получен в заголовке IP во время обычной передачи. Таким образом, это служит индикатором перегрузки сети (или предстоящей перегрузки) для TCP-отправителя.
    • URG (1 бит). Устанавливается, если необходимо передать ссылку на поле указателя срочности (Urgent pointer).
    • ACK (1 бит). Устанавливается, когда пакет содержит значение номера подтверждения в поле подтверждения. Все пакеты после стартового пакета SYN будут иметь установленный флаг ACK.
    • PSH (1 бит). Делает этот пакет пакетом PUSH (проталкивания). При нормальном потоке передачи данных система получателя не будет подтверждать получение каждого пакета сразу же после его получения. Вместо этого система получателя в течении некоторого времени будет собирать и хранить полученные данные в буфере, пока не передаст их приложению пользователя. Пакет PUSH инструктирует систему получателя немедленно передать все полученные ранее данные из буфера в приложение пользователя и сразу же отправить сообщение с подтверждением.
    • RST (1 бит): сброс данного соединения. Отправкой пакета RST одна из сторон сообщает о немедленном разрыве соединения. При этом соединение обрывается, а буфер очищается. Самые распространенные причины отправки пакета с установленным флагом RST — ответ на пакет, полученный для закрытого сокета; пользователь сам прервал соединение (например, закрыв браузер, не дожидаясь ответа); соединение не было нормально закрыто, но находится в неактивном состоянии некоторое время.
    • SYN (1 бит). Начинает соединение и синхронизирует порядковые номера. Первый пакет, отправленный с каждой стороны, должен в обязательном порядке иметь установленным этот флаг.
    • FIN (1 бит). Одна из конечных точек отправляет пакет с установленным флагом FIN для другой конечной точки, чтобы сообщить, что все пакеты были отправлены, и соединение пора завершить.
  • Window size (16 бит): размер окна приема. В нем указывается количество байт данных, считая от последнего номера подтверждения, которые готов принять отправитель данного пакета. Другими словами, отправитель данного пакета в этом поле сообщает другой стороне, каким доступным на данный момент размером буфера приема данных он располагает.
  • Checksum (16 бит): контрольная сумма. Используется для проверки на наличие ошибок при передаче и/или приеме отправленного пакета. Рассчитывается с учетом заголовка (все поля заголовка, кроме самой контрольной суммы), полезной нагрузки (неслужебные данные с полезной информацией, которая, собственно, и передается), а также псевдо-заголовка (IP-адрес источника, IP-адрес назначения, номер протокола и длина TCP-сегмента, в которой учитывается как длина полей заголовка, так и длина данных полезной нагрузки).
  • Urgent pointer (16 бит): указатель срочности. Если установлен флаг URG, то это означает, что поле указателя срочности содержит численное значение положительного смещения от порядкового номера в сообщении, указывающее на последний байт срочных данных. После получения TCP-сегмента с флагом URG, установленным в значение «1», приемное устройство смотрит на поле указателя срочности и по его значению определяет, какие данные в сегменте являются срочными. Затем эти срочные данные сразу же направляются в приложение пользователя с указанием того, что отправитель пометил данные как срочные. Остальные данные в данном сегменте, как, к слову, и накопившиеся до этого в буфере приема, обрабатываются в нормальном режиме. Этим принцип обработки в сообщении флага URG отличается от обработки флага PSH, при получении которого вся информация из буфера, а не только срочная из сообщения, немедленно передается в приложение пользователя.

Перед тем, как данные могут быть переданы между двумя узлами, в TCP, в отличие от UDP, предусмотрена стадия установки соединения. Также, после того, как все данные были переданы, наступает стадия завершения соединения. Таким образом, осуществление каждого TCP-соединения можно условно разделить на три фазы:

Инициализация соединения. Установка соединения осуществляется с помощью, так называемого трехстороннего рукопожатия TCP. Инициатором соединения может выступать любая сторона, например, клиент инициализирует соединение с сервером.

Определение функции.
Трехстороннее рукопожатие TCP.
  • (Пакет №1). Клиент отправляет пакет с установленным флагом SYN и случайным числом («R1»), включенным в поле порядкового номера (sequence number).
  • (Пакет №2). При получении пакета №1 сервер в ответ отправляет пакет с установленным флагом SYN, а также с установленным флагом ACK. Поле порядкового номера будет содержать новое случайное число («R2»), а поле номера подтверждения будет содержать значение порядкового номера клиента, увеличенного на единицу (то есть «R1 + 1»). Таким образом, он будет соответствовать следующему порядковому номеру, который сервер ожидает получить от клиента.
  • (Пакет №3). В ответ на пакет SYN от сервера (пакет №2) клиент отправляет пакет с установленным флагом ACK и полем номера подтверждения с числом «R2 + 1». По аналогии, это число будет соответствовать следующему порядковому номеру, который клиент ожидает получить от сервера.

После инициализации соединения полезная нагрузка будет перемещаться в обоих направлениях TCP-соединения. Все пакеты в обязательном порядке будут содержать установленный флаг ACK. Другие флаги, такие как, например, PSH или URG, могут быть, а могут и не быть установленными.

При нормальном завершении TCP-соединения в большинстве случаев инициализируется процедура, называемая двухсторонним рукопожатием, в ходе которой каждая сторона закрывает свой конец виртуального канала и освобождает все задействованные ресурсы. Обычно эта фаза начинается с того, что один из задействованных процессов приложения сигнализирует своему уровню TCP, что сеанс связи больше не нужен. Со стороны этого устройства отправляется сообщение с установленным флагом FIN (отметим, что этот пакет не обязательно должен быть пустым, он также может содержать полезную нагрузку), чтобы сообщить другому устройству о своем желании завершить открытое соединение. Затем получение этого сообщения подтверждается (сообщение от отвечающего устройства с установленным флагом ACK, говорящем о получении сообщения FIN). Когда отвечающее устройство готово, оно также отправляет сообщение с установленным флагом FIN, и, после получения в ответ подтверждающего получение сообщения с установленным флагом ACK или ожидания определенного периода времени, предусмотренного для получения ACK, сеанс полностью закрывается. Состояния, через которые проходят два соединенных устройства во время обычного завершения соединения, отличаются, потому что устройство, инициирующее завершение сеанса, ведет себя несколько иначе, чем устройство, которое получает запрос на завершение. В частности, TCP на устройстве, получающем начальный запрос на завершение, должен сразу информировать об этом процесс своего приложения и дождаться от него сигнала о том, что приложение готово к этой процедуре. Инициирующему устройству не нужно это делать, поскольку именно приложение и выступило инициатором.

Определение функции.
Завершение TCP-соединения.

На уровне TCP нет сообщений типа «keep-alive», и поэтому, даже если сеанс соединения в какой-то момент времени становится неактивным, он все равно будет продолжаться до тех пор, пока не будет отправлен следующий пакет.

Когда мы отправляем HTTP-запрос по сети, нам сразу нужно создать TCP-соединение. Однако в HTTP 1.0 возможность повторного использования соединения по умолчанию закрыта (если заголовок «keep-alive = close» дополнительно не включен в заголовок HTTP), то есть TCP-соединение автоматически закрывается после получения запроса и отправки ответа. Так как процесс создания TCP-соединения относительно затратный (он требует дополнительных затрат процессорных ресурсов и памяти, а также увеличивает сетевой обмен между сервером и клиентом, что особенно становится актуальным при создании защищенных соединений), то все это увеличивает количество лагов и повышает вероятность перегрузки сети. Поэтому для HTTP 1.1 было решено оставлять TCP-соединение открытым до тех пор, пока одна из сторон не решит прекратить его.

С другой стороны, если соединения не будут закрываться после того, как клиенты получат все необходимые им данные, задействованные ресурсы сервера для поддержания этих соединений не будут доступны другим клиентам. Поэтому HTTP-серверы, чтобы обеспечить больший контроль над потоком данных, используют временные интервалы (таймауты) для поддержки функциональности «keep-alive» для неактивных соединений (длящихся по умолчанию, в зависимости от архитектуры и конфигурации сервера, не более нескольких десятков секунд, а то и просто нескольких секунд), а также максимальное число отправляемых запросов «keep-alive», прежде чем сеанс без активного соединения будет остановлен.

Существуют два основных компонента, от которых зависит Web: сетевой протокол TCP/IP и HTTP. Почти все события в Web происходят через HTTP, и этот протокол преимущественно используется для обмена документами (такими, как Web-страницы) в World Wide Web.

Обмен данными по сети в C# (протокол TCP)

В .NET за работу с TCP отвечают три класса из пространства имён System.Net.Sockets.

  • Socket — обеспечивает базовый функционал TCP и UDP сокетов. В прикладных целях рекомендуется вместо класса Socket использовать классы TcpListener, TcpClient или UdpClient, которые построены на его основе;
  • TcpListener — этот класс обеспечивает функционал TCP сервера;
  • TcpClient – этот класс работает как TCP клиент. С его помощью осуществляется передача данных от клиента серверу и, как ни странно наоборот.

Рассмотрим пример сетевого взаимодействия по протоколу TCP на основе классов TcpListenet и TcpClient.

Клиентская часть использует только класс TcpClient. По протоколу TCP мы можем передавать только двоичные данные. Поэтому после инициализации объекта класса TcpClient нужно подготовить массив байт для чтения и записи двоичных данных, а также открыть поток для передачи. Отправка данных на сервер производится при помощи метода Write. Первым параметром он принимает оправляемые двоичные данные, второй параметр задаёт сдвиг от начала массива байт (с какого байта начинать передачу, обычно равен нулю (массив байт отправляется целиком)), третий параметр размер двоичных данных (в данном примере массив байт передаётся целиком).

            
                    using System;
                    using System.Net.Sockets;
                    using System.Threading.Tasks;

                    namespace ConsoleApp
                    {
                        class Program
                        {
                            static void Connect(String server, String message)
                            {
                                try
                                {
                                    // Create a TcpClient.
                                    // Note, for this client to work you need to have a TcpServer
                                    // connected to the same address as specified by the server, port
                                    // combination.
                                    Int32 port = 15000;
                                    TcpClient client = new TcpClient(server, port);

                                    // Translate the passed message into ASCII and store it as a Byte array.
                                    Byte[] data = System.Text.Encoding.ASCII.GetBytes(message);

                                    // Get a client stream for reading and writing.
                                    //  Stream stream = client.GetStream();
                                    NetworkStream stream = client.GetStream();

                                    // Send the message to the connected TcpServer.
                                    stream.Write(data, 0, data.Length);

                                    Console.WriteLine("Sent: {0}", message);

                                    // Receive the TcpServer.response.
                                    // Buffer to store the response bytes.
                                    data = new Byte[256];

                                    // String to store the response ASCII representation.
                                    String responseData = String.Empty;

                                    // Read the first batch of the TcpServer response bytes.
                                    Int32 bytes = stream.Read(data, 0, data.Length);
                                    responseData = System.Text.Encoding.ASCII.GetString(data, 0, bytes);
                                    Console.WriteLine("Received: {0}", responseData);

                                    // Close everything.
                                    stream.Close();
                                    client.Close();
                                }
                                catch (ArgumentNullException e)
                                {
                                    Console.WriteLine("ArgumentNullException: {0}", e);
                                }
                                catch (SocketException e)
                                {
                                    Console.WriteLine("SocketException: {0}", e);
                                }

                                Console.WriteLine("\n Press Enter to continue...");
                                Console.Read();
                            }

                            static async Task Main(string[] args)
                            {
                                Connect("127.0.0.1", "Hello world!");
                            }
                        }
                    }

                

            

С получением данных гораздо сложнее. Мы не знаем их истинного объёма и потому вынуждены считывать их в цикле до тех пор пока они не закончатся. До тех пор пока в потоке есть данные (свойство DataAvailable равно true) происходит поэтапное формирование поступившей с сервера строки при помощи класса StringBuilder теми данными, что были прочитаны в буфер в ходе текущей итерации. После завершения прочтения всех данных из потока возвращается готовое строковое сообщение. После завершения передачи или получения поток и сетевое соединение должны быть закрыты.

            
                    using System;
                    using System.Net;
                    using System.Net.Sockets;
                    using System.Text;

                    namespace ConsoleApp
                    {
                        class Program
                        {
                            static void Main(string[] args)
                            {
                                TcpListener server = new TcpListener(IPAddress.Any, 15000);
                                server.Start();  // запускаем сервер
                                while (true)   // бесконечный цикл обслуживания клиентов
                                {
                                    TcpClient client = server.AcceptTcpClient();  // ожидаем подключение клиента
                                    NetworkStream stream = client.GetStream(); // для получения и отправки сообщений
                                    try
                                    {
                                        if (stream.CanRead)
                                        {
                                            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);
                                            Console.WriteLine("Received: {0}", myCompleteMessage); // выводим на экран полученное сообщение в виде строки
                                            Byte[] responseData = Encoding.UTF8.GetBytes(myCompleteMessage + " Hello client!");
                                            stream.Write(responseData, 0, responseData.Length);
                                            Console.WriteLine("Sent: {0}", Encoding.Default.GetString(responseData, 0, responseData.Length)); // выводим на экран полученное сообщение в виде строки
                                        }
                                    }
                                    finally
                                    {
                                        stream.Close();
                                        client.Close();
                                    }
                                }
                            }
                        }
                    }
                
            

Обмен данными по сети может занимать значительное время. В свою очередь ожидание подключения клиента на сервере может вообще продолжаться неопределённо долго. Всё это может привести к тому, что программа окажется недоступной («зависнет»). Чтобы этого избежать настоятельно рекомендуется работать с сетью в отельном потоке.

Дополнительная информация по работе с TCP протоколом в C# досупна на сайте Microsoft.

Обмен данными по сети в C# (Протокол HTTP)

HTTP — это упрощенный протокол прикладного уровня, который размещается поверх TCP и в основном известен как транспортный канал для World Wide Web и локальных интрасетей. Однако это классический протокол, который используется помимо гипертекста для многих других задач, например, в серверах доменных имен и системах распределенного управления объектами посредством своих методов запросов, кодов ошибок и заголовков. Сообщение HTTP представляется в MIME-подобном формате; оно содержит метаданные о сообщении (например, тип его содержания и длину) и информацию о запросе и ответе, например, метод, используемый для отправки запроса. HTTP — это протокол приложения клиент-сервер, через который взаимодействуют две системы, обычно использующие соединение TCP/IP. HTTP-сервер — это программа, слушающая на порте машины входящие HTTP-запросы. HTTP-клиент через сокет открывает соединение с сервером, отправляет сообщение с запросом на конкретный документ и ждет ответа от сервера. Сервер отправляет сообщение, содержащее код нормального или аварийного завершения, заголовки с информацией об ответе и (если запрос обработан успешно) требуемый документ.

Рассмотрим пример сетевого взаимодействия по протоколу Http на основе класса HttpClient.

            
                    using System;
                    using System.Net.Http;
                    using System.Net.Sockets;
                    using System.Threading.Tasks;

                    namespace ConsoleApp
                    {
                        class Program
                        {
                            static readonly HttpClient client = new HttpClient();

                            static async Task Main(string[] args)
                            {
                                // Call asynchronous network methods in a try/catch block to handle exceptions.
                                try
                                {
                                    HttpResponseMessage response = await client.GetAsync("http://www.energyed.ru/");
                                    response.EnsureSuccessStatusCode();
                                    string responseBody = await response.Content.ReadAsStringAsync();
                                    // Above three lines can be replaced with new helper method below
                                    // string responseBody = await client.GetStringAsync(uri);

                                    Console.WriteLine(responseBody);
                                }
                                catch (HttpRequestException e)
                                {
                                    Console.WriteLine("\nException Caught!");
                                    Console.WriteLine("Message :{0} ", e.Message);
                                }
                            }
                        }
                    }
                
            

Дополнительная информация по работе с Http протоколом в C# досупна на сайте Microsoft.