Работа с сетевыми функциями, или MySQL без DLL: Часть I - коннектор
Serhii Shevchuk | 20 января, 2020
Содержание
- Введение
- Сокеты
- Анализатор трафика Wireshark
- Обмен данными
- Класс транзакции MySQL
- Применение
- Документация
- Заключение
Введение
Примерно год назад список сетевых функций в 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 показано окно анализатора трафика с захваченными пакетами, где:
- Строка фильтра отображения. Выражение "tcp.port==3306" означает, что будут отображены только те пакеты, у которых локальный или удалённый TCP порт равен 3306 (порт MySQL сервера по умолчанию).
- Окно с пакетами. Здесь виден процесс установки соединения, приветствие сервера, запрос авторизации и последующий обмен.
- Содержимое выбранного пакета в шестнадцатеричном виде. В данном случае показано содержимое пакета приветствия сервера MySQL.
- Транспортный уровень (TCP). При использовании функций для работы с сокетами мы находимся здесь.
- Прикладной уровень (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(). В справке сказано, что в данном случае можно получить ошибку лишь в двух, практически невозможных ситуациях:
- Ошибка ERR_NETSOCKET_TOO_MANY_OPENED, которая сигнализирует о том, что открыто больше 128 сокетов.
- Ошибка 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.
Рис. 5. Пакет MySQL
Формат пакета MySQL показан на рисунке 5. Первые три байта определяют размер полезной нагрузки в пакете, следующий байт означает порядковый
номер пакета в последовательности, за ним следуют данные. Порядковый номер устанавливается в ноль в начале каждого обмена. Например,
пакет приветствия будет иметь номер 0, запрос авторизации клиента — номер 1, ответ сервера — номер 2 (окончание фазы соединения). Далее,
при отправке запроса клиента, значение порядкового номера должно быть снова установлено в ноль, и будет увеличиваться в каждом пакете
ответа сервера. Если количество пакетов будет больше чем 255, значение номера перейдёт через ноль.
Простейший пакет (MySQL 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 на прикладном уровне программы.
Рис. 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. Результат работы тестового скрипта
Более сложные примеры с использованием множественных запросов и режима постоянного соединения рассмотрим во второй части статьи.
Документация
Содержание
- Класс работы с авторизацией CMySQLLoginRequest
- Класс ответа сервера CMySQLResponse
- Структура ошибки сервера MySQLServerError
- Класс поля CMySQLField
- Класс ряда CMySQLRow
Класс транзакции 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 с проверкой соответствия типа |
Метод |
Возвращаемое значение |
---|---|
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: пример использования коннектора, разобранный в разделе Применение.