Клиентские программы эхо и чат-сервисов на MQL5

Для подключения к эхо-сервису напишем простой скрипт MQL5/Experts/MQL5Book/p7/wsEcho/wsecho.mq5 (обратите внимание, что это именно скрипт, но мы расположили его внутри папки MQL5/Experts/MQL5Book/p7/, сделав её единым контейнером для MQL-программ, связанных с веб, поскольку все последующие примеры будут экспертами). Поскольку в данной главе мы рассматриваем создание комплексов программ в рамках проектов, оформим скрипт как часть mqproj-проекта, в который также включим и серверную составляющую.

Входные параметры скрипта позволяют указать адрес сервиса и текст сообщения. По умолчанию используется незащищенное соединение. Если сервер wsecho.js будет запущен с поддержкой TLS, нужно поменять протокол на защищенный wss. Имейте в виду, что установление защищенного соединения требует больше времени (например, пару секунд), чем обычного.

input string Server = "ws://localhost:9000/";
input string Message = "My outbound message";
   
#include <MQL5Book/AutoPtr.mqh>
#include <MQL5Book/ws/wsclient.mqh>

В функции OnStart создадим экземпляр WebSocket-клиента (wss) для заданного адреса и вызовем метод open. В случае успешного подключения мы ждем приветственного сообщения от сервиса с помощью вызова wss.readMessage в блокирующем режиме (ожидание до 5 секунд, по умолчанию). Мы использует автоуказатель для получаемого объекта, чтобы не вызывать в конце delete вручную.

void OnStart()
{
   Print("\n");
   WebSocketClient<Hybiwss(Server);
   Print("Opening...");
   if(wss.open())
   {
      Print("Waiting for welcome message (if any)");
      AutoPtr<IWebSocketMessagewelcome(wss.readMessage());
      ...

Напомним, что класс WebSocketClient содержит заглушки обработчиков событий, в том числе и простой метод onMessage, который распечатает приветствие в журнал.

Затем мы отправляем свое сообщение и снова ждем ответа от сервера. Эхо-сообщение также будет выведено в журнал.

      Print("Sending message...");
      wss.send(Message);
      Print("Receiving echo...");
      AutoPtr<IWebSocketMessageecho(wss.readMessage());
   }
   ...

В завершение мы закрываем соединение.

   if(wss.isConnected())
   {
      Print("Closing...");
      wss.close();
   }
}

На основе файла скрипта создадим файл проекта (wsecho.mqproj). Заполним в свойствах проекта номер версии (1.0), копирайт, и описание. Добавим в ветвь Settings and files серверные файлы эхо-сервиса (это, как минимум, станет напоминанием разработчику о наличии тестового сервера). После компиляции в иерархии появятся зависимости (заголовочные файлы).

Все должно выглядеть примерно как на скриншоте.

Проект эхо-сервиса, клиентский скрипт и сервер

Проект эхо-сервиса, клиентский скрипт и сервер

Если бы скрипт располагался внутри папки Shared Projects, например, в MQL5/Shared Projects/MQL5Book/wsEcho/, то после успешной компиляции его ex5-файл был бы автоматически перемещен в папку MQL5/Scripts/Shared Projects/MQL5Book/wsEcho/, о чем была бы выведена соответствующая запись в журнал компиляции. Это стандартное поведение компиляции любых MQL-программ в разделяемых проектах.

Во всех примерах данной главы не забываем стартовать сервер, прежде чем тестировать MQL-скрипт. В данном случае выполняем команду: node.exe wsecho.js, находясь в папке Web.

Далее запустим скрипт wsecho.ex5. В журнале появятся записи о происходящих действиях, а также уведомления о сообщениях.

Opening...
Connecting to localhost:9000
Buffer: 'HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: mIpas63g5xGMqJcKtreHKpSbY1w=
'
Headers: 
                               [,0]                           [,1]
[0,] "upgrade"                      "websocket"                   
[1,] "connection"                   "Upgrade"                     
[2,] "sec-websocket-accept"         "mIpas63g5xGMqJcKtreHKpSbY1w="
 > Connected ws://localhost:9000/
Waiting for welcome message (if any)
 > Message ws://localhost:9000/ server#Hello, user1
Sending message...
Receiving echo...
 > Message ws://localhost:9000/ user1#My outbound message
Closing...
Close requested
Waiting...
SocketRead failed: 5273 Available: 1
 > Disconnected ws://localhost:9000/
Server close ack

Приведенные выше HTTP-заголовки — это ответ сервера в процессе "рукопожатия". Если заглянуть в окно консоли, где запущен сервер, обнаружим HTTP-заголовки, полученные сервером от нашего клиента.

Серверный журнал эхо-сервиса

Серверный журнал эхо-сервиса

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

Проделаем аналогичную работу для чат-сервиса: создадим WebSocket-клиент на MQL5, проект под него, и протестируем. На этот раз тип клиентской программы будет эксперт, потому что для чата необходима поддержка интерактивных событий от клавиатуры на графике. Эксперт прилагается к книге в папке MQL5/MQL5Book/p7/wsChat/wschat.mq5.

Для демонстрации технологии получения событий в методах-обработчиках определим собственный класс MyWebSocket, производный от WebSocketClient.

class MyWebSocketpublic WebSocketClient<Hybi>
{
public:
   MyWebSocket(const string addressconst bool compress = false):
      WebSocketClient(addresscompress) { }
   
   /* void onConnected() override { } */
   
   void onDisconnect() override
   {
      // можем сделать что-то еще и вызвать (или не вызывать) унаследованный код
      WebSocketClient<Hybi>::onDisconnect();
   }
   
   void onMessage(IWebSocketMessage *msgoverride
   {
      // TODO: мы могли бы отсекать копии собственных сообщений,
      // но они оставлены для отладки
      Alert(msg.getString());
      delete msg;
   }
};

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

В глобальном контексте опишем объект нашего класса wss и строку message, где будет аккумулироваться ввод пользователя с клавиатуры.

MyWebSocket wss(Server);
string message = "";

Функция OnInit содержит необходимую подготовку, в частности запускает таймер и открывает соединение.

int OnInit()
{
  ChartSetInteger(0CHART_QUICK_NAVIGATIONfalse);
  EventSetTimer(1);
  wss.setTimeOut(1000);
  Print("Opening...");
  return wss.open() ? INIT_SUCCEEDED : INIT_FAILED;
}

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

void OnTimer()
{
   wss.checkMessages(false); // в таймере используем неблокирующую проверку
}

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

void OnChartEvent(const int idconst long &lparamconst double &dparam,
   const string &sparam)
{
   if(id == CHARTEVENT_KEYDOWN)
   {
      if(lparam == VK_RETURN)
      {
         const static string longmessage = ...
         if(message == "long"wss.send(longmessage);
         else if(message == "bye"wss.close();
         else wss.send(message);
         message = "";
      }
      else if(lparam == VK_BACK)
      {
         StringSetLength(messageStringLen(message) - 1);
      }
      else
      {
         ResetLastError();
         const short c = TranslateKey((int)lparam);
         if(_LastError == 0)
         {
            message += ShortToString(c);
         }
      }
      Comment(message);
   }
}

Если ввести текст "long", программа отправит специально подготовленный довольно длинный текст. Если текст сообщения "bye", программа закрывает соединение. Также соединение закроется при выходе из программы.

void OnDeinit(const int)
{
   if(wss.isConnected())
   {
      Print("Closing...");
      wss.close();
   }
}

Создадим под эксперт проект (файл wschat.mqproj), заполним его свойства и добавим серверную часть в ветвь Settings and files. На этот раз покажем, как файл проекта выглядит изнутри. В mqproj-файле ветвь Dependencies хранится в свойстве "files", а ветвь Settings and files — в свойстве "tester".

{

  "platform"    :"mt5",

  "program_type":"expert",

  "copyright"   :"Copyright 2022, MetaQuotes Ltd.",

  "version"     :"1.0",

  "description" :"WebSocket-client for chat-service.\r\nType and send text messages for all connected users.\r\nShow alerts with messages from others.",

  "optimize"    :"1",

  "fpzerocheck" :"1",

  "tester_no_cache":"0",

  "tester_everytick_calculate":"0",

  "unicode_character_set":"0",

  "static_libraries":"0",

  "files":

  [

    {

      "path":"wschat.mq5",

      "compile":true,

      "relative_to_project":true

    },

    {

      "path":"MQL5\\Include\\MQL5Book\\ws\\wsclient.mqh",

      "compile":false,

      "relative_to_project":false

    },

    {

      "path":"MQL5\\Include\\MQL5Book\\URL.mqh",

      "compile":false,

      "relative_to_project":false

    },

    {

      "path":"MQL5\\Include\\MQL5Book\\ws\\wsframe.mqh",

      "compile":false,

      "relative_to_project":false

    },

    {

      "path":"MQL5\\Include\\MQL5Book\\ws\\wstools.mqh",

      "compile":false,

      "relative_to_project":false

    },

    {

      "path":"MQL5\\Include\\MQL5Book\\ws\\wsinterfaces.mqh",

      "compile":false,

      "relative_to_project":false

    },

    {

      "path":"MQL5\\Include\\MQL5Book\\ws\\wsmessage.mqh",

      "compile":false,

      "relative_to_project":false

    },

    {

      "path":"MQL5\\Include\\MQL5Book\\ws\\wstransport.mqh",

      "compile":false,

      "relative_to_project":false

    },

    {

      "path":"MQL5\\Include\\MQL5Book\\ws\\wsprotocol.mqh",

      "compile":false,

      "relative_to_project":false

    },

    {

      "path":"MQL5\\Include\\VirtualKeys.mqh",

      "compile":false,

      "relative_to_project":false

    }

  ],

  "tester":

  [

    {

      "type":"file",

      "path":"..\\Web\\MQL5Book.crt",

      "relative_to_project":true

    },

    {

      "type":"file",

      "path":"..\\Web\\MQL5Book.key",

      "relative_to_project":true

    },

    {

      "type":"file",

      "path":"..\\Web\\wschat.htm",

      "relative_to_project":true

    },

    {

      "type":"file",

      "path":"..\\Web\\wschat.js",

      "relative_to_project":true

    },

    {

      "type":"file",

      "path":"..\\Web\\wschat_client.js",

      "relative_to_project":true

    }

  ]

}

Если бы эксперт находился внутри папки Shared Projects, например, в MQL5/Shared Projects/MQL5Book/wsChat/, после успешной компиляции его ex5-файл был бы автоматически перемещен в папку MQL5/Experts/Shared Projects/MQL5Book/wsChat/.

Стартуем сервер node.exe wschat.js. Теперь можно запустить пару копий эксперта на разных графиках. В принципе, сервис предполагает "общение" между разными терминалами и даже разными компьютерами, но никто не запрещает тестировать его из одного терминала.

Вот пример общения между чартами EURUSD и GBPUSD.

(EURUSD,H1)        
(EURUSD,H1)        Opening...
(EURUSD,H1)        Connecting to localhost:9000
(EURUSD,H1)        Buffer: 'HTTP/1.1 101 Switching Protocols
(EURUSD,H1)        Upgrade: websocket
(EURUSD,H1)        Connection: Upgrade
(EURUSD,H1)        Sec-WebSocket-Accept: Dg+aQdCBwNExE5mEQsfk5w9J+uE=
(EURUSD,H1)        
(EURUSD,H1)        '
(EURUSD,H1)        Headers: 
(EURUSD,H1)                                       [,0]                           [,1]
(EURUSD,H1)        [0,] "upgrade"                      "websocket"                   
(EURUSD,H1)        [1,] "connection"                   "Upgrade"                     
(EURUSD,H1)        [2,] "sec-websocket-accept"         "Dg+aQdCBwNExE5mEQsfk5w9J+uE="
(EURUSD,H1)         > Connected ws://localhost:9000/
(EURUSD,H1)        Alert: server#Hello, user1
(GBPUSD,H1)        
(GBPUSD,H1)        Opening...
(GBPUSD,H1)        Connecting to localhost:9000
(GBPUSD,H1)        Buffer: 'HTTP/1.1 101 Switching Protocols
(GBPUSD,H1)        Upgrade: websocket
(GBPUSD,H1)        Connection: Upgrade
(GBPUSD,H1)        Sec-WebSocket-Accept: NZENnc8p05T4amvngeop/e/+gFw=
(GBPUSD,H1)        
(GBPUSD,H1)        '
(GBPUSD,H1)        Headers: 
(GBPUSD,H1)                                       [,0]                           [,1]
(GBPUSD,H1)        [0,] "upgrade"                      "websocket"                   
(GBPUSD,H1)        [1,] "connection"                   "Upgrade"                     
(GBPUSD,H1)        [2,] "sec-websocket-accept"         "NZENnc8p05T4amvngeop/e/+gFw="
(GBPUSD,H1)         > Connected ws://localhost:9000/
(GBPUSD,H1)        Alert: server#Hello, user2
(EURUSD,H1)        Alert: user1#I'm typing this on EURUSD chart
(GBPUSD,H1)        Alert: user1#I'm typing this on EURUSD chart
(GBPUSD,H1)        Alert: user2#Got it on GBPUSD chart!
(EURUSD,H1)        Alert: user2#Got it on GBPUSD chart!

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

Общение видно и на стороне сервера.

Серверный журнал чат-сервиса

Серверный журнал чат-сервиса

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