WebSocket для MetaTrader 5

Francis Dube | 21 января, 2021

Введение

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

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

Для начала кратко опишем основы сетевого протокола WebSocket.

Введение в WebSocket

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

Зачем нужен протокол WebSocket

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

К ним относятся:

Все перечисленные методы допускают различные уровни обмена данными между клиентом и сервером, но, в отличие от протокола WebSocket, имеют следующие главные недостатки:

Особенности WebSocket

WebSocket — это протокол на основе TCP, который может быть расширен для поддержки других приложений или дочерних протоколов. Поскольку он основан на TCP, он может работать через стандартные HTTP-порты 80, 443 и имеет аналогичную схему URL-локатора. Адреса WebSocket-серверов имеют префиксы ws или wss вместо http, но при этом располагают той же структурой url-адресов, что и http, например: 

ws(s)://websocketexampleurl.com:80/hello.php

                   

Принципы работы протокола Websocket

Чтобы понять, как можно реализовать WebSocket-клиент в MQL5, необходимо ознакомиться с основами организации вычислительной сети. Протокол WebSocket похож на HTTP, в котором в клиентских запросах к серверу используются заголовки. Протокол WebSocket также использует заголовки. Его отличительная черта в том, что такие запросы нужны только установки или инициализации веб-сокета. Клиент делает http-запрос, который затем переключается на WebSocket.

Этот процесс называется рукопожатием. Переключение протоколов происходит, только если исходный http-запрос содержит определенный заголовок или заголовки. Затем сервер должен ответить соответствующим образом, подтвердив желание установить WebSocket-соединение. Информация о специальных заголовках и возможных ответах сервера задокументирована в RFC 6455.

После установки веб-сокета использовать http-запросы больше не нужно. В этом заключается различие протоколов в работе. При обмене данными с использованием протокола WebSocket принят другой формат. Этот формат более оптимизирован и использует гораздо меньше необработанных битов по сравнению с http-запросом. Используемый формат называется протоколом кадровой синхронизации (framing protocol). Данные, которыми обмениваются в одной транзакции между хостами, называются кадром, или фреймом.

Каждый кадр представляет собой последовательность битов, упорядоченных в соответствии с RFC 6455. Каждый кадр веб-сокета содержит биты, которые определяют код операции, размер данных и сами данные. Протокол также определяет, как эти биты расположены и упакованы в кадре. Код операции — это просто зарезервированное числовое значение для классификации кадра.

Базовые коды операций для веб-сокетов определены следующим образом:

0 — кадр продолжения: неполные данные, ожидаются следующие кадры. Эта функция обеспечивает фрагментацию кадра. Данные разделяются на блоки, которые упаковываются в разные кадры.

1 — текстовый кадр: данные представляют собой текст.

2 — бинарный кадр: данные имеют бинарную форму.

8 — кадр закрытия: каждая из конечных точек намеревается закрыть установленное соединение. Этот тип кадров называется кадром управления. Он имеет значение сам по себе и не всегда содержит какие-либо данные.

9 — пинг-кадр (кадр проверки связи): кадр управления для определения, подключена ли конечная точка

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

Это базовые коды операций, которые должны поддерживаться любым веб-сокетом. Протокол позволяет увеличивать число возможных кодов для API или дочерних протоколов на основе WebSocket.

Последний важный аспект, касающийся кадров, — это маскирование. RFC 6455 предписывает, чтобы все кадры, отправляемые от клиента на сервер, были замаскированы. Маскирование служит основной мерой безопасности для протокола WebSocket. Данные шифруются случайным образом сгенерированным 4-байтовым значением (ключом) с использованием заранее определенного алгоритма. Алгоритм описан в RFC 6455. Все кадры, отправляемые клиентом (включая фрагментированные), должны использовать специально сгенерированный для них случайный ключ.

Чтобы узнать больше, обратитесь к документации стандарта RFC6455. Думаю, с этими знаниями разобраться в реализации кода будет намного проще.


WebSocket-клиент на MQL5 — обзор библиотеки

Для начала код будет разбит на три класса.

CSocket инкапсулирует сетевые функции MQL5 API.

CFrame — кадр веб-сокета, используемый в первую очередь для дешифровки кадров, получаемых от серверов.

CWebSocketClient  — WebSocket-клиент

CSocket

//+------------------------------------------------------------------+
//| structs                                                          |
//+------------------------------------------------------------------+
struct CERT
  {
   string            cert_subject;
   string            cert_issuer;
   string            cert_serial;
   string            cert_thumbprint;
   datetime          cert_expiry;
  };


//+------------------------------------------------------------------+
//| Class CSocket.                                                   |
//| Purpose: Base class of socket operations.                        |
//|                                                                  |
//+------------------------------------------------------------------+

class CSocket
  {
private:
   static int        m_usedsockets;   // tracks number of sockets in use in single program
   bool              m_log;           // logging state
   bool              m_usetls;        //  tls state
   uint              m_tx_timeout;    //  send system socket timeout in milliseconds
   uint              m_rx_timeout;    //  receive system socket timeout in milliseconds
   int               m_socket;        //  socket handle
   string            m_address;       //  server address
   uint              m_port;          //  port


   CERT              m_cert;          //  Server certificate info

public:
                     CSocket();
                    ~CSocket();
   //--- methods to get private properties
   int               SocketID(void)           const { return(m_socket); }
   string            Address(void)            const { return(m_address);   }
   uint              Port(void)               const { return(m_port);  }
   bool              IsSecure(void)           const { return(m_usetls); }
   uint              RxTimeout(void)          const { return(m_rx_timeout); }
   uint              TxTimeout(void)          const { return(m_tx_timeout); }
   bool              ServerCertificate(CERT& certificate);


   //--- methods to set private properties
   bool              SetTimeouts(uint tx_timeout, uint rx_timeout);
   //--- general methods for working sockets
   void              Log(const string custom_message,const int line,const string func);
   static uint       SocketsInUse(void)        {   return(m_usedsockets);  }
   bool              Open(const string server,uint port,uint timeout,bool use_tls=false,bool enablelog=false);
   bool              Close(void);
   uint              Readable(void);
   bool              Writable(void);
   bool              IsConnected(void);
   int               Read(uchar& out[],uint out_len,uint ms_timeout,bool read_available);
   int               Send(uchar& in[],uint in_len);

  };

int CSocket::m_usedsockets=0;
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CSocket::CSocket():m_socket(INVALID_HANDLE),
   m_address(""),
   m_port(0),
   m_usetls(false),
   m_log(false),
   m_rx_timeout(150),
   m_tx_timeout(150)
  {
   ZeroMemory(m_cert);
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CSocket::~CSocket()
  {
//--- check handle
   if(m_socket!=INVALID_HANDLE)
      Close();
  }
//+------------------------------------------------------------------+
//| set system socket timeouts                                       |
//+------------------------------------------------------------------+
bool CSocket::SetTimeouts(uint tx_timeout,uint rx_timeout)
  {
   if(m_socket==INVALID_HANDLE)
     {
      Log("Invalid socket",__LINE__,__FUNCTION__);
      return(false);
     }

   if(SocketTimeouts(m_socket,tx_timeout,rx_timeout))
     {
      m_tx_timeout=tx_timeout;
      m_rx_timeout=rx_timeout;
      Log("Socket Timeouts set",__LINE__,__FUNCTION__);
      return(true);
     }

   return(false);
  }

//+------------------------------------------------------------------+
//| certificate                                                      |
//+------------------------------------------------------------------+
bool CSocket::ServerCertificate(CERT& certificate)
  {

   if(m_socket==INVALID_HANDLE)
     {
      Log("Invalid socket",__LINE__,__FUNCTION__);
      return(false);
     }

   if(SocketTlsCertificate(m_socket,m_cert.cert_subject,m_cert.cert_issuer,m_cert.cert_serial,m_cert.cert_thumbprint,m_cert.cert_expiry))
     {
      certificate=m_cert;
      Log("Server certificate retrieved",__LINE__,__FUNCTION__);
      return(true);
     }

   return(false);

  }
//+------------------------------------------------------------------+
//|connect()                                                         |
//+------------------------------------------------------------------+
bool CSocket::Open(const string server,uint port,uint timeout,bool use_tls=false,bool enablelog=false)
  {
   if(m_socket!=INVALID_HANDLE)
      Close();

   if(m_usedsockets>=128)
     {
      Log("Too many sockets open",__LINE__,__FUNCTION__);
      return(false);
     }

   m_usetls=use_tls;

   m_log=enablelog;

   m_socket=SocketCreate();
   if(m_socket==INVALID_HANDLE)
     {
      Log("Invalid socket",__LINE__,__FUNCTION__);
      return(false);
     }
   ++m_usedsockets;
   m_address=server;

   if(port==0)
     {
      if(m_usetls)
         m_port=443;
      else
         m_port=80;
     }
   else
      m_port=port;
//---
   if(!m_usetls && m_port==443)
      m_usetls=true;
//---
   Log("Connecting to "+m_address,__LINE__,__FUNCTION__);
//---
   if(m_usetls)
     {
      if(m_port!=443)
        {
         if(SocketConnect(m_socket,server,port,timeout))
            return(SocketTlsHandshake(m_socket,server));
        }
      else
        {
         return(SocketConnect(m_socket,server,port,timeout));
        }
     }

   return(SocketConnect(m_socket,server,port,timeout));
  }
//+------------------------------------------------------------------+
//|close()                                                           |
//+------------------------------------------------------------------+
bool CSocket::Close(void)
  {
//---
   if(m_socket==INVALID_HANDLE)
     {
      Log("Socket Disconnected",__LINE__,__FUNCTION__);
      return(true);
     }
//---
   if(SocketClose(m_socket))
     {
      m_socket=INVALID_HANDLE;
      --m_usedsockets;
      Log("Socket Disconnected from "+m_address,__LINE__,__FUNCTION__);
      m_address="";
      ZeroMemory(m_cert);
      return(true);
     }
//---
   Log("",__LINE__,__FUNCTION__);
   return(false);
  }
//+------------------------------------------------------------------+
//|readable()                                                        |
//+------------------------------------------------------------------+
uint CSocket::Readable(void)
  {
   if(m_socket==INVALID_HANDLE)
     {
      Log("Invalid socket",__LINE__,__FUNCTION__);
      return(0);
     }
//---
   Log("Is Socket Readable ",__LINE__,__FUNCTION__);
//---
   return(SocketIsReadable(m_socket));
  }
//+------------------------------------------------------------------+
//|writable()                                                        |
//+------------------------------------------------------------------+
bool CSocket::Writable(void)
  {
   if(m_socket==INVALID_HANDLE)
     {
      Log("Invalid socket",__LINE__,__FUNCTION__);
      return(false);
     }
//---
   Log("Is Socket Writable ",__LINE__,__FUNCTION__);
//---
   return(SocketIsWritable(m_socket));
  }
//+------------------------------------------------------------------+
//|isconnected()                                                     |
//+------------------------------------------------------------------+
bool CSocket::IsConnected(void)
  {
   if(m_socket==INVALID_HANDLE)
     {
      Log("Invalid socket",__LINE__,__FUNCTION__);
      return(false);
     }
//---
   Log("Is Socket Connected ",__LINE__,__FUNCTION__);
//---
   return(SocketIsConnected(m_socket));
  }
//+------------------------------------------------------------------+
//|read()                                                            |
//+------------------------------------------------------------------+
int CSocket::Read(uchar& out[],uint out_len,uint ms_timeout,bool read_available=false)
  {
   if(m_socket==INVALID_HANDLE)
     {
      Log("Invalid socket",__LINE__,__FUNCTION__);
      return(-1);
     }
//---
   Log("Reading from "+m_address,__LINE__,__FUNCTION__);

   if(m_usetls)
     {
      if(read_available)
         return(SocketTlsReadAvailable(m_socket,out,out_len));
      else
         return(SocketTlsRead(m_socket,out,out_len));
     }
   else
      return(SocketRead(m_socket,out,out_len,ms_timeout));

   return(-1);
  }
//+------------------------------------------------------------------+
//|send()                                                            |
//+------------------------------------------------------------------+
int CSocket::Send(uchar& in[],uint in_len)
  {
   if(m_socket==INVALID_HANDLE)
     {
      Log("Invalid socket",__LINE__,__FUNCTION__);
      return(-1);
     }
//---
   Log("Sending to "+m_address,__LINE__,__FUNCTION__);
//---
   if(m_usetls)
      return(SocketTlsSend(m_socket,in,in_len));
   else
      return(SocketSend(m_socket,in,in_len));
//---
   return(-1);
  }
//+------------------------------------------------------------------+
//|log()                                                             |
//+------------------------------------------------------------------+
void CSocket::Log(const string custom_message,const int line,const string func)
  {
   if(m_log)
     {
      //---
      int eid=GetLastError();
      //---
      if(eid!=0)
        {
         PrintFormat("[MQL error ID: %d][%s][Line: %d][Function: %s]",eid,custom_message,line,func);
         ResetLastError();
         return;
        }
      if(custom_message!="")
         PrintFormat("[%s][Line: %d][Function: %s]",custom_message,line,func);
     }
//---
  }
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

Класс сокета определяет структуру CERT, которая капсулирует данные о сертификате сервера.

Методы, получающие частные свойства:

SocketID — возвращает хэндл сокета для успешно созданного сокета.
Address — возвращает удаленный адрес, к которому подключен сокет, в виде строки.
Port — возвращает удаленный порт, к которому подключен активный сокет.
IsSecure — возвращает true или false в зависимости от того, включен ли в сокете протокол TLS или нет.
RxTimeout — возвращает установленный таймаут в миллисекундах для чтения из сокета.
TxTimeout — возвращает установленный таймаут в миллисекундах для записи в сокет.
ServerCertificate — возвращает данные о сертификате сервера, к которому подключен сокет.
SocketsInUse — возвращает общее количество сокетов, используемых в настоящее время в одной программе.

Методы установки частных свойств:

SetTimeouts — установить таймауты для чтения и записи в сокет в миллисекундах.
Общие методы рабочих сокетов
Log — служебный метод для регистрации активности сокета. Для вывода сообщений в журнал терминала необходимо установить логирование при инициализации сокета методом Open.
Open — метод установления соединения с удаленным сервером, при котором создается новый сокет.
Close — метод отключения от удаленного сервера и деинициализации сокета.
Readable — возвращает количество байтов, доступных для чтения в сокете
Writable — запрашивает, доступен ли сокет для каких-либо операций отправки.
IsConnected — проверяет, активно ли сокет-соединение.
Read — считывает данные из сокета.
Send — метод выполнения операций отправки на активном сокете.

CFrame

//+------------------------------------------------------------------+
//| enums                                                            |
//+------------------------------------------------------------------+
enum ENUM_FRAME_TYPE     // type of websocket frames (ie, message types)
  {
   CONTINUATION_FRAME=0x0,
   TEXT_FRAME=0x1,
   BINARY_FRAME= 0x2,
   CLOSE_FRAME = 8,
   PING_FRAME = 9,
   PONG_FRAME = 0xa,
  };
//+------------------------------------------------------------------+
//| class frame                                                      |
//| represents a websocket message frame                             |
//+------------------------------------------------------------------+



//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CFrame
  {
private:
   uchar             m_array[];
   uchar             m_isfinal;
   ENUM_FRAME_TYPE   m_msgtype;

   int               Resize(int size) {return(ArrayResize(m_array,size,size));}

public:
                     CFrame():m_isfinal(0),m_msgtype(0) {   }

                    ~CFrame() {      }
   int               Size(void) {return(ArraySize(m_array));}
   bool              Add(const uchar value);
   bool              Fill(const uchar &array[],const int src_start,const int count);
   void              Reset(void);
   uchar             operator[](int index);
   string            ToString(void);
   ENUM_FRAME_TYPE   MessageType(void) { return(m_msgtype);}
   bool              IsFinal(void) { return(m_isfinal==1);}
   void              SetMessageType(ENUM_FRAME_TYPE mtype) { m_msgtype=mtype;}
   void              SetFinal(void) { m_isfinal=1;}

  };
//+------------------------------------------------------------------+
//| Receiving an element by index                                    |
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
uchar CFrame::operator[](int index)
  {
   static uchar invalid_value;
//---
   int max=ArraySize(m_array)-1;
   if(index<0 || index>=ArraySize(m_array))
     {
      PrintFormat("%s index %d is not in range (0-%d)!",__FUNCTION__,index,max);
      return(invalid_value);
     }
//---
   return(m_array[index]);
  }
//+------------------------------------------------------------------+
//| Adding element                                                   |
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CFrame::Fill(const uchar &array[],const int src_start,const int count)
  {
   int p_size=Size();
//---
   int size=Resize(p_size+count);
//---
   if(size>0)
      return(ArrayCopy(m_array,array,p_size,src_start,count)==count);
   else
      return(false);
//---
  }
//+------------------------------------------------------------------+
//| Assigning for the array                                          |
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CFrame::Add(const uchar value)
  {
   int size=Resize(Size()+1);
//---
   if(size>0)
      m_array[size-1]=value;
   else
      return(false);
//---
   return(true);
//---
  }
//+------------------------------------------------------------------+
//|  Reset                                                           |
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CFrame::Reset(void)
  {
   if(Size())
      ArrayFree(m_array);
//---

   m_isfinal=0;

   m_msgtype=0;

  }
//+------------------------------------------------------------------+
//|converting array to string                                        |
//+------------------------------------------------------------------+
string CFrame::ToString(void)
  {
   if(Size())
      if(m_msgtype==CLOSE_FRAME)
         return(CharArrayToString(m_array,2,WHOLE_ARRAY,CP_UTF8));
   else
      return(CharArrayToString(m_array,0,WHOLE_ARRAY,CP_UTF8));
   else
      return(NULL);
  }

Класс кадра определяет перечисление ENUM_FRAME_TYPE, которое описывает различные типы кадров, задокументированные протоколом WebSocket.

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

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

Метод MessageType возвращает тип кадра как тип ENUM_FRAME_TYPE.

Метод IsFinal проверяет, является ли кадр последним, что означает, что все полученные данные следует считать целыми. Это позволяет различать фрагментированное и, следовательно, неполное сообщение от полного.

operator[] — перегрузка оператора subscript позволяет извлекать любой элемент кадра в формате массива.

Класс CFrame будет использоваться в клиенте WebSocket при чтении из объекта CSocket. Для заполнения кадра используются методы Add и Fill, позволяющие заполнить кадр либо отдельным элементом, либо соответствующим массивом.

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

CWebSocketClient

Класс содержит константы, реализованные как #defines. Символы с префиксом HEADER связаны с полями заголовка http, необходимыми для создания открывающего рукопожатия. GUID — это международный уникальный идентификатор, используемый протоколом веб-сокета на стороне сервера при генерации части заголовков ответа. Класс использует его для подтверждения и демонстрации правильности процесса установления связи, но, по сути, в этом нет необходимости. Клиенту нужно только проверить наличие поля заголовка |Sec-WebSocket-Accept|, чтобы подтвердить успешное рукопожатие.

#include <Socket.mqh>
#include <Frame.mqh>


//+------------------------------------------------------------------+
//| defines                                                          |
//+------------------------------------------------------------------+
#define SH1                 "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
#define HEADER_EOL          "\r\n"
#define HEADER_GET          "GET /"
#define HEADER_HOST         "Host: "
#define HEADER_UPGRADE      "Upgrade: websocket"+HEADER_EOL
#define HEADER_CONNECTION   "Connection: Upgrade"+HEADER_EOL
#define HEADER_KEY          "Sec-WebSocket-Key: "
#define HEADER_WS_VERSION   "Sec-WebSocket-Version: 13"+HEADER_EOL+HEADER_EOL
#define HEADER_HTTP         " HTTP/1.1"


Тип перечисления ENUM_STATUS_CLOSE_CODE перечисляет коды закрытия, которые можно отправить или получить вместе с кадром закрытия. Перечисление ENUM_WEBSOCKET_CLIENT_STATE отображает различные состояния, которые может принимать веб-сокет.

Closed — это начальное состояние до того, как какой-либо сокет будет выделен для клиента, или после того, как клиент сбросил соединение и базовый сокет был закрыт.

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

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

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
enum ENUM_CLOSE_CODE                 // possible reasons for disconnecting sent with a close frame
  {
   NORMAL_CLOSE = 1000,            // normal closure initiated by choice
   GOING_AWAY_CLOSE,               // close code for client navigating away from end point, used in browsers
   PROTOCOL_ERROR_CLOSE,           // close caused by some violation of a protocol, usually application defined
   FRAME_TYPE_ERROR_CLOSE,         // close caused by an endpoint receiving frame type that is not supportted or allowed
   UNDEFINED_CLOSE_1,              // close code is not defined by websocket protocol
   UNUSED_CLOSE_1,                 // unused
   UNUSED_CLOSE_2,                 // values
   ENCODING_TYPE_ERROR_CLOSE,      // close caused data in message is of wrong encoding type, usually referring to strings
   APP_POLICY_ERROR_CLOSE,         // close caused by violation of user policy
   MESSAGE_SIZE_ERROR_CLOSE,       // close caused by endpoint receiving message that is too large
   EXTENSION_ERROR_CLOSE,          // close caused by non compliance to or no support for specified extension of websocket protocol
   SERVER_SIDE_ERROR_CLOSE,        // close caused by some error that occurred on the server
   UNUSED_CLOSE_3 = 1015,          // unused
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
enum ENUM_WEBSOCKET_STATE
  {
   CLOSED=0,
   CLOSING,
   CONNECTING,
   CONNECTED
  };


Метод ClientState извлекает свойство, определяющее состояние подключения любого WebSocket-клиента.

//+------------------------------------------------------------------+
//| ClientState()                                                    |
//+------------------------------------------------------------------+
ENUM_WEBSOCKET_STATE CWebSocketClient::ClientState(void)
  {
   if(m_socket.IsConnected())
      return(m_wsclient);
//---
   if(m_wsclient!=CLOSED)
     {
      m_socket.Close();
      m_wsclient=CLOSED;
     }
//---
   return(m_wsclient);
  }

SetMaxSendSize() используется для настройки фрагментации кадра WebSocket-клиента. Метод устанавливает максимальный размер в байтах для одного кадра, отправляемого от клиента на сервер. Это обеспечивает гибкость клиента для использования с любым API, который устанавливает ограничения на размер кадра.

void              SetMaxSendSize(int maxsend) {if(maxsend>=0) m_maxsendsize=maxsend;  else m_maxsendsize=0; }


Метод Connect используется для установления WebSocket-соединения. Параметр secure - это булевое значение для настройки веб-сокета с TLS или без него. Метод сначала вызывает метод open класса CSocket, чтобы установить начальное TCP-соединение. В случае успеха состояние WebSocket меняется на "соединение", после чего в работу вступает вспомогательный метод обновления. В его обязанности входит создание необходимого http-заголовка для переключения на протокол WebSocket. Состояние веб-сокета проверяется при выходе из функции.

//+------------------------------------------------------------------+
//| Connect(): Used to establish connection  to websocket server     |
//+------------------------------------------------------------------+
bool CWebSocketClient::Connect(const string url,const uint port,const uint timeout,bool use_tls=false,bool enablelog=false)
  {
   reset();
//---
   m_timeout=timeout;
//---
   if(!m_socket.Open(url,port,m_timeout,use_tls,enablelog))
     {
      m_socket.Log("Connect error",__LINE__,__FUNCTION__);
      return(false);
     }
   else
      m_wsclient=CONNECTING;
//---
   if(!upgrade())
      return(false);
//---
   m_socket.Log("ws client state "+EnumToString(m_wsclient),__LINE__,__FUNCTION__);
//---
   if(m_wsclient!=CONNECTED)
     {
      m_wsclient=CLOSED;
      m_socket.Close();
      reset();
     }
//---
   return(m_wsclient==CONNECTED);
  }

Для закрытия или разрыва соединения используется метод ClientClose. У него есть два параметра по умолчанию: код закрытия и тело сообщения, которое будет отправлено на сервер в виде кадра закрытия. Тело сообщения будет усечено, если оно превышает ограничение в 122 символа. Согласно спецификации WebSocket, если конечная точка (сервер или клиент) впервые получает закрывающий кадр, получатель должен ответить, а отправитель должен ожидать ответа как подтверждение запроса на закрытие. Как видно из кода ClientClose, после отправки кадра закрытия базовый TCP-сокет закрывается, не дожидаясь ответа, даже если закрытие было инициировано клиентом. Ожидание ответа на данном этапе жизненного цикла клиента представляется пустой тратой ресурсов, поэтому оно не было реализовано.

//+------------------------------------------------------------------+
//| Close() inform server client is disconnecting                    |
//+------------------------------------------------------------------+
bool CWebSocketClient::Close(ENUM_CLOSE_CODE close_code=NORMAL_CLOSE,const string close_reason="")
  {
   ClientState();
//---
   if(m_wsclient==0)
     {
      m_socket.Log("Client Disconnected",__LINE__,__FUNCTION__);
      //---
      return(true);
     }
//---
   if(ArraySize(m_txbuf)<=0)
     {
      if(close_reason!="")
        {
         int len=StringToCharArray(close_reason,m_txbuf,2,120,CP_UTF8)-1;
         if(len<=0)
            return(false);
         else
            ArrayRemove(m_txbuf,len,1);
        }
      else
        {
         if(ArrayResize(m_txbuf,2)<=0)
           {
            m_socket.Log("array resize error",__LINE__,__FUNCTION__);
            return(false);
           }
        }
      m_txbuf[0]=(uchar)(close_code>>8) & 0xff;
      m_txbuf[1]=(uchar)(close_code>>0) & 0xff;
      //---
     }
//---
   m_msgsize=ArraySize(m_txbuf);
   m_sent=false;
//---
   send(CLOSE_FRAME);
//---
   m_socket.Close();
//---
   reset();
//---
   return(true);
//---
  }


При отправке произвольных данных на сервер можно выбрать один из двух методов. В качестве входных данных SendString принимает строку, а SendData — массив.

SendPing и SendPong — специальные методы для отправки пингов и понгов. Оба допускают необязательное тело сообщения, к которому применяется ограничение в 122 символа.


Все общедоступные методы отправки упаковывают соответствующие входные данные в массив m_txbuff. Частный метод отправки устанавливает тип кадра и использует filltxbuffer() для включения фрагментации сообщения в зависимости от значения свойства m_maxsendsize. FillTxbuffer() подготавливает единственный кадр, упаковывая его в массив m_send. Как только m_send подготовлен, он отправляется на сервер. Все это выполняется в цикле, пока не будет отправлено всё содержимое m_txbuffer.

//+------------------------------------------------------------------+
//| Send() sends text data to websocket server                       |
//+------------------------------------------------------------------+
int CWebSocketClient::SendString(const string message)
  {
   ClientState();
//---
   if(m_wsclient==CLOSED || m_wsclient==CLOSING)
     {
      m_socket.Log("invalid ws client handle",__LINE__,__FUNCTION__);
      return(0);
     }
//---
   if(message=="")
     {
      m_socket.Log("no message specified",__LINE__,__FUNCTION__);
      return(0);
     }
//---
   int len=StringToCharArray(message,m_txbuf,0,WHOLE_ARRAY,CP_UTF8)-1;
   if(len<=0)
     {
      m_socket.Log("string char array error",__LINE__,__FUNCTION__);
      return(0);
     }
   else
      ArrayRemove(m_txbuf,len,1);
//---
   m_msgsize=ArraySize(m_txbuf);
   m_sent=false;
//---
   return(send(TEXT_FRAME));
  }
//+------------------------------------------------------------------+
//| Send() sends user supplied array buffer                          |
//+------------------------------------------------------------------+
int CWebSocketClient::SendData(uchar &message_buffer[])
  {
   ClientState();
//---
   if(m_wsclient==CLOSED || m_wsclient==CLOSING)
     {
      m_socket.Log("invalid ws client handle",__LINE__,__FUNCTION__);
      return(0);
     }
//---
   if(ArraySize(message_buffer)==0)
     {
      m_socket.Log("array is empty",__LINE__,__FUNCTION__);
      return(0);
     }
//---
   if(ArrayResize(m_txbuf,ArraySize(message_buffer))<0)
     {
      m_socket.Log("array resize error",__LINE__,__FUNCTION__);
      return(0);
     }
   else
      ArrayCopy(m_txbuf,message_buffer);
//---
   m_msgsize=ArraySize(m_txbuf);
   m_sent=false;
//---
   return(send(BINARY_FRAME));
  }
//+------------------------------------------------------------------+
//| SendPong() sends pong response upon receiving ping               |
//+------------------------------------------------------------------+
int CWebSocketClient::SendPong(const string msg="")
  {
   ClientState();
//---
   if(m_wsclient==CLOSED || m_wsclient==CLOSING)
     {
      m_socket.Log("invalid ws client handle",__LINE__,__FUNCTION__);
      return(0);
     }
//---
   if(ArraySize(m_txbuf)<=0)
     {
      if(msg!="")
        {
         int len=StringToCharArray(msg,m_txbuf,0,122,CP_UTF8)-1;
         if(len<=0)
           {
            m_socket.Log("string to char array error",__LINE__,__FUNCTION__);
            return(0);
           }
         else
            ArrayRemove(m_txbuf,len,1);
        }
     }
//---
   m_msgsize=ArraySize(m_txbuf);
   m_sent=false;
//---
   return(send(PONG_FRAME));
  }
//+------------------------------------------------------------------+
//| SendPing() ping  the server                                      |
//+------------------------------------------------------------------+
int CWebSocketClient::SendPing(const string msg="")
  {
   ClientState();
//---
   if(m_wsclient==CLOSED || m_wsclient==CLOSING)
     {
      m_socket.Log("invalid ws client handle",__LINE__,__FUNCTION__);
      return(0);
     }
//---
   if(ArraySize(m_txbuf)<=0)
     {
      if(msg!="")
        {
         int len=StringToCharArray(msg,m_txbuf,0,122,CP_UTF8)-1;
         if(len<=0)
           {
            m_socket.Log("string to char array error",__LINE__,__FUNCTION__);
            return(0);
           }
         else
            ArrayRemove(m_txbuf,len,1);
        }
     }
//---
   m_msgsize=ArraySize(m_txbuf);
   m_sent=false;
//---
   return(send(PING_FRAME));
  }


//+------------------------------------------------------------------+
//|prepareSendBuffer()prepares array buffer for socket dispatch      |
//+------------------------------------------------------------------+
bool CWebSocketClient::fillTxBuffer(ENUM_FRAME_TYPE ftype)
  {
   uchar header[];
   static int it;
   static int start;
   uchar masking_key[4]={0};
   int maxsend=(m_maxsendsize<7)?m_msgsize:((m_maxsendsize<126)?m_maxsendsize-6:((m_maxsendsize<65536)?m_maxsendsize-8:m_maxsendsize-14));
//---
   for(int i=0; i<4; i++)
     {
      masking_key[i]=(uchar)(255*MathRand()/32767);
     }
//---
   m_socket.Log("[send]max size - "+IntegerToString(maxsend),__LINE__,__FUNCTION__);
   m_socket.Log("[send]should be max size - "+IntegerToString(m_maxsendsize),__LINE__,__FUNCTION__);
   int message_size=(((start+maxsend)-1)<=(m_msgsize-1))?maxsend:m_msgsize%maxsend;
   bool isfinal=((((start+maxsend)-1)==(m_msgsize-1)) || (message_size<maxsend) ||(message_size<=0))?true:false;
   bool isfirst=(start==0)?true:false;
//---
   m_socket.Log("[send]message size - "+IntegerToString(message_size),__LINE__,__FUNCTION__);
   if(isfirst)
      m_socket.Log("[send]first frame",__LINE__,__FUNCTION__);
   if(isfinal)
      m_socket.Log("[send]final frame",__LINE__,__FUNCTION__);
//---
   if(ArrayResize(header,2+(message_size>=126 ? 2 : 0)+(message_size>=65536 ? 6 : 0)+(4))<0)
     {
      m_socket.Log("array resize error",__LINE__,__FUNCTION__);
      return(false);
     }
//header[0] = (isfinal)? (0x80 | 0x1) :( );
   switch(ftype)
     {
      case CLOSE_FRAME:
         header[0]=uchar(0x80|CLOSE_FRAME);
         m_socket.Log("[building]close frame",__LINE__,__FUNCTION__);
         break;
      case PING_FRAME:
         header[0]=uchar(0x80|PING_FRAME);
         m_socket.Log("[building]ping frame",__LINE__,__FUNCTION__);
         break;
      case PONG_FRAME:
         header[0]=uchar(0x80|PONG_FRAME);
         m_socket.Log("[building]pong frame",__LINE__,__FUNCTION__);
         break;
      default:
         header[0]=(isfinal)? 0x80:0x0;
         m_socket.Log("[building]"+EnumToString(ftype),__LINE__,__FUNCTION__);
         if(isfirst)
            header[0]|=uchar(ftype);
         break;

     }
//---
   if(message_size<126)
     {
      header[1] = (uchar)(message_size & 0xff) |  0x80;
      header[2] = masking_key[0];
      header[3] = masking_key[1];
      header[4] = masking_key[2];
      header[5] = masking_key[3];
     }
   else
   if(message_size<65536)
     {
      header[1] = 126 |  0x80;
      header[2] = (uchar)(message_size >> 8) & 0xff;
      header[3] = (uchar)(message_size >> 0) & 0xff;
      header[4] = masking_key[0];
      header[5] = masking_key[1];
      header[6] = masking_key[2];
      header[7] = masking_key[3];
     }
   else
     {
      header[1] = 127 | 0x80;
      header[2] = (uchar)(message_size >> 56) & 0xff;
      header[3] = (uchar)(message_size >> 48) & 0xff;
      header[4] = (uchar)(message_size >> 40) & 0xff;
      header[5] = (uchar)(message_size >> 32) & 0xff;
      header[6] = (uchar)(message_size >> 24) & 0xff;
      header[7] = (uchar)(message_size >> 16) & 0xff;
      header[8] = (uchar)(message_size >>  8) & 0xff;
      header[9] = (uchar)(message_size >>  0) & 0xff;

      header[10] = masking_key[0];
      header[11] = masking_key[1];
      header[12] = masking_key[2];
      header[13] = masking_key[3];

     }
//---
   if(ArrayResize(m_send,ArraySize(header),message_size)<0)
     {
      m_socket.Log("array resize error",__LINE__,__FUNCTION__);
      return(false);
     }
//---
   ArrayCopy(m_send,header,0,0);
//---
   if(message_size)
     {
      if(ArrayResize(m_send,ArraySize(header)+message_size)<0)
        {
         m_socket.Log("array resize error",__LINE__,__FUNCTION__);
         return(false);
        }
      //---
      ArrayCopy(m_send,m_txbuf,ArraySize(header),start,message_size);
      //---
      int bufsize=ArraySize(m_send);
      //---
      int message_offset=bufsize-message_size;
      //---
      for(int i=0; i<message_size; i++)
        {
         m_send[message_offset+i]^=masking_key[i&0x3];
        }
     }
//---
   if(isfinal)
     {
      it=0;
      start=0;
      m_sent=true;
      ArrayFree(m_txbuf);
     }
   else
     {
      it++;
      start=it*maxsend;
     }
//---
   return(true);

  }


//+------------------------------------------------------------------+
//|int  sendMessage() helper                                         |
//+------------------------------------------------------------------+
int  CWebSocketClient::send(ENUM_FRAME_TYPE frame_type)
  {
//---
   bool done=false;
   int bytes_sent=0,sum_sent=0;

   while(!m_sent)
     {
      done=fillTxBuffer(frame_type);
      if(done && m_socket.Writable())
        {
         bytes_sent=m_socket.Send(m_send,(uint)ArraySize(m_send));
         //---
         if(bytes_sent<0)
            break;
         else
           {
            sum_sent+=bytes_sent;
            ArrayFree(m_send);
           }
         //---
        }
      else
         break;
     }
//---
   if(ArraySize(m_send)>0)
      ArrayFree(m_send);
//---
   m_socket.Log("",__LINE__,__FUNCTION__);
//---
   return(sum_sent);
  }

Любые данные, которые отправляются клиенту, буферизуются в массиве m_rxbuff частным методом fillrxbuffer() всякий раз, когда вызывается открытый метод Readable(). Он возвращает размер массива m_rxbuff, указывающий на доступность извлекаемых данных, с помощью вызова метода Read().

//+------------------------------------------------------------------+
//| receiver()fills rxbuf with raw message                           |
//+------------------------------------------------------------------+
int CWebSocketClient::fillRxBuffer(void)
  {
   uint leng=0;
   int rsp_len=-1;

//---
   uint timeout_check=GetTickCount()+m_timeout;
//---
   do
     {
      leng=m_socket.Readable();
      if(leng)
         rsp_len+=m_socket.Read(m_rxbuf,leng,m_timeout);
      leng=0;
     }
   while(GetTickCount()<timeout_check);
//---
   m_socket.Log("receive size "+IntegerToString(rsp_len),__LINE__,__FUNCTION__);
//---
   int m_rxsize=ArraySize(m_rxbuf);
//---
   if(m_rxsize<3)
      return(0);
//---
   switch((uint)m_rxbuf[1])
     {
      case 126:
         if(m_rxsize<4)
           {
            m_rxsize=0;
           }
         break;
      case 127:
         if(m_rxsize<10)
           {
            m_rxsize=0;
           }
         break;
      default:
         break;
     }
//---
   return(m_rxsize);
  }


int               Readable(void) {  return(fillRxBuffer());}


Метод Read() принимает в качестве входных данных массив типа CFrame, в который будут записаны все кадры. Метод использует частную функцию parse() для декодирования байтовых данных, чтобы их можно было правильно организовать для удобства чтения. Метод parse() отделяет полезную нагрузку от байтов заголовка, которые кодируют описательную информацию о только что полученных кадрах.

//+------------------------------------------------------------------+
//| parse() cleans up raw data buffer discarding unnecessary elements|
//+------------------------------------------------------------------+
bool CWebSocketClient::parse(CFrame &out[])
  {
   uint i,data_len=0,frames=0;
   uint s=0;
   m_total_len=0;
//---
   int shift=0;
   for(i=0; i<(uint)ArraySize(m_rxbuf); i+=(data_len+shift))
     {
      ++frames;
      m_socket.Log("value of frame is "+IntegerToString(frames)+" Value of i is "+IntegerToString(i),__LINE__,__FUNCTION__);
      switch((uint)m_rxbuf[i+1])
        {
         case 126:
            data_len=((uint)m_rxbuf[i+2]<<8)+((uint)m_rxbuf[i+3]);
            shift=4;
            break;
         case 127:
            data_len=((uint)m_rxbuf[i+2]<<56)+((uint)m_rxbuf[i+3]<<48)+((uint)m_rxbuf[i+4]<<40)+
            ((uint)m_rxbuf[i+5]<<32)+((uint)m_rxbuf[i+6]<<24)+((uint)m_rxbuf[i+7]<<16)+
            ((uint)m_rxbuf[i+8]<<8)+((uint)m_rxbuf[i+9]);
            shift=10;
            break;
         default:
            data_len=(uint)m_rxbuf[i+1];
            shift=2;
            break;
        }
      m_total_len+=data_len;
      if(data_len>0)
        {
         if(ArraySize(out)<(int)frames)
           {
            if(ArrayResize(out,frames,1)<=0)
              {
               m_socket.Log("array resize error",__LINE__,__FUNCTION__);
               return(false);
              }
           }
         //---
         if(!out[frames-1].Fill(m_rxbuf,i+shift,data_len))
           {
            m_socket.Log("Error adding new frame",__LINE__,__FUNCTION__);
            return(false);
           }
         //---
         switch((uchar)m_rxbuf[i])
           {
            case 0x1:
               if(out[frames-1].MessageType()==0)
               out[frames-1].SetMessageType(TEXT_FRAME);
               break;
            case 0x2:
               if(out[frames-1].MessageType()==0)
               out[frames-1].SetMessageType(BINARY_FRAME);
               break;
            case 0x80:
            case 0x81:
               if(out[frames-1].MessageType()==0)
               out[frames-1].SetMessageType(TEXT_FRAME);
            case 0x82:
               if(out[frames-1].MessageType()==0)
               out[frames-1].SetMessageType(BINARY_FRAME);
               m_socket.Log("received last frame",__LINE__,__FUNCTION__);
               out[frames-1].SetFinal();
               break;
            case 0x88:
               m_socket.Log("received close frame",__LINE__,__FUNCTION__);
               out[frames-1].SetMessageType(CLOSE_FRAME);
               if(m_wsclient==CONNECTED)
                 {
                  ArrayCopy(m_txbuf,m_rxbuf,0,i+shift,data_len);
                  m_wsclient=CLOSING;
                 }
               break;
            case 0x89:
               m_socket.Log("received ping frame",__LINE__,__FUNCTION__);
               out[frames-1].SetMessageType(PING_FRAME);
               if(m_wsclient==CONNECTED)
                  ArrayCopy(m_txbuf,m_rxbuf,0,i+shift,data_len);
               break;
            case 0x8a:
               m_socket.Log("received pong frame",__LINE__,__FUNCTION__);
               out[frames-1].SetMessageType(PONG_FRAME);
               break;
            default:
               break;
           }
        }
     }
//---  
   return(true);
  }


uint CWebSocketClient::Read(CFrame &out[])
  {
   ClientState();
//---
   if(m_wsclient==0)
     {
      m_socket.Log("invalid ws client handle",__LINE__,__FUNCTION__);
      return(0);
     }
//---
   int rx_size=ArraySize(m_rxbuf);
//---
   if(rx_size<=0)
     {
      m_socket.Log("receive buffer is empty, Make sure to call Readable first",__LINE__,__FUNCTION__);
      return(0);
     }
//---clean up rxbuf
   if(!parse(out))
     {
      ArrayFree(m_rxbuf);
      return(0);
     }
//---
   ArrayFree(m_rxbuf);
//---
   return(m_total_len);
  }


Использование класса

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



Не забудьте включить WebsocketClient.mqh, а затем выполните следующие действия:

CWebSocketClient wsc;

Если вы хотите указать максимальный размер отправки для всех операций отправки, связанных с подключением, сейчас подходящий момент сделать это. При инициализации экземпляра m_maxsendsize равно 0, что указывает на отсутствие ограничений на размер кадра.

   

wsc.SetMaxSendSize(129); // max size in bytes set
if(wsc.Connect(Address,Port,Timeout,usetls,true))
{
 //// 
}

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

sent=wsc.SendString("string message");
// or 
// prepare and fill arbitrary array[] with data and send
sent=wsc.SendData(array);


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

sent=wsc.SendPing("optional message");


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

if(wsc.Readable()>0)
 {
  //read message....
  //declare frame object to receive message
  // and pass it to read method.
  CFrame msg_frames[];
  received=wsc.Read(msg_frames);
  Print(msg_frames[0].ToString());
  if(msg_frames[0].IsFinal())
   {
     Print("\n Final frame received");
   }


Для получения проверьте наличие данных, доступных для чтения из сокета читаемым методом. Если метод указывает на читаемый сокет, вызовите клиентский метод чтения с массивом объектов типа Frame. Затем веб-сокет запишет в массив объектов все полученные фрагменты сообщения. Здесь вы можете использовать методы типа кадров для запроса содержимого массива кадров. Как упоминалось ранее, если один из полученных кадров является ping-кадром, рекомендуется как можно скорее ответить pong-кадром. Чтобы выполнить это требование, клиент WebSocket создаст кадр ответа pong при получении любого пинга. Все, что нужно сделать пользователю, — это вызвать метод отправки пинга без каких-либо аргументов.


Если один из полученных кадров является кадром закрытия, состояние клиента WebSocket изменится на состояние закрытия. Это означает, что сервер отправил запрос на закрытие и готовится разорвать соединение с этим клиентом. В закрытом состоянии операции отправки ограничены. Клиент может отправить только ответный кадр обязательного закрытия. Как и при получении кадра ping, получение кадра закрытия означает, что клиент WebSocket создает кадр закрытия, готовый к отправке.

wsc.Close(NORMAL_CLOSE,"good bye");
// can also be called with out any arguments.
// wsc.Close();
 

Когда это сделано с использованием клиента WebSocket, вызовите метод закрытия соединения. Обычно достаточно вызвать метод без указания каких-либо аргументов, если нет чего-то, о чем вы хотите уведомить сервер. В этом случае используйте одну из причин кода закрытия вместе с коротким заключительным сообщением. Это сообщение будет принудительно ограничено 122 символами. Символы, превышающие лимит, отбрасываются.

Локальный WebSocket-сервер

В целях тестирования приложенный zip-файл включает WebSocket-сервер, предоставляющий возможность отправлять эхо-запросы. Сервер построен с использованием библиотеки libwebsocket. Исходный код доступен для загрузки на github. Для создания требуется только Visual Studio. Всё остальное есть в репозитории github.

Запуск сервера и тестирование библиотеки

Чтобы запустить эхо-сервер, дважды кликните по exe-файлу приложения. Сервер должен заработать. Установленный брандмауэр может заблокировать сервер, поэтому дайте ему необходимые разрешения. Сопутствующие файлы .dll, содержащиеся в каталоге серверного приложения, необходимы. Сервер не сможет работать без них.

Неактивный сервер

Быстро протестируем класс WebSocketClient. Вот пример программы.

//+------------------------------------------------------------------+
//|                                         Websocketclient_test.mq5 |
//|                        Copyright 2019, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2019, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property strict
#include<WebSocketClient.mqh>

input string Address="127.0.0.1";
input int    Port   =7681;
input bool   ExtTLS =false;
input int    MaxSize=256;
input int Timeout=5000;


//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
string _msg="For the mql5-program to operate, it must be compiled (Compile button or F7 key). Compilation should"
            "pass without errors (some warnings are possible; they should be analyzed). At this process, an"
            "executable file with the same name and with EX5 extension must be created in the corresponding"
            "directory, terminal_dir\\MQL5\\Experts, terminal_dir\\MQL5\\indicators or terminal_dir\\MQL5\\scripts."
            "This file can be run."
            "Operating features of MQL5 programs are described in the following sections:"
            "- Program running – order of calling predefined event-handlers."
            "- Testing trading strategies – operating features of MQL5 programs in the Strategy Tester."
            "- Client terminal events – description of events, which can be processed in programs."
            "- Call of imported functions – description order, allowed parameters, search details and call agreement"
            "for imported functions."
            "· Runtime errors – getting information about runtime and critical errors."
            "Expert Advisors, custom indicators and scripts are attached to one of opened charts by Drag'n'Drop"
            "method from the Navigator window."
            "For an expert Advisor to stop operating, it should be removed from a chart. To do it select 'Expert'"
            "'list' in chart context menu, then select an Expert Advisor from list and click 'Remove' button."
            "Operation of Expert Advisors is also affected by the state of the 'AutoTrading' button."
            "In order to stop a custom indicator, it should be removed from a chart."
            "Custom indicators and Expert Advisors work until they are explicitly removed from a chart;"
            "information about attached Expert Advisors and Indicators is saved between client terminal sessions."
            "Scripts are executed once and are deleted automatically upon operation completion or change of the"
            "current chart state, or upon client terminal shutdown. After the restart of the client terminal scripts"
            "are not started, because the information about them is not saved."
            "Maximum one Expert Advisor, one script and unlimited number of indicators can operate in one chart."
            "Services do not require to be bound to a chart to work and are designed to perform auxiliary functions."
            "For example, in a service, you can create a custom symbol, open its chart, receive data for it in an"
            "endless loop using the network functions and constantly update it."
            "Each script, each service and each Expert Advisor runs in its own separate thread. All indicators"
            "calculated on one symbol, even if they are attached to different charts, work in the same thread."
            "Thus, all indicators on one symbol share the resources of one thread."
            "All other actions associated with a symbol, like processing of ticks and history synchronization, are"
            "also consistently performed in the same thread with indicators. This means that if an infinite action is"
            "performed in an indicator, all other events associated with its symbol will never be performed."
            "When running an Expert Advisor, make sure that it has an actual trading environment and can access"
            "the history of the required symbol and period, and synchronize data between the terminal and the"
            "server. For all these procedures, the terminal provides a start delay of no more than 5 seconds, after"
            "which the Expert Advisor will be started with available data. Therefore, in case there is no connection"
            "to the server, this may lead to a delay in the start of an Expert Advisor.";
//---
CWebSocketClient wsc;
//---
int sent=-1;
uint received=-1;
//---
// string subject,issuer,serial,thumbprint;
//---
// datetime expiration;
//---
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- create timer
   EventSetTimer(2);
//---
   wsc.SetMaxSendSize(MaxSize);
//---
   if(wsc.Connect(Address,Port,Timeout,ExtTLS,true))
     {
      sent=wsc.SendString(_msg);
      //--
      Print("sent data is "+IntegerToString(sent));
      //---
     }
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- destroy timer
   EventKillTimer();
   Print("Deinit call");
   wsc.Close();

  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---

  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnTimer()
  {
   if(wsc.Readable()>0)
     {
      CFrame msg_frames[];
      received=wsc.Read(msg_frames);
      if(received>0)
        {
         int ll=ArraySize(msg_frames);
         Print("number of received frames is "+IntegerToString(ll));
         for(int i=0; i<ll; i++)
           {
            Print(msg_frames[i].ToString());
           }

         if(msg_frames[ll-1].IsFinal())
           {
            Print("\n Final frame received");
            wsc.Close(NORMAL_CLOSE,"good bye");
            ExpertRemove();
           }
        }
     }
   else
     {
      Print("\n Nothing readable in socket");
      if(wsc.ClientState()!=CONNECTED)
        {
         Print("\n Client disconnected");
         ExpertRemove();
        }
     }
  }
//+------------------------------------------------------------------+ 


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

Советник проверяет наличие сообщений от сервера в функции onTimer. Полученное сообщение выводится в терминал MetaTrader 5, после чего соединение с веб-сокетом разрывается. При следующем событии Ontimer, если соединение будет закрыто, советник удалится с графика. Вот что выдает вкладка экспертов в MetaTrader 5.

Вывод заголовков


Разбор данных

Получение кадров

Формирование кадра закрытия

Сообщения с WebSocket-сервера.


Скриншот сервера

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


Заключение

Эта статья началась с краткого обзора протокола WebSocket. Затем я подробно описал, как WebSocket может быть реализован в MetaTrader 5 с использованием исключительно языка программирования MQL5. Затем мы создали сервер, который использовали для тестирования нашего клиента MetaTrader 5. Надеюсь, вы найдете описанные здесь инструменты полезными. Весь исходный код доступен для скачивания ниже. 

Содержание прикрепленного архива

Папка 
 Содержание Описание
MT5zip\server
echo_websocket_server.exe, websockets.dll,ssleay32.dll,libeay32.dll
 Серверное приложение вместе с необходимыми компонентами
MT5zip\Mql5\include
Frame.mqh, Socket.mqh, WebsocketClient.mqh
 Include-файлы, содержащие код для классов CFrame, CSocket и CWebsocket
MT5zip\Mql5\Experts Websocketclient_test.mq5  Советник MetaTrader, демонстрирующий использование класса CWebsocket