Trabalhando com as funções de rede ou MySQL sem DLL: Parte I - Conector

11 maio 2020, 10:03
Serhii Shevchuk
0
1 564

Conteúdo

Introdução

Há um ano, as funções de rede em MQL5 foram reabastecidos com as funções para se trabalhar com sockets. Isso abriu grandes oportunidades para os programadores que desenvolvem produtos para o Mercado. Agora eles podem implementar coisas que antes exigiam bibliotecas dinâmicas. Vamos considerar um desses exemplos nesta série de dois artigos. No primeiro artigo, eu vou considerar os princípios do conector MySQL, enquanto no segundo, desenvolverei as aplicações mais simples usando o conector, ou seja, o serviço para coletar as propriedades dos sinais disponíveis no terminal e o programa para visualizar suas alterações ao longo do tempo (veja a Fig. 1).


O programa para visualizar alterações nas propriedades do sinal dentro de um certo período de tempo

Fig. 1. O programa para visualizar alterações nas propriedades do sinal dentro de um certo período de tempo


Sockets

Um socket é uma interface de software para a troca de dados entre processos. Os processos podem ser iniciados em um único PC ou em diferentes PCs conectados em rede.

A MQL5 fornece apenas sockets do tipo cliente TCP. Isso significa que somos capazes de iniciar uma conexão, mas não podemos esperar por ela do lado de fora. Portanto, se nós precisarmos fornecer uma conexão entre programas MQL5 via sockets, nós precisaremos de um servidor que atue como intermediário. O servidor aguarda uma conexão com a porta ouvida e executa determinadas funções a pedido do cliente. Para se conectar ao servidor, nós precisamos saber o seu endereço IP e porta.

Uma porta é um número que varia de 0 a 65535. Existem três intervalos de portas: do sistema (0-1023), do usuário (1024-49151) e dinâmicas (49152-65535). Algumas portas são alocadas para trabalhar com determinadas funções. A alocação é realizada pela IANA - uma organização que gerencia as zonas de endereço IP e domínios de nível superior, além de registrar os tipos de dados MIME.

A porta 3306 é alocada para o MySQL por padrão. Nós nos conectaremos a ele ao acessar o servidor. Observe que esse valor pode ser alterado. Portanto, ao desenvolver um EA, a porta deve ser configurada nas entradas junto com o endereço IP.

A seguinte abordagem é usada ao trabalhar com sockets:

  • Criamos um socket (obtemos um identificador ou erro)
  • Nos conectamos ao servidor
  • Há a troca de dados
  • Encerramos o soquete

Ao trabalhar com várias conexões, lembre-se da limitação de 128 sockets abertos simultaneamente para um único programa MQL5.


Analisador de tráfego Wireshark

O analisador de tráfego facilita a depuração do código de um programa que aplica sockets. Sem ele, todo o processo se assemelha à reparação de eletrônicos sem um osciloscópio. O analisador captura os dados da interface de rede selecionada e os exibe de forma legível. Ele rastreia o tamanho dos pacotes, o intervalo de tempo entre eles, a presença de retransmissões e quedas de conexão, além de muitos outros dados úteis. Ele também descriptografa muitos protocolos.

Pessoalmente, eu uso o Wireshark para esses fins.

Analisador de tráfego

Fig. 2. Analisador de tráfego Wireshark

A Figura 2 exibe a janela do analisador de tráfego com os pacotes capturados em que:

  1. Filtro de exibição das linhas. "tcp.port==3306" significa que apenas os pacotes com a porta TCP 3306 local ou remota são exibidos (porta do servidor MySQL padrão).
  2. Pacotes. Aqui nós podemos ver o processo de configuração da conexão, a saudação do servidor, a solicitação de autorização e a subsequente troca de dados.
  3. Conteúdo do pacote selecionado em formato hexadecimal. Nesse caso, nós podemos ver o conteúdo do pacote de saudação do servidor MySQL.
  4. Nível de transporte (TCP). Estamos localizados aqui ao usar as funções para trabalhar com sockets.
  5. Nível da aplicação (MySQL). É isso que nós devemos considerar neste artigo.

O filtro de exibição não limita a captura de pacotes. Isso é claramente visível na barra de status, indicando que 35 pacotes capturados de 2623 pacotes localizados na memória estão sendo manipulados no momento. Para reduzir a carga no PC, nós devemos definir o filtro de captura ao selecionar a interface de rede, conforme mostrado na Fig. 3. Isso deve ser feito apenas se todos os outros pacotes não forem realmente úteis.

Filtro de captura de pacotes

Fig. 3. Filtro de captura de pacotes

Para nos familiarizarmos com o analisador de tráfego, vamos tentar estabelecer uma conexão com o servidor "google.com" e acompanhar o processo. Para fazer isso, escrevemos um pequeno script.

void OnStart()
  {
//--- Get socket handle
   int socket=SocketCreate();
   if(socket==INVALID_HANDLE)
      return;
//--- Establish connection
   if(SocketConnect(socket,"google.com",80,2000)==false)
     {
      return;
     }
   Sleep(5000);
//--- Close connection
   SocketClose(socket);
  }

Então, primeiro nós criamos um socket e obtemos o seu identificador usando a função SocketCreate(). A referência diz que, neste caso, você pode obter um erro em dois casos quase impossíveis:

  1. O erro ERR_NETSOCKET_TOO_MANY_OPENED indica que mais de 128 sockets estão abertos.
  2. O erro ERR_FUNCTION_NOT_ALLOWED aparece ao tentar chamar uma criação de socket a partir de um indicador, no qual esse recurso está desativado.

Depois de receber o identificador, tentamos estabelecer a conexão. Neste exemplo, nos conectamos ao servidor "google.com" (não esqueça de adicioná-lo aos endereços permitidos nas configurações da plataforma), ou seja, a porta 80 com o tempo limite de 2 000 milissegundos. Depois de estabelecer a conexão, aguardamos 5 segundos e fechamos a conexão. Agora vamos ver como fica na janela do analisador de tráfego.

Estabelecendo e fechando uma conexão

Fig. 4. Estabelecendo e fechando uma conexão

Na Figura 4, nós podemos ver a troca de dados entre nosso script e o servidor "google.com" com o endereço IP "172.217.16.14". As consultas DNS não são exibidas aqui, pois a linha de filtro apresenta a expressão "tcp.port==80".

Os três pacotes superiores representam o estabelecimento de uma conexão, enquanto os três inferiores representam seu fechamento. A coluna Time exibe o tempo entre os pacotes e podemos ver 5 segundos de tempo de inatividade. Observe que os pacotes são coloridos em verde, diferente dos da Figura 2. Isso ocorre porque, no caso anterior, o analisador detectou o protocolo MySQL na troca. No caso atual, nenhum dado foi passado e o analisador destacou os pacotes com a cor TCP padrão.


Troca de dados

De acordo com o protocolo, o servidor MySQL deve enviar uma saudação depois de estabelecer uma conexão. Em resposta, o cliente envia uma solicitação de autorização. Esse funcionamento é descrito em detalhes na seção Connection Phase na página dev.mysql.com. Se a saudação não for recebida, o endereço IP é inválido ou o servidor está ouvindo outra porta. De qualquer forma, isso significa que nos conectamos a algo que definitivamente não é um servidor MySQL. Em uma situação normal, nós precisamos receber os dados (ler do socket) e analisá-los.


Recepção

Na classe CMySQLTransaction (a ser descrito em detalhes um pouco mais tarde), o recebimento de dados foi implementado da seguinte maneira:

//+------------------------------------------------------------------+
//| Data receipt                                                     |
//+------------------------------------------------------------------+
bool CMySQLTransaction::ReceiveData(ushort error_code=0)
  {
   char buf[];
   uint   timeout_check=GetTickCount()+m_timeout;
   do
     {
      //--- Get the amount of data that can be read from the socket
      uint len=SocketIsReadable(m_socket);
      if(len)
        {
         //--- Read data from the socket to the buffer
         int rsp_len=SocketRead(m_socket,buf,len,m_timeout);
         m_rx_counter+= rsp_len;
         //--- Send the buffer for handling
         ENUM_TRANSACTION_STATE res = Incoming(buf,rsp_len);
         //--- Get the result the following actions will depend on
         if(res==MYSQL_TRANSACTION_COMPLETE) // server response fully accepted
            return true;   // exit (successful)
         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;  // exit (error)
              }
         //--- In case of another result, continue waiting for data in the loop
        }
     }
   while(GetTickCount()<timeout_check && !IsStopped());
//--- If waiting for the completion of the server response receipt took longer than m_timeout,
//--- exit with the error
   SetUserError(error_code);
   return false;
  }
Aqui o m_socket é um identificador de socket obtido anteriormente ao criá-lo, enquanto m_timeout é o tempo limite de leitura de dados usado como o SocketRead() um argumento de função para aceitar um fragmento de dados, bem como na forma do tempo limite de recebimento de todos os dados. Antes de inserir o loop, definimos um timestamp. Atingindo-o, ele é considerado o tempo limite do recebimento de dados:
uint   timeout_check=GetTickCount()+m_timeout;

Em seguida, fazemos a leitura do resultado da função SocketIsReadable() em um loop e aguardamos até que ele retorne um valor diferente de zero. Depois disso, fazemos a leitura dos dados no buffer e passamos eles para processamento.

      uint len=SocketIsReadable(m_socket);
      if(len)
        {
         //--- Read data from the socket to the buffer
         int rsp_len=SocketRead(m_socket,buf,len,m_timeout);
         m_rx_counter+= rsp_len;
         //--- Send the buffer for handling
         ENUM_TRANSACTION_STATE res = Incoming(buf,rsp_len);

        ...

        }

Nós não podemos contar com a capacidade de aceitar o pacote inteiro se houver dados no socket. Há várias situações em que os dados podem chegar em pequenas porções. Por exemplo, pode ser uma conexão ruim por meio de um modem 4G com um grande número de retransmissões. Portanto, nosso manipulador deve ser capaz de coletar os dados em alguns grupos indivisíveis com os quais é possível trabalhar. Vamos usar os pacotes MySQL para isso.

O método CMySQLTransaction::Incoming() é usado para acumular e processar os dados:

   //--- Handle received data
   ENUM_TRANSACTION_STATE  Incoming(uchar &data[], uint len);

O resultado retornado nos permite saber o que fazer em seguida — continuar, concluir ou interromper o processo de recebimento dos dados:

enum ENUM_TRANSACTION_STATE
  {
   MYSQL_TRANSACTION_ERROR=-1,         // Error
   MYSQL_TRANSACTION_IN_PROGRESS=0,    // In progress
   MYSQL_TRANSACTION_COMPLETE,         // Fully completed
   MYSQL_TRANSACTION_SUBQUERY_COMPLETE // Partially completed
  };

No caso de um erro interno, bem como ao obter um erro no servidor ou ao concluir o recebimento dos dados, a leitura dos dados do socket deve ser interrompida. Em todos os outros casos, ele deve continuar. O valor MYSQL_TRANSACTION_SUBQUERY_COMPLETE indica que uma das respostas do servidor as várias consultas de um cliente foram aceitas. É equivalente a MYSQL_TRANSACTION_IN_PROGRESS para o algoritmo de leitura.

O pacote MySQL

Fig. 5. O pacote MySQL

O formato do pacote MySQL é exibido na Fig. 5. Os três primeiros bytes definem o tamanho de uma carga útil no pacote, enquanto o próximo byte significa o número de série do pacote na sequência, sendo seguido pelos dados. O número de série é definido como zero no início de cada transmissão. Por exemplo, o pacote de saudação é 0, solicitação de autorização do cliente — 1, resposta do servidor — 2 (final da fase de conexão). Em seguida, ao enviar uma consulta do cliente, o valor do número de sequência deve ser definido como zero novamente e aumentado em cada pacote de resposta do servidor. Se o número de pacotes exceder 255, o valor numérico passa para zero.

O pacote mais simples (ping do MySQL) tem a seguinte aparência no analisador de tráfego:

Pacote de ping no analisador de tráfego

Fig. 6. Pacote de ping no analisador de tráfego

O pacote Ping contém um byte de dados com o valor 14 (ou 0x0E na forma hexadecimal).

Vamos considerar o método CMySQLTransaction::Incoming() que reúne os dados para os pacotes e os passa para os manipuladores. Seu código fonte resumido é fornecido abaixo.

ENUM_TRANSACTION_STATE CMySQLTransaction::Incoming(uchar &data[], uint len)
  {
   int ptr=0; // index of the current byte in the 'data' buffer
   ENUM_TRANSACTION_STATE result=MYSQL_TRANSACTION_IN_PROGRESS; // result of handling accepted data
   while(len>0)
     {
      if(m_packet.total_length==0)
        {
         //--- If the amount of data in the packet is unknown
         while(m_rcv_len<4 && len>0)
           {
            m_hdr[m_rcv_len] = data[ptr];
            m_rcv_len++;
            ptr++;
            len--;
           }
         //--- Received the amount of data in the packet
         if(m_rcv_len==4)
           {
            //--- Reset error codes etc.
            m_packet.Reset();
            m_packet.total_length = reader.TotalLength(m_hdr);
            m_packet.number = m_hdr[3];
            //--- Length received, reset the counter of length bytes
            m_rcv_len = 0;
            //--- Highlight the buffer of a specified size
            if(ArrayResize(m_packet.data,m_packet.total_length)!=m_packet.total_length)
               return MYSQL_TRANSACTION_ERROR;  // internal error
           }
         else // if the amount of data is still not accepted
            return MYSQL_TRANSACTION_IN_PROGRESS;
        }
      //--- Collect packet data
      while(len>0 && m_rcv_len<m_packet.total_length)
        {
         m_packet.data[m_rcv_len] = data[ptr];
         m_rcv_len++;
         ptr++;
         len--;
        }
      //--- Make sure the package has been collected already
      if(m_rcv_len<m_packet.total_length)
         return MYSQL_TRANSACTION_IN_PROGRESS;

      //--- Handle received MySQL packet
      //...
      //---      

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

O primeiro passo é coletar o cabeçalho do pacote — os primeiros 4 bytes contendo o comprimento dos dados e o número de série na sequência. Para acumular o cabeçalho, usamos o buffer m_hdr e o contador de bytes m_rcv_len. Quando 4 bytes são coletados, obtemos o seu comprimento e alteramos o buffer m_packet.data com base nele. Os dados do pacote recebido são copiados para ele. Quando o pacote estiver pronto, passamos para o manipulador.

E se o comprimento dos dados recebidos len ainda não for zero após o recebimento do pacote, isso significa que recebemos vários pacotes. Nós podemos lidar com vários pacotes inteiros ou vários parciais em uma única chamada do método Incoming().

Os tipos de pacotes são fornecidos abaixo:

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
  };

Cada um deles tem seu próprio manipulador, que analisa sua sequência e seu conteúdo de acordo com o protocolo. Os valores recebidos durante a análise são atribuídos aos membros das classes correspondentes. Na implementação atual do conector, todos os dados recebidos nos pacotes são analisados. Isso pode parecer um pouco redundante, pois as propriedades dos campos "Table" e "Original table" geralmente coincidem. Além disso, os valores de algumas flags raramente são necessários (veja a Fig. 7). No entanto, a disponibilidade dessas propriedades permite construir de maneira flexível a lógica de interação com o servidor MySQL na camada de aplicação do programa.


Pacotes no analisador Wireshark

Fig. 7. Pacote de descrição do campo


Transmissão

O envio de dados é um pouco mais fácil aqui.

//+------------------------------------------------------------------+
//| Form and send ping                                               |
//+------------------------------------------------------------------+
bool CMySQLTransaction::ping(void)
  {
   if(reset_rbuf()==false)
     {
      SetUserError(MYSQL_ERR_INTERNAL_ERROR);
      return false;
     }
//--- Prepare the output buffer
   m_tx_buf.Reset();
//--- Reserve a place for the packet header
   m_tx_buf.Add(0x00,4);
//--- Place the command code
   m_tx_buf+=uchar(0x0E);
//--- Form a header
   m_tx_buf.AddHeader(0);
   uint len = m_tx_buf.Size();
//--- Send a packet
   if(SocketSend(m_socket,m_tx_buf.Buf,len)!=len)
      return false;
   m_tx_counter+= len;
   return true;
  }

O código fonte do método de envio de ping é fornecido acima. Copiamos os dados para o buffer preparado. No caso do ping, este é o código do comando 0x0E. Em seguida, formamos o cabeçalho considerando a quantidade de dados e o número de série do pacote. Para um ping, o número de série é sempre igual a zero. Depois disso, tentamos enviar o pacote montado usando a função SocketSend().

O método de envio de uma consulta (Query) é semelhante ao envio de um ping:

//+------------------------------------------------------------------+
//| Form and send a query                                            |
//+------------------------------------------------------------------+
bool CMySQLTransaction::query(string s)
  {
   if(reset_rbuf()==false)
     {
      SetUserError(MYSQL_ERR_INTERNAL_ERROR);
      return false;
     }
//--- Prepare the output buffer
   m_tx_buf.Reset();
//--- Reserve a place for the packet header
   m_tx_buf.Add(0x00,4);
//--- Place the command code
   m_tx_buf+=uchar(0x03);
//--- Add the query string
   m_tx_buf+=s;
//--- Form a header
   m_tx_buf.AddHeader(0);
   uint len = m_tx_buf.Size();
//--- Send a packet
   if(SocketSend(m_socket,m_tx_buf.Buf,len)!=len)
      return false;
   m_tx_counter+= len;
   return true;
  }

A única diferença é que a carga útil consiste no código de comando (0x03) e a string de consulta.

O envio de dados é sempre seguido pelo método de recebimento CMySQLTransaction::ReceiveData() que consideramos anteriormente. Se não retornar nenhum erro, a transação será considerada bem-sucedida.


Classe de transação MySQL

Agora é hora de considerar a classe CMySQLTransaction classe com mais detalhes.

//+------------------------------------------------------------------+
//| MySQL transaction class                                          |
//+------------------------------------------------------------------+
class CMySQLTransaction
  {
private:
   //--- Authorization data
   string            m_host;        // MySQL server IP address
   uint              m_port;        // TCP port
   string            m_user;        // User name
   string            m_password;    // Password
   //--- Timeouts
   uint              m_timeout;        // timeout of waiting for TCP data (ms)
   uint              m_timeout_conn;   // timeout of establishing a server connection
   //--- Keep Alive
   uint              m_keep_alive_tout;      // time(ms), after which the connection is closed; the value of 0 - Keep Alive is not used
   uint              m_ping_period;          // period of sending ping (in ms) in the Keep Alive mode
   bool              m_ping_before_query;    // send 'ping' before 'query' (this is reasonable in case of large ping sending periods)
   //--- Network
   int               m_socket;      // socket handle
   ulong             m_rx_counter;  // counter of bytes received
   ulong             m_tx_counter;  // counter of bytes passed
   //--- Timestamps
   ulong             m_dT;                   // last query time
   uint              m_last_resp_timestamp;  // last response time
   uint              m_last_ping_timestamp;  // last ping time
   //--- Server response
   CMySQLPacket      m_packet;      // accepted packet
   uchar             m_hdr[4];      // packet header
   uint              m_rcv_len;     // counter of packet header bytes
   //--- Transfer buffer
   CData             m_tx_buf;
   //--- Authorization request class
   CMySQLLoginRequest m_auth;
   //--- Server response buffer and its size
   CMySQLResponse    m_rbuf[];
   uint              m_responses;
   //--- Waiting and accepting data from the socket
   bool              ReceiveData(ushort error_code);
   //--- Handle received data
   ENUM_TRANSACTION_STATE  Incoming(uchar &data[], uint len);
   //--- Packet handlers for each type
   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);
   //--- Miscellaneous
   bool              ping(void);                // send ping
   bool              query(string s);           // send a query
   bool              reset_rbuf(void);          // initialize the server response buffer
   uint              tick_diff(uint prev_ts);   // get the timestamp difference
   //--- Parser class
   CMySQLPacketReader   reader;
public:
                     CMySQLTransaction();
                    ~CMySQLTransaction();
   //--- Set connection parameters
   bool              Config(string host,uint port,string user,string password,uint keep_alive_tout);
   //--- Keep Alive mode
   void              KeepAliveTimeout(uint tout);                       // set timeout
   void              PingPeriod(uint period) {m_ping_period=period;}    // set ping period in seconds
   void              PingBeforeQuery(bool st) {m_ping_before_query=st;} // enable/disable ping before a query
   //--- Handle timer events (relevant when using Keep Alive)
   void              OnTimer(void);
   //--- Get the pointer to the class for working with authorization
   CMySQLLoginRequest *Handshake(void) {return &m_auth;}
   //--- Send a request
   bool              Query(string q);
   //--- Get the number of server responses
   uint              Responses(void) {return m_responses;}
   //--- Get the pointer to the server response by index
   CMySQLResponse    *Response(uint idx);
   CMySQLResponse    *Response(void) {return Response(0);}
   //--- Get the server error structure
   MySQLServerError  GetServerError(void) {return m_packet.error;}
   //--- Options
   ulong             RequestDuration(void) {return m_dT;}                     // get the last transaction duration
   ulong             RxBytesTotal(void) {return m_rx_counter;}                // get the number of received bytes
   ulong             TxBytesTotal(void) {return m_tx_counter;}                // get the number of passed bytes
   void              ResetBytesCounters(void) {m_rx_counter=0; m_tx_counter=0;} // reset the counters of received and passed bytes
  };

Vamos dar uma olhada nos seguintes membros privados:

  • m_packet do tipo CMySQLPacket — classe do pacote MySQL processado no momento atual (código fonte com comentários no arquivo MySQLPacket.mqh)
  • m_tx_buf do tipo CData — classe do buffer de transferência criado para a conveniência de gerar uma consulta (arquivo Data.mqh)
  • m_auth do tipo CMySQLLoginRequest — classe para trabalhar com autorizações (codificação de senhas, armazenamento de parâmetros de servidor obtidos e parâmetros de cliente especificados, o código fonte está em MySQLLoginRequest.mqh)
  • m_rbuf do tipo CMySQLResponse — buffer de resposta do servidor; a resposta aqui é o pacote do tipo "Ok" ou "Data" (MySQLResponse.mqh)
  • readerdo tipo CMySQLPacketReader — classe de analisador de pacotes MySQL

Os métodos públicos são descritos em detalhes na documentação.

Para a camada de aplicação, a classe de transação é exibida como mostra a Figura 8.

Classes

Fig. 8. Estrutura da classe CMySQLTransaction

onde:

  • CMySQLLoginRequest — deve ser configurada antes de estabelecer uma conexão ao especificar os parâmetros do cliente cujos valores são diferentes dos predefinidos (opcional);
  • CMySQLResponse — resposta do servidor se uma transação for concluída sem erros
    • CMySQLField — descrição do campo;
    • CMySQLRow — linha (buffer de valores do campo em forma de texto);
  • MySQLServerError — estrutura de descrição do erro no caso de falha de uma transação.

Não há métodos públicos responsáveis por estabelecer e fechar uma conexão. Isso é feito automaticamente ao chamar o método CMySQLTransaction::Query(). Ao usar o modo de conexão constante, ele é estabelecido durante a primeira chamada de CMySQLTransaction::Query () e fechado após o tempo limite definido.

Importante: No modo de conexão constante, o manipulador de eventos OnTimer deve receber a chamada do método CMySQLTransaction::OnTimer(). Nesse caso, o período do timer deve ser menor que os períodos de ping e tempo limite.

Os parâmetros de conexão, conta do usuário e valores especiais de parâmetros do cliente devem ser definidos antes de chamar o método CMySQLTransaction::Query().

Em geral, a interação com a classe de transação é realizada de acordo com o seguinte princípio:

Trabalhando com a classe de transação

Fig. 9. Trabalhando com a classe CMySQLTransaction



Aplicação

Vamos considerar o exemplo mais simples da aplicação do conector. Para fazer isso, escrevemos um script enviando a consulta SELECT para o banco de dados de teste 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

//--- Connect MySQL transaction class
#include  <MySQL\MySQLTransaction.mqh>
CMySQLTransaction mysqlt;

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- Configure MySQL transaction class
   mysqlt.Config(inp_server,inp_port,inp_login,inp_password);
//--- Make a query
   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))
        {
         // in case of a server error
         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());
        }
  }

Suponha que nossa tarefa seja obter uma lista de países com o valor do continente "Oceania" classificados por área, do maior para o menor, com o máximo de 10 itens na lista. Vamos executar as seguintes ações:

  • Declaramos uma instância da classe de transação mysqlt
  • Definimos os parâmetros de conexão
  • Criamos a consulta apropriada
  • Se a transação for bem sucedida, verificamos se o número de respostas é igual ao valor esperado
  • Obtemos o ponteiro para a classe de resposta do servidor
  • Obtemos o número de linhas na resposta
  • Exibimos os valores das linhas

A transação pode falhar devido a um dos três motivos:

Se as entradas forem especificadas corretamente, o resultado da operação do script é o seguinte:

Resultado da operação do script de teste

Fig. 10. Resultado da operação do script de teste

Exemplos mais complexos que aplicam várias consultas e o modo de conexão constante serão descritos na segunda parte.


Documentação

Conteúdo


    Classe de transação CMySQLTransaction

    Lista de métodos da classe CMySQLTransaction

    Método
    Ação
    Config
    Define os parâmetros de conexão
    KeepAliveTimeout
    Define o tempo limite para o modo Keep Alive em segundos
    PingPeriod
    Define o período de ping para o modo Keep Alive em segundos
    PingBeforeQuery
    Ativa/desativa o ping antes de uma consulta
    OnTimer
    Processa os eventos do timer (relevante ao usar o Keep Alive)
    Handshake
    Obtém o ponteiro da classe para trabalhar com a autorização
    Query
    Envia uma consulta
    Responses
    Obtém o número de respostas do servidor
    Response
    Obtém o ponteiro para a classe de resposta do servidor
    GetServerError
    Obtém a estrutura de erros do servidor
    RequestDuration
    Duração da transação em microssegundos
    RxBytesTotal
    O contador de bytes aceitos desde o início do programa
    TxBytesTotal
    O contador de bytes enviados desde o início do programa
    ResetBytesCounters
    Redefine os contadores de bytes aceitos e enviados

    Abaixo está uma breve descrição de cada método.

    Config

    Define os parâmetros de conexão.
    bool  Config(
       string host,         // server name
       uint port,           // port
       string user,         // user name
       string password,     // password
       string base,         // database name
       uint keep_alive_tout // constant connection timeout (0 - not used)
       );
    

    Valor de retorno: true se for bem sucedido; caso contrário - false (símbolos inválidos nos argumentos do tipo string).

    KeepAliveTimeout

    Ativa o modo de conexão constante e define o tempo limite. O valor do tempo limite é um tempo em segundos a partir do momento do envio da última consulta, após o qual a conexão é fechada. Se as consultas forem repetidas com mais frequência do que o valor de tempo limite definido, a conexão não será fechada.

    void  KeepAliveTimeout(
       uint tout            // set the constant connection timeout in seconds (0 - disable)
       );
    

    PingPeriod

    Define o período de envio de pacotes 'ping' no modo de conexão constante. Isso impede que o servidor feche a conexão. O ping é enviado após o tempo especificado na última consulta ou no ping anterior.

    void  PingPeriod(
       uint period          // set the ping period in seconds (for the constant connection mode)
       );
    

    Valor de retorno: nenhum.

    PingBeforeQuery

    Permite enviar o pacote 'ping' antes de uma consulta. A conexão pode ser fechada ou encerrada por algum motivo no modo de conexão constante em intervalos de tempo entre as consultas. Nesse caso, é possível enviar o ping ao servidor MySQL para garantir que a conexão esteja ativa antes de enviar uma consulta.

    void  PingBeforeQuery(
       bool st              // enable (true)/disable (false) ping before a query
       );
    

    Valor de retorno: nenhum.

    OnTimer

    Utilizado no modo de conexão constante. O método deve ser chamado a partir do manipulador de eventos OnTimer. O período do timer não deve exceder o valor mínimo dos períodos KeepAliveTimeout e PingPeriod.

    void  OnTimer(void);

    Valor de retorno: nenhum.

    Handshake

    Obtém o ponteiro para a classe de trabalho com a autorização. Ele pode ser usado para definir as flags dos recursos do cliente e o tamanho máximo do pacote antes de estabelecer uma conexão com o servidor. Após a autorização, permite receber a versão e as flags dos recursos do servidor.

    CMySQLLoginRequest *Handshake(void);

    Valor de retorno: ponteiro para a classe CMySQLLoginRequest de trabalho com autorização.

    Query

    Envia uma consulta.

    bool  Query(
       string q             // query body
       );
    

    Valor de retorno: resultado da execução; bem sucedido - true, erro - false.

    Responses

    Obtém o número de respostas.

    uint  Responses(void);

    Valor de retorno: número de respostas do servidor.

    Pacotes dos tipos "Ok" ou "Data" são considerados respostas. Se a consulta for executada com sucesso, uma ou mais respostas (para várias consultas) serão aceitas.

    Response

    Obtém o ponteiro para a classe de resposta do servidor MySQL

    CMySQLResponse  *Response(
       uint idx                     // server response index
       );
    

    Valor de retorno: ponteiro para a classe de resposta do servidor CMySQLResponse. Passar um valor inválido como argumento retorna NULL.

    O método sobrecarregado sem especificar um índice é equivalente a Response(0).

    CMySQLResponse  *Response(void);

    Valor de retorno: ponteiro para a classe de resposta do servidor CMySQLResponse. Se não houver respostas, NULL será retornado.

    GetServerError

    Obtém a estrutura que armazena o código e a mensagem de erro do servidor. Ele pode ser chamado após a classe de transação retornar o erro MYSQL_ERR_SERVER_ERROR.

    MySQLServerError  GetServerError(void);

    Valor de retorno: estrutura de erro MySQLServerError

    RequestDuration

    Obtém a duração da execução da solicitação.

    ulong  RequestDuration(void);

    Valor de retorno: duração da consulta em microssegundos desde o momento do envio até o final do processamento

    RxBytesTotal

    Obtém o número de bytes aceitos.

    ulong  RxBytesTotal(void);

    Valor de retorno: número de bytes aceitos (nível TCP) desde o início do programa. O método ResetBytesCounters é usado para uma redefinição.

    TxBytesTotal

    Obtém o número de bytes enviados.

    ulong  TxBytesTotal(void);

    Valor de retorno: número de bytes passados (nível TCP) desde o início do programa. O método ResetBytesCounters é usado para uma redefinição.

    ResetBytesCounters

    Redefine os contadores de bytes aceitos e enviados.

    void  ResetBytesCounters(void);


    Classe de gerenciamento de autorização CMySQLLoginRequest

    Métodos da classe CMySQLLoginRequest

    Método
    Ação
    SetClientCapabilities
    Define as flags de recursos do cliente. Valor predefinido: 0x005FA685
    SetMaxPacketSize
    Define o tamanho máximo permitido do pacote em bytes. Valor predefinido: 16777215
    SetCharset
    Define o conjunto de símbolos usados. Valor predefinido: 8
    Version
    Retorna a versão do servidor MySQL. Por exemplo: "5.7.21-log".
    ThreadId
    Retorna o ID da thread de conexão atual. Corresponde ao valor CONNECTION_ID.
    ServerCapabilities
    Obtém as flags dos recursos do servidor
    ServerLanguage
    Retorna a codificação e a representação do banco de dados ID

    Classe de resposta do servidor CMySQLResponse

    Um pacote do tipo "Ok" ou "Data" é considerado uma resposta do servidor. Dado que eles diferem significativamente, a classe possui um conjunto separado de métodos para trabalhar com cada tipo de pacote.

    Métodos gerais da classe CMySQLResponse:

    Método
    Valor de Retorno
    Type
    Tipo de resposta do servidor: MYSQL_RESPONSE_DATA ou MYSQL_RESPONSE_OK

    Métodos para os pacotes do tipo "Data":

    Método
    Valor de Retorno
    Fields
    Número de campos
    Field
    Ponteiro para a classe de campo por índice (método sobrecarregado - obtendo o índice do campo pelo nome)
    Field Índice do campo por nome
    Rows
    Número de linhas em uma resposta do servidor
    Row
    O ponteiro para uma classe de linha por índice
    Value
    Valor de string por índices de linha e campo
    Value Valor de string por índice de linha e nome do campo
    ColumnToArray O resultado da leitura de uma coluna do array do tipo string
    ColumnToArray
    O resultado da leitura de uma coluna no array do tipo int com verificação de tipo
    ColumnToArray
    O resultado da leitura de uma coluna no array do tipo long com verificação de tipo
    ColumnToArray
    O resultado da leitura de uma coluna no array do tipo double com verificação de tipo
    Métodos para pacotes do tipo "Ok":
    Método
    Valor de Retorno
    AffectedRows
    Número de linhas afetadas pela última operação
    LastId
    Valor LAST_INSERT_ID
    ServerStatus
    Flags do estado do servidor
    Warnings
    Número de avisos
    Message
    Mensagem de texto do servidor

    Estrutura de erro do servidor MySQLServerError

    Elementos da estrutura MySQLServerError

    Elemento
    Tipo
    Propósito
    code
     ushort Código de erro
    sqlstate
     uint Estado
    message  string Mensagem de texto do servidor


    Classe de campo CMySQLField

    Métodos da classe CMySQLField

    Método
     Valor de Retorno
    Catalog
    Nome de um diretório ao qual pertence a tabela
    Database
    Nome de um banco de dados ao qual pertence a tabela
    Table
    Pseudônimo de uma tabela à qual pertence o campo
    OriginalTable
    Nome original de uma tabela à qual pertence o campo
    Name
    Pseudônimo de campo
    OriginalName
    Nome do campo original
    Charset
    Número de codificação aplicado
    Length
    Comprimento do valor
    Type
    Tipo de valor
    Flags
    Flags que definem atributos de valor
    Decimals
    Casas decimais permitidas
    MQLType
    Tipo de campo na forma de valor ENUM_DATABASE_FIELD_TYPE (exceto para DATABASE_FIELD_TYPE_NULL)


    Classe de linha CMySQLRow

    Métodos da classe CMySQLRow

    Método
    Ação
    Value
    Retorna o valor do campo pelo número como uma string
    operator[]
    Retorna o valor do campo pelo nome como uma string
    MQLType
    Retorna o tipo de campo por número como o valor ENUM_DATABASE_FIELD_TYPE
    MQLType
    Retorna o tipo de campo pelo nome como o valor ENUM_DATABASE_FIELD_TYPE
    Text
    Obtém o valor do campo pelo número como uma string com verificação de tipo
    Text
    Obtém o valor do campo pelo nome como uma string com verificação de tipo
    Integer
    Obtém o valot do tipo int pelo número do campo com verificação de tipo
    Integer
    Obtém o valor do tipo int pelo nome do campo com verificação de tipo
    Long
    Obtém o valor do tipo long pelo número do campo com verificação de tipo
    Long
    Obtém o valor do tipo long pelo nome do campo com verificação de tipo
    Double
    Obtém o valor do tipo double pelo número do campo com verificação de tipo
    Double
    Obtém o valor do tipo double pelo nome do campo com verificação de tipo
    Blob
    Obtém o valor na forma de array uchar pelo número do campo com verificação de tipo
    Blob
    Obtém o valor na forma de array uchar pelo nome do campo com verificação de tipo

    Observação. A verificação de tipo significa que o campo legível do método que trabalha com o tipo int deve ser igual a DATABASE_FIELD_TYPE_INTEGER. No caso de uma incompatibilidade, nenhum valor é recebido e o método retorna 'false'. Convertendo IDs de tipo de campo MySQL para ENUM_DATABASE_FIELD_TYPE, o valor do tipo é implementado no método CMySQLField::MQLType() cujo código-fonte é fornecido abaixo.

    //+------------------------------------------------------------------+
    //| Return the field type as the ENUM_DATABASE_FIELD_TYPE value      |
    //+------------------------------------------------------------------+
    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;
         }
      }
    


    Conclusão

    Neste artigo, nós examinamos o uso de funções para trabalhar com sockets usando a implementação do conector MySQL como exemplo. Esta foi a parte teórica. A segunda parte do artigo é de natureza mais prática: desenvolveremos um serviço para coletar propriedades do sinal e um programa para visualizar as alterações nelas.

    O arquivo anexado contém os seguintes arquivos:

    • Caminho Include\MySQL\: códigos-fonte do conector
    • Arquivo Scripts\test_mysql.mq5: o exemplo do uso do conector considerado na seção Aplicação.

    Traduzido do russo pela MetaQuotes Software Corp.
    Artigo original: https://www.mql5.com/ru/articles/7117

    Arquivos anexados |
    MQL5.zip (23.17 KB)
    Biblioteca para criação simples e rápida de programas para MetaTrader (Parte XXXIV): ordens de negociação pendentes - exclusão de ordens, modificação de ordens/posições por condições Biblioteca para criação simples e rápida de programas para MetaTrader (Parte XXXIV): ordens de negociação pendentes - exclusão de ordens, modificação de ordens/posições por condições

    Neste artigo, concluiremos a descrição do conceito de solicitações de negociação pendentes e criaremos uma funcionalidade para excluir ordens pendentes e modificar ordens/posições de acordo com as condições definidas. Assim, teremos toda uma funcionalidade com a qual poderemos criar estratégias personalizadas simples, mais precisamente alguma lógica para o EA se comportar quando ocorrerem as condições especificadas pelo usuário.

    Como criar gráficos 3D usando o DirectX no MetaTrader 5 Como criar gráficos 3D usando o DirectX no MetaTrader 5

    Os gráficos 3D fornecem excelentes meios para analisar grandes quantidades de dados, pois permitem a visualização de padrões ocultos. Essas tarefas podem ser resolvidas diretamente em MQL5, enquanto as funções do DireсtX permitem a criação de objetos tridimensionais. Assim, é ainda possível criar programas de qualquer complexidade, até jogos 3D para a MetaTrader 5. Comece a aprender gráficos 3D desenhando formas tridimensionais simples.

    Trabalhando com as funções de rede ou MySQL sem DLL: Parte II - Programa para monitorar as alterações nas propriedades do sinal Trabalhando com as funções de rede ou MySQL sem DLL: Parte II - Programa para monitorar as alterações nas propriedades do sinal

    Na parte anterior, nós consideramos a implementação do conector MySQL. Neste artigo, nós consideraremos sua aplicação implementando o serviço para coletar as propriedades do sinal e o programa para visualizar suas alterações ao longo do tempo. O exemplo implementado tem sentido prático se os usuários precisarem observar alterações nas propriedades que não são exibidas na página da web do sinal.

    Previsão de séries temporais (parte 1): decomposição do modo empírico (EMD) Previsão de séries temporais (parte 1): decomposição do modo empírico (EMD)

    O artigo estuda a teoria e a aplicação prática de um algoritmo de previsão de séries temporais com base na decomposição em modos empíricos, além disso, propõe sua implementação em MQL5 e fornece indicadores de teste e EAs.