• Unirse

Trabajando con las funciones de red, o MySQL sin DLL: Parte I - el conector

29 enero 2020, 12:33
Serhii Shevchuk
0
497

Contenido

Introducción

Hace aproximadamente un año, las funciones de red en MQL5 se vieron complementadas por las funciones de trabajo con sockets. Este hecho abre un amplio abanico de posibilidades para los programadores que desarrollan productos para el Mercado, ya que ahora es posible implementar aquello que antes no se podía conseguir sin bibliotecas dinámicas. Analizaremos uno de estos ejemplos en el presente ciclo de dos artículos. En el primer artículo, estudiaremos el principio de funcionamiento del conector MySQL, mientras que en el segundo escribiremos aplicaciones simples que usarán el mismo: un servicio para recopilar las propiedades de las señales que llegan al terminal, así como un programa para cambiarlas a lo largo del tiempo (ver figura 1).


Programa para visualizar los cambios de las señales en un tiempo determinado

Fig. 1. Programa para visualizar los cambios de las señales a lo largo del tiempo


Sockets

Llamamos socket a la interfaz programática destinada al intercambio de datos entre procesos. En este caso, además, los procesos pueden haber sido iniciados tanto en un PC, como varios distintos, conectados en una red.

En MQL5, solo tendremos disponibles los sockets TCP de cliente. Esto significa que podemos inicializar la conexión, pero no esperarla desde fuera. Por eso, si es necesario posibilitar la conexión entre los programas MQL5 a través de sockets, necesitaremos un servidor que actúe como intermediario. El servidor espera la conexión a un puerto "escuchado" y ejecuta determinadas funciones según la solicitud del cliente. Para conectarse con un servidor, debemos conocer su dirección ip y su puerto.

Un pueto es un número que puede adoptar valores de 0 a 65535. Los puertos se han dividido en tres intervalos: de sistema (0 - 1023), de usuario (1024-49151) y dinámicos (49152-65535). Parte de los puertos está destinada al trabajo con ciertas funciones. De este comentido se encarga la IANA, la organización que gestiona el espacio de las direcciones IP y los dominios de nivel superior, y que también registra los tipos de datos MIME.

Para MySQL, se ha designado por defecto el puerto 3306, al que nos conectaremos al recurrir al servidor. Debemos tener en cuenta que este valor puede ser modificado. Por eso, al crear un ejemplar del experto, debemos sacar el puerto a los parámetros de entrada, junto con la dirección IP.

Al trabajar con sockets, se usa el enfoque siguiente:

  • Creamos un socket (obtenemos su manejador o error)
  • Nos conectamos al servidor
  • Intercambiamos los datos
  • Cerramos el socket

Si necesitamos trabajar con varias conexiones, deberemos recordar que existe una limitación de 128 sockets abiertos simultáneamente para un programa MQL5.


Analizador de tráfico Wireshark

La depuración del código de un programa donde se usan sockets, es mejor realizarla con un analizador de tráfico. En caso contrario, sería como arreglar un aparato eléctrico sin oscilador. El analizador de tráfico capta los datos de la interfaz de red seleccionada y los representa de una forma cómodamente legible. Asimismo, permite monitorear el tamaño de los paquetes, la diferencia temporal entre los mismos, la presencia de retransmisiones, cortes de conexión y mucha otra información útil. Además, descifra multitud de protocolos.

Para estos cometidos, nosotros usamos Wireshark.

Analizador de tráfico

Fig. 2. Analizador de tráfico Wireshark

En la figura 2, podemos ver la ventana del analizador con los paquetes captados, donde se muestra:

  1. La línea del filtro de representación. La expresión "tcp.port==3306" indica que se representarán solo aquellos paquetes cuyo puerto local o puerto TCP remoto sea igual a 3306 (el puerto MySQL del servidor por defecto).
  2. La ventana con los paquetes. Aquí podemos ver el progreso en el establecimiento de la conexión, la bienvenida del servidor, la solicitud de autorización y el posterior intercambio.
  3. El contenido del paquete seleccionado en forma hexadecimal. En este caso, se muestra el contenido del paquete de bienvenida del servidor MySQL.
  4. El nivel de transporte (TCP). Al usar las funciones para trabajar con sockets, nos encontramos aquí.
  5. El nivel aplicado (MySQL). Es lo que analizaremos en el presente artículo.

Debemos tener en cuenta que el filtro de representación no limita la captura de los paquetes, lo que se puede ver bien en la barra de estado. En ella se indica que en estos momentos se representan 35 paquetes de los 2623 capturados que se hallan ahora en la memoria. Para reducir la carga sobre el PC, debemos establecer el filtro de captura con anterioridad, al seleccionar la interfaz de red de la forma mostrada en la figura 3. Pero solo merece la pena hacerlo en el caso de que los demás paquetes no nos sirvan de verdad.

Filtro de captura de paquetes

Fig. 3. Filtro de captura de paquetes

Para familiarizarnos con el funcionamiento del analizador de tráfico, trataremos de establecer una conexión con el servidor "google.com" y monitorear este proceso. Para ello, escribiremos un pequeño script.

void OnStart()
  {
//--- Obteniendo el manejador del socket
   int socket=SocketCreate();
   if(socket==INVALID_HANDLE)
      return;
//--- Estableciendo la conexión
   if(SocketConnect(socket,"google.com",80,2000)==false)
     {
      return;
     }
   Sleep(5000);
//--- Cerrando la conexión
   SocketClose(socket);
  }

Bien, primero creamos un socket y obtenemos su manejador con la ayuda de la función SocketCreate(). En la guía de ayuda se dice que, en este caso, podemos solo obtener error en dos situaciones prácticamente imposibles:

  1. El error ERR_NETSOCKET_TOO_MANY_OPENED, que indica que hay abiertos más de 128 sockets.
  2. El error ERR_FUNCTION_NOT_ALLOWED, si el usuario trata de llamar a la creación de un socket desde un indicador donde esto está prohibido.

Una vez hemos obtenido el manejador, intentamos establecer la conexión. En este ejemplo, nos conectamos al servidor "google.com" (no olvide añadirlo a las direcciones permitidas en los ajustes del terminal), al puerto 80, con un timeout de 2000 milisegundos. Después de establecer la conexión con éxito, esperamos 5 segundos y lo cerramos. Vamos a ver ahora qué aspecto tiene en la ventana del analizador de tráfico.

Estableciendo y cerrando una conexión

Fig. 4. Estableciendo y cerrando una conexión

Bien, en la figura 4 podemos ver el intercambio de datos entre neustro script y el servidor "google.com" con la dirección ip "172.217.16.14". Aquí no se representan las solicitudes DNS, porque en la línea del filtro se ha introducido la expresión "tcp.port==80".

Los tres paquetes superiores se refieren al estableciemiento de la conexión. Los tres inferiores, al cierre. En la columna Time, se representa el tiempo entre paquetes, y podemos ver nuestros 5 segundos de espera. Preste atención a que los paquetes han sido coloreado de verde, a diferencia de los paquetes en la figura 2. Esto es así porque en el caso anterior el analizador detectó en el intercambio el protocolo MySQL. Aquí no se ha transmitido ningún dato, por lo que el analizador ha destacado los paquetes con el color para TCP "por defecto".


Intercambio de datos

Según el protocolo, después de establecer la conexión, el servidor MySQL debe enviar el saludo de bienvenida. Como respuesta al mismo, el cliente envía una solicitud de autorización. Este mecanismo se describe en el apartado " Connection Phase", en el sitio web "dev.mysql.com". Si no se ha obtenido el saludo, significa que, o bien se ha usado una dirección IP incorrecta, o bien el servidor está escuchando otro puerto. En cualquier caso, hemos conectado con algo que no es un servidor MySQL. En una situación normal, debemos obtener los datos (leerlos dle socket) y analizarlos.


Recepción

En la clase CMySQLTransaction, que analizaremos con detalle un poco más tarde, la transmisión de datos se ha implementado de la forma siguiente:

//+------------------------------------------------------------------+
//| Obteniendo datos                                                     |
//+------------------------------------------------------------------+
bool CMySQLTransaction::ReceiveData(ushort error_code=0)
  {
   char buf[];
   uint   timeout_check=GetTickCount()+m_timeout;
   do
     {
      //--- Obtenemos el número de datos que se puede leer del socket
      uint len=SocketIsReadable(m_socket);
      if(len)
        {
         //--- Leemos los datos del socket en el búfer
         int rsp_len=SocketRead(m_socket,buf,len,m_timeout);
         m_rx_counter+= rsp_len;
         //--- Enviamos el búfer al procesamiento
         ENUM_TRANSACTION_STATE res = Incoming(buf,rsp_len);
         //--- Obtenemos el resultado del que dependerán las siguientes acciones
         if(res==MYSQL_TRANSACTION_COMPLETE) // la respuesta del servidor ha sido totalmente recibida
            return true;   // salida (con éxito)
         else
            if(res==MYSQL_TRANSACTION_ERROR) // error
              {
               if(m_packet.error.code)
                  SetUserError(MYSQL_ERR_SERVER_ERROR);
               else
                  SetUserError(MYSQL_ERR_INTERNAL_ERROR);
               return false;  // salida (error)
              }
         //--- Si el valor del resultado es distinto, continuamos esperando los datos en el ciclo
        }
     }
   while(GetTickCount()<timeout_check && !IsStopped());
//--- Si el tiempo de finalización de la obtención de la respuesta se ha prolongado por un tiempo superior al valor m_timeout,
//--- salimos con error
   SetUserError(error_code);
   return false;
  }
Aquí, m_socket es el manejador del socket obtenido anteriormente al crear el mismo, mientras que m_timeout es el timeout de lectura de datos usado como argumento de la función SocketRead() para obtener el fragmento de datos, así como el timeout para todos los datos al completo. Antes de entrar en el ciclo, postergamos la marca temporal cuyo alcance se considerará el timeout de la operación de recepción de datos:
uint   timeout_check=GetTickCount()+m_timeout;

A continuación, entramos en un ciclo en la lectura del resultado de la función SocketIsReadable() y esperamos a que retorne un valor distinto a cero. Después de ello, leemos los datos en el búfer y lo transmitimos al procesamiento.

      uint len=SocketIsReadable(m_socket);
      if(len)
        {
         //--- Leemos los datos del socket en el búfer
         int rsp_len=SocketRead(m_socket,buf,len,m_timeout);
         m_rx_counter+= rsp_len;
         //--- Enviamos el búfer al procesamiento
         ENUM_TRANSACTION_STATE res = Incoming(buf,rsp_len);

        ...

        }

No debemos contar con que, en el caso de haber datos en el socket, vayamos a obtener todo el paquete. Hay una serie de situaciones en las que los datos pueden llegar en pequeñas porciones. Por ejemplo, debido a una mala cconexión a través de un módem 4G con un gran número de retransmisiones. Por eso, nuestro procesador debe saber reunir los datos leídos en ciertos grupos indivisibles con los que se pueda trabajar. Tomaremos como dichos grupos los paquetes MySQL.

De la recopilación de datos y su posterior procesamiento se encargará el método CMySQLTransaction::Incoming():

   //--- Procesando los datos obtenidos
   ENUM_TRANSACTION_STATE  Incoming(uchar &data[], uint len);

El resultado que retorna nos indicará qué hacer a continuación: continuar la recopilación de datos, finalizarla o interrumpirla:

enum ENUM_TRANSACTION_STATE
  {
   MYSQL_TRANSACTION_ERROR=-1,         // Error
   MYSQL_TRANSACTION_IN_PROGRESS=0,    // En el proceso
   MYSQL_TRANSACTION_COMPLETE,         // Completamente finalizado
   MYSQL_TRANSACTION_SUBQUERY_COMPLETE // Parcialmente finalizado
  };

En el caso se que surja un error interno, un error del servidor o un error de finalización de la recepción, deberemos interrumpir la lectura de datos del socket; en el resto de los casos, continuaremos. El valor MYSQL_TRANSACTION_SUBQUERY_COMPLETE indica que se ha recibido una de las respuestas del servidor a una solicitud múltiple del cliente. Para el algoritmo de lectura, es igual a MYSQL_TRANSACTION_IN_PROGRESS.

El paquete MySQL

Fig. 5. El paquete MySQL

El formato del paquete MySQL se muestra en la figura 5. Los tres primeros bytes determinan el tamaño de la carga útil en el paquete, el siguiente byte indica el número ordinal del paquete en la secuencia, y tras él van los datos. El número ordinal se establece en cero al inicio de cada intercambio. Por ejemplo, el paquete de bienvenida tendrá el número 0, la solicitud de autorización del cliente, el número 1, la respuesta del servidor, el número 2 (finalización de la fase de conexión). A continuación, al enviar una solicitud al cliente, el valor del número ordinal deberá establecerse de nuevo en cero, aumentándose en cada paquete de respuesta del servidor. Si el número de paquetes es superior a 255, el valor del número pasará por el cero.

El paquete más sencillo (MySQL ping) en el analizador de tráfico tiene el aspecto siguiente:

El paquete Ping en el analizador de tráfico

Fig. 6. El paquete Ping en el analizador de tráfico

El paquete Ping contiene un byte de datos con el valor 14 (o 0x0E en forma hexadecimal).

Vamos a analizar el método CMySQLTransaction::Incoming(), que reúne los datos en paquetes y los transmite a los manejadores. Más abajo, mostramos su código fuente de forma abreviada.

ENUM_TRANSACTION_STATE CMySQLTransaction::Incoming(uchar &data[], uint len)
  {
   int ptr=0; // índice del byte actual en el búfer data
   ENUM_TRANSACTION_STATE result=MYSQL_TRANSACTION_IN_PROGRESS; // resultado del procesamiento de los datos recibidos
   while(len>0)
     {
      if(m_packet.total_length==0)
        {
         //--- Si se desconoce el número de datos en el paquete
         while(m_rcv_len<4 && len>0)
           {
            m_hdr[m_rcv_len] = data[ptr];
            m_rcv_len++;
            ptr++;
            len--;
           }
         //--- Obtenido el número de datos en el paquete
         if(m_rcv_len==4)
           {
            //--- Reseteamos los códigos de error, etcétera
            m_packet.Reset();
            m_packet.total_length = reader.TotalLength(m_hdr);
            m_packet.number = m_hdr[3];
            //--- Longitud obtenida, reseteamos el contador de bytes de longitud
            m_rcv_len = 0;
            //--- Destacamos el búfer del tamaño indicado
            if(ArrayResize(m_packet.data,m_packet.total_length)!=m_packet.total_length)
               return MYSQL_TRANSACTION_ERROR;  // error interno
           }
         else // si el número de datos aún no ha sido recibido
            return MYSQL_TRANSACTION_IN_PROGRESS;
        }
      //--- Recopilamos los datos del paquete
      while(len>0 && m_rcv_len<m_packet.total_length)
        {
         m_packet.data[m_rcv_len] = data[ptr];
         m_rcv_len++;
         ptr++;
         len--;
        }
      //--- Comprobamos que el paquete ya haya sido recopilado
      if(m_rcv_len<m_packet.total_length)
         return MYSQL_TRANSACTION_IN_PROGRESS;

      //--- Procesando el paquete MySQL recibido
      //...
      //---      

      m_rcv_len = 0;
      m_packet.total_length = 0;
     }
   return result;
  }

En primer lugar, debemos recopilar los encabezados del paquete, los 4 primeros bytes donde se contiene la longitud y el número ordinal en la secuencia. Para acumular un encabezado, usamos el búfer m_hdr y el contador de bytes m_rcv_len. Cuando los 4 bytes han sido recopilados, obtenemos de ellos la longitud, y, partiendo de su valor, cambiamos el búfer m_packet.data, en el que copiaremos los datos del paquete recibidos. Cuando el paquete ha sido totalmente recopilado, lo transmitimos para su procesamiento.

Si después de recibir el paquete, la longitud de los datos recibidos len sigue siendo igual a cero, significará que hemos recibido varios paquetes. En una llamada del método Incoming() se pueden procesar tanto varios paquetes, como ni siquiera uno completo (solo una parte).

Los tipos de paquete se muestran más abajo:

enum ENUM_PACKET_TYPE
  {
   MYSQL_PACKET_NONE=0,    // None
   MYSQL_PACKET_DATA,      // Data
   MYSQL_PACKET_EOF,       // End of file
   MYSQL_PACKET_OK,        // Ok
   MYSQL_PACKET_GREETING,  // Greeting
   MYSQL_PACKET_ERROR      // Error
  };

Para cada uno de ellos existe su propio manejador, que analiza su secuencia y contenido de acuerdo con un protocolo. Los valores obtenidos como resultado del parseo se asignan a los miembros de las clases correspondientes. Aquí debemos prestar atención a que en esta implementación del conector se analizan absolutamente todos los datos recibidos en los paquetes. Esto puede resultar un poco excesivo, dado que las propiedades "Table" y "Original table" son con frecuencia iguales, además, el valor de algunas banderas no resultará necesario a casi nadie (ver figura 7). No obstante, la disponibilidad de estas propiedades permite ajustar de forma flexible la lógica de interacción con el servidor MySQL al nivel aplicado del programa.


Paquetes en el analizador Wireshark

Fig. 7. Paquete de descripción del campo


Transmisión

En lo que respecta al envío de datos, aquí todo es un poco más sencillo.

//+------------------------------------------------------------------+
//| Formando y enviando el ping                                    |
//+------------------------------------------------------------------+
bool CMySQLTransaction::ping(void)
  {
   if(reset_rbuf()==false)
     {
      SetUserError(MYSQL_ERR_INTERNAL_ERROR);
      return false;
     }
//--- Preparando el búfer de salida
   m_tx_buf.Reset();
//--- Reservando espacio para el encabezado del paquete
   m_tx_buf.Add(0x00,4);
//--- Ubicando el código del comando
   m_tx_buf+=uchar(0x0E);
//--- Formando los encabezados
   m_tx_buf.AddHeader(0);
   uint len = m_tx_buf.Size();
//--- Enviando el paquete
   if(SocketSend(m_socket,m_tx_buf.Buf,len)!=len)
      return false;
   m_tx_counter+= len;
   return true;
  }

Más arriba hemos mostrado el código del método de envío del ping. Copiamos los datos en el búfer preparado. En el caso del ping, se trata del código de comando 0x0E. A continuación, formulamos los encabezados a partir del número de datos y el número ordinal del paquete. Para el ping, el número ordinal siempre será igual a cero. Después de esto, intentamos enviar el paquete reunido con la ayuda de la función SocketSend().

El método de envío de la solicitud (Query) es similar al envío del ping:

//+------------------------------------------------------------------+
//| Formando y enviando la solicitud                                  |
//+------------------------------------------------------------------+
bool CMySQLTransaction::query(string s)
  {
   if(reset_rbuf()==false)
     {
      SetUserError(MYSQL_ERR_INTERNAL_ERROR);
      return false;
     }
//--- Preparando el búfer de salida
   m_tx_buf.Reset();
//--- Reservando espacio para el encabezado del paquete
   m_tx_buf.Add(0x00,4);
//--- Ubicando el código del comando
   m_tx_buf+=uchar(0x03);
//--- Añadiendo la línea de solicitud
   m_tx_buf+=s;
//--- Formando los encabezados
   m_tx_buf.AddHeader(0);
   uint len = m_tx_buf.Size();
//--- Enviando el paquete
   if(SocketSend(m_socket,m_tx_buf.Buf,len)!=len)
      return false;
   m_tx_counter+= len;
   return true;
  }

La única diferencia es que aquí la carga útil consta del código de comando (0x03) y la línea de solicitud.

Tras el envío de datos, siempre viene el método de recepción ya conocido CMySQLTransaction::ReceiveData(). Si este no ha retornado error, la transacción se considera exitosa.


Clase de transacción MySQL

Ha llegado el momento de analizar la clase CMySQLTransaction con mayor detalle.

//+------------------------------------------------------------------+
//| Clase de transacción MySQL                                           |
//+------------------------------------------------------------------+
class CMySQLTransaction
  {
private:
   //--- Datos para la autorización
   string            m_host;        // Dirección IP del servidor MySQL
   uint              m_port;        // Puerto TCP
   string            m_user;        // Nombre de usuario
   string            m_password;    // Contraseña
   //--- Timeouts
   uint              m_timeout;        // timeout de espera de datos TCP (ms)
   uint              m_timeout_conn;   // timeout de establecimiento de conexión con el servidor
   //--- Keep Alive
   uint              m_keep_alive_tout;      // tiempo(ms) transcurrido el cual se cerrará la conexión; el valor 0 indica que Keep Alive no se usa
   uint              m_ping_period;          // periodo de envío del ping (en ms) en el modo Keep Alive
   bool              m_ping_before_query;    // enviar el ping a través de query (tiene sentido cuando el valor del periodo de envío del ping es muy grande)
   //--- Red
   int               m_socket;      // manejador del socket
   ulong             m_rx_counter;  // contador de bytes recibidos
   ulong             m_tx_counter;  // contador de bytes transmitidos
   //--- Marcas temporales
   ulong             m_dT;                   // tiempo de ejecución de la última solicitud
   uint              m_last_resp_timestamp;  // tiempo de recepción de la última respuesta
   uint              m_last_ping_timestamp;  // hora del último ping
   //--- Respuesta del servidor
   CMySQLPacket      m_packet;      // paquete recibido
   uchar             m_hdr[4];      // encabezado del paquete
   uint              m_rcv_len;     // contador de bytes del encabezado del paquete
   //--- Búfer de transmisión
   CData             m_tx_buf;
   //--- Clase de formación de la solicitud de autorización
   CMySQLLoginRequest m_auth;
   //--- Búfer de respuesta del servidor y tamaño del mismo
   CMySQLResponse    m_rbuf[];
   uint              m_responses;
   //--- Espera y recepción de datos del socket
   bool              ReceiveData(ushort error_code);
   //--- Procesando los datos obtenidos
   ENUM_TRANSACTION_STATE  Incoming(uchar &data[], uint len);
   //--- Manejadores de los paquetes para cada uno de los tipos
   ENUM_TRANSACTION_STATE  PacketOkHandler(CMySQLPacket *p);
   ENUM_TRANSACTION_STATE  PacketGreetingHandler(CMySQLPacket *p);
   ENUM_TRANSACTION_STATE  PacketDataHandler(CMySQLPacket *p);
   ENUM_TRANSACTION_STATE  PacketEOFHandler(CMySQLPacket *p);
   ENUM_TRANSACTION_STATE  PacketErrorHandler(CMySQLPacket *p);
   //--- Otros
   bool              ping(void);                // envío del ping
   bool              query(string s);           // envío de la solicitud
   bool              reset_rbuf(void);          // inicializando el búfer de respuesta del servidor
   uint              tick_diff(uint prev_ts);   // obteniendo la diferencia de las marcas temporales
   //--- Clase del parser
   CMySQLPacketReader   reader;
public:
                     CMySQLTransaction();
                    ~CMySQLTransaction();
   //--- Establecimiento de los parámetros de conexión
   bool              Config(string host,uint port,string user,string password,uint keep_alive_tout);
   //--- Modo Keep Alive
   void              KeepAliveTimeout(uint tout);                       // estableciendo el timeout
   void              PingPeriod(uint period) {m_ping_period=period;}    // estableciendo el periodo del ping en segundos
   void              PingBeforeQuery(bool st) {m_ping_before_query=st;} // activar/desactivar el ping antes de la solicitud
   //--- Procesando los eventos del temporizador (relevante solo al usar Keep Alive)
   void              OnTimer(void);
   //--- Obtener el puntero a la clase de trabajo con la autorización
   CMySQLLoginRequest *Handshake(void) {return &m_auth;}
   //--- Enviando solicitud
   bool              Query(string q);
   //--- Obteniendo el número de respuestas del servidor
   uint              Responses(void) {return m_responses;}
   //--- Obteniendo el puntero a la respuesta del servidor según el índice
   CMySQLResponse    *Response(uint idx);
   CMySQLResponse    *Response(void) {return Response(0);}
   //--- Obteniendo la estructura del error del servidor
   MySQLServerError  GetServerError(void) {return m_packet.error;}
   //--- Opciones
   ulong             RequestDuration(void) {return m_dT;}                     // obteniendo la duración de la última transacción
   ulong             RxBytesTotal(void) {return m_rx_counter;}                // obteniendo el número de bytes recibidos
   ulong             TxBytesTotal(void) {return m_tx_counter;}                // obteniendo el número de bytes transmitidos
   void              ResetBytesCounters(void) {m_rx_counter=0; m_tx_counter=0;} // reseteando los contadores de los bytes recibidos y transmitidos
  };

Entre los miembros privados, querríamos destacar los siguientes:

  • m_packet del tipo CMySQLPacket: clase de paquete MySQL procesado en el momento actual (el código fuente con los comentarios se encuentra en el archivo MySQLPacket.mqh)
  • m_tx_buf del tipo CData: clase de búfer de transmisión creado para que resulte cómodo formar la solicitud (archivo Data.mqh)
  • m_auth del tipo CMySQLLoginRequest: clase para trabajar con la autorización (contraseña de cifrado, guardado de los parámetros del servidor recibidos y los parámetros del cliente, código fuente en el archivo MySQLLoginRequest.mqh)
  • m_rbuf del tipo CMySQLResponse: búfer de respuesta del servidor; aquí se considera respuesta un paquete del tipo "Ok" o "Data" (archivo MySQLResponse.mqh)
  • reader del tipo CMySQLPacketReader: clase del parser de paquetes MySQL

Los métodos públicos se describen en el apartado correspondiente de la documentación.

Para el nivel aplicado, la clase de la transacción tendrá el aspecto mostrado en la figura 8.

Clases

Fig. 8. Construcción de la clase CMySQLTransaction

Donde:

  • CMySQLLoginRequest debe estar configurado hasta el establecimiento de la conexión, si es necesario indicar parámetros de cliente cuyos valores se diferencien de los establecidos (no es obligatorio) ;
  • CMySQLResponse — respuesta del servidor, si la transacción se ha finalizado sin errores
    • CMySQLField — descripción del campo;
    • CMySQLRow — fila (búfer de valores de los campos en forma de texto);
  • MySQLServerError — estructura de la descripción del error, si la transacción ha finalizado sin éxito;

Entre los métodos públicos no hay ninguno encargado de establecer y cerrar la conexión. Esto sucede automáticamente al llamar al método CMySQLTransaction::Query(). En caso de usar el modo de conexión constante, este se establecerá en la primera llamada de CMySQLTransaction::Query(), siendo cerrado al finalizar el timeout establecido.

Importante: en el modo de conexión constante, se deberá añadir al manejador del evento OnTimer la llamada del método CMySQLTransaction::OnTimer(). En este caso, además, el periodo del temporizador deberá ser inferior al ping y al timeout.

Los parámetros de conexión y la cuenta de usuario del usuario, así como los valores especiales de los parámetros de cliente deberán ser establecidos antes de la llamada de CMySQLTransaction::Query().

En general, la interacción con la clase de la transacción se ejecuta según el principio siguiente:

Trabajo con la clase de la transacción

Fig. 9. Trabajo con la clase CMySQLTransaction



Aplicación

Vamos a analizar un ejemplo sencillo de aplicación del conector. Para ello, escribiremos un script que enviará una solicitud SELECT a la base de datos de prueba world.

//--- input parameters
input string   inp_server     = "127.0.0.1";          // MySQL server address
input uint     inp_port       = 3306;                 // TCP port
input string   inp_login      = "admin";              // Login
input string   inp_password   = "12345";              // Password
input string   inp_db         = "world";              // Database name

//--- Activando la clase de transacción MySQL
#include  <MySQL\MySQLTransaction.mqh>
CMySQLTransaction mysqlt;

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- Configurando la clase de transacción MySQL
   mysqlt.Config(inp_server,inp_port,inp_login,inp_password);
//--- Creando la solicitud
   string q = "select `Name`,`SurfaceArea` "+
              "from `"+inp_db+"`.`country` "+
              "where `Continent`='Oceania' "+
              "order by `SurfaceArea` desc limit 10";
   if(mysqlt.Query(q)==true)
     {
      if(mysqlt.Responses()!=1)
         return;
      CMySQLResponse *r = mysqlt.Response();
      if(r==NULL)
         return;
      Print("Name: ","Surface Area");
      uint rows = r.Rows();
      for(uint i=0; i<rows; i++)
        {
         double area;
         if(r.Row(i).Double("SurfaceArea",area)==false)
            break;
         PrintFormat("%s: %.2f",r.Row(i)["Name"],area);
        }
     }
   else
      if(GetLastError()==(ERR_USER_ERROR_FIRST+MYSQL_ERR_SERVER_ERROR))
        {
         // si se trata de un error del servidor
         Print("MySQL Server Error: ",mysqlt.GetServerError().code," (",mysqlt.GetServerError().message,")");
        }
      else
        {
         if(GetLastError()>=ERR_USER_ERROR_FIRST)
            Print("Transaction Error: ",EnumToString(ENUM_TRANSACTION_ERROR(GetLastError()-ERR_USER_ERROR_FIRST)));
         else
            Print("Error: ",GetLastError());
        }
  }

Nuestra tarea consistirá en conseguir la lista de los países que incluyan el significado del continente "Oceanía", clasificados por superficie en orden descendente, con un máximo de 10 denominaciones en la lista. Para ello, ejecutaremos las siguientes acciones:

  • Declaramos el ejemplar de clase de transacción mysqlt
  • Establecemos los parámetros de la conexión
  • Creamos la solicitud correspondiente
  • Si la transacción ha tenido éxito, comprobamos que el número de respuestas sea igual al valor esperado
  • Obtenemos el puntero a la clase de respuesta del servidor
  • Obtenemos el número de filas en la respuesta
  • Mostramos los valores de las filas

Si la transacción no ha tenido éxito, podrían darse tres escenarios de desarrollo de los eventos:

Si los parámetros de entrada se han indicado correctamente, el resultado del trabajo del script será el siguiente:

Resultado del funcionamiento del script de prueba

Fig. 10. Resultado del funcionamiento del script de prueba

En la segunda parte del artículo veremos ejemplos más complicados, con multitud de solicitudes y el modo de conexión constante.


Documentación

Contenido


    La clase de transacción CMySQLTransaction

    Lista de métodos de la clase CMySQLTransaction

    Método
    Acción
    Config
    Establecimiento de los parámetros de conexión
    KeepAliveTimeout
    Establecimiento del timeout para el modo Keep Alive en segundos
    PingPeriod
    Establecimiento del ping para el modo Keep Alive en segundos
    PingBeforeQuery
    Activar/desactivar el ping antes de la solicitud
    OnTimer
    Procesamiento de los eventos del temporizador (relevante solo al usar Keep Alive)
    Handshake
    Obtención del puntero a la clase de trabajo con la autorización
    Query
    Envío de la solicitud
    Responses
    Obteción del número de respuestas del servidor
    Response
    Obtención del puntero a la clase de respuesta del servidor
    GetServerError
    Obteción de la estructura del error del servidor
    RequestDuration
    Duración de la transacción en microsegundos
    RxBytesTotal
    Contador de bytes recibidos desde el inicio del programa
    TxBytesTotal
    Contador de bytes enviados desde el inicio del programa
    ResetBytesCounters
    Reseteando los contadores de bytes recibidos y enviados

    Más abajo, se muestra una breve guía de ayuda sobre cada uno de los métodos.

    Config

    Establece los parámetros de conexión.
    bool  Config(
       string host,         // nombre del servidor
       uint port,           // puerto
       string user,         // nombre del usuario
       string password,     // contraseña
       string base,         // nombre de la base de datos
       uint keep_alive_tout // timeout de la conexión constante (0, si no se usa)
       );
    

    Valor retornado: true en el caso de éxito, de lo contrario, false (hay símbolos prohibidos en los argumentos del tipo string).

    KeepAliveTimeout

    Activa el modo de conexión constante y establece su timeout. El valor de timeout supone el tiempo en segundos desde el momento del envío de la última solicitud; al finalizar el mismo, la conexión se cerrará. Si las solicitudes se repiten con una frecuencia mayor a la establecida para el timeout, la conexión no se cerrará.

    void  KeepAliveTimeout(
       uint tout            // estableciendo el timeout de conexión constante en segundos (0 - desactivar)
       );
    

    PingPeriod

    Establece el periodo de envío de paquetes ping en el modo de conexión constante. Resulta necesario para que el servidor no cierre la conexión según su propio criterio. El ping se enviará después del tiempo indicado tras la última solicitud o el ping anterior.

    void  PingPeriod(
       uint period          // estableciendo el periodo de ping en segundos (para el modo de conexión constante)
       );
    

    Valor retornado: no

    PingBeforeQuery

    Activa el envío del paquete ping antes de la solicitud. En el modo de conexión constante, en los intervalos entre solicitudes, la conexión podría cerrarse o interrumpirse por algún motivo. En este caso, podemos enviar el ping al servidor MySQL para asegurarnos de que está en contacto, y solo entonces realizar la solicitud.

    void  PingBeforeQuery(
       bool st              // activar (true) o desactivar (false) el ping antes de la solicitud
       );
    

    Valor retornado: no

    OnTimer

    Se usa en el modo de conexión constante. El método se llama desde el manejador de eventos OnTimer. En este caso, además, el periodo del temporizador no debe superar el periodo mínimo de KeepAliveTimeout y PingPeriod.

    void  OnTimer(void);

    Valor retornado: no

    Handshake

    Obtención del puntero a la clase de trabajo con la autorización. Puede ser utilizado para establecer las banderas de posibilidad del cliente y el tamaño máximo del paquete antes de establecer la conexión con el servidor. Después de la autorización, permite obtener la versión y las banderas de posibilidad del servidor.

    CMySQLLoginRequest *Handshake(void);

    Valor retornado: puntero a la clase de trabajo con la autorización CMySQLLoginRequest.

    Query

    Envío de la solicitud.

    bool  Query(
       string q             // cuerpo de la solicitud
       );
    

    Valor retornado: resultado de la ejecución; éxito - true, error - false.

    Responses

    Obteción del número de respuestas.

    uint  Responses(void);

    Valor retornado: número de respuestas del servidor.

    Se considerarán respuestas los paquetes del tipo "Ok" o "Data". Si la solicitud se ejecuta con éxito, se recibirán una u más respuestas (para las solicitudes múltiples).

    Response

    Obtención del puntero a la clase de respuesta del servidor MySQL.

    CMySQLResponse  *Response(
       uint idx                     // índice de respuesta del servidor
       );
    

    Valor retornado: puntero a la clase de respuesta del servidor CMySQLResponse. Si se transmite como argumento un valor incorrecto, se retornará NULL.

    Un método sobrecargado del que no se ha indicado el índice equivale Response(0).

    CMySQLResponse  *Response(void);

    Valor retornado: puntero a la clase de respuesta del servidor CMySQLResponse. Si no hay respuesta, se retornará NULL.

    GetServerError

    Obteción de la estructura donde se guarda el código y el mensaje de error del servidor. Puede ser llamada después de que la clase de transacción haya retornado el error MYSQL_ERR_SERVER_ERROR.

    MySQLServerError  GetServerError(void);

    Valor retornado: estructura de error MySQLServerError

    RequestDuration

    Obtención de la duración de la ejecución de la solicitud.

    ulong  RequestDuration(void);

    Valor retornado: duración de la solicitud en microsegundos desde el momento del envío hasta la finalización del procesamiento

    RxBytesTotal

    Obtención del número de bytes recibidos.

    ulong  RxBytesTotal(void);

    Valor retornado: número de bytes recibidos (nivel TCP) desde el inicio del programa. Para realizar el reseteo, se usa el método ResetBytesCounters.

    TxBytesTotal

    Obtención del número de bytes enviados.

    ulong  TxBytesTotal(void);

    Valor retornado: número de bytes transmitidos (nivel TCP) desde el inicio del programa. Para realizar el reseteo, se usa el método ResetBytesCounters.

    ResetBytesCounters

    Resetea los contadores de los bytes recibidos y transmitidos.

    void  ResetBytesCounters(void);


    Clase de trabajo con la autorización CMySQLLoginRequest

    Métodos de la clase CMySQLLoginRequest

    Método
    Acción
    SetClientCapabilities
    Establece las banderas de posibilidad del cliente . Valor preestablecido: 0x005FA685
    SetMaxPacketSize
    Establece el tamaño máximo permitido del paquete en bytes. Valor preestablecido: 16777215
    SetCharset
    Establece el conjunto de símbolos utilizados. Valor preestablecido: 8
    Version
    Retorna la versión del servidor MySQL. Por ejemplo: "5.7.21-log".
    ThreadId
    Retorna el identificador de flujo de la conexión actual. Se corresponde con el valor CONNECTION_ID.
    ServerCapabilities
    Retorna las banderas de posibilidad del servidor
    ServerLanguage
    Retorna el identificadorde la codificación y la representación de las bases de datos

    Clase de respuesta del servidor CMySQLResponse

    Se considerará respuesta el paquete del tipo "Ok" o "Data". Teniendo en cuenta que se distinguen sustancialmente, la clase tiene un conjunto de métodos aparte para trabajar con cada uno de los tipos de paquete.

    Métodos generales de la clase CMySQLResponse:

    Método
    Valor retornado
    Type
    Tipo de respuesta del servidor: MYSQL_RESPONSE_DATA o MYSQL_RESPONSE_OK

    Métodos para los paquetes del tipo "Data":

    Método
    Valor retornado
    Fields
    Número de campos
    Field
    Puntero a la clase de campo según el índice (índice sobrecargado - obtención del índice del campo según el nombre)
    Field Índice del campo según el nombre
    Rows
    Número de filas en la respuesta del servidor
    Row
    Puntero a la clase de fila según el índice
    Value
    Valor con forma de línea según el índice de la fila y el índice del campo
    Value Valor con forma de línea según el índice de la fila y el nombre del campo
    ColumnToArray Resultado de la lectura de una columna en una matriz de tipo string
    ColumnToArray
    Resultado de la lectura de una columna en una matriz de tipo int con comprobación de la correspondencia del tipo
    ColumnToArray
    Resultado de la lectura de una columna en una matriz de tipo long con comprobación de la correspondencia del tipo
    ColumnToArray
    Resultado de la lectura de una columna en una matriz de tipo double con comprobación de la correspondencia del tipo
    Métodos para los paquetes del tipo "Ok":
    Método
    Valor retornado
    AffectedRows
    Número de filas afectadas por la última operación
    LastId
    Valor LAST_INSERT_ID
    ServerStatus
    Banderas de estado del servidor
    Warnings
    Número de advertencias
    Message
    Mensaje de texto del servidor

    Estructura del error del servidor MySQLServerError

    Elementos de la estructura MySQLServerError

    Elemento
    Tipo
    Designación
    code
     ushort Código de error
    sqlstate
     uint Estado
    message  string Mensaje de texto del servidor


    Clase del campo CMySQLField

    Métodos de la clase CMySQLField

    Método
     Valor retornado
    Catalog
    Nombre del catálogo al que pertenece el recuadro
    Database
    Nombre de la base de datos a la que pertenece el recuadro
    Table
    Pseudónimo del recuadro al que pertenece el campo
    OriginalTable
    Nombre original del recuadro al que pertenece el campo
    Nombre
    Pseudónimo del campo
    OriginalName
    Nombre original del campo
    Charset
    Número de la codificación utilizada
    Length
    Longitud del valor
    Type
    Tipo de valor
    Flags
    Banderas que determinan los atributos de designación
    Decimals
    Número permitido de dígitos decimales
    MQLType
    Tipo de campo en forma de valor ENUM_DATABASE_FIELD_TYPE (salvo el valor DATABASE_FIELD_TYPE_NULL)


    Clase de la fila CMySQLRow

    Métodos de la clase CMySQLRow

    Método
    Acción
    Value
    Retorna el valor del campo según el número en forma de línea
    operator[]
    Retorna el valor del campo según el nombre en forma de línea
    MQLType
    Retorna el tipo del campo según el número en forma de valor ENUM_DATABASE_FIELD_TYPE
    MQLType
    Retorna el tipo del campo según el nombre en forma de valor ENUM_DATABASE_FIELD_TYPE
    Text
    Obtiene el valor del campo según el número en forma de línea con comprobación de la corrección del tipo
    Text
    Obtiene el valor del campo según el nombre en forma de línea con comprobación de la corrección del tipo
    Integer
    Obtiene el valor del tipo int según el número del campo con comprobación de la corrección del tipo
    Integer
    Obtiene el valor del tipo int según el nombre del campo con comprobación de la corrección del tipo
    Long
    Obtiene el valor del tipo long según el número del campo con comprobación de la corrección del tipo
    Long
    Obtiene el valor del tipo long según el nombre del campo con comprobación de la corrección del tipo
    Double
    Obtiene el valor del tipo double según el número del campo con comprobación de la corrección del tipo
    Double
    Obtiene el valor del tipo double según el nombre del campo con comprobación de la corrección del tipo
    Blob
    Obtiene el valor en forma de matriz uchar según el número del campo con comprobación de la corrección del tipo
    Blob
    Obtiene el valor en forma de matriz uchar según el nombre del campo con comprobación de la corrección del tipo

    Observación. La comprobación de la correspondencia de tipos indica que para un método que trabaje con el tipo int, el valor del campo legible deberá ser D ATABASE_FIELD_TYPE_INTEGER. Si los tipos no se corresponden, no se obtendrá el valor, y el método retornará false. La conversión de los identificadores del tipo de campo MySQL en un valor del tipo ENUM_DATABASE_FIELD_TYPE ha sido implementada en el método CMySQLField::MQLType(), cuyo código fuente se muestra más abajo.

    //+------------------------------------------------------------------+
    //| Retorna el tipo del campo en forma de valor ENUM_DATABASE_FIELD_TYPE     |
    //+------------------------------------------------------------------+
    ENUM_DATABASE_FIELD_TYPE CMySQLField::MQLType(void)
      {
       switch(m_type)
         {
          case 0x00:  // decimal
          case 0x04:  // float
          case 0x05:  // double
          case 0xf6:  // newdecimal
             return DATABASE_FIELD_TYPE_FLOAT;
          case 0x01:  // tiny
          case 0x02:  // short
          case 0x03:  // long
          case 0x08:  // longlong
          case 0x09:  // int24
          case 0x10:  // bit
          case 0x07:  // timestamp
          case 0x0c:  // datetime
             return DATABASE_FIELD_TYPE_INTEGER;
          case 0x0f:  // varchar
          case 0xfd:  // varstring
          case 0xfe:  // string
             return DATABASE_FIELD_TYPE_TEXT;
          case 0xfb:  // blob
             return DATABASE_FIELD_TYPE_BLOB;
          default:
             return DATABASE_FIELD_TYPE_INVALID;
         }
      }
    


    Conclusión

    En este artículo, hemos analizado el uso de funciones para trabajar con sockets utilizando como ejemplo la implementación de un conector MySQL. Esta ha sido la parte teórica. En la segunda parte del artículo, nos espera la práctica: escribiremos un servicio para recopilar las propiedades de las señales, así como un programa para visualizar los cambios de sus propiedades.

    En el directorio adjunto se encuentran los siguientes archivos:

    • Ruta Include\MySQL\: códigos fuente del conector
    • Archivo Scripts\test_mysql.mq5: ejemplo de uso del conector analizado en el apartado Aplicación.

    Traducción del ruso hecha por MetaQuotes Software Corp.
    Artículo original: https://www.mql5.com/ru/articles/7117

    Archivos adjuntos |
    MQL5.zip (24.31 KB)
    Monitoreo multidivisas de las señales comerciales (Parte 1): Desarrollando la estructura de la aplicación Monitoreo multidivisas de las señales comerciales (Parte 1): Desarrollando la estructura de la aplicación

    En este artículo, analizaremos la idea del monitoreo multidivisas de las señales comerciales, desarrollaremos la estructura y el prototipo de la futura aplicación, así como, crearemos su plantilla para el trabajo ulterior. Crearemos paso a paso una aplicación multidivisas ajustada de forma flexible que permite tanto crear las señales comerciales, como ayudar a los traders a buscarlas.

    Ampliamos la funcionalidad del Constructor de estrategias Ampliamos la funcionalidad del Constructor de estrategias

    En dos artículos anteriores, analizamos el uso de las figuras técnicas de Merrill aplicándolas a diferentes tipos de datos. Fue desarrollada una aplicación para la simulación a base de esta idea. En este artículo, continuaremos nuestro trabajo con el Constructor de estrategias, mejoraremos su funcionamiento, lo haremos más cómodo, así como ampliaremos su funcionalidad y capacidades.

    Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte XXV): Procesando los errores retornados por el servidor comercial Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte XXV): Procesando los errores retornados por el servidor comercial

    Después de enviar una orden comercial al servidor, no deberíamos pensar que "ya está todo hecho", ya que ahora tendremos que comprobar los códigos de error, o bien la ausencia de los mismos. En el presente artículo, vamos a implementar el procesamiento de los errores retornados por el servidor comercial, preparando asimismo la base para crear solicitudes comerciales pendientes.

    Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte XXVI): Trabajando con las solicitudes comerciales pendientes - primera implementación (apertura de posiciones) Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte XXVI): Trabajando con las solicitudes comerciales pendientes - primera implementación (apertura de posiciones)

    En el presente artículo, vamos a organizar el guardado de ciertos datos en el valor del número mágico de las órdenes y posiciones, y también implementaremos las solicitudes comerciales. Para comprobar el concepto, crearemos una primera solicitud pendiente de prueba para abrir posiciones de mercado al recibir del servidor un error que requiera la espera y el envío de una solicitud repetida.