English Español Deutsch 日本語 Português
preview
Разработка MQTT-клиента для MetaTrader 5: методология TDD (Часть 4)

Разработка MQTT-клиента для MetaTrader 5: методология TDD (Часть 4)

MetaTrader 5Интеграция | 6 марта 2024, 15:12
647 1
Jocimar Lopes
Jocimar Lopes

"Объекты программного обеспечения должны быть открыты для расширения, но закрыты для модификации" (принцип открытости-закрытости объектно-ориентированного программирования)

Введение

Для начала - небольшой обзор. В первой части этой серии мы начали писать простые тесты для функции, способной генерировать фиксированный заголовок управляющего пакета MQTT. Мы начали с первого бита первого байта.

Во второй части мы ввели некоторые общие функции и определения в двух заголовочных файлах.

В третьей части мы начали считывать флаги подтверждения CONNACK и коды причин подключения. Это была наша первая встреча с разделом операционного поведения Стандарта OASIS. 

До этого момента все было статично и привязано к попытке подключения. Если первый байт был неправильным, пользователь получал ответ об ошибке брокера и мог попытаться снова подключиться с правильно сформированным пакетом. 

Свойства – это отдельная история. Они являются динамическими атрибутами MQTT Application Message и могут меняться после соединения. Максимальное значение QoS брокера может временно измениться по операционным причинам. Receive Maximum мог быть изменен из-за узких мест в сети. Кроме того, некоторые свойства являются динамическими по своей конструкции, например Content-Type, все свойства Will и свойство User. Эти свойства будут постоянно меняться.

Стандарт OASIS довольно однозначен в своих спецификациях, но разработчикам клиентов оставлено много возможностей для решения, как читать, реагировать, сохранять и обновлять текущие свойства. К тому же, слой сохранения (persistence layer) для управления Session находится в полной ответственности разработчика клиента. Нам придется реализовать слой сохранения для правильного управления свойствами между сеансами. Выбор алгоритма здесь будет иметь решающее значение для соответствия, надежности и производительности.

Комментарии в разделе "Как мы читаем свойства MQTT v5.0 в MQL5" могут быть полезны в качестве неформальной документации для тех, кто будет сопровождать библиотеку. Некоторые конечные пользователи библиотеки также могут получить пользу от этих комментариев. В этом разделе мы рассмотрим свойства с точки зрения разработчика библиотеки. Мы рассмотрим их типы данных, идентификаторы и расположение в массиве байтов, чтобы лучше описать, как мы их читаем. Ниже мы рассмотрим свойства с точки зрения пользователя библиотеки. Мы постараемся описать их семантику в каждом из возможных вариантов использования.

Примечание: Если не указано иное, все цитаты взяты из Стандарта OASIS


Что такое свойства в MQTT v5.0?

Свойства являются частью "механизмов расширяемости", добавленных в MQTT v5.0. Их не было в предыдущей версии 3.1.1, которая была последней перед этим крупным обновлением, но при этом повсеместно встречаются в MQTT v5.0. Но что такое свойства MQTT? Свойства чего именно?

Ответ - свойства Application Message (сообщение приложения). Согласно Стандарту OASIS, Application Message - это

"Данные, передаваемые по протоколу MQTT по сети для приложения. Когда сообщение приложения передается по MQTT, оно содержит полезные данные, качество обслуживания (Quality of Service, QoS), набор свойств и тему" (выделено мной).

Взгляните на желтый прямоугольник Payload data (полезная нагрузка) на Рисунке 1 ниже. Существует важное терминологическое различие, на которое мы хотим обратить ваше внимание.

Абстрактная схема сообщения приложения MQTT 5.0

Рис. 01. Абстрактная схема сообщения приложения MQTT 5.0

В контексте протокола обмена сообщениями, когда мы видим слово message (сообщение), мы обычно думаем о сообщении пользователя, часто текстовом сообщении. Чаще всего мы не думаем о сообщении как о приложении в целом. 

Но здесь сообщение, отправленное пользователями через MQTT, является частью данных полезной нагрузки, а свойства являются частью абстрактной модели протокола под названием Application Message. Таким образом, когда мы отправляем пользовательское сообщение через MQTT, мы можем иметь не только свойства, связанные с этим "пользовательским сообщением", но также свойства, связанные с сообщением приложения в целом: свойства для соединения, публикации, подписки на темы и отписки от них, свойства для аутентификации и т.д.

Кроме того, имеются Will-свойства, прикрепленные к Will-сообщению.

"Сообщение Will состоит из Will-свойств, Will-темы и полезной нагрузки Will в полезной нагрузке CONNECT (CONNECT Payload)". "

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


Для чего используются свойства?

Помимо переноса метаданных полезной нагрузки, свойства можно использовать для настройки каждого аспекта взаимодействия между Клиентом и Сервером (брокером), а также взаимодействия между различными Клиентами. От подключения до отключения их можно использовать для установки формата типа контента, запроса информации у брокера, определения срока действия сообщения, выбора метода аутентификации и даже выполнения перенаправления сервера, а также других вариантов использования. Как видно из таблиц ниже, за исключением пакетов PINGREQ и PINGRESP, которые используются для обновления периода Keep Alive, все типы пакетов могут содержать некоторые специфические свойства в зависимости от контекста пакета.

User Property (свойство пользователя) — это особый случай свойства, которое может использоваться во всех пакетах и значение которого определяется приложением, то есть его семантика не определяется протоколом. Мы кратко рассмотрим User Property в последнем разделе, где говорится о том, как свойства можно использовать для расширения протокола.

Хотя имена свойств явно определяют их назначение, нам необходимо знать:

  • когда их можно использовать
  • когда их необходимо использовать
  • и что произойдет, если они установлены неправильно

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

В последующих описаниях мы используем слова НЕОБХОДИМО/ДОЛЖНО (MUST) и МОЖЕТ/МОЖНО (MAY) так, как они используются в Стандарте OASIS, который, в свою очередь, использует их, как описано в документе RFC 2119 технической комиссии Интернет (IETF).


Свойства соединения
CONNECT Session Expiry Interval (интервал окончания сеанса), Receive Maximum (получить максимум), Maximum Packet Size (максимальный размер пакета), Topic Alias Maximum (максимальный псевдоним темы), Request Response Information (информация об ответе на запрос), Request Problem Information (информация о проблеме запроса), User Property (свойство пользователя), Authentication Method (метод аутентификации), Authentication Data (данные аутентификации)
CONNECT Payload (полезная нагрузка CONNECT) Will Delay Interval (интервал задержки Will), Payload Format Indicator (индикатор формата полезной нагрузки), Message Expiry Interval (интервал истечения срока действия сообщения), Content Type (тип содержания), Response Topic (тема ответа), Correlation Data (данные корреляции), User Property (свойство пользователя)
CONNACK Session Expiry Interval (интервал окончания сеанса), Receive Maximum (получить максимум), Maximum QoS (максимальное качество обслуживания), Retain Available (доступное сохранение), Maximum Packet Size (максимальный размер пакета), Assigned Client Identifier (назначенный идентификатор клиента), Topic Alias Maximum (максимальный псевдоним темы), Reason String (строка причины), User Property (свойство пользователя), Wildcard Subscription Available (доступна подписка с подстановочными знаками), Subscriptions Identifiers Available (доступные идентификаторы подписок), Shared Subscription Available (доступна общая подписка), Server Keep Alive (поддержание активности сервера), Response Information (информация об ответе), Server Reference (указание на сервер), Authentication Method (метод аутентификации), Authentication Data (данные аутентификации)
DISCONNECT Session Expiry Interval (интервал окончания сеанса), Reason String (строка причины), User Property (свойство пользователя), Server Reference (указание на сервер)

Таблица 1. MQTT v5.0 - Свойства, сгруппированные по функциональности — Свойства подключения

Assigned Client Identifier - НЕОБХОДИМО значение CONNECT. Если не установлено, брокер МОЖЕТ назначить идентификатор на CONNACK.

Идентификатор клиента является обязательным для пакетов CONNECT. Однако брокеру разрешено принимать идентификатор длиной ноль байт и присваивать идентификатор клиенту. В этом случае брокер вернет его в пакете CONNACK с этим свойством.

Maximum Packet Size – МОЖНО не устанавливать, но не может быть равно нулю.

Представляет собой "общее количество байтов в управляющем пакете". Используется Клиентом и Брокером для определения максимального размера пакета, который они готовы принять. Если наш клиент устанавливает это свойство, а брокер отправляет пакет, превышающий этот предел, мы должны отключиться (DISCONNECT) с кодом причины 0x95 (Packet too large - слишком большой пакет). Брокер не будет отправлять строки причин или некоторые свойства пользователя, если их включение сделает пакет больше, чем это свойство.

Maximum QoS – используется брокером на CONNACK и МОЖЕТ быть не установлено

Информирует клиента о способности брокера обрабатывать уровни QoS. Если брокер принимает уровень QoS 2, это свойство не будет установлено. Нашему клиенту НЕОБХОДИМО соблюдать это ограничение сервера, не отправляя пакеты PUBLISH с более высоким уровнем QoS. Нам разрешено поддерживать в нашем клиенте только уровень QoS 0 и при этом сохранять соответствие.

Ненормативный комментарий

"Клиенту не обязательно поддерживать пакеты QoS 1 или QoS 2 PUBLISH. В этом случае Клиент просто ограничивает максимальное поле QoS в любых отправляемых им командах SUBSCRIBE значением, которое он может поддерживать".

Message Expiry Interval – МОЖНО не устанавливать.

Также является частью свойств PUBLISH. Устанавливает время жизни Will-сообщения. Если этот параметр не установлен, когда брокер опубликует Will-сообщение, у него не будет определенного срока действия.

"Если интервал истечения сообщения прошел, а Серверу не удалось начать дальнейшую доставку соответствующему подписчику, то ему НЕОБХОДИМО удалить копию сообщения для этого подписчика".

Его также можно установить в свойствах Will для CONNECT Payload. Если параметр не установлен, срок действия сообщения не ограничен.

Payload Format Indicator – НЕОБХОДИМО установить, если мы отправляем символьные данные.

Payload Format Indicator можно установить в свойствах Will для CONNECT Payload. Здесь указывается, является ли сообщение Will символьными данными в кодировке UTF-8 или это "неопределенные байты". 

"Сервер МОЖЕТ проверить формат Will-сообщения и факт неотправки CONNACK с кодом причины 0x99 (Payload format invalid - неверный формат полезной нагрузки)"

Это часть свойств PUBLISH, указывающая формат Payload. Проверка брокера также не является обязательной. Но при проверке формата полезной нагрузки, мы можем ожидать PUBACK, PUBREC или DISCONNECT с тем же кодом причины (0x99), если формат отличается от объявленного.

Если свойство не установлено, предполагается, что формат полезной нагрузки представляет собой "неопределенные байты". То есть, если мы отправляем символьные данные, установка этого свойства является обязательной.

Reason String – МОЖЕТ использоваться на всех ACK, DISCONNECT и AUTH клиентом или брокером.

Reason String — одна из новых функций MQTT v5.0. Ее можно использовать для дополнения кода причины понятным для человека диагностическим инструментом. Его можно использовать, например, для ведения журнала. Мы можем попросить брокера не отправлять его, установив для параметра Request Problem Information значение 0 в свойствах CONNECT. 

Receive Maximum – МОЖНО не устанавливать, но не может быть равно нулю. 

Наш клиент может использовать это свойство в CONNECT, чтобы ограничить количество публикаций QoS 1 и QoS 2, которые мы готовы обрабатывать одновременно в текущем сетевом соединении. Брокер может установить его на CONNACK. Если не установлено, используется значение по умолчанию 65,535.

При QoS 0 нет необходимости ждать PUBACK (QoS 1) или PUBCOMP (QoS 2), потому что, как мы знаем, QoS 0 работает по принципу "выстрелил и забыл". Это свойство устанавливает, сколько сообщений наш клиент или брокер готов отправить/получить перед получением соответствующей PUBACK или PUBCOMP. Мы можем рассматривать это свойство как средство сказать, сколько сообщений может находиться в состоянии "ожидания подтверждения", пока не будут отправлены новые сообщения.

Request Problem Information – МОЖНО установить CONNECT.

Используется для информирования сервера о том, что мы хотим получить строку(-и) причины и свойства пользователя в случае сбоев. Если не установлено, брокер может их отправлять.

Request Response Information – МОЖНО установить CONNECT.

Является частью взаимодействия "запрос/ответ" через MQTT вместо обычного взаимодействия "публикация/подписка". Если значение не установлено, мы сообщаем брокеру, что не хотим, чтобы он отправлял ответную информацию. Брокеру разрешено не отправлять ответную информацию, даже если мы ее запрашиваем. Если свойство отсутствует, значение по умолчанию не установлено.

Response Information – МОЖНО установить CONNECT.

Является частью взаимодействия "запрос/ответ" через MQTT вместо обычного взаимодействия "публикация/подписка". 

Ненормативный комментарий

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

Response Topic – МОЖНО установить CONNECT или PUBLISH. 

Является частью взаимодействия "запрос/ответ" через MQTT вместо обычного взаимодействия "публикация/подписка". При включении брокер интерпретирует Will-сообщение как запрос. В отличие от Topic Filter, используемого в пакетах SUBSCRIBE, Response Topic не может содержать подстановочные знаки.

"Request Message - это Application Message с Response Topic".

Таким образом, это свойство характеризует Application Message как часть взаимодействия запроса/ответа.

Retain Available – МОЖЕТ присутствовать на CONNACK.

Свойство Retain Available сообщает нашему клиенту, поддерживает ли брокер сохраненные сообщения. Если отсутствует, сохраненные сообщения доступны.

Server Keep Alive – МОЖЕТ присутствовать на CONNACK.

Свойство Server Keep Alive имеет приоритет над свойством Keep Alive, запрошенным в CONNECT. Если свойства нет в CONNACK, мы можем использовать Keep Alive. В противном случае действуют правила Server Keep Alive.

Server Reference – МОЖЕТ присутствовать на CONNACK или DISCONNECT.

Информирует нашего клиента о перенаправлении сервера. Может относиться к временному или постоянному перенаправлению. В обоих случаях другой сервер может быть уже известен нашему клиенту или он будет указан с помощью этого свойства.

Ненормативный комментарий

Примеры Server Reference:

myserver.xyz.org

myserver.xyz.org:8883

10.10.151.22:8883 [fe80::9610:3eff:fe1c]:1883

Брокеру разрешено никогда не отправлять это свойство, а нашему клиенту разрешено игнорировать его.

Session Expiry Interval – МОЖНО установить CONNECT.

Определяет, как долго будет сохраняться сеанс после отключения. Если параметр не установлен или отсутствует, сеанс завершается при отключения соединения. Можно установить неограниченное время действия сеанса, установив для этого свойства значение UINT_MAX. Мы должны сохранить Session State, если это свойство больше нуля. Мы можем проверить его по флагу Session Present на CONNACK.

Это свойство может быть полезно, когда сетевое соединение прерывистое, позволяя нашему клиенту возобновлять сеанс всякий раз при возобновлении сетевого соединения.

Shared Subscription Available – МОЖЕТ присутствовать на CONNACK.

Информирует нашего клиента, поддерживает ли брокер общие подписки. Если свойство отсутствует, то поддерживает.

Subscription Identifiers Available – МОЖЕТ присутствовать на CONNACK.

Информирует нашего клиента, поддерживает ли брокер идентификаторы подписки. Если свойство отсутствует, то поддерживает.

Topic Alias Maximum – МОЖНО установить CONNECT и МОЖЕТ присутствовать на CONNACK.

Сообщает брокеру о максимальном количестве псевдонимов темы, которое наш клиент готов принять для этого конкретного соединения. Если свойство равно нулю или не установлено, брокер не будет отправлять псевдоним темы в этом соединении. Верно и обратное: если это свойство отсутствует в CONNACK или присутствует, но его значение равно нулю, наш клиент не должен отправлять псевдоним темы.

Wildcard Subscription Available – МОЖЕТ присутствовать на CONNACK.

Если свойство не установлено (равно нулю), брокер не поддерживает подписки с подстановочными знаками. В этом случае брокер отключится (DISCONNECT) после получения подписки (SUBSCRIBE) с запросом на подстановочные знаки (Wildcard Subscription). Но даже если брокер поддерживает эту функцию, ему разрешено отклонить конкретный запрос на подписку с подстановочными знаками и вернуть SUBACK с тем же кодом причины 0xA2 (Wildcard Subscriptions not supported - подписки с подстановочными знаками не поддерживаются). Если свойство отсутствует в CONNACK, брокер поддерживает эту функцию.

Will Delay Interval – МОЖНО установить Will Properties полезной нагрузки CONNECT.

Устанавливает задержку в секундах, которую должен соблюдать брокер перед отправкой Will-сообщения. Это свойство особенно полезно для предотвращения отправки Will-сообщения при нестабильном сетевом соединении.


Свойства публикации
PUBLISH Payload Format Indicator (индикатор формата полезной нагрузки), Message Expiry Interval (интервал истечения срока действия сообщения), Topic Alias (псевдоним темы), Response Topic (тема ответа), Correlation Data (данные корреляции), User Property (свойство пользователя), Subscription Identifier (идентификатор подписки), Content Type (тип содержания)
PUBACK Reason String (строка причины), User Property (свойство пользователя)
PUBREC Reason String, User Property
PUBREL Reason String, User Property
PUBCOMP Reason String, User Property

Таблица 2. MQTT v5.0 - Свойства, сгруппированные по функциональности - Свойства публикации

Topic Alias – МОЖНО установить PUBLISH

Topic Alias - также одна из новых функций MQTT v5.0. Она позволяет брокеру или клиенту уменьшить размер пакетов, заменив имя темы небольшим целым числом - псевдонимом (alias). Сокращение размеров может быть значительным, поскольку имена тем представляют собой строки, длина которых может достигать 65 535 байт (UINT_MAX).

Correlation Data – МОЖНО установить PUBLISH и Will Properties

Является частью взаимодействия "запрос/ответ" через MQTT вместо обычного взаимодействия "публикация/подписка". Значение свойства важно только для приложения (брокера и клиентов). Это двоичные данные, используемые в запросе/ответе "отправителем сообщения, чтобы определить, для какого запроса предназначено ответное сообщение при его получении".

Content Type – МОЖНО установить PUBLISH и Will Properties

Также можно использовать в CONNECT для настройки типа содержания (Content Type) Will-сообщения. 

Брокер проверяет только кодировку самого свойства. Значение свойства зависит от клиента.


Свойства подписки/отписки
SUBSCRIBE Subscription Identifier (идентификатор подписки), User Property (свойство пользователя)
SUBACK Reason String (строка причины), User Property
UNSUBSCRIBE User Property
UNSUBACK  Reason String, User Property

Таблица 3: MQTT v5.0 - Свойства, сгруппированные по функциональности - Свойства подписки/отписки

Subscription Identifier – МОЖНО установить SUBSCRIBE

Это числовой идентификатор, который можно установить на SUBSCRIBE. Он будет возвращен брокером в сообщении, что позволит клиенту(ам) определить, какая подписка(и) привела к доставке сообщения. Может принимать значения от 1 до 268 435 455. НЕ ДОЛЖНО быть установлено на ноль и НЕ ДОЛЖНО использоваться при публикации (PUBLISH) с клиента на сервер.


Свойства аутентификации
AUTH
Authentication Method (метод аутентификации), Authentication Data (данные аутентификации), Reason String (строка причины), User Property (свойство пользователя)

Таблица 4. MQTT v5.0 - Свойства, сгруппированные по функциональности - Свойства аутентификации

Неудивительно, что эти свойства также можно использовать в соединениях.

Authentication Method

Помимо базовой сетевой аутентификации с использованием имени пользователя и пароля, MQTT v5.0 поддерживает "расширенную аутентификацию". Это свойство сообщает о выбранном методе. Метод выбирается разработчиками приложения. Брокер сообщит, поддерживается ли метод.

Authentication Method обычно представляет собой механизм SASL (Simple Authentication and Security Layer, простой уровень аутентификации и безопасности), и использование такого зарегистрированного имени облегчает обмен. Однако метод аутентификации не ограничивается использованием зарегистрированных механизмов SASL.

Authentication Data

Это свойство используется клиентом и брокером для обмена данными аутентификации в соответствии с выбранным методом аутентификации.


Как мы читаем свойства MQTT v5.0 в MQL5

До сих пор, в предыдущих частях этой серии, мы имели дело с настройками "на каждый сеанс", настроенными с помощью битовых флагов, а именно, флагов подключения при CONNECT, кода причины CONNACK и флага CONNACK Session Present. Эти настройки читаются/записываются/сохраняются один раз за сеанс. А вот со свойствами дела обстоят иначе. Они являются частью Application Message и могут содержать большие объемы важных данных в некоторых профилях приложений. Таким образом, наш клиент должен быть готов постоянно читать и записывать свойства.

Чтобы написать тест на чтение свойств, отправленных сервером, нам нужен образец массива байтов. Мы начнем с образца массива байтов для пакета CONNACK, поскольку это первый пакет, с которым будет иметь дело наш Клиент. Как и все управляющие пакеты MQTT, он имеет двухбайтовый фиксированный заголовок и двухбайтовый переменный заголовок, состоящий из одного байта для флагов Connect Acknowledge и одного байта для кода Connect Reason. Свойства — это последнее поле в пакете CONNACK. Оно не имеет идентификатора пакета и полезной нагрузки.

MQTT 5.0 - Структура пакета CONNACK

Рис. 02. Структура пакета CONNACK в MQTT 5.0

Из Стандарта мы знаем, что:

"Набор свойств состоит из длины свойства (Property Length), за которой следуют свойства".

Мы также знаем, что:

"Длина свойства кодируется как переменное целое число (Variable Byte Integer). Длина свойства не включает байты, используемые для его кодирования, но включает длину свойств. Если свойств нет, это ДОЛЖНО быть указано путем включения длины свойства, равной нулю".

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

Итак, наш пример массива байтов может выглядеть следующим образом для CONNACK без свойств:

uchar connack_response[] = {2, X, 0, 0, 0};

где X — оставшаяся длина фиксированного заголовка. Алгоритм декодирования целого числа переменного байта предусмотрен стандартом. В MQL5 этот алгоритм можно записать так:

uint DecodeVariableByteInteger(uint &buf[], uint idx)
  {
   uint multiplier = 1;
   uint value = 0;
   uint encodedByte;
   do
     {
      encodedByte = buf[idx];
      value += (encodedByte & 127) * multiplier;
      if(multiplier > 128 * 128 * 128)
        {
         Print("Error(Malformed Variable Byte Integer)");
         return -1;
        }
      multiplier *= 128;
     }
   while((encodedByte & 128) != 0);
   return value;
  };

где buf[idx] представляет "следующий байт из потока".

Хотя алгоритм декодирования целочисленных переменных байтов предусмотрен Стандартом, мы также написали для него очень простой тест, просто чтобы убедиться, что на данном этапе реализация работает так, как ожидалось:

bool TEST_DecodeVariableByteInteger()
  {
   Print(__FUNCTION__);
   uint buf[] = {1, 127, 0, 0, 0};
   uint expected = 127;
   uint result = DecodeVariableByteInteger(buf, 1);
   ZeroMemory(buf);
   return AssertEqual(expected, result);
  }

Очевидно, что для целей тестирования оставшееся значение длины будет жестко закодировано. Для приведенного выше CONNACK без каких-либо свойств это будет:

uchar connack_response[] = {2, 3, 0, 0, 0};

Пример массива байтов для CONNACK с однобайтовым свойством индикатора формата полезной нагрузки, установленным в формат полезной нагрузки строки в кодировке UTF-8, может выглядеть примерно так:

uchar connack_response_one_byte_property = {2, 5, 0, 0, 2, 1, 1};

Как видите, проверка наличия свойств в CONNACK довольно проста. Нам просто нужно прочитать пятый байт, содержащий длину свойства. Если она не равна нулю, у нас есть свойства. Наш первый тест выглядит так:

bool TestProtectedMethods::TEST_HasProperties_CONNACK_No_Props()
  {
   Print(__FUNCTION__);
//--- Arrange
   bool expected = false;
   uchar connack_no_props[5] = {2, 3, 0, 0, 0};
//--- Act
   CSrvResponse *cut = new CSrvResponse();
   bool result =  this.HasProperties(connack_no_props);
//--- Assert
   bool isTrue = AssertEqual(expected, result);
//--- cleanup
   delete cut;
   ZeroMemory(result);
   return  isTrue ? true : false;
  }

Ознакомьтесь с предыдущей статьей, чтобы увидеть тестирование защищенных методов, а также взгляните на прикрепленный код, чтобы увидеть вариант неудачного теста.

Первая реализация, достаточная для прохождения тестов на текущем этапе, выглядит так:

bool CSrvResponse::HasProperties(uchar &resp_buf[])
  {
   return resp_buf[4] != 0 ? true : false;
  }

Здесь мы используем тернарный оператор, чтобы минимизировать код.

Мы должны учитывать, что расположение байта длины свойства зависит от типа пакета. Это связано с тем, что, хотя свойства всегда являются последним полем в заголовке переменной, существуют пакеты, для которых перед длиной свойства требуется двухбайтовый идентификатор пакета. В CONNACK это не проблема.

Вы можете сказать: "Тогда этот код не будет работать для других типов пакетов!" Да, вы будете правы. Но помните, что здесь мы используем подход TDD. Одним из главных преимуществ этого подхода является то, что мы можем сосредоточиться на поставленной задаче, а не пытаться справиться со всеми возможными будущими проблемами на первых стадиях разработки. Мы займемся другими типами пакетов, когда придет время и наш тест провалится. После этого мы перепишем наш тест(ы), в конечном итоге проведя рефакторинг кода.

Сначала это может показаться немного нелогичным, но потом вы не сможете писать код так, как раньше. Этот подход упрощает работу и даже доставляет удовольствие. Кстати, в следующей части этой серии мы начнем писать и читать свойства пакетов PUBLISH. Так что следите за обновлениями!

Если длина свойства не равна нулю, мы можем искать идентификатор свойства в следующем байте. Идентификатор свойства дает нам тип данных свойства.

"Свойство состоит из идентификатора, который определяет его использование и тип данных, за которым следует значение".

uchar CSrvResponse::GetPropertyIdentifier(uchar &resp_buf[])
  {
   return resp_buf[5];
  }

Тип данных дает нам количество байтов, которые нужно прочитать. Тип данных может быть:

Однобайтовым целым числом

uchar CSrvResponse::ReadOneByteProperty(uchar &resp_buf[])
  {
   return resp_buf[6];
  }

Двухбайтовым целым числом

void CSrvResponse::ReadTwoByteProperty(uchar &resp_buf[], uchar &dest_buf[])
  {
   ArrayCopy(dest_buf, resp_buf, 0, 6, 2);
  }

Четырехбайтовым целым числом

void CSrvResponse::ReadFourByteProperty(uchar &resp_buf[], uchar &dest_buf[])
  {
   ArrayCopy(dest_buf, resp_buf, 0, 6, 4);
  }

Переменным байтовым целым числом (только для идентификатора подписки) 

void CSrvResponse::ReadVariableByteProperty(uint &resp_buf[], uint &dest_buf[], uint start_idx)
  {
   uint value = DecodeVariableByteInteger(resp_buf, start_idx);
   ArrayResize(dest_buf,value,7);
   ArrayFill(dest_buf, 0, 1, value);
  }

Чтение/декодирование (и запись/кодирование) этого свойства требует гораздо большего объема данных, чем то, что этот тест проверяет сейчас.

"Переменное байтовое целое число кодируется с использованием схемы кодирования, которая использует один байт для значений до 127. Большие значения обрабатываются следующим образом. Младшие семь битов каждого байта кодируют данные, а старший бит используется для указания того, есть ли в представлении следующие байты. Таким образом, каждый байт кодирует 128 значений и 'бит продолжения'"(выделено мной).

Мы будем иметь дело со свойствами Variable Byte Integer при реализации пакетов SUBSCRIBE, поскольку они используются только для свойства Subscription Identifier. Также имеются следующие три типа данных: строки в кодировке UTF-8, двоичные данные и пары строк UTF-8. Они будут подробно описаны в контексте использования запроса/ответа и реализации особого случая свойства пользователя.

Строки в кодировке UTF-8 имеют префикс их длины.

"Каждая из этих строк имеет префикс с полем длины двухбайтового целого числа, которое указывает количество байтов в самой строке в кодировке UTF-8, как показано на Рисунке 1.1 Структура строк в кодировке UTF-8 ниже. Следовательно, максимальный размер строки в кодировке UTF-8 составляет 65 535 байт. Если не указано иное, все строки в кодировке UTF-8 могут иметь любую длину в диапазоне от 0 до 65 535 байт".

MQTT-v5-utf8-encoded-strings-structure-OASIS

Рис. 03. MQTT 5.0 - Структура строк в кодировке UTF-8 - Снимок экрана из таблицы OASIS

Строки в кодировке UTF-8 должны быть проверены на наличие запрещенных кодовых точек Unicode (подробнее об этом позже).

"В разделе 1.6.4 описаны запрещенные кодовые точки Юникода, которые не следует включать в строку в кодировке UTF-8. Реализация Клиента или Сервера может выбрать, следует ли проверять, что эти кодовые точки не используются в строках с кодировкой UTF-8, таких как имя темы или свойства".

Двоичные данные также имеют префикс их длины.

"Двоичные данные представлены двухбайтовым целым числом, которое указывает количество байтов данных, за которым следует это количество байтов. Таким образом, длина двоичных данных ограничена диапазоном от 0 до 65 535 байт".

Мы продолжаем подсчитывать количество прочитанных байтов, чтобы знать, когда мы прочитали все свойства. Нам не нужно беспокоиться о порядке свойств.

"Порядок свойств с разными идентификаторами не имеет значения".


Как свойства могут использоваться для расширения протокола

Как указано в начале этой статьи, свойства являются частью "механизма расширяемости" MQTT 5.0, и наиболее важным свойством этого механизма является User Property (свойство пользователя), которое можно использовать в любом управляющем пакете MQTT. Свойства пользователя — это пары ключ-значение, смысл которых непрозрачен для протокола. То есть его значение определяется приложением.

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

  • Брокер А использует GOLD
  • Брокер B использует XAUUSD.
  • Брокер C использует XAUUSD.s

Кроме того, каждый поставщик сигналов может использовать более одного брокера. Итак, пара поставщик_сигналов: провайдер_брокер может измениться в любой момент, даже во время торговой сессии. (Да, здесь наблюдается квазикомбинаторный взрыв.) Получателю необходимо знать (в идеале за миллисекунды) значение имени получаемого им символа, чтобы иметь возможность преобразовать его в имя символа, которое его брокер использует для правильного воспроизведения торгового запроса.

Без свойств пользователя, как это было в предыдущих версиях протокола, эти метаданные (поставщик_сигналов: провайдер_брокер) должны были встраиваться в полезную нагрузку, где можно было бы ожидать найти (и проанализировать) только необходимые данные торговых сигналов.

Напротив, если каждый поставщик сигналов имеет собственное свойство пользователя с именем своего брокера, полезная нагрузка может содержать только необходимые данные сигнала.

Это упрощенный пример такого варианта использования. Но эти метаданные можно расширить до любой важной информации, включая строки JSON/XML и даже целые файлы. Возможности в некотором смысле безграничны.


Заключение

В четвертой части нашей серии статей мы представили краткое описание свойств MQTT v5.0, их семантики и некоторых вариантов использования. Мы также рассмотрели их реализацию для CONNACK, и привели простой пример того, как их можно использовать для расширения протокола. В следующей части мы будем применять их в контексте пакетов PUBLISH, всегда используя подход TDD, чтобы справиться со сложностью спецификаций.

Если вы можете внести вклад в разработку собственного MQL5-клиента, который станет частью CodeBase, пожалуйста, оставьте сообщение в комментариях или напишите в чате. Приветствуется любая помощь! :)


Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/13651

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (1)
Yousuf Mesalm
Yousuf Mesalm | 19 нояб. 2023 в 20:13
спасибо за ваши усилия
ждем пятую часть.
Нейросети — это просто (Часть 80): Генеративно-состязательная модель Трансформера графов (GTGAN) Нейросети — это просто (Часть 80): Генеративно-состязательная модель Трансформера графов (GTGAN)
В данной статье я предлагаю Вам познакомиться с алгоритмом GTGAN, который был представлен в январе 2024 года для решения сложных задач по созданию архитектурного макета с ограничениями на граф.
Разработка системы репликации (Часть 32): Система ордеров (I) Разработка системы репликации (Часть 32): Система ордеров (I)
Из всего, что было разработано до настоящего момента, данная система, как вы наверняка заметите и со временем согласитесь, - является самым сложным. Сейчас нам нужно сделать нечто очень простое: заставить нашу систему имитировать работу торгового сервера на практике. Эта необходимость точно реализовывать способ моделирования действий торгового сервера кажется простым делом. По крайней мере, на словах. Но нам нужно сделать это так, чтобы для пользователя системы репликации/моделирования всё происходило как можно более незаметно или прозрачно.
Популяционные алгоритмы оптимизации: Устойчивость к застреванию в локальных экстремумах (Часть II) Популяционные алгоритмы оптимизации: Устойчивость к застреванию в локальных экстремумах (Часть II)
Продолжение эксперимента, цель которого - исследовать поведение популяционных алгоритмов оптимизации в контексте их способности эффективно покидать локальные минимумы при низком разнообразии в популяции и достигать глобальных максимумов. Результаты исследования.
Разработка системы репликации (Часть 31): Проект советника — класс C_Mouse (V) Разработка системы репликации (Часть 31): Проект советника — класс C_Mouse (V)
Разрабатывать способ установки таймера необходимо таким образом, чтобы во время репликации/моделирования он мог сообщить нам, сколько времени осталось, что может показаться на первый взгляд простым и быстрым решением. Многие просто пытаются приспособиться и использовать ту же систему, что и в случае с торговым сервером. Но есть один момент, который многие не учитывают, когда думают о таком решении: при репликации, и это не говоря уже о моделировании, часы работают по-другому. Всё это усложняет создание подобной системы.