Работа с сетевыми функциями, или MySQL без DLL: Часть I - коннектор

20 января 2020, 13:47
Serhii Shevchuk
0
1 373

Содержание

Введение

Примерно год назад список сетевых функций в MQL5 пополнился функциями для работы с сокетами. Это открыло широкие возможности для программистов, которые разрабатывают продукты для Маркета, поскольку теперь можно реализовать то, чего раньше нельзя было сделать без динамических библиотек. Один из таких примеров мы рассмотрим в данном цикле из двух статей. В первой статье мы разберём принцип работы коннектора MySQL, а во второй напишем простейшие приложения с его применением — сервис сбора свойств сигналов, доступных в терминале, и программу для просмотра их изменения с течением времени (см. рисунок 1).


Программа для просмотра изменения свойств сигналов за определённое время

Рис. 1. Программа для просмотра изменения свойств сигналов с течением времени


Сокеты

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

В MQL5 нам доступны только клиентские TCP сокеты. Это значит, что мы можем инициировать соединение, но не ждать его извне. Поэтому, если нужно обеспечить связь между MQL5-программами через сокеты, нужен сервер, который будет выступать посредником. Сервер ожидает соединение на прослушиваемый порт и по запросу клиента выполняет определённые функции. Чтобы соединиться с сервером, нужно знать его ip-адрес и порт.

Порт является числом, которое может принимать значения от 0 до 65535. Выделяют три диапазона портов: системные (0 - 1023), пользовательские (1024-49151) и динамические (49152-65535). Часть портов назначена для работы с определёнными функциями. Этим назначением занимается IANA — организация, которая управляет пространствами IP-адресов, доменов верхнего уровня, и регистрирует типы данных MIME.

Для MySQL по-умолчанию назначен порт 3306, на него мы и будем соединяться при обращении к серверу. Следует учесть, что данное значение может быть изменено. Поэтому при создании эксперта порт нужно выносить во входные параметры наряду с IP-адресом.

При работе с сокетами используется следующий подход:

  • Создать сокет (получить хендл или ошибку)
  • Подключиться к серверу
  • Обменяться данными
  • Закрыть сокет

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


Анализатор трафика Wireshark

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

Лично я для этих целей использую Wireshark.

Анализатор трафика

Рис. 2. Анализатор трафика Wireshark

На рисунке 2 показано окно анализатора трафика с захваченными пакетами, где:

  1. Строка фильтра отображения. Выражение "tcp.port==3306" означает, что будут отображены только те пакеты, у которых локальный или удалённый TCP порт равен 3306 (порт MySQL сервера по умолчанию).
  2. Окно с пакетами. Здесь виден процесс установки соединения, приветствие сервера, запрос авторизации и последующий обмен.
  3. Содержимое выбранного пакета в шестнадцатеричном виде. В данном случае показано содержимое пакета приветствия сервера MySQL.
  4. Транспортный уровень (TCP). При использовании функций для работы с сокетами мы находимся здесь.
  5. Прикладной уровень (MySQL). Это то, что мы будем рассматривать в даной статье.

Следует иметь ввиду, что фильтр отображения не ограничивает захват пакетов, что хорошо видно в строке состояния. В ней указано, что в данный момент отображается 35 пакетов из захваченных 2623, которые в это время находятся в памяти. Для того чтобы снизить нагрузку на ПК, следует задать фильтр захвата ранее при выборе сетевого интерфейса, как показано ниже на рисунке 3. Но это стоит делать только в том случае, если все остальные пакеты действительно не пригодятся.

Фильтр захвата пакетов

Рис. 3. Фильтр захвата пакетов

Для ознакомления с работой анализатора трафика попробуем установить соединение с сервером "google.com" и отследить данный процесс. Для этого напишем небольшой скрипт.

void OnStart()
  {
//--- Получение хендла сокета
   int socket=SocketCreate();
   if(socket==INVALID_HANDLE)
      return;
//--- Установка соединения
   if(SocketConnect(socket,"google.com",80,2000)==false)
     {
      return;
     }
   Sleep(5000);
//--- Закрываем соединение
   SocketClose(socket);
  }

Итак, сначала создаём сокет и получаем его хендл при помощи функции SocketCreate(). В справке сказано, что в данном случае можно получить ошибку лишь в двух, практически невозможных ситуациях:

  1. Ошибка ERR_NETSOCKET_TOO_MANY_OPENED, которая сигнализирует о том, что открыто больше 128 сокетов.
  2. Ошибка ERR_FUNCTION_NOT_ALLOWED, если вы попытаетесь вызвать создание сокета из индикатора, где это запрещено.

Когда хендл получен, пробуем установить соединение. В данном примере мы соединяемся с сервером "google.com" (не забудьте добавить его в разрешенные адреса в настройках терминала) на порт 80 с таймаутом 2000 миллисекунд. После успешной установки соединения ждём 5 секунд и закрываем его. Теперь посмотрим, как это выглядит в окне анализатора трафика.

Установка и закрытие соединения

Рис. 4. Установка и закрытие соединения

Итак, на рисунке 4 мы видим обмен данными между нашим скриптом и сервером "google.com" с ip-адресом "172.217.16.14". Здесь не отображены запросы DNS, потому что в строке фильтра введено выражение "tcp.port==80".

Верхние три пакета — установка соединения. Нижние три — закрытие. В колонке Time отображается время между пакетами и мы видим наши 5 секунд простоя. Обратите внимание, что пакеты окрашены в зеленый цвет, в отличие от пакетов на рисунке 2. Это потому, что в предыдущем случае анализатор обнаружил в обмене протокол MySQL. Здесь же никаких данных передано не было и анализатор подсветил пакеты цветом для TCP "по умолчанию".


Обмен данными

Согласно протоколу, после установки соединения сервер MySQL должен прислать приветствие. В ответ на него клиент отправляет запрос авторизации. Данный механизм подробно описан в разделе " Connection Phase" на сайте "dev.mysql.com". Если приветствие не было получено, значит, либо использован неверный IP-адрес, либо сервер слушает другой порт. В любом случае мы соединились с чем-то, что точно не является сервером MySQL. В нормальной ситуации нужно принять данные (прочитать из сокета) и разобрать их.


Приём

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

//+------------------------------------------------------------------+
//| Приём данных                                                     |
//+------------------------------------------------------------------+
bool CMySQLTransaction::ReceiveData(ushort error_code=0)
  {
   char buf[];
   uint   timeout_check=GetTickCount()+m_timeout;
   do
     {
      //--- Получаем количество данных, которые можно прочитать из сокета
      uint len=SocketIsReadable(m_socket);
      if(len)
        {
         //--- Читаем данные из сокета в буфер
         int rsp_len=SocketRead(m_socket,buf,len,m_timeout);
         m_rx_counter+= rsp_len;
         //--- Отправляем буфер на обработку
         ENUM_TRANSACTION_STATE res = Incoming(buf,rsp_len);
         //--- Получаем результат, от которого будут зависеть следующие действия
         if(res==MYSQL_TRANSACTION_COMPLETE) // ответ сервера полностью принят
            return true;   // выход (успешно)
         else
            if(res==MYSQL_TRANSACTION_ERROR) // ошибка
              {
               if(m_packet.error.code)
                  SetUserError(MYSQL_ERR_SERVER_ERROR);
               else
                  SetUserError(MYSQL_ERR_INTERNAL_ERROR);
               return false;  // выход (ошибка)
              }
         //--- При других значениях результата продолжаем ожидание данных в цикле
        }
     }
   while(GetTickCount()<timeout_check && !IsStopped());
//--- Если ожидание завершения приёма ответа серваре продлилось дольше значения m_timeout,
//--- выходим с ошибкой
   SetUserError(error_code);
   return false;
  }
Здесь m_socket — хендл сокета, полученный ранее при его создании, а m_timeout — таймаут чтения данных, который используется как аргумент функции SocketRead() для приёма фрагмента данных, а также как таймаут приема всех данных целиком. Перед вхождением в цикл откладываем временную метку, достижение которой будет считаться таймаутом операции приема данных:
uint   timeout_check=GetTickCount()+m_timeout;

Далее зацикливаемся в чтении результата функции SocketIsReadable() и ожидаем, когда она вернёт ненулевое значение. После чего читаем данные в буфер и передаём его на обработку.

      uint len=SocketIsReadable(m_socket);
      if(len)
        {
         //--- Читаем данные из сокета в буфер
         int rsp_len=SocketRead(m_socket,buf,len,m_timeout);
         m_rx_counter+= rsp_len;
         //--- Отправляем буфер на обработку
         ENUM_TRANSACTION_STATE res = Incoming(buf,rsp_len);

        ...

        }

Нельзя рассчитывать на то, что при наличии данных в сокете мы примем весь пакет. Есть ряд ситуаций, когда данные могут приходить малыми порциями. Например, это может быть плохая связь через 4G-модем с большим числом ретрансмитов. Поэтому наш обработчик должен уметь собирать прочитанные данные в некоторые неделимые группы, с которыми можно работать. В качестве таковых выберем пакеты MySQL.

Накоплением данных и последующей обработкой занимается метод CMySQLTransaction::Incoming():

   //--- Обработка принятых данных
   ENUM_TRANSACTION_STATE  Incoming(uchar &data[], uint len);

Результат, который он возвращает, даст нам знать, как поступить дальше — продолжать приём данных, завершить его или прервать:

enum ENUM_TRANSACTION_STATE
  {
   MYSQL_TRANSACTION_ERROR=-1,         // Ошибка
   MYSQL_TRANSACTION_IN_PROGRESS=0,    // В процессе
   MYSQL_TRANSACTION_COMPLETE,         // Полностью завершено
   MYSQL_TRANSACTION_SUBQUERY_COMPLETE // Частично завершено
  };

При возникновении внутренней ошибки, а также получения ошибки сервера или завершения приёма, следует прекратить чтение данных из сокета, в остальных случаях — продолжать. Значение MYSQL_TRANSACTION_SUBQUERY_COMPLETE сигнализирует о том, что принят один из ответов сервера на множественный запрос клиента. Для алгоритма чтения он равносилен MYSQL_TRANSACTION_IN_PROGRESS.

Пакет MySQL

Рис. 5. Пакет MySQL

Формат пакета MySQL показан на рисунке 5. Первые три байта определяют размер полезной нагрузки в пакете, следующий байт означает порядковый номер пакета в последовательности, за ним следуют данные. Порядковый номер устанавливается в ноль в начале каждого обмена. Например, пакет приветствия будет иметь номер 0, запрос авторизации клиента — номер 1, ответ сервера — номер 2 (окончание фазы соединения). Далее, при отправке запроса клиента, значение порядкового номера должно быть снова установлено в ноль, и будет увеличиваться в каждом пакете ответа сервера. Если количество пакетов будет больше чем 255, значение номера перейдёт через ноль.

Простейший пакет (MySQL ping) в анализаторе трафика выглядит следующим образом:

Пакет Ping в анализаторе трафика

Рис. 6. Пакет Ping в анализаторе трафика

Пакет Ping содержит один байт данных со значением 14 (или 0x0E в шестнадцатиричном виде).

Рассмотрим метод CMySQLTransaction::Incoming(), который собирает данные в пакеты и передаёт их обработчикам. Ниже приведён его исходный код в сокращённом виде.

ENUM_TRANSACTION_STATE CMySQLTransaction::Incoming(uchar &data[], uint len)
  {
   int ptr=0; // индекс текущего байта в буфере data
   ENUM_TRANSACTION_STATE result=MYSQL_TRANSACTION_IN_PROGRESS; // результат обработки принятых данных
   while(len>0)
     {
      if(m_packet.total_length==0)
        {
         //--- Если неизвестно количество данных в пакете
         while(m_rcv_len<4 && len>0)
           {
            m_hdr[m_rcv_len] = data[ptr];
            m_rcv_len++;
            ptr++;
            len--;
           }
         //--- Получено количество данных в пакете
         if(m_rcv_len==4)
           {
            //--- Сбрасываем коды ошибок и т.д.
            m_packet.Reset();
            m_packet.total_length = reader.TotalLength(m_hdr);
            m_packet.number = m_hdr[3];
            //--- Длина получена, сбрасываем счётчик байтов длины
            m_rcv_len = 0;
            //--- Выделяем буфер указанного размера
            if(ArrayResize(m_packet.data,m_packet.total_length)!=m_packet.total_length)
               return MYSQL_TRANSACTION_ERROR;  // внутренняя ошибка
           }
         else // если количество данных всё ещё не принято
            return MYSQL_TRANSACTION_IN_PROGRESS;
        }
      //--- Собираем данные пакета
      while(len>0 && m_rcv_len<m_packet.total_length)
        {
         m_packet.data[m_rcv_len] = data[ptr];
         m_rcv_len++;
         ptr++;
         len--;
        }
      //--- Проверяем, что пакет уже собран
      if(m_rcv_len<m_packet.total_length)
         return MYSQL_TRANSACTION_IN_PROGRESS;

      //--- Обработка принятого пакета MySQL
      //...
      //---      

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

Первым делом нужно собрать заголовок пакета — первые 4 байта, где содержится длина данных и порядковый номер в последовательности. Для накопления заголовка используем буфер m_hdr и счётчик байт m_rcv_len. Когда 4 байта собраны, получаем из них длину и исходя из её значения изменяем размер буфера m_packet.data, в который будем копировать принятые данные пакета. Когда пакет полностью собран, передаём его обработчику.

Если после приёма пакета длина принятых данных len всё ещё не равна нулю, значит мы приняли несколько пакетов. За один вызов метода Incoming() может быть обработано как несколько пакетов, так и ни одного целого (а только часть).

Типы пакетов приведены ниже:

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

Для каждого из них есть свой обработчик, который разбирает их последовательность и содержимое согласно протоколу. Значения, полученные в следствие парсинга, присваиваются членам соответствующих классов. Здесь я обращаю внимание на то, что в данной реализации коннектора разбираются абсолютно все данные, полученные в пакетах. Это может показаться несколько избыточным, потому как свойства поля "Table" и "Original table" часто равны, да и значения некоторых флагов мало кому понадобятся (см. рисунок 7). Тем не менее, доступность этих свойств позволяет гибко построить логику взаимодействия с сервером MySQL на прикладном уровне программы.


Пакеты в анализаторе Wireshark

Рис. 7. Пакет описания поля


Передача

Что касается отправки данных, здесь всё немного проще.

//+------------------------------------------------------------------+
//| Формирование и отправка пинга                                    |
//+------------------------------------------------------------------+
bool CMySQLTransaction::ping(void)
  {
   if(reset_rbuf()==false)
     {
      SetUserError(MYSQL_ERR_INTERNAL_ERROR);
      return false;
     }
//--- Подготовка выходного буфера
   m_tx_buf.Reset();
//--- Резервируем место для заголовка пакета
   m_tx_buf.Add(0x00,4);
//--- Размещаем код команды
   m_tx_buf+=uchar(0x0E);
//--- Формируем заголовок
   m_tx_buf.AddHeader(0);
   uint len = m_tx_buf.Size();
//--- Отправляем пакет
   if(SocketSend(m_socket,m_tx_buf.Buf,len)!=len)
      return false;
   m_tx_counter+= len;
   return true;
  }

Выше приведён исходный код метода отправки пинга. В подготовленный буфер копируем данные. В случае с пингом это код команды 0x0E. Затем формируем заголовок, исходя из количества данных и порядкового номера пакета. Для пинга порядковый номер всегда будет равен нулю. После этого пробуем отправить собранный пакет при помощи функции SocketSend().

Метод отправки запроса (Query) похож на отправку пинга:

//+------------------------------------------------------------------+
//| Формирование и отправка запроса                                  |
//+------------------------------------------------------------------+
bool CMySQLTransaction::query(string s)
  {
   if(reset_rbuf()==false)
     {
      SetUserError(MYSQL_ERR_INTERNAL_ERROR);
      return false;
     }
//--- Подготовка выходного буфера
   m_tx_buf.Reset();
//--- Резервируем место для заголовка пакета
   m_tx_buf.Add(0x00,4);
//--- Размещаем код команды
   m_tx_buf+=uchar(0x03);
//--- Добавляем строку запроса
   m_tx_buf+=s;
//--- Формируем заголовок
   m_tx_buf.AddHeader(0);
   uint len = m_tx_buf.Size();
//--- Отправляем пакет
   if(SocketSend(m_socket,m_tx_buf.Buf,len)!=len)
      return false;
   m_tx_counter+= len;
   return true;
  }

Отличие лишь в том, что здесь полезная нагрузка состоит из кода команды (0x03) и строки запроса.

За отправкой данных всегда следует уже знакомый нам метод приёма CMySQLTransaction::ReceiveData(). Если он не вернул ошибку, транзакция считается успешной.


Класс транзакции MySQL

Пришло время рассмотреть класс CMySQLTransaction более подробно.

//+------------------------------------------------------------------+
//| Класс транзакции MySQL                                           |
//+------------------------------------------------------------------+
class CMySQLTransaction
  {
private:
   //--- Данные для авторизации
   string            m_host;        // IP-адрес сервера MySQL
   uint              m_port;        // Порт TCP
   string            m_user;        // Имя пользователя
   string            m_password;    // Пароль
   //--- Таймауты
   uint              m_timeout;        // таймаут ожидания данных TCP (ms)
   uint              m_timeout_conn;   // таймаут установки соединения с сервером
   //--- Keep Alive
   uint              m_keep_alive_tout;      // время(ms), спустя которое будет закрыто соединение; значение 0 - Keep Alive не используется
   uint              m_ping_period;          // период отправки пинга (в ms) в режиме Keep Alive
   bool              m_ping_before_query;    // посылать ping перед query (имеет смысл при больших значениях периода отправки пинга)
   //--- Сеть
   int               m_socket;      // хендл сокета
   ulong             m_rx_counter;  // счётчик полученных байтов
   ulong             m_tx_counter;  // счётчик переданных байтов
   //--- Временные метки
   ulong             m_dT;                   // время выполнения последнего запроса
   uint              m_last_resp_timestamp;  // время получения последнего ответа
   uint              m_last_ping_timestamp;  // время последнего пинга
   //--- Ответ сервера
   CMySQLPacket      m_packet;      // принятый пакет
   uchar             m_hdr[4];      // заголовок пакета
   uint              m_rcv_len;     // счётчик байтов заголовка пакета
   //--- Буфер передачи
   CData             m_tx_buf;
   //--- Класс формирования запроса авторизации
   CMySQLLoginRequest m_auth;
   //--- Буфер ответов сервера и его размер
   CMySQLResponse    m_rbuf[];
   uint              m_responses;
   //--- Ожидание и приём данных из сокета
   bool              ReceiveData(ushort error_code);
   //--- Обработка принятых данных
   ENUM_TRANSACTION_STATE  Incoming(uchar &data[], uint len);
   //--- Обработчики пакетов для каждого из типов
   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);
   //--- Разное
   bool              ping(void);                // отправка пинга
   bool              query(string s);           // отправка запроса
   bool              reset_rbuf(void);          // инициализация буфера ответов сервера
   uint              tick_diff(uint prev_ts);   // получение разницы временных меток
   //--- Класс парсера
   CMySQLPacketReader   reader;
public:
                     CMySQLTransaction();
                    ~CMySQLTransaction();
   //--- Установка параметров соединения
   bool              Config(string host,uint port,string user,string password,uint keep_alive_tout);
   //--- Режим Keep Alive
   void              KeepAliveTimeout(uint tout);                       // установка таймаута
   void              PingPeriod(uint period) {m_ping_period=period;}    // установить период пинга в секундах
   void              PingBeforeQuery(bool st) {m_ping_before_query=st;} // включить/выключить пинг перед запросом
   //--- Обработка событий таймера (актуально при использовании Keep Alive)
   void              OnTimer(void);
   //--- Получить указатель на класс работы с авторизацией
   CMySQLLoginRequest *Handshake(void) {return &m_auth;}
   //--- Отправить запрос
   bool              Query(string q);
   //--- Получить количество ответов сервера
   uint              Responses(void) {return m_responses;}
   //--- Получить указатель на ответ сервера по индексу
   CMySQLResponse    *Response(uint idx);
   CMySQLResponse    *Response(void) {return Response(0);}
   //--- Получить структуру ошибки сервера
   MySQLServerError  GetServerError(void) {return m_packet.error;}
   //--- Опции
   ulong             RequestDuration(void) {return m_dT;}                     // получить длительность последней транзакции
   ulong             RxBytesTotal(void) {return m_rx_counter;}                // получить количество принятых байтов
   ulong             TxBytesTotal(void) {return m_tx_counter;}                // получить количество переданных байтов
   void              ResetBytesCounters(void) {m_rx_counter=0; m_tx_counter=0;} // сбросить счётчики принятых и переданных байтов
  };

Из приватных членов хотелось бы выделить следующие:

  • m_packet типа CMySQLPacket — класс пакета MySQL, который обрабатывается в данный момент (исходный код с комментариями в файле MySQLPacket.mqh)
  • m_tx_buf типа CData — класс буфера передачи, который создан для удобства формирования запроса (файл Data.mqh)
  • m_auth типа CMySQLLoginRequest — класс для работы с авторизацией (скремблирование пароля, хранение полученных параметров сервера и установленных параметров клиента, исходный код в файле MySQLLoginRequest.mqh)
  • m_rbuf типа CMySQLResponse — буфер ответов сервера; здесь ответом считается пакет типа "Ok" или "Data" (файл MySQLResponse.mqh)
  • reader типа CMySQLPacketReader — класс парсера пакетов MySQL

Публичные методы подробно описаны в разделе документации.

Для прикладного уровня класс транзакции будет выглядеть так, как показано на рисунке 8.

Классы

Рис. 8. Строение класса CMySQLTransaction

Где:

  • CMySQLLoginRequest — должен быть сконфигурирован до установки соединения, если требуется задать параметры клиента, значения которые отличаются от предустановленных (не обязательно) ;
  • CMySQLResponse — ответ сервера, если транзакция была завершена без ошибок
    • CMySQLField — описание поля;
    • CMySQLRow — ряд (буфер значений полей в текстовом виде);
  • MySQLServerError — структура описания ошибки, если транзакция завершилась неудачей;

Среди публичных методов нет тех, которые бы отвечали за установку и закрытие соединения. Это происходит автоматически при вызове метода CMySQLTransaction::Query(). В случае использования режима постоянного соединения оно будет установлено при первом вызове CMySQLTransaction::Query() и закрыто по истечении установленного таймаута.

Важно: в режиме постоянного соединения в обработчик события OnTimer должен быть добавлен вызов метода CMySQLTransaction::OnTimer(). При этом период таймера должен быть меньше периода пинга и таймаута.

Параметры соединения, учётной записи пользователя, а также особые значения параметров клиента должны быть установлены до вызова CMySQLTransaction::Query().

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

Работа с классом транзакции

Рис. 9. Работа с классом CMySQLTransaction



Применение

Рассмотрим простейший пример применения коннектора. Для этого напишем скрипт, который отправляет запрос SELECT к тестовой базе данных 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

//--- Подключение класса транзакции MySQL
#include  <MySQL\MySQLTransaction.mqh>
CMySQLTransaction mysqlt;

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- Сконфигурируем класс транзакции MySQL
   mysqlt.Config(inp_server,inp_port,inp_login,inp_password);
//--- Составим запрос
   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))
        {
         // если это ошибка сервера
         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());
        }
  }

Пусть наша задача состоит в том, чтобы получить список стран со значением континента "Океания", отсортированных по площади от большего к меньшему, максимум 10 наименований в списке. Для этого выполним следующие действия:

  • Объявим экземпляр класса транзакции mysqlt
  • Установим параметры соединения
  • Составим соответствующий запрос
  • Если транзакция прошла успешно, проверим, что количество ответов равно ожидаемому значению
  • Получаем указатель на класс ответа сервера
  • Получаем количество рядов в ответе
  • Выводим значения рядов

Если транзакция прошла с ошибкой, может быть три варинта развития событий:

  • Это ошибка сервера - получаем её описание, используя метод CMySQLTransaction::GetServerError()
  • Это внутрення ошибка - чтобы получить описание, используем функцию EnumToString()
  • В противном случае получаем код ошибки, используя GetLastError()

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

Результат работы тестового скрипта

Рис. 10. Результат работы тестового скрипта

Более сложные примеры с использованием множественных запросов и режима постоянного соединения рассмотрим во второй части статьи.


Документация

Содержание


    Класс транзакции CMySQLTransaction

    Список методов класса CMySQLTransaction

    Метод
    Действие
    Config
    Установка параметров соединения
    KeepAliveTimeout
    Установка таймаута для режима Keep Alive в секундах
    PingPeriod
    Установка периода пинга для режима Keep Alive в секундах
    PingBeforeQuery
    Включить/выключить пинг перед запросом
    OnTimer
    Обработка событий таймера (актуально только при использовании Keep Alive)
    Handshake
    Получение указателя на класс работы с авторизацией
    Query
    Отправка запроса
    Responses
    Получение количество ответов сервера
    Response
    Получение указателя на класс ответа сервера
    GetServerError
    Получение структуры ошибки сервера
    RequestDuration
    Длительность транзакции в микросекундах
    RxBytesTotal
    Счётчик принятых байтов с начала запуска программы
    TxBytesTotal
    Счётчик отправленных байтов с начала запуска программы
    ResetBytesCounters
    Сброс счётчиков принятых и отправленных байтов

    Ниже приведена краткая справка по каждому из методов.

    Config

    Устанавливает параметры соединения.
    bool  Config(
       string host,         // имя сервера
       uint port,           // порт
       string user,         // имя пользователя
       string password,     // пароль
       string base,         // имя базы данных
       uint keep_alive_tout // таймаут постоянного соединения (0 - если не используется)
       );

    Возвращаемое значение: true в случае успеха, иначе false (недопустимые символы в аргументах типа string).

    KeepAliveTimeout

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

    void  KeepAliveTimeout(
       uint tout            // установка таймаута постоянного соединения в секундах (0 - отключить)
       );

    PingPeriod

    Устанавливает период отправки пакетов ping в режиме постоянного соединения. Нужен для того, чтобы сервер по каким-то своим соображениям не закрыл соединение. Пинг будет отправлен через указанное время после последнего запроса или предыдущего пинга.

    void  PingPeriod(
       uint period          // установка периода пинга в секундах (для режима постоянного соединения)
       );

    Возвращаемое значение: нет

    PingBeforeQuery

    Включает отправку пакета ping перед запросом. В режиме постоянного соединения, в промежутках времени между запросами, соединение по каким-то причинам могло быть закрыто или оборвано. В этом случае, можно послать пинг серверу MySQL, чтобы убедиться, что он на связи, и только потом слать запрос.

    void  PingBeforeQuery(
       bool st              // включить (true) или выключить (false) пинг перед запросом
       );

    Возвращаемое значение: нет

    OnTimer

    Используется в режиме постоянного соединения. Метод должен вызываться из обработчика событий OnTimer. При этом период таймера не должен превышать минимальный из периодов KeepAliveTimeout и PingPeriod.

    void  OnTimer(void);

    Возвращаемое значение: нет

    Handshake

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

    CMySQLLoginRequest *Handshake(void);

    Возвращаемое значение: указатель на класс работы с авторизацией CMySQLLoginRequest.

    Query

    Отправка запроса.

    bool  Query(
       string q             // тело запроса
       );

    Возвращаемое значение: результат выполнения; успешно - true, ошибка - false.

    Responses

    Получение количества ответов.

    uint  Responses(void);

    Возвращаемое значение: количество ответов сервера.

    Ответами считаются пакеты типа "Ok" или "Data". В случае успешного выполнения запроса будет принят один или более ответов (для множественных запросов).

    Response

    Получение указателя на класс ответа сервера MySQL.

    CMySQLResponse  *Response(
       uint idx                     // индекс ответа сервера
       );

    Возвращаемое значение: указатель на класс ответа сервера CMySQLResponse. В случае передачи в качестве аргумента некорректного значения будет возвращен NULL.

    Перегруженный метод без указания индекса, равносильно Response(0).

    CMySQLResponse  *Response(void);

    Возвращаемое значение: указатель на класс ответа сервера CMySQLResponse. Если ответов нет, будет возвращён NULL.

    GetServerError

    Получение структуры, где хранится код и сообщение ошибки сервера. Может быть вызван после того, как класс транзакции вернул ошибку MYSQL_ERR_SERVER_ERROR.

    MySQLServerError  GetServerError(void);

    Возвращаемое значение: структура ошибки MySQLServerError

    RequestDuration

    Получение длительности выполнения запроса.

    ulong  RequestDuration(void);

    Возвращаемое значение: длительность запроса в микросекундах от момента отправки до окончания обработки

    RxBytesTotal

    Получение количества принятых байтов.

    ulong  RxBytesTotal(void);

    Возвращаемое значение: количество принятых байт (уровень TCP) с начала запуска программы. Для сброса использовать метод ResetBytesCounters.

    TxBytesTotal

    Получение количества отправленных байтов.

    ulong  TxBytesTotal(void);

    Возвращаемое значение: количество переданных байт (уровень TCP) с начала запуска программы. Для сброса использовать метод ResetBytesCounters.

    ResetBytesCounters

    Сбрасывает счётчики принятых и отправленных байтов.

    void  ResetBytesCounters(void);


    Класс работы с авторизацией CMySQLLoginRequest

    Методы класса CMySQLLoginRequest

    Метод
    Действие
    SetClientCapabilities
    Устанавливает флаги возможностей клиента . Предустановленное значение: 0x005FA685
    SetMaxPacketSize
    Устанавливает максимально допустимый размер пакета в байтах. Предустановленное значение: 16777215
    SetCharset
    Устанавливает набор используемых символов. Предустановленное значение: 8
    Version
    Возвращает версию сервера MySQL. Например: "5.7.21-log".
    ThreadId
    Возвращает идентификатор потока текущего соединения. Соответствует значению CONNECTION_ID.
    ServerCapabilities
    Возвращает флаги возможностей сервера
    ServerLanguage
    Возвращает идентификаторкодировки и представления базы данных

    Класс ответа сервера CMySQLResponse

    Ответом сервера считается пакет типа "Ok" или "Data". Учитывая то, что они существенно отличаются, класс имеет отдельный набор методов для работы с каждым из типов пакетов.

    Общие методы класса CMySQLResponse:

    Метод
    Возвращаемое значение
    Type
    Тип ответа сервера: MYSQL_RESPONSE_DATA или MYSQL_RESPONSE_OK

    Методы для пакетов типа "Data":

    Метод
    Возвращаемое значение
    Fields
    Количество полей
    Field
    Указатель на класс поля по индексу (перегруженный метод - получение индекса поля по имени)
    Field Индекс поля по имени
    Rows
    Количества рядов в ответе сервера
    Row
    Указатель на класс ряда по индексу
    Value
    Значение в строковом виде по индексу ряда и индексу поля
    Value Значение в строковом виде по индексу ряда и имени поля
    ColumnToArray Результат чтения столбца в массив типа string
    ColumnToArray
    Результат чтения столбца в массив типа int с проверкой соответствия типа
    ColumnToArray
    Результат чтения столбца в массив типа long с проверкой соответствия типа
    ColumnToArray
    Результат чтения столбца в массив типа double с проверкой соответствия типа
    Методы для пакетов типа "Ok":
    Метод
    Возвращаемое значение
    AffectedRows
    Количество рядов, затронутых последней операцией
    LastId
    Значение LAST_INSERT_ID
    ServerStatus
    Флаги состояния сервера
    Warnings
    Количество предупреждений
    Message
    Текстовое сообщение сервера

    Структура ошибки сервера MySQLServerError

    Элементы структуры MySQLServerError

    Элемент
    Тип
    Назначение
    code
     ushort Код ошибки
    sqlstate
     uint Состояние
    message  string Текстовое сообщение сервера


    Класс поля CMySQLField

    Методы класса CMySQLField

    Метод
     Возвращаемое значение
    Catalog
    Имя каталога, к которому принадлежит таблица
    Database
    Имя базы данных, к которой принадлежит таблица
    Table
    Псевдоним таблицы, к которой принадлежит поле
    OriginalTable
    Оригинальное имя таблицы, к которой принадлежит поле
    Name
    Псевдоним поля
    OriginalName
    Оригинальное имя поля
    Charset
    Номер используемой кодировки
    Length
    Длина значения
    Type
    Тип значения
    Flags
    Флаги, определяющие атрибуты значения
    Decimals
    Допустимое количество знаков после запятой
    MQLType
    Тип поля в виде значения ENUM_DATABASE_FIELD_TYPE (кроме значения DATABASE_FIELD_TYPE_NULL)


    Класс ряда CMySQLRow

    Методы класса CMySQLRow

    Метод
    Действие
    Value
    Возвращает значение поля по номеру в виде строки
    operator[]
    Возвращает значение поля по имени в виде строки
    MQLType
    Возвращает тип поля по номеру в виде значения ENUM_DATABASE_FIELD_TYPE
    MQLType
    Возвращает тип поля по имени в виде значения ENUM_DATABASE_FIELD_TYPE
    Text
    Получает значение поля по номеру в виде строки с проверкой соответствия типа
    Text
    Получает значение поля по имени в виде строки с проверкой соответствия типа
    Integer
    Получает значение типа int по номеру поля с проверкой соответствия типа
    Integer
    Получает значение типа int по имени поля с проверкой соответствия типа
    Long
    Получает значение типа long по номеру поля с проверкой соответствия типа
    Long
    Получает значение типа long по имени поля с проверкой соответствия типа
    Double
    Получает значение типа double по номеру поля с проверкой соответствия типа
    Double
    Получает значение типа double по имени поля с проверкой соответствия типа
    Blob
    Получает значение в виде массива uchar по номеру поля с проверкой соответствия типа
    Blob
    Получает значение в виде массива uchar по имени поля с проверкой соответствия типа

    Примечание. Проверка на соответствие типов означает, что для метода, который работает с типом int значение читаемого поля должно быть D ATABASE_FIELD_TYPE_INTEGER. В случае несоответствия типов, значение получено не будет, и метод вернёт false. Перевод идентификаторов типа поля MySQL в значение типа ENUM_DATABASE_FIELD_TYPE реализовано в методе CMySQLField::MQLType(), исходный код которого приведён ниже.

    //+------------------------------------------------------------------+
    //| Возвращает тип поля в виде значения 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;
         }
      }


    Заключение

    В данной статье мы рассмотрели использование функций для работы с сокетами на примере реализации коннектора MySQL. Это была теоретическая часть. Во второй части статьи нас ждёт практика: мы напишем сервис для сбора свойств сигналов и программу для просмотра изменения их свойств.

    Во вложенном архиве находятся следующие файлы:

    • Путь Include\MySQL\: исходные коды коннектора
    • Файл Scripts\test_mysql.mq5: пример использования коннектора, разобранный в разделе Применение.

    Прикрепленные файлы |
    MQL5.zip (24.88 KB)
    Мультивалютный мониторинг торговых сигналов (Часть 1): Разработка структуры приложения Мультивалютный мониторинг торговых сигналов (Часть 1): Разработка структуры приложения

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

    Нейросети - это просто Нейросети - это просто

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

    Применение OLAP в трейдинге (Часть 3): анализ котировок в целях выработки торговых стратегий Применение OLAP в трейдинге (Часть 3): анализ котировок в целях выработки торговых стратегий

    В данной статье мы продолжим рассматривать технологию OLAP в применении к трейдингу, расширяя функционал, представленный в первых двух статьях. На этот раз оперативному анализу подвергнутся котировки. Показано выдвижение и проверка гипотез о торговых стратегиях на основе агрегированных показателей истории. Представлены эксперты для исследований побаровых закономерностей и адаптивной торговли.

    Библиотека для простого и быстрого создания программ для MetaTrader (Часть XXXII): Отложенные торговые запросы - установка ордеров по условиям Библиотека для простого и быстрого создания программ для MetaTrader (Часть XXXII): Отложенные торговые запросы - установка ордеров по условиям

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