Protocolo WebSocket en MQL5

Ya hemos examinado con anterioridad los Fundamentos teóricos del protocolo WebSockets. La especificación completa es bastante extensa, y una descripción detallada de su aplicación requeriría mucho espacio y tiempo. Por ello, presentamos la estructura general de las clases ya creadas y sus interfaces de programación. Todos los archivos se encuentran en el directorio MQL5/Include/MQL5Book/ws/.

  • wsinterfaces.mqh - descripción general abstracta de todas las interfaces, constantes y tipos;
  • wstransport.mqh - clase MqlWebSocketTransport que implementa la interfaz de transferencia de datos de red de bajo nivel IWebSocketTransport basada en funciones de socket de MQL5;
  • wsframe.mqh - clases WebSocketFrame y WebSocketFrameHixie que implementan la interfaz IWebSocketFrame, que oculta los algoritmos de generación (codificación y decodificación) de frames para los protocolos Hybi e Hixie, respectivamente;
  • wsmessage.mqh - clases WebSocketMessage y WebSocketMessageHixie que implementan la interfaz IWebSocketMessage, que formaliza la formación de mensajes a partir de frames para los protocolos Hybi y Hixie, respectivamente;
  • wsprotocol.mqh - clases WebSocketConnection, WebSocketConnectionHybi, WebSocketConnectionHixie heredadas de IWebSocketConnection; en ellas tiene lugar la administración coordinada de la formación de frames, mensajes, saludos y desconexión según la especificación, para lo que se utilizan las interfaces anteriores;
  • wsclient.mqh - implementación ya hecha de un cliente WebSocket; una clase de plantilla WebSocketClient que admite la interfaz IWebSocketObserver (para el procesamiento de eventos) y espera WebSocketConnectionHybi o WebSocketConnectionHixie como tipo parametrizado;
  • wstools.mqh - utilidades prácticas del espacio de nombres WsTools.

Estos archivos de encabezado se incluirán automáticamente en nuestros futuros proyectos mqporj como dependencias de las directivas #include.

Diagrama de clase WebSocket en MQL5

Diagrama de clase WebSocket en MQL5

La interfaz de red de bajo nivel IWebSocketTransport dispone de los siguientes métodos.

interface IWebSocketTransport
{
   int write(const uchar &data[]); // write the array of bytes to the network
   int read(uchar &buffer[]);      // read data from network into byte array
   bool isConnected(voidconst;   // check for connection
   bool isReadable(voidconst;    // check for the ability to read from the network
   bool isWritable(voidconst;    // check for the possibility of writing to the network
   int getHandle(voidconst;      // system socket descriptor
   void close(void);               // close connection
};

No es difícil adivinar por los nombres de los métodos qué funciones del socket de la API de MQL5 se utilizarán para construirlos, pero si es necesario, quienes lo deseen pueden implementar esta interfaz por sus propios medios, por ejemplo, a través de una DLL.

La clase MqlWebSocketTransport que implementa esta interfaz requiere el protocolo, el nombre de host y el número de puerto al que se realiza la conexión de red cuando se crea una instancia. Además, puede especificar un valor de tiempo de espera.

Los tipos de frame se recopilan en el enum WS_FRAME_OPCODE.

enum WS_FRAME_OPCODE
{
   WS_DEFAULT = 0,
   WS_CONTINUATION_FRAME = 0x00,
   WS_TEXT_FRAME = 0x01,
   WS_BINARY_FRAME = 0x02,
   WS_CLOSE_FRAME = 0x08,
   WS_PING_FRAME = 0x09,
   WS_PONG_FRAME = 0x0A
};

La interfaz para trabajar con frames contiene métodos estáticos y regulares relacionados con instancias de frame. Los métodos estáticos actúan como fábricas para crear frames del tipo requerido por el lado emisor (create) y frames entrantes (decode).

class IWebSocketFrame
{
public:
   class StaticCreator
   {
   public:
      virtual IWebSocketFrame *decode(uchar &data[], IWebSocketFrame *head = NULL) = 0;
      virtual IWebSocketFrame *create(WS_FRAME_OPCODE typeconst string data = NULL,
         const bool deflate = false) = 0;
      virtual IWebSocketFrame *create(WS_FRAME_OPCODE typeconst uchar &data[],
         const bool deflate = false) = 0;
   };
   ...

La presencia de métodos de fábrica en las clases descendientes se hace obligatoria debido a la presencia de una plantilla Creator y una instancia del método getCreator que la devuelve (suponiendo la devolución «solitario» («singleton»)).

protected:
   template<typename P>
   class Creatorpublic StaticCreator
   {
   public:
     // decode received binary data in IWebSocketFrame
     // (in case of continuation, previous frame in 'head')
      virtual IWebSocketFrame *decode(uchar &data[],
         IWebSocketFrame *head = NULLoverride
      {
         return P::decode(datahead);
      }
      // create a frame of the desired type (text/closing/other) with optional text
      virtual IWebSocketFrame *create(WS_FRAME_OPCODE typeconst string data = NULL,
         const bool deflate = falseoverride
      {
         return P::create(typedatadeflate);
      };
      // create a frame of the desired type (binary/text/closure/other) with data
      virtual IWebSocketFrame *create(WS_FRAME_OPCODE typeconst uchar &data[],
         const bool deflate = falseoverride
      {
         return P::create(typedatadeflate);
      };
   };
public:
   // require a Creator instance
   virtual IWebSocketFrame::StaticCreator *getCreator() = 0;
   ...

Los métodos restantes de la interfaz proporcionan todas las manipulaciones necesarias con datos en frames (codificación/decodificación, recepción de datos y diversas banderas).

   // encode the "clean" contents of the frame into data for transmission over the network
   virtual int encode(uchar &encoded[]) = 0;
   
   // get data as text
   virtual string getData() = 0;
   
   // get data as bytes, return size
   virtual int getData(uchar &buf[]) = 0;
   
   // return frame type (opcode)
   virtual WS_FRAME_OPCODE getType() = 0;
  
   // check if the frame is a control frame or with data:
   // control frames are processed inside classes
   virtual bool isControlFrame()
   {
      return (getType() >= WS_CLOSE_FRAME);
   }
   
   virtual bool isReady() { return true; }
   virtual bool isFinal() { return true; }
   virtual bool isMasked() { return false; }
   virtual bool isCompressed() { return false; }
};

La interfaz IWebSocketMessage contiene métodos para realizar acciones similares pero a nivel de mensaje.

class IWebSocketMessage
{
public:
   // get an array of frames that make up this message
   virtual void getFrames(IWebSocketFrame *&frames[]) = 0;
   
   // set text as message content
   virtual bool setString(const string &data) = 0;
  
   // return message content as text
   virtual string getString() = 0;
  
   // set binary data as message content
   virtual bool setData(const uchar &data[]) = 0;
   
   // return the contents of the message in "raw" binary form
   virtual bool getData(uchar &data[]) = 0;
  
   // sign of completeness of the message (all frames received)
   virtual bool isFinalised() = 0;
  
   // add a frame to the message
   virtual bool takeFrame(IWebSocketFrame *frame) = 0;
};

Teniendo en cuenta las interfaces de frames y mensajes, se define una interfaz común para las conexiones WebSocket IWebSocketConnection.

interface IWebSocketConnection
{
   // open a connection with the specified URL and its parts,
   // and optional custom headers
   bool handshake(const string urlconst string hostconst string origin,
      const string custom = NULL);
   
   // low-level read frames from the server
   int readFrame(IWebSocketFrame *&frames[]);
   
   // low-level send frame (e.g. close or ping)
   bool sendFrame(IWebSocketFrame *frame);
   
   // low-level message sending
   bool sendMessage(IWebSocketMessage *msg);
   
   // custom check for new messages (event generation)
   int checkMessages();
   
   // custom text submission
   bool sendString(const string msg);
   
   // custom posting of binary data
   bool sendData(const uchar &data[]);
   
   // close the connection
   bool disconnect(void);
};

Las notificaciones sobre desconexión y nuevos mensajes se reciben a través de los métodos de la interfaz IWebSocketObserver.

interface IWebSocketObserver
{
  void onConnected();
  void onDisconnect();
  void onMessage(IWebSocketMessage *msg);
};

En concreto, la clase WebSocketClient se convirtió en sucesora de esta interfaz y, por defecto, simplemente envía información al registro. El constructor de la clase espera una dirección para conectarse al protocolo ws o wss.

template<typename T>
class WebSocketClientpublic IWebSocketObserver
{
protected:
   IWebSocketMessage *messages[];
   
   string scheme;
   string host;
   string port;
   string origin;
   string url;
   int timeOut;
   ...
public:
   WebSocketClient(const string address)
   {
      string parts[];
      URL::parse(addressparts);
   
      url = address;
      timeOut = 5000;
  
      scheme = parts[URL_SCHEME];
      if(scheme != "ws" && scheme != "wss")
      {
        Print("WebSocket invalid url scheme: "scheme);
        scheme = "ws";
      }
  
      host = parts[URL_HOST];
      port = parts[URL_PORT];
  
      origin = (scheme == "wss" ? "https://" : "http://") + host;
   }
   ...
  
   void onDisconnect() override
   {
      Print(" > Disconnected "url);
   }
  
   void onConnected() override
   {
      Print(" > Connected "url);
   }
  
   void onMessage(IWebSocketMessage *msgoverride
   {
      // NB: message can be binary, print it just for notification
      Print(" > Message "url" " , msg.getString());
      WsTools::push(messagesmsg);
   }
   ...
};

La clase WebSocketClient recoge todos los objetos de mensaje en un array y se encarga de borrarlos si el programa MQL no lo hace.

La conexión se establece en el método open.

template<typename T>
class WebSocketClientpublic IWebSocketObserver
{
protected:
   IWebSocketTransport *socket;
   IWebSocketConnection *connection;
   ...
public:
   ...
   bool open(const string custom_headers = NULL)
   {
      uint _port = (uint)StringToInteger(port);
      if(_port == 0)
      {
         if(scheme == "ws"_port = 80;
         else _port = 443;
      }
  
      socket = MqlWebSocketTransport::create(schemehost_porttimeOut);
      if(!socket || !socket.isConnected())
      {
         return false;
      }
  
      connection = new T(&thissocket);
      return connection.handshake(urlhostorigincustom_headers);
   }
   ...

Las formas más cómodas de enviar datos son los métodos sobrecargados send para datos de texto y binarios.

   bool send(const string str)
   {
      return connection ? connection.sendString(str) : false;
   }
    
   bool send(const uchar &data[])
   {
      return connection ? connection.sendData(data) : false;
   }

Para comprobar si hay nuevos mensajes entrantes, puede llamar al método checkMessages. Dependiendo de su parámetro blocking, el método esperará un mensaje en un bucle hasta que se agote el tiempo de espera o devolverá inmediatamente si no hay mensajes. Los mensajes irán al manejador IWebSocketObserver::onMessage.

   void checkMessages(const bool blocking = true)
   {
      if(connection == NULLreturn;
      
      uint stop = GetTickCount() + (blocking ? timeOut : 1);
      while(ArraySize(messages) == 0 && GetTickCount() < stop && isConnected())
      {
         // all frames are collected into the appropriate messages, and they become
         // available through event notifications IWebSocketObserver::onMessage,
         // however, control frames have already been internally processed and removed by now
         if(!connection.checkMessages()) // while no messages, let's make micro-pause
         {
            Sleep(100);
         }
      }
   }

Una forma alternativa de recibir mensajes se implementa en el método readMessage: devuelve un puntero al mensaje al código de llamada (en otras palabras, el manejador de la aplicación onMessage no es necesario). Después, el programa MQL se encarga de liberar el objeto.

   IWebSocketMessage *readMessage(const bool blocking = true)
   {
      if(ArraySize(messages) == 0checkMessages(blocking);
      
      if(ArraySize(messages) > 0)
      {
         IWebSocketMessage *top = messages[0];
         ArrayRemove(messages01);
         return top;
      }
      return NULL;
   }

La clase también permite cambiar el tiempo de espera, comprobar la conexión y cerrarla.

   void setTimeOut(const int ms)
   {
      timeOut = fabs(ms);
   }
   
   bool isConnected() const
   {
      return socket && socket.isConnected();
   }
   
   void close()
   {
      if(isConnected())
      {
         if(connection)
         {
            connection.disconnect(); // this will close socket after server acknowledge
            delete connection;
            connection = NULL;
         }
         if(socket)
         {
            delete socket;
            socket = NULL;
         }
      }
   }
};

La biblioteca de las clases consideradas permite crear aplicaciones cliente para servicios de eco y chat.