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

Разработка MQTT-клиента для MetaTrader 5: методология TDD

MetaTrader 5Интеграция | 25 сентября 2023, 12:39
1 040 0
Jocimar Lopes
Jocimar Lopes

Введение

"... стратегия такова: сначала всё должно работать, потом всё должно работать правильно и наконец всё должно работать быстро.Стивен Джонсон и Брайан Керниган "Язык C и модели системного программирования", журнал Byte, август 1983 г.

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

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

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


Что такое MQTT?

"MQTT - это протокол обмена данными по принципу "издатель - подписчик". Он легкий, открытый, простой и разработан так, чтобы его было легко внедрить. Эти характеристики позволяют эффективно использовать его во многих ситуациях, включая среды с ограниченными возможностями, например, для связи в системе Машина-Машина (M2M) и в Интернете вещей (IoT), где требуется небольшой объем кода и/или достаточная пропускная способность сети".

Приведенное выше определение взято из документации OASIS - владельца и разработчика протокола как открытого стандарта с 2013 года.

«В 2013 году IBM представила MQTT v3.1 в орган по спецификации OASIS с уставом, который допускал лишь незначительные изменения в спецификации. Приняв на себя обязанность по поддержке стандарта от IBM, OASIS выпустила версию 3.1.1 29 октября 2014 года. 7 марта 2019 года было выпущено более существенное обновление MQTT версии 5, включающее несколько новых функций" (английская Википедия)

IBM начала разработку протокола в 1999 году, чтобы удовлетворить промышленную потребность в мониторинге нефтепроводов с помощью датчиков и отправке данных через спутник в центры удаленного управления. По словам Арлена Ниппера, соавтора протокола совместно с доктором Энди Стэнфордом-Кларком, цель заключалась в том, чтобы предоставить в эти центры поток данных в реальном времени.

"Мы попытались использовать промежуточное программное обеспечение IBM того времени - MQ Integrator. В то время моей задачей в индустрии связи было связать воедино коммутируемые линии на 1200 и 300 бод с VSAT-станцией, обладавшей очень ограниченной пропускной способностью".

Несмотря на то, что протокол должен был отличаться надежностью, быстротой и дешевизной из-за технологических ограничений и высоких сетевых затрат, от него требовалось обеспечить качественную доставку данных с непрерывным отображением данных о сеансе (session awareness), что позволяло бы ему справляться с ненадежным или даже прерывистым Интернет-соединением.

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

Так как протокол работает по принципу "издатель/подписчик" (pub/sub), он, в отличие от основанных на принципе "запрос/ответ", является двунаправленным. Другими словами, как только соединение клиент/сервер установлено, данные могут передаваться от клиента к серверу и от сервера к клиенту в любое время без необходимости предварительного запроса, в отличие от HTTP WebRequest. Как только данные поступают, сервер немедленно пересылает их получателям. Эта особенность является краеугольным камнем обмена данными в реальном времени, поскольку она позволяет добиться минимальных задержек между конечными точками. Некоторые поставщики заявляют о задержке порядка миллисекунд.

Тип, формат, кодек или что-либо еще в данных не имеет значения. MQTT инвариантен к типам данных (data-agnostic). Пользователь может отправлять/получать необработанные байты, текстовые форматы (объекты XML, JSON), буферы протоколов, изображения, видеофрагменты и т. д.

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

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

Неудивительно, что MQTT — это не только набор спецификаций высокого уровня, но и широко применяемая в нескольких отраслях технология.

"Сегодня MQTT используется в самых разных отраслях, таких как автомобилестроение, производство, телекоммуникации, нефть и газ и т. д." (mqtt.org).


Основные компоненты

Публикация/подписка — очень известная модель обмена сообщениями. Клиент подключается к серверу и публикует сообщение на определенную тему. После этого все клиенты, подписанные на эту тему получают сообщение(я). Это основной механизм модели. 

Сервер действует как посредник, стоящий между клиентами для получения как подписок, так и публикаций. TCP/IP — это базовый транспортный протокол, а клиентами являются любые устройства, которые понимают TCP/IP и MQTT. Сообщение обычно представляет собой полезную нагрузку JSON или XML, но может быть чем угодно, включая необработанную последовательность байтов.

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

  • office/machine01/account123456

  • office/machine02/account789012

  • home/machine01/account345678

Мы также можем использовать решетку (#) в качестве подстановочного знака для подписки на тему. Например, чтобы подписаться на все учетные записи в machine01 из дома:
  • home/machine01/# 
Или подписаться на все machine из офиса:
  • office/# 

Итак, MQTT был разработан для межмашинного взаимодействия. Он широко используется в Интернете вещей, он надежен, быстр и дешев. Но вы можете спросить: какую пользу он может принести в торговле? Каковы могут быть варианты использования MQTT в MetaTrader?

Как было сказано выше, наиболее очевидным вариантом использования MQTT в торговой среде является копирование сделок. Но также можно подумать о загрузке машинного обучения данными в реальном времени, изменении поведения советника в соответствии с данными в реальном времени, полученными из веб-сервиса, или удаленном управлении MetaTrader с любого устройства.

Мы можем подумать о применении MQTT в любом сценарии, где требуется поток данных в реальном времени между машинами.


Применение MQTT в MetaTrader

Существуют бесплатные клиентские библиотеки с открытым исходным кодом для наиболее популярных языков общего назначения, включая соответствующие варианты для мобильных и встраиваемых устройств. Итак, чтобы использовать MQTT из MQL5, мы можем сгенерировать и импортировать соответствующую DLL из C, C++ или C#. 

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

Но, как мы знаем, использование DLL имеет некоторые негативные последствия для экосистемы MQL5, наиболее заметным из которых является то, что на Маркете не принимаются советники, зависящие от DLL. Кроме того, таким советникам запрещено выполнять оптимизацию тестирования на истории в облаке MQL5. Чтобы избежать зависимости от DLL и моста Python, идеальным решением является разработка собственной клиентской библиотеки MQTT для MetaTrader.

Именно этим мы и будем заниматься в ближайшее время: внедрять протокол MQTT-v5.0 на стороне клиента для MetaTrader 5.

Реализация клиента MQTT может считаться "относительно простой" по сравнению с другими сетевыми протоколами. Но "относительно легко" не обязательно значит, что всё пройдет гладко. Итак, мы начнем с подхода "снизу вверх", разработки через тестирование (TDD). Надеемся на обратную связь и результатов тестов членов сообщества.

Хотя TDD может использоваться (и часто используется) как очередное модное словечко, означающее почти всё что угодно, эта методология очень хорошо подходит, когда у нас есть набор формализованных спецификаций, что как раз относится к стандартизированному сетевому протоколу.

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

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


Повышенные требования, малые шаги: разбор протокола MQTT

Как и в случае большинства (если не всех) сетевых протоколов, MQTT работает путем разделения передаваемых данных на так называемые пакеты. Таким образом, если получатель знает, что означает каждый тип пакета, он может принять правильное рабочее поведение в зависимости от типа полученного пакета. В случае MQTT тип пакета называется Control Packet Type (тип управляющего пакета) и может иметь до трех частей:

  • фиксированный заголовок (fixed header) присутствует во всех пакетах

  • переменный заголовок (variable header) присутствует в некоторых пакетах

  • полезная нагрузка (payload) также присутствует только в некоторых пакетах

В MQTT-v5.0 имеется пятнадцать типов управляющих пакетов:

Таблица 1. Типы управляющих пакетов MQTT (таблица из спецификации OASIS)

Имя Значение Направление потока Описание
Reserved
0 Запрещено
Зарезервировано
CONNECT 1 Клиент-сервер Запрос на подключение
CONNACK 2 Сервер-клиент Подтверждение подключения
PUBLISH 3 Клиент-сервер или сервер-клиент Публикация сообщения
PUBREC 5 Клиент-сервер или сервер-клиент

Публикация получена (доставка QoS 2, часть 1)
PUBREL 6 Клиент-сервер или сервер-клиент
Выпуск публикации (доставка QoS 2, часть 2)
PUBCOMP 7 Клиент-сервер или сервер-клиент

Публикация завершена (доставка QoS 2, часть 3)
SUBSCRIBE 8 Клиент-сервер Запрос на подписку
SUBACK 9 Сервер-клиент Подтверждение подписки
UNSUBSCRIBE 10 Клиент-сервер Запрос на отмену подписки
UNSUBACK 11 Сервер-клиент
Подтверждение отмены подписки
PINGREQ 12 Клиент-сервер Запрос PING
PINGRESP 13 Сервер-клиент Ответ PING
DISCONNECT 14 Клиент-сервер или сервер-клиент Уведомление об отключении
AUTH 15 Клиент-сервер или сервер-клиент Обменная аутентификация

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

Рис. 1. Формат фиксированного заголовка MQTT

Формат фиксированного заголовка MQTT

Поскольку мы ничего не можем сделать, пока у нас не будет соединения между нашим Клиентом и Сервером, и учитывая, что в стандарте есть четкое заявление, которое гласит:

"После того, как сетевое подключение установлено Клиентом к Серверу, первый пакет, отправленный от Клиента к Серверу, ДОЛЖЕН быть пакетом CONNECT". 

Давайте посмотрим, как должен быть отформатирован фиксированный заголовок пакета CONNECT.

Рис.2. Формат фиксированного заголовка MQTT для пакета CONNECT

Формат фиксированного заголовка MQTT пакета CONNECT

Значит, нам нужно заполнить его двумя байтами: первый байт должен иметь двоичное значение 00010000, а второй - значение так называемой "оставшейся длины" ("Remaining Length").

Стандарт определяет оставшуюся длину как

"переменное байтовое целое число (Variable Byte Integer), которое представляет количество байтов, оставшихся в текущем управляющем пакете, включая данные в заголовке переменной и полезной нагрузке. Оставшаяся длина не включает байты, используемые для ее кодирования. Размер пакета — это общее количество байтов в управляющем пакете MQTT, оно равно длине фиксированного заголовка плюс оставшаяся длина" (выделено нами).

Стандарт также определяет схему кодирования для переменного байтового целого числа.

“Переменное байтовое целое число кодируется с использованием схемы кодирования, которая использует один байт для значений до 127. Большие значения обрабатываются следующим образом. Младшие семь битов каждого байта кодируют данные, а старший бит используется для указания того, есть ли в представлении следующие байты. Таким образом, каждый байт кодирует 128 значений и "бит продолжения". Максимальное количество байтов в поле переменного байтового целого числа — четыре. Закодированное значение ДОЛЖНО использовать минимальное количество байтов, необходимое для представления значения".

Как видим, в этих абзацах содержится довольно большой объем полезной информации. А мы ведь еще только заполняем второй байт!

К счастью, стандарт предоставляет "алгоритм кодирования неотрицательного целого числа (X) в схему кодирования переменного байтового целого числа".

do
   encodedByte = X MOD 128
   X = X DIV 128
   // if there are more data to encode, set the top bit of this byte
   if (X > 0)
      encodedByte = encodedByte OR 128
   endif
   'output' encodedByte
while (X > 0)

"Где MOD - это оператор по модулю (% в C), DIV - целочисленное деление (/ в C), а OR - побитовое (| в C).”

Теперь у нас есть:

  • список всех типов управляющих пакетов, 

  • формат фиксированного заголовка пакета CONNECT с двумя байтами,

  • значение первого байта,

  • и алгоритм кодирования переменного байтового целого числа, которое заполнит второй байт.

Мы можем начать подготовку нашего первого теста. 

Поскольку мы применяем восходящий подход TDD, мы будем писать тесты до реализации. Мы изначально будем 1) писать тесты, которые не работают, затем 2) вводить только код, необходимый для прохождения теста, а затем 3) проводить рефакторинг при необходимости. Не имеет значения, является ли первоначальная реализация наивной, уродливой или имеет плохую производительность. Мы разберемся с этими проблемами, как только у нас будет работающий код. Производительность находится в конце нашего списка задач.

Итак откроем MetaEditor и создадим скрипт TestFixedHeader со следующим содержанием.

#include <MQTT\mqtt.mqh>
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   Print(TestFixedHeader_Connect());
  }
//---
bool TestFixedHeader_Connect()
  {
   uchar content_buffer[]; //empty
//---
   uchar expected[2];
   expected[0] = 1; //pkt type
   expected[1] = 0; //remaining length
//---
   uchar fixed_header[];
//---
   GenFixedHeader(CONNECT, content_buffer, fixed_header);
//---
   if(!ArrayCompare(expected, fixed_header) == 0)
     {
      Print(__FUNCTION__);
      for(uint i = 0; i < expected.Size(); i++)
        {
         Print("expected: ", expected[i], " result: ", fixed_header[i]);
        }
      return false;
     }
   return true;
  }

Также создадим заголовок mqtt.mqh, где мы начнем разработку наших функций и наполним его приведенным ниже кодом.
void GenFixedHeader(uint pkt_type, uchar& buf[], uchar& head[])
  {
   ArrayFree(head);
   ArrayResize(head, 2);
//---
   head[0] = uchar(pkt_type);
//---
//Remaining Length
   uint x;
   x = ArraySize(buf);
   do
     {
      uint encodedByte = x % 128;
      x = (uint)(x / 128);
      if(x > 0)
        {
         encodedByte = encodedByte | 128;
        }
      head[1] = uchar(encodedByte);
     }
   while(x > 0);
  }
//+------------------------------------------------------------------+
enum ENUM_PKT_TYPE
  {
   CONNECT      =  1, // Connection request
   CONNACK      =  2, // Connect acknowledgment
   PUBLISH      =  3, // Publish message
   PUBACK       =  4, // Publish acknowledgment (QoS 1)
   PUBREC       =  5, // Publish received (QoS 2 delivery part 1)
   PUBREL       =  6, // Publish release (QoS 2 delivery part 2)
   PUBCOMP      =  7, // Publish complete (QoS 2 delivery part 3)
   SUBSCRIBE    =  8, // Subscribe request
   SUBACK       =  9, // Subscribe acknowledgment
   UNSUBSCRIBE 	=  10, // Unsubscribe request
   UNSUBACK     =  11, // Unsubscribe acknowledgment
   PINGREQ      =  12, // PING request
   PINGRESP     =  13, // PING response
   DISCONNECT  	=  14, // Disconnect notification
   AUTH         =  15, // Authentication exchange
  };

Запустив скрипт, вы должны увидеть следующее на вкладке "Эксперты".

Рис. 3. Фиксированный заголовок - Тест пройден

Output Test Fixed Header - True

Чтобы быть уверенным, что наш тест работает, нам нужно увидеть, когда он не работает. Поэтому настоятельно рекомендую изменить входной параметр content_buffer, оставив ожидаемую переменную неизменной. На вкладке "Эксперты" вы должны увидеть что-то вроде следующего.

Рис. 4. Фиксированный заголовок - Тест не пройден

Output Test Fixed Header - Тест не пройден

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

Сейчас мы можем повторить функцию TestFixedHeader_Connect для других типов пакетов. Мы будем игнорировать те, которые имеют направление потока только от сервера к клиенту. Это CONNACK, PUBACK, SUBACK,  UNSUBACK и PINGRESP. Эти типы и ответ ping будут сгенерированы сервером. Мы займемся ими позже.

Чтобы убедиться, что наши тесты работают должным образом, нам нужно включить тесты, которые, как ожидается, потерпят неудачу. Эти тесты возвращают true при неудаче.
#include <MQTT\mqtt.mqh>
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   Print(TestFixedHeader_Connect());
   Print(TestFixedHeader_Connect_RemainingLength1_Fail());
   Print(TestFixedHeader_Publish());
   Print(TestFixedHeader_Publish_RemainingLength1_Fail());
   Print(TestFixedHeader_Puback());
   Print(TestFixedHeader_Puback_RemainingLength1_Fail());
   Print(TestFixedHeader_Pubrec());
   Print(TestFixedHeader_Pubrec_RemainingLength1_Fail());
   Print(TestFixedHeader_Pubrel());
   Print(TestFixedHeader_Pubrel_RemainingLength1_Fail());
   Print(TestFixedHeader_Pubcomp());
   Print(TestFixedHeader_Pubcomp_RemainingLength1_Fail());
   Print(TestFixedHeader_Subscribe());
   Print(TestFixedHeader_Subscribe_RemainingLength1_Fail());
   Print(TestFixedHeader_Puback());
   Print(TestFixedHeader_Puback_RemainingLength1_Fail());
   Print(TestFixedHeader_Unsubscribe());
   Print(TestFixedHeader_Unsubscribe_RemainingLength1_Fail());
   Print(TestFixedHeader_Pingreq());
   Print(TestFixedHeader_Pingreq_RemainingLength1_Fail());
   Print(TestFixedHeader_Disconnect());
   Print(TestFixedHeader_Disconnect_RemainingLength1_Fail());
   Print(TestFixedHeader_Auth());
   Print(TestFixedHeader_Auth_RemainingLength1_Fail());
  }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool TestFixedHeader_Connect()
  {
   uchar content_buffer[]; //empty
//---
   uchar expected[2];
   expected[0] = 1; //pkt type
   expected[1] = 0; //remaining length
//---
   uchar fixed_header[];
//---
   GenFixedHeader(CONNECT, content_buffer, fixed_header);
//---
   if(!ArrayCompare(expected, fixed_header) == 0)
     {
      Print(__FUNCTION__);
      for(uint i = 0; i < expected.Size(); i++)
        {
         Print("expected: ", expected[i], " result: ", fixed_header[i]);
        }
      return false;
     }
   Print(__FUNCTION__);
   return true;
  }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool TestFixedHeader_Connect_RemainingLength1_Fail()
  {
   uchar content_buffer[]; //empty
   ArrayResize(content_buffer, 1);
   content_buffer[0] = 1;
//---
   uchar expected[2];
   expected[0] = 1; //pkt type
   expected[1] = 0; //remaining length should be 1
//---
   uchar fixed_header[];
//---
   GenFixedHeader(CONNECT, content_buffer, fixed_header);
//---
   if(!ArrayCompare(expected, fixed_header) == 0)
     {
      Print(__FUNCTION__);
      for(uint i = 0; i < expected.Size(); i++)
        {
         Print("expected: ", expected[i], " result: ", fixed_header[i]);
        }
      return true;
     }
   Print(__FUNCTION__);
   return false;
  }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool TestFixedHeader_Publish()
  {
   uchar content_buffer[]; //empty
//---
   uchar expected[2];
   expected[0] = 3; //pkt type
   expected[1] = 0; //remaining length
//---
   uchar fixed_header[];
//---
   GenFixedHeader(PUBLISH, content_buffer, fixed_header);
//---
   if(!ArrayCompare(expected, fixed_header) == 0)
     {
      Print(__FUNCTION__);
      for(uint i = 0; i < expected.Size(); i++)
        {
         Print("expected: ", expected[i], " result: ", fixed_header[i]);
        }
      return false;
     }
   Print(__FUNCTION__);
   return true;
  }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool TestFixedHeader_Publish_RemainingLength1_Fail()
  {
   uchar content_buffer[]; //empty
   ArrayResize(content_buffer, 1);
   content_buffer[0] = 1;
//---
   uchar expected[2];
   expected[0] = 3; //pkt type
   expected[1] = 0; //remaining length should be 1
//---
   uchar fixed_header[];
//---
   GenFixedHeader(PUBLISH, content_buffer, fixed_header);
//---
   if(!ArrayCompare(expected, fixed_header) == 0)
     {
      Print(__FUNCTION__);
      for(uint i = 0; i < expected.Size(); i++)
        {
         Print("expected: ", expected[i], " result: ", fixed_header[i]);
        }
      return true;
     }
   Print(__FUNCTION__);
   return false;
  }
.
.
.
(окончание не приводится для краткости)

Мы видим много шаблонного кода и копипасты. 

В долгосрочной перспективе это принесет хорошую отдачу. Эти простые (даже элементарные) тесты позволяют построить своего рода сеть безопасности для нашего развития. Они должны нам помочь 

  • оставаться сосредоточенными на поставленной задаче

  • избегать чрезмерного усложнения

  • и выявлять ошибки регрессии.

Я настоятельно рекомендую вам писать код самостоятельно, а не просто использовать прикрепленный файл. Вы увидите, сколько крошечных, "безобидных" ошибок можно обнаружить с самого начала. По мере того как мы углубимся в рабочие характеристики нашего клиента, эти тесты (как и некоторые другие) докажут свою ценность. Кроме того, мы избегаем распространенной технической недоработки: оставлять написание тестов на потом. Обычно в таком случае тесты никогда не будут написаны.

Рис. 5. Фиксированный заголовок - Все тесты пройдены

Output Test Fixed Header - Все тесты пройдены

Давайте посмотрим, распознается ли наш двухбайтовый заголовок CONNECT MQTT-посредником как валидный.


Установка MQTT-брокера (и клиента) для разработки и тестирования

В Интернете доступно множество производственных MQTT-брокеров, и большинство из них предлагают разного рода пробные URL для разработки и тестирования. Достаточно вбить в поисковик "MQTT-брокер" и вы увидите много предложений.

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

Опять же, есть несколько вариантов для Windows, Linux и Mac. Я установил Mosquitto в подсистему Windows для Linux (WSL). Помимо того, что Mosquitto бесплатен и имеет открытый исходный код, он очень удобен тем, что поставляется с двумя очень полезными для разработки приложениями командной строки: mosquitto_pub и mosquitto_sub для публикации и подписки на MQTT-топики. Я также установил его свою машину для разработки на Windows, чтобы иметь возможность перепроверить некоторые ошибки.

Помните, что в MetaTrader необходимо указывать любой внешний URL-адрес в меню "Сервис" > "Настройки" > "Советники" и что вам разрешен доступ только к портам 80 или 443 из MetaTrader. Таким образом, если вы установите брокера на WSL, не забудьте указать IP-адрес его хоста, а также перенаправить сетевой трафик, поступающий на порт 80, на 1883, который является портом MQTT (и Mosquitto) по умолчанию. Инструмент redir позволяет быстро и надежно осуществить перенаправление.

Рис. 6. Диалог MetaTrader 5 - Разрешить URL веб-запроса

Диалог MetaTrader 5 - Разрешить URL веб-запроса


Чтобы получить IP-адрес WSL, выполните команду ниже.

Рис. 7. WSL - Получение имени хоста

Рис. 7. WSL - Получение имени хоста


После установки Mosquitto будет автоматически настроен для запуска в качестве "сервиса" при загрузке. Таким образом, просто перезагрузите WSL, чтобы запустить Mosquitto на порту по умолчанию 1883.

Чтобы перенаправить сетевой трафик с порта 80 на 1883 с помощью redir, выполните команду ниже.

Рис. 8. Перенаправление сетевого трафика с помощью redir

Перенаправление портов с использованием командной строки redir


Наконец, мы можем проверить, распознается ли наш двухбайтовый фиксированный заголовок CONNECT как валидный заголовок MQTT-брокером, соответствующим спецификациям. Создадим временный скрипт и вставим следующий код. (Не забудьте изменить IP-адрес в переменной broker_ip в соответствии с выходными данными команды get hostname -I.)

#include <MQTT\mqtt.mqh>

string broker_ip = "172.20.155.236";
int broker_port = 80;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   int socket = SocketCreate();
   if(socket != INVALID_HANDLE)
     {
      if(SocketConnect(socket, broker_ip, broker_port, 1000))
        {
         Print("Connected ", broker_ip);
         //---
         uchar fixed_header[];
         uchar content_buffer[]; //empty
         //---
         GenFixedHeader(CONNECT, content_buffer, fixed_header);
         //---
         if(SocketSend(socket, fixed_header, ArraySize(fixed_header)) < 0)
           {
            Print("Failed sending fixed header ", GetLastError());
           }
        }
     }
  }

На вкладке "Эксперты" вы должны увидеть что-то вроде следующего.

Рис. 9. Подсоединение к локальному брокеру

Подсоединение к локальному брокеру

В логах Mosquitto увидим следующее.

Рис. 10. Подсоединение к локальному брокеру - логи Mosquitto

Подсоединение к локальному брокеру - логи Mosquitto

Наш фиксированный заголовок CONNECT был распознан Mosquitto, но неизвестный (<unknown>) клиент был немедленно отключен «из-за ошибки протокола». Ошибка произошла из-за того, что мы еще не включили заголовок переменной с именем протокола, уровнем протокола и другими связанными метаданными. Мы исправим это на следующем шаге.

Как видите в самом начале приведенной выше команды, мы используем команду tail -f {pathToLogFile}. Мы можем использовать ее во время разработки, чтобы следить за обновлениями журнала Mosquitto без необходимости открывать и перезагружать файл.

На следующем этапе мы реализуем заголовок переменной CONNECT (и других) для поддержания стабильного соединения с нашим брокером. Мы также опубликуем (PUBLISH) сообщение и будем работать с пакетами CONNACK, возвращаемыми брокером, и их соответствующими кодами причин (Reason Codes). Следующий шаг будет содержать несколько интересных побитовых операций для заполнения наших флагов подключения (Connect Flags). Следующий шаг также потребует от нас существенного улучшения наших тестов, чтобы справиться со сложностями, которые возникнут в результате взаимодействия клиента и брокера.


Заключение

В статье приведен краткий обзор протокола обмена сообщениями в режиме реального времени по принципу "издатель - подписчик" MQTT, его истоки и основные компоненты. Мы также указали на некоторые возможные варианты использования MQTT для обмена сообщениями в реальном времени в контексте трейдинга и на то, как использовать его для автоматизированных операций в MetaTrader 5 либо путем импорта DLL, сгенерированных из C, C++ или C#, либо с помощью библиотеки MQTT Python через модуль Python для MetaTrader 5.

Учитывая ограничения, налагаемые на использование DLL на торговой площадке MetaQuotes и в MetaQuotes Cloud Tester, мы также предложили и описали наши первые шаги по реализации нативного клиента MQL5 MQTT с использованием подхода разработки через тестирование (TDD).


Полезные ссылки

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

  • Программы, включая брокеров, библиотеки и инструменты.
  • Ресурсы, относящиеся к MQTT на GitHub.

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


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

Прикрепленные файлы |
TestFixedHeader.mq5 (19.48 KB)
mqtt.mqh (2.19 KB)
Как обнаруживать тренды и графические паттерны с помощью MQL5 Как обнаруживать тренды и графические паттерны с помощью MQL5
В статье представлен метод автоматического обнаружения моделей ценовых действий с помощью MQL5, таких как тренды (восходящий, нисходящий, боковой) и графические модели (двойная вершина, двойное дно).
Объектно-ориентированное программирование (ООП) в MQL5 Объектно-ориентированное программирование (ООП) в MQL5
Как разработчикам, нам необходимо научиться создавать и разрабатывать программное обеспечение, которое можно использовать многократно и гибко, без дублирования кода, особенно если у нас есть разные объекты с разным поведением. Это можно легко сделать, используя методы и принципы объектно-ориентированного программирования. В этой статье представлены основы объектно-ориентированного программирования в MQL5.
Разработка системы репликации - Моделирование рынка (Часть 12): Появление СИМУЛЯТОРА (II) Разработка системы репликации - Моделирование рынка (Часть 12): Появление СИМУЛЯТОРА (II)
Разработка симулятора может оказаться гораздо интереснее, чем кажется. Сегодня мы сделаем еще несколько шагов в этом направлении, потому что всё становится интереснее.
Язык визуального программиования ДРАКОН (Drakon) — средство общения для разработчика MQL и заказчика Язык визуального программиования ДРАКОН (Drakon) — средство общения для разработчика MQL и заказчика
ДРАКОН — язык визуального программирования, специально разработанный для упрощения взаимодействия между специалистами разных отраслей (биологами, физиками, инженерами...) с программистами в российских космических проектах (например, при создании создание комплекса "Буран"). В этой статье я расскажу о том, как ДРАКОН делает создание алгоритмов доступным и интуитивно понятным, даже если вы никогда не сталкивались с кодом, а также - как заказчику легче объяснить свои мысли при заказе торговых роботов, а программисту - совершать меньше ошибок в сложных функциях.