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

Serhii Shevchuk | 20 января, 2020

Содержание

Введение

Примерно год назад список сетевых функций в 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;} // сбросить счётчики принятых и переданных байтов
  };

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

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

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

Классы

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

Где:

Среди публичных методов нет тех, которые бы отвечали за установку и закрытие соединения. Это происходит автоматически при вызове метода 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 наименований в списке. Для этого выполним следующие действия:

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

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

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

Рис. 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. Это была теоретическая часть. Во второй части статьи нас ждёт практика: мы напишем сервис для сбора свойств сигналов и программу для просмотра изменения их свойств.

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