Trabajando con las funciones de red, o MySQL sin DLL: Parte I - el conector
Contenido
- Introducción
- Sockets
- Analizador de tráfico Wireshark
- Intercambio de datos
- Clase de transacción MySQL
- Aplicación
- Documentación
- Conclusión
Introducción
Hace aproximadamente un año, las funciones de red en MQL5 se vieron
complementadas por las funciones de trabajo con sockets. Este hecho
abre un amplio abanico de posibilidades para los programadores que desarrollan productos para el Mercado, ya que ahora es posible
implementar aquello que antes no se podía conseguir sin bibliotecas dinámicas. Analizaremos uno de estos ejemplos en el presente ciclo de
dos artículos. En el primer artículo, estudiaremos el principio de funcionamiento del conector MySQL, mientras que en el segundo
escribiremos aplicaciones simples que usarán el mismo: un servicio para recopilar las propiedades de las señales que llegan al terminal,
así como un programa para cambiarlas a lo largo del tiempo (ver figura 1).
Fig. 1. Programa para visualizar los cambios de las señales a lo largo del tiempo
Sockets
Llamamos socket a la interfaz programática destinada al intercambio de datos entre procesos. En este caso, además, los procesos pueden haber sido iniciados tanto en un PC, como varios distintos, conectados en una red.
En MQL5, solo tendremos disponibles los sockets TCP de cliente. Esto significa que podemos inicializar la conexión, pero no esperarla desde fuera. Por eso, si es necesario posibilitar la conexión entre los programas MQL5 a través de sockets, necesitaremos un servidor que actúe como intermediario. El servidor espera la conexión a un puerto "escuchado" y ejecuta determinadas funciones según la solicitud del cliente. Para conectarse con un servidor, debemos conocer su dirección ip y su puerto.
Un pueto es un número que puede adoptar valores de 0 a 65535. Los puertos se han dividido en tres intervalos: de sistema (0 - 1023), de usuario (1024-49151) y dinámicos (49152-65535). Parte de los puertos está destinada al trabajo con ciertas funciones. De este comentido se encarga la IANA, la organización que gestiona el espacio de las direcciones IP y los dominios de nivel superior, y que también registra los tipos de datos MIME.
Para MySQL, se ha designado por defecto el puerto 3306, al que nos conectaremos al recurrir al servidor. Debemos tener en cuenta que este valor puede ser modificado. Por eso, al crear un ejemplar del experto, debemos sacar el puerto a los parámetros de entrada, junto con la dirección IP.
Al trabajar con sockets, se usa el enfoque siguiente:
- Creamos un socket (obtenemos su manejador o error)
- Nos conectamos al servidor
- Intercambiamos los datos
- Cerramos el socket
Si necesitamos trabajar con varias conexiones, deberemos recordar que existe una limitación de 128 sockets abiertos simultáneamente
para un programa MQL5.
Analizador de tráfico Wireshark
La depuración del código de un programa donde se usan sockets, es mejor realizarla con un analizador de tráfico. En caso contrario, sería
como arreglar un aparato eléctrico sin oscilador. El analizador de tráfico capta los datos de la interfaz de red seleccionada y los
representa de una forma cómodamente legible. Asimismo, permite monitorear el tamaño de los paquetes, la diferencia temporal entre los
mismos, la presencia de retransmisiones, cortes de conexión y mucha otra información útil. Además, descifra multitud de protocolos.
Para estos cometidos, nosotros usamos Wireshark.
Fig. 2. Analizador de tráfico Wireshark
En la figura 2, podemos ver la ventana del analizador con los paquetes captados, donde se muestra:
- La línea del filtro de representación. La expresión "tcp.port==3306" indica que se representarán solo aquellos paquetes cuyo puerto local o puerto TCP remoto sea igual a 3306 (el puerto MySQL del servidor por defecto).
- La ventana con los paquetes. Aquí podemos ver el progreso en el establecimiento de la conexión, la bienvenida del servidor, la
solicitud de autorización y el posterior intercambio.
- El contenido del paquete seleccionado en forma hexadecimal. En este caso, se muestra el contenido del paquete de bienvenida del
servidor MySQL.
- El nivel de transporte (TCP). Al usar las funciones para trabajar con sockets, nos encontramos aquí.
- El nivel aplicado (MySQL). Es lo que analizaremos en el presente artículo.
Debemos tener en cuenta que el filtro de representación no limita la captura de los paquetes, lo que se puede ver bien en la barra de estado. En ella se
indica que en estos momentos se representan 35 paquetes de los 2623 capturados que se hallan ahora en la memoria. Para reducir la carga sobre
el PC, debemos establecer el filtro de captura con anterioridad, al seleccionar la interfaz de red de la forma mostrada en la figura 3. Pero
solo merece la pena hacerlo en el caso de que los demás paquetes no nos sirvan de verdad.
Fig. 3. Filtro de captura de paquetes
Para familiarizarnos con el funcionamiento del analizador de tráfico, trataremos de establecer una conexión con el servidor "google.com" y monitorear este proceso. Para ello, escribiremos un pequeño script.
void OnStart() { //--- Obteniendo el manejador del socket int socket=SocketCreate(); if(socket==INVALID_HANDLE) return; //--- Estableciendo la conexión if(SocketConnect(socket,"google.com",80,2000)==false) { return; } Sleep(5000); //--- Cerrando la conexión SocketClose(socket); }
Bien, primero creamos un socket y obtenemos su manejador con la ayuda de la función SocketCreate(). En la guía de ayuda se dice que, en este caso, podemos solo obtener error en dos situaciones prácticamente imposibles:
- El error ERR_NETSOCKET_TOO_MANY_OPENED, que indica que hay abiertos más de 128 sockets.
- El error ERR_FUNCTION_NOT_ALLOWED, si el usuario trata de llamar a la creación de un socket desde un indicador donde esto está prohibido.
Una vez hemos obtenido el manejador, intentamos establecer la conexión. En este ejemplo, nos conectamos al servidor "google.com" (no olvide añadirlo a las direcciones permitidas en los ajustes del terminal), al puerto 80, con un timeout de 2000 milisegundos. Después de establecer la conexión con éxito, esperamos 5 segundos y lo cerramos. Vamos a ver ahora qué aspecto tiene en la ventana del analizador de tráfico.
Fig. 4. Estableciendo y cerrando una conexión
Bien, en la figura 4 podemos ver el intercambio de datos entre neustro script y el servidor "google.com" con la dirección ip "172.217.16.14". Aquí no se representan las solicitudes DNS, porque en la línea del filtro se ha introducido la expresión "tcp.port==80".
Los tres paquetes superiores se refieren al estableciemiento de la conexión. Los tres inferiores, al cierre. En la columna Time, se
representa el tiempo entre paquetes, y podemos ver nuestros 5 segundos de espera. Preste atención a que los paquetes han sido coloreado de
verde, a diferencia de los paquetes en la figura 2. Esto es así porque en el caso anterior el analizador detectó en el intercambio el protocolo
MySQL. Aquí no se ha transmitido ningún dato, por lo que el analizador ha destacado los paquetes con el color para TCP "por defecto".
Intercambio de datos
Según el protocolo, después de establecer la conexión, el servidor MySQL debe enviar el saludo de bienvenida. Como respuesta al mismo, el cliente envía una solicitud de autorización. Este mecanismo se describe en el apartado " Connection Phase", en el sitio web "dev.mysql.com". Si no se ha obtenido el saludo, significa que, o bien se ha usado una dirección IP incorrecta, o bien el servidor está escuchando otro puerto. En cualquier caso, hemos conectado con algo que no es un servidor MySQL. En una situación normal, debemos obtener los datos (leerlos dle socket) y analizarlos.
Recepción
En la clase CMySQLTransaction, que analizaremos con detalle un poco más tarde, la transmisión de datos se ha implementado de la forma siguiente:
//+------------------------------------------------------------------+ //| Obteniendo datos | //+------------------------------------------------------------------+ bool CMySQLTransaction::ReceiveData(ushort error_code=0) { char buf[]; uint timeout_check=GetTickCount()+m_timeout; do { //--- Obtenemos el número de datos que se puede leer del socket uint len=SocketIsReadable(m_socket); if(len) { //--- Leemos los datos del socket en el búfer int rsp_len=SocketRead(m_socket,buf,len,m_timeout); m_rx_counter+= rsp_len; //--- Enviamos el búfer al procesamiento ENUM_TRANSACTION_STATE res = Incoming(buf,rsp_len); //--- Obtenemos el resultado del que dependerán las siguientes acciones if(res==MYSQL_TRANSACTION_COMPLETE) // la respuesta del servidor ha sido totalmente recibida return true; // salida (con éxito) else if(res==MYSQL_TRANSACTION_ERROR) // error { if(m_packet.error.code) SetUserError(MYSQL_ERR_SERVER_ERROR); else SetUserError(MYSQL_ERR_INTERNAL_ERROR); return false; // salida (error) } //--- Si el valor del resultado es distinto, continuamos esperando los datos en el ciclo } } while(GetTickCount()<timeout_check && !IsStopped()); //--- Si el tiempo de finalización de la obtención de la respuesta se ha prolongado por un tiempo superior al valor m_timeout, //--- salimos con error SetUserError(error_code); return false; }Aquí, m_socket es el manejador del socket obtenido anteriormente al crear el mismo, mientras que m_timeout es el timeout de lectura de datos usado como argumento de la función SocketRead() para obtener el fragmento de datos, así como el timeout para todos los datos al completo. Antes de entrar en el ciclo, postergamos la marca temporal cuyo alcance se considerará el timeout de la operación de recepción de datos:
uint timeout_check=GetTickCount()+m_timeout;
A continuación, entramos en un ciclo en la lectura del resultado de la función SocketIsReadable() y esperamos a que retorne un valor distinto a cero. Después de ello, leemos los datos en el búfer y lo transmitimos al procesamiento.
uint len=SocketIsReadable(m_socket); if(len) { //--- Leemos los datos del socket en el búfer int rsp_len=SocketRead(m_socket,buf,len,m_timeout); m_rx_counter+= rsp_len; //--- Enviamos el búfer al procesamiento ENUM_TRANSACTION_STATE res = Incoming(buf,rsp_len); ... }
No debemos contar con que, en el caso de haber datos en el socket, vayamos a obtener todo el paquete. Hay una serie de situaciones
en las que los datos pueden llegar en pequeñas porciones. Por ejemplo, debido a una mala cconexión a través de un módem 4G con un gran número de
retransmisiones. Por eso, nuestro procesador debe saber reunir los datos leídos en ciertos grupos indivisibles con los que se pueda
trabajar. Tomaremos como dichos grupos los paquetes MySQL.
De la recopilación de datos y su posterior procesamiento se encargará el método CMySQLTransaction::Incoming():
//--- Procesando los datos obtenidos ENUM_TRANSACTION_STATE Incoming(uchar &data[], uint len);
El resultado que retorna nos indicará qué hacer a continuación: continuar la recopilación de datos, finalizarla o interrumpirla:
enum ENUM_TRANSACTION_STATE { MYSQL_TRANSACTION_ERROR=-1, // Error MYSQL_TRANSACTION_IN_PROGRESS=0, // En el proceso MYSQL_TRANSACTION_COMPLETE, // Completamente finalizado MYSQL_TRANSACTION_SUBQUERY_COMPLETE // Parcialmente finalizado };
En el caso se que surja un error interno, un error del servidor o un error de finalización de la recepción, deberemos interrumpir la lectura de datos del socket; en el resto de los casos, continuaremos. El valor MYSQL_TRANSACTION_SUBQUERY_COMPLETE indica que se ha recibido una de las respuestas del servidor a una solicitud múltiple del cliente. Para el algoritmo de lectura, es igual a MYSQL_TRANSACTION_IN_PROGRESS.
Fig. 5. El paquete MySQL
El formato del paquete MySQL se muestra en la figura 5. Los tres primeros bytes determinan el tamaño de la carga útil en el paquete, el
siguiente byte indica el número ordinal del paquete en la secuencia, y tras él van los datos. El número ordinal se establece en cero al inicio
de cada intercambio. Por ejemplo, el paquete de bienvenida tendrá el número 0, la solicitud de autorización del cliente, el número 1, la
respuesta del servidor, el número 2 (finalización de la fase de conexión). A continuación, al enviar una solicitud al cliente, el valor del
número ordinal deberá establecerse de nuevo en cero, aumentándose en cada paquete de respuesta del servidor. Si el número de paquetes es
superior a 255, el valor del número pasará por el cero.
El paquete más sencillo (MySQL ping) en el analizador de tráfico tiene el aspecto siguiente:
Fig. 6. El paquete Ping en el analizador de tráfico
El paquete Ping contiene un byte de datos con el valor 14 (o 0x0E en forma hexadecimal).
Vamos a analizar el método CMySQLTransaction::Incoming(), que reúne los datos en paquetes y los transmite a los manejadores. Más abajo, mostramos su código fuente de forma abreviada.
ENUM_TRANSACTION_STATE CMySQLTransaction::Incoming(uchar &data[], uint len) { int ptr=0; // índice del byte actual en el búfer data ENUM_TRANSACTION_STATE result=MYSQL_TRANSACTION_IN_PROGRESS; // resultado del procesamiento de los datos recibidos while(len>0) { if(m_packet.total_length==0) { //--- Si se desconoce el número de datos en el paquete while(m_rcv_len<4 && len>0) { m_hdr[m_rcv_len] = data[ptr]; m_rcv_len++; ptr++; len--; } //--- Obtenido el número de datos en el paquete if(m_rcv_len==4) { //--- Reseteamos los códigos de error, etcétera m_packet.Reset(); m_packet.total_length = reader.TotalLength(m_hdr); m_packet.number = m_hdr[3]; //--- Longitud obtenida, reseteamos el contador de bytes de longitud m_rcv_len = 0; //--- Destacamos el búfer del tamaño indicado if(ArrayResize(m_packet.data,m_packet.total_length)!=m_packet.total_length) return MYSQL_TRANSACTION_ERROR; // error interno } else // si el número de datos aún no ha sido recibido return MYSQL_TRANSACTION_IN_PROGRESS; } //--- Recopilamos los datos del paquete while(len>0 && m_rcv_len<m_packet.total_length) { m_packet.data[m_rcv_len] = data[ptr]; m_rcv_len++; ptr++; len--; } //--- Comprobamos que el paquete ya haya sido recopilado if(m_rcv_len<m_packet.total_length) return MYSQL_TRANSACTION_IN_PROGRESS; //--- Procesando el paquete MySQL recibido //... //--- m_rcv_len = 0; m_packet.total_length = 0; } return result; }
En primer lugar, debemos recopilar los encabezados del paquete, los 4 primeros bytes donde se contiene la longitud y el número ordinal en la secuencia. Para acumular un encabezado, usamos el búfer m_hdr y el contador de bytes m_rcv_len. Cuando los 4 bytes han sido recopilados, obtenemos de ellos la longitud, y, partiendo de su valor, cambiamos el búfer m_packet.data, en el que copiaremos los datos del paquete recibidos. Cuando el paquete ha sido totalmente recopilado, lo transmitimos para su procesamiento.
Si después de recibir el paquete, la longitud de los datos recibidos len sigue siendo igual a cero, significará que hemos
recibido varios paquetes. En una llamada del método Incoming() se pueden procesar tanto varios paquetes, como ni siquiera uno completo
(solo una parte).
Los tipos de paquete se muestran más abajo:
enum ENUM_PACKET_TYPE { MYSQL_PACKET_NONE=0, // None MYSQL_PACKET_DATA, // Data MYSQL_PACKET_EOF, // End of file MYSQL_PACKET_OK, // Ok MYSQL_PACKET_GREETING, // Greeting MYSQL_PACKET_ERROR // Error };
Para cada uno de ellos existe su propio manejador, que analiza su secuencia y contenido de acuerdo con un protocolo. Los valores obtenidos como resultado del parseo se asignan a los miembros de las clases correspondientes. Aquí debemos prestar atención a que en esta implementación del conector se analizan absolutamente todos los datos recibidos en los paquetes. Esto puede resultar un poco excesivo, dado que las propiedades "Table" y "Original table" son con frecuencia iguales, además, el valor de algunas banderas no resultará necesario a casi nadie (ver figura 7). No obstante, la disponibilidad de estas propiedades permite ajustar de forma flexible la lógica de interacción con el servidor MySQL al nivel aplicado del programa.
Fig. 7. Paquete de descripción del campo
Transmisión
En lo que respecta al envío de datos, aquí todo es un poco más sencillo.
//+------------------------------------------------------------------+ //| Formando y enviando el ping | //+------------------------------------------------------------------+ bool CMySQLTransaction::ping(void) { if(reset_rbuf()==false) { SetUserError(MYSQL_ERR_INTERNAL_ERROR); return false; } //--- Preparando el búfer de salida m_tx_buf.Reset(); //--- Reservando espacio para el encabezado del paquete m_tx_buf.Add(0x00,4); //--- Ubicando el código del comando m_tx_buf+=uchar(0x0E); //--- Formando los encabezados m_tx_buf.AddHeader(0); uint len = m_tx_buf.Size(); //--- Enviando el paquete if(SocketSend(m_socket,m_tx_buf.Buf,len)!=len) return false; m_tx_counter+= len; return true; }
Más arriba hemos mostrado el código del método de envío del ping. Copiamos los datos en el búfer preparado. En el caso del ping, se trata del código de comando 0x0E. A continuación, formulamos los encabezados a partir del número de datos y el número ordinal del paquete. Para el ping, el número ordinal siempre será igual a cero. Después de esto, intentamos enviar el paquete reunido con la ayuda de la función SocketSend().
El método de envío de la solicitud (Query) es similar al envío del ping:
//+------------------------------------------------------------------+ //| Formando y enviando la solicitud | //+------------------------------------------------------------------+ bool CMySQLTransaction::query(string s) { if(reset_rbuf()==false) { SetUserError(MYSQL_ERR_INTERNAL_ERROR); return false; } //--- Preparando el búfer de salida m_tx_buf.Reset(); //--- Reservando espacio para el encabezado del paquete m_tx_buf.Add(0x00,4); //--- Ubicando el código del comando m_tx_buf+=uchar(0x03); //--- Añadiendo la línea de solicitud m_tx_buf+=s; //--- Formando los encabezados m_tx_buf.AddHeader(0); uint len = m_tx_buf.Size(); //--- Enviando el paquete if(SocketSend(m_socket,m_tx_buf.Buf,len)!=len) return false; m_tx_counter+= len; return true; }
La única diferencia es que aquí la carga útil consta del código de comando (0x03) y la línea de solicitud.
Tras el envío de datos, siempre viene el método de recepción ya conocido CMySQLTransaction::ReceiveData(). Si este no ha retornado error, la
transacción se considera exitosa.
Clase de transacción MySQL
Ha llegado el momento de analizar la clase CMySQLTransaction con mayor detalle.
//+------------------------------------------------------------------+ //| Clase de transacción MySQL | //+------------------------------------------------------------------+ class CMySQLTransaction { private: //--- Datos para la autorización string m_host; // Dirección IP del servidor MySQL uint m_port; // Puerto TCP string m_user; // Nombre de usuario string m_password; // Contraseña //--- Timeouts uint m_timeout; // timeout de espera de datos TCP (ms) uint m_timeout_conn; // timeout de establecimiento de conexión con el servidor //--- Keep Alive uint m_keep_alive_tout; // tiempo(ms) transcurrido el cual se cerrará la conexión; el valor 0 indica que Keep Alive no se usa uint m_ping_period; // periodo de envío del ping (en ms) en el modo Keep Alive bool m_ping_before_query; // enviar el ping a través de query (tiene sentido cuando el valor del periodo de envío del ping es muy grande) //--- Red int m_socket; // manejador del socket ulong m_rx_counter; // contador de bytes recibidos ulong m_tx_counter; // contador de bytes transmitidos //--- Marcas temporales ulong m_dT; // tiempo de ejecución de la última solicitud uint m_last_resp_timestamp; // tiempo de recepción de la última respuesta uint m_last_ping_timestamp; // hora del último ping //--- Respuesta del servidor CMySQLPacket m_packet; // paquete recibido uchar m_hdr[4]; // encabezado del paquete uint m_rcv_len; // contador de bytes del encabezado del paquete //--- Búfer de transmisión CData m_tx_buf; //--- Clase de formación de la solicitud de autorización CMySQLLoginRequest m_auth; //--- Búfer de respuesta del servidor y tamaño del mismo CMySQLResponse m_rbuf[]; uint m_responses; //--- Espera y recepción de datos del socket bool ReceiveData(ushort error_code); //--- Procesando los datos obtenidos ENUM_TRANSACTION_STATE Incoming(uchar &data[], uint len); //--- Manejadores de los paquetes para cada uno de los tipos ENUM_TRANSACTION_STATE PacketOkHandler(CMySQLPacket *p); ENUM_TRANSACTION_STATE PacketGreetingHandler(CMySQLPacket *p); ENUM_TRANSACTION_STATE PacketDataHandler(CMySQLPacket *p); ENUM_TRANSACTION_STATE PacketEOFHandler(CMySQLPacket *p); ENUM_TRANSACTION_STATE PacketErrorHandler(CMySQLPacket *p); //--- Otros bool ping(void); // envío del ping bool query(string s); // envío de la solicitud bool reset_rbuf(void); // inicializando el búfer de respuesta del servidor uint tick_diff(uint prev_ts); // obteniendo la diferencia de las marcas temporales //--- Clase del parser CMySQLPacketReader reader; public: CMySQLTransaction(); ~CMySQLTransaction(); //--- Establecimiento de los parámetros de conexión bool Config(string host,uint port,string user,string password,uint keep_alive_tout); //--- Modo Keep Alive void KeepAliveTimeout(uint tout); // estableciendo el timeout void PingPeriod(uint period) {m_ping_period=period;} // estableciendo el periodo del ping en segundos void PingBeforeQuery(bool st) {m_ping_before_query=st;} // activar/desactivar el ping antes de la solicitud //--- Procesando los eventos del temporizador (relevante solo al usar Keep Alive) void OnTimer(void); //--- Obtener el puntero a la clase de trabajo con la autorización CMySQLLoginRequest *Handshake(void) {return &m_auth;} //--- Enviando solicitud bool Query(string q); //--- Obteniendo el número de respuestas del servidor uint Responses(void) {return m_responses;} //--- Obteniendo el puntero a la respuesta del servidor según el índice CMySQLResponse *Response(uint idx); CMySQLResponse *Response(void) {return Response(0);} //--- Obteniendo la estructura del error del servidor MySQLServerError GetServerError(void) {return m_packet.error;} //--- Opciones ulong RequestDuration(void) {return m_dT;} // obteniendo la duración de la última transacción ulong RxBytesTotal(void) {return m_rx_counter;} // obteniendo el número de bytes recibidos ulong TxBytesTotal(void) {return m_tx_counter;} // obteniendo el número de bytes transmitidos void ResetBytesCounters(void) {m_rx_counter=0; m_tx_counter=0;} // reseteando los contadores de los bytes recibidos y transmitidos };
Entre los miembros privados, querríamos destacar los siguientes:
- m_packet del tipo CMySQLPacket: clase de paquete MySQL procesado en el momento actual (el código fuente con los comentarios
se encuentra en el archivo MySQLPacket.mqh)
- m_tx_buf del tipo CData: clase de búfer de transmisión creado para que resulte cómodo formar la solicitud (archivo Data.mqh)
- m_auth del tipo CMySQLLoginRequest: clase para trabajar con la autorización (contraseña de cifrado, guardado de los parámetros del servidor recibidos y los parámetros del cliente, código fuente en el archivo MySQLLoginRequest.mqh)
- m_rbuf del tipo CMySQLResponse: búfer de respuesta del servidor; aquí se considera respuesta un paquete del tipo "Ok" o "Data" (archivo MySQLResponse.mqh)
- reader del tipo CMySQLPacketReader: clase del parser de paquetes MySQL
Los métodos públicos se describen en el apartado correspondiente de la documentación.
Para el nivel aplicado, la clase de la transacción tendrá el aspecto mostrado en la figura 8.
Fig. 8. Construcción de la clase CMySQLTransaction
Donde:
- CMySQLLoginRequest debe estar configurado hasta el establecimiento de la conexión, si es necesario indicar parámetros de
cliente cuyos valores se diferencien de los establecidos (no es obligatorio) ;
- CMySQLResponse — respuesta del servidor, si la transacción se ha finalizado sin errores
- CMySQLField — descripción del campo;
- CMySQLRow — fila (búfer de valores de los campos en forma de texto);
- MySQLServerError — estructura de la descripción del error, si la transacción ha finalizado sin éxito;
Entre los métodos públicos no hay ninguno encargado de establecer y cerrar la conexión. Esto sucede automáticamente al llamar al método CMySQLTransaction::Query(). En caso de usar el modo de conexión constante, este se establecerá en la primera llamada de CMySQLTransaction::Query(), siendo cerrado al finalizar el timeout establecido.
Importante: en el modo de conexión constante, se deberá añadir al manejador del evento OnTimer
la llamada del método CMySQLTransaction::OnTimer(). En este caso, además, el periodo del temporizador deberá ser inferior al ping y al
timeout.
Los parámetros de conexión y la cuenta de usuario del usuario, así como los valores especiales de los parámetros de cliente deberán ser establecidos antes de la llamada de CMySQLTransaction::Query().
En general, la interacción con la clase de la transacción se ejecuta según el principio siguiente:
Fig. 9. Trabajo con la clase CMySQLTransaction
Aplicación
Vamos a analizar un ejemplo sencillo de aplicación del conector. Para ello, escribiremos un script que enviará una solicitud SELECT a la base de datos de prueba world.
//--- input parameters input string inp_server = "127.0.0.1"; // MySQL server address input uint inp_port = 3306; // TCP port input string inp_login = "admin"; // Login input string inp_password = "12345"; // Password input string inp_db = "world"; // Database name //--- Activando la clase de transacción MySQL #include <MySQL\MySQLTransaction.mqh> CMySQLTransaction mysqlt; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- Configurando la clase de transacción MySQL mysqlt.Config(inp_server,inp_port,inp_login,inp_password); //--- Creando la solicitud string q = "select `Name`,`SurfaceArea` "+ "from `"+inp_db+"`.`country` "+ "where `Continent`='Oceania' "+ "order by `SurfaceArea` desc limit 10"; if(mysqlt.Query(q)==true) { if(mysqlt.Responses()!=1) return; CMySQLResponse *r = mysqlt.Response(); if(r==NULL) return; Print("Name: ","Surface Area"); uint rows = r.Rows(); for(uint i=0; i<rows; i++) { double area; if(r.Row(i).Double("SurfaceArea",area)==false) break; PrintFormat("%s: %.2f",r.Row(i)["Name"],area); } } else if(GetLastError()==(ERR_USER_ERROR_FIRST+MYSQL_ERR_SERVER_ERROR)) { // si se trata de un error del servidor Print("MySQL Server Error: ",mysqlt.GetServerError().code," (",mysqlt.GetServerError().message,")"); } else { if(GetLastError()>=ERR_USER_ERROR_FIRST) Print("Transaction Error: ",EnumToString(ENUM_TRANSACTION_ERROR(GetLastError()-ERR_USER_ERROR_FIRST))); else Print("Error: ",GetLastError()); } }
Nuestra tarea consistirá en conseguir la lista de los países que incluyan el significado del continente "Oceanía", clasificados por superficie en orden descendente, con un máximo de 10 denominaciones en la lista. Para ello, ejecutaremos las siguientes acciones:
- Declaramos el ejemplar de clase de transacción mysqlt
- Establecemos los parámetros de la conexión
- Creamos la solicitud correspondiente
- Si la transacción ha tenido éxito, comprobamos que el número de respuestas sea igual al valor esperado
- Obtenemos el puntero a la clase de respuesta del servidor
- Obtenemos el número de filas en la respuesta
- Mostramos los valores de las filas
Si la transacción no ha tenido éxito, podrían darse tres escenarios de desarrollo de los eventos:
- Se trata de un error de servidor - obtenemos su descripción usando el método CMySQLTransaction::GetServerError()
- Se trata de un error interno - para obtener su descripción, usamos la función EnumToString()
- En caso contrario, obtenemos el código de error usando GetLastError()
Si los parámetros de entrada se han indicado correctamente, el resultado del trabajo del script será el siguiente:
Fig. 10. Resultado del funcionamiento del script de prueba
En la segunda parte del artículo veremos ejemplos más complicados, con multitud de solicitudes y el modo de conexión constante.
Documentación
Contenido
- Clase de trabajo con la autorización CMySQLLoginRequest
- Clase de respuesta del servidor CMySQLResponse
- Estructura del error del servidor MySQLServerError
- Clase del campo CMySQLField
- Clase de la fila CMySQLRow
La clase de transacción CMySQLTransaction
Lista de métodos de la clase CMySQLTransaction
Método |
Acción |
---|---|
Config |
Establecimiento de los parámetros de conexión |
KeepAliveTimeout |
Establecimiento del timeout para el modo Keep Alive en segundos |
PingPeriod |
Establecimiento del ping para el modo Keep Alive en segundos |
PingBeforeQuery |
Activar/desactivar el ping antes de la solicitud |
OnTimer |
Procesamiento de los eventos del temporizador (relevante solo al usar Keep Alive) |
Handshake |
Obtención del puntero a la clase de trabajo con la autorización |
Query |
Envío de la solicitud |
Responses |
Obteción del número de respuestas del servidor |
Response |
Obtención del puntero a la clase de respuesta del servidor |
GetServerError |
Obteción de la estructura del error del servidor |
RequestDuration |
Duración de la transacción en microsegundos |
RxBytesTotal |
Contador de bytes recibidos desde el inicio del programa |
TxBytesTotal |
Contador de bytes enviados desde el inicio del programa |
ResetBytesCounters |
Reseteando los contadores de bytes recibidos y enviados |
Más abajo, se muestra una breve guía de ayuda sobre cada uno de los métodos.
Config
Establece los parámetros de conexión.bool Config( string host, // nombre del servidor uint port, // puerto string user, // nombre del usuario string password, // contraseña string base, // nombre de la base de datos uint keep_alive_tout // timeout de la conexión constante (0, si no se usa) );
Valor retornado: true en el caso de éxito, de lo contrario, false (hay símbolos prohibidos en los argumentos del tipo string).
KeepAliveTimeout
Activa el modo de conexión constante y establece su timeout. El valor de timeout supone el tiempo en segundos desde el momento del envío de la
última solicitud; al finalizar el mismo, la conexión se cerrará. Si las solicitudes se repiten con una frecuencia mayor a la establecida
para el timeout, la conexión no se cerrará.
void KeepAliveTimeout( uint tout // estableciendo el timeout de conexión constante en segundos (0 - desactivar) );
PingPeriod
Establece el periodo de envío de paquetes ping en el modo de conexión constante. Resulta necesario para que el servidor no cierre la conexión según su
propio criterio. El ping se enviará después del tiempo indicado tras la última solicitud o el ping anterior.
void PingPeriod( uint period // estableciendo el periodo de ping en segundos (para el modo de conexión constante) );
Valor retornado: no
PingBeforeQuery
Activa el envío del paquete ping antes de la solicitud. En el modo de conexión constante, en los intervalos entre solicitudes, la conexión podría
cerrarse o interrumpirse por algún motivo. En este caso, podemos enviar el ping al servidor MySQL para asegurarnos de que está en contacto, y
solo entonces realizar la solicitud.
void PingBeforeQuery( bool st // activar (true) o desactivar (false) el ping antes de la solicitud );
Valor retornado: no
OnTimer
Se usa en el modo de conexión constante. El método se llama desde el manejador de eventos OnTimer.
En este caso, además, el periodo del temporizador no debe superar el periodo mínimo de KeepAliveTimeout y PingPeriod.
void OnTimer(void);
Valor retornado: no
Handshake
Obtención del puntero a la clase de trabajo con la autorización. Puede ser utilizado para establecer las banderas de posibilidad del cliente y el
tamaño máximo del paquete antes de establecer la conexión con el servidor. Después de la autorización, permite obtener la versión y las
banderas de posibilidad del servidor.
CMySQLLoginRequest *Handshake(void);
Valor retornado: puntero a la clase de trabajo con la autorización CMySQLLoginRequest.
Query
Envío de la solicitud.
bool Query( string q // cuerpo de la solicitud );
Valor retornado: resultado de la ejecución; éxito - true, error - false.
Responses
Obteción del número de respuestas.
uint Responses(void);
Valor retornado: número de respuestas del servidor.
Se considerarán respuestas los paquetes del tipo "Ok" o "Data". Si la solicitud se ejecuta con éxito, se recibirán una u más respuestas (para las solicitudes múltiples).
Response
Obtención del puntero a la clase de respuesta del servidor MySQL.
CMySQLResponse *Response( uint idx // índice de respuesta del servidor );
Valor retornado: puntero a la clase de respuesta del servidor CMySQLResponse. Si se transmite como argumento un valor incorrecto, se retornará NULL.
Un método sobrecargado del que no se ha indicado el índice equivale Response(0).
CMySQLResponse *Response(void);
Valor retornado: puntero a la clase de respuesta del servidor CMySQLResponse. Si no hay respuesta, se retornará NULL.
GetServerError
Obteción de la estructura donde se guarda el código y el mensaje de error del servidor. Puede ser llamada después de que la clase de transacción haya
retornado el error MYSQL_ERR_SERVER_ERROR.
MySQLServerError GetServerError(void);
Valor retornado: estructura de error MySQLServerError
RequestDuration
Obtención de la duración de la ejecución de la solicitud.
ulong RequestDuration(void);
Valor retornado: duración de la solicitud en microsegundos desde el momento del envío hasta la finalización del procesamiento
RxBytesTotal
Obtención del número de bytes recibidos.
ulong RxBytesTotal(void);
Valor retornado: número de bytes recibidos (nivel TCP) desde el inicio del programa. Para realizar el reseteo, se usa el método ResetBytesCounters.
TxBytesTotal
Obtención del número de bytes enviados.
ulong TxBytesTotal(void);
Valor retornado: número de bytes transmitidos (nivel TCP) desde el inicio del programa. Para realizar el reseteo, se usa el método ResetBytesCounters.
ResetBytesCounters
Resetea los contadores de los bytes recibidos y transmitidos.
void ResetBytesCounters(void);
Clase de trabajo con la autorización CMySQLLoginRequest
Métodos de la clase CMySQLLoginRequest
Método |
Acción |
---|---|
SetClientCapabilities |
Establece las banderas de posibilidad del cliente . Valor preestablecido: 0x005FA685 |
SetMaxPacketSize |
Establece el tamaño máximo permitido del paquete en bytes. Valor preestablecido: 16777215 |
SetCharset |
Establece el conjunto de símbolos utilizados. Valor preestablecido: 8 |
Version |
Retorna la versión del servidor MySQL. Por ejemplo: "5.7.21-log". |
ThreadId |
Retorna el identificador de flujo de la conexión actual. Se corresponde con el valor CONNECTION_ID. |
ServerCapabilities |
Retorna las banderas de posibilidad del servidor |
ServerLanguage |
Retorna el identificadorde
la codificación y la representación de las bases de datos |
Clase de respuesta del servidor CMySQLResponse
Se considerará respuesta el paquete del tipo "Ok" o "Data". Teniendo en cuenta que se distinguen sustancialmente, la clase tiene un
conjunto de métodos aparte para trabajar con cada uno de los tipos de paquete.
Métodos generales de la clase CMySQLResponse:
Método |
Valor retornado |
---|---|
Type |
Tipo de respuesta del servidor: MYSQL_RESPONSE_DATA o MYSQL_RESPONSE_OK |
Métodos para los paquetes del tipo "Data":
Método |
Valor retornado |
---|---|
Fields |
Número de campos |
Field |
Puntero a la clase de campo según el índice
(índice sobrecargado - obtención del índice del campo según el nombre) |
Field | Índice del campo según el nombre |
Rows |
Número de filas en la respuesta del servidor |
Row |
Puntero a la clase de fila según el índice |
Value |
Valor con forma de línea según el índice de la fila y el índice del campo |
Value | Valor con forma de línea según el índice de la fila y el nombre del campo |
ColumnToArray | Resultado de la lectura de una columna en una matriz de tipo string |
ColumnToArray |
Resultado de la lectura de una columna en una matriz de tipo int con comprobación de la correspondencia
del tipo |
ColumnToArray |
Resultado de la lectura de una columna en una matriz de tipo long con comprobación de la
correspondencia del tipo |
ColumnToArray |
Resultado de la lectura de una columna en una matriz de tipo double con comprobación de la
correspondencia del tipo |
Método |
Valor retornado |
---|---|
AffectedRows |
Número de filas afectadas por la última operación |
LastId |
Valor LAST_INSERT_ID |
ServerStatus |
Banderas de estado
del servidor |
Warnings |
Número de advertencias |
Message |
Mensaje de texto del servidor |
Estructura del error del servidor MySQLServerError
Elementos de la estructura MySQLServerError
Elemento |
Tipo |
Designación |
---|---|---|
code |
ushort | Código de error |
sqlstate |
uint | Estado |
message | string | Mensaje de texto del servidor |
Clase del campo CMySQLField
Métodos de la clase CMySQLField
Método |
Valor retornado |
---|---|
Catalog |
Nombre del catálogo al que pertenece el recuadro |
Database |
Nombre de la base de datos a la que pertenece el recuadro |
Table |
Pseudónimo del recuadro al que pertenece el campo |
OriginalTable |
Nombre original del recuadro al que pertenece el campo |
Nombre |
Pseudónimo del campo |
OriginalName |
Nombre original del campo |
Charset |
Número de la codificación utilizada |
Length |
Longitud del valor |
Type |
Tipo
de valor |
Flags |
Banderas que determinan los atributos de designación |
Decimals |
Número permitido de dígitos decimales |
MQLType |
Tipo de campo en forma de valor ENUM_DATABASE_FIELD_TYPE
(salvo el valor DATABASE_FIELD_TYPE_NULL) |
Clase de la fila CMySQLRow
Métodos de la clase CMySQLRow
Método |
Acción |
---|---|
Value |
Retorna el valor del campo según el número en forma de línea |
operator[] |
Retorna el valor del campo según el nombre en forma de línea |
MQLType |
Retorna el tipo del campo según el número en forma de valor ENUM_DATABASE_FIELD_TYPE |
MQLType |
Retorna el tipo del campo según el nombre en forma de valor ENUM_DATABASE_FIELD_TYPE |
Text |
Obtiene el valor del campo según el número en forma de línea con comprobación de la corrección del tipo |
Text |
Obtiene el valor del campo según el nombre en forma de línea con comprobación de la corrección del tipo |
Integer |
Obtiene el valor del tipo int según el número del campo con comprobación de la corrección del tipo |
Integer |
Obtiene el valor del tipo int según el nombre del campo con comprobación de la corrección del tipo |
Long |
Obtiene el valor del tipo long según el número del campo con comprobación de la corrección del tipo |
Long |
Obtiene el valor del tipo long según el nombre del campo con comprobación de la corrección del tipo |
Double |
Obtiene el valor del tipo double según el número del campo con comprobación de la corrección del tipo |
Double |
Obtiene el valor del tipo double según el nombre del campo con comprobación de la corrección del tipo |
Blob |
Obtiene el valor en forma de matriz uchar según el número del campo con comprobación de la corrección del tipo |
Blob |
Obtiene el valor en forma de matriz uchar según el nombre del campo con comprobación de la corrección del tipo |
Observación. La comprobación de la correspondencia de tipos indica que para un método que trabaje con el tipo int,
el valor del campo legible deberá ser D ATABASE_FIELD_TYPE_INTEGER. Si los tipos no se corresponden, no se obtendrá el valor, y el método retornará false. La
conversión de los identificadores
del tipo de campo MySQL en un valor del tipo ENUM_DATABASE_FIELD_TYPE
ha sido implementada en el método CMySQLField::MQLType(), cuyo código fuente se muestra más abajo.
//+------------------------------------------------------------------+ //| Retorna el tipo del campo en forma de valor ENUM_DATABASE_FIELD_TYPE | //+------------------------------------------------------------------+ ENUM_DATABASE_FIELD_TYPE CMySQLField::MQLType(void) { switch(m_type) { case 0x00: // decimal case 0x04: // float case 0x05: // double case 0xf6: // newdecimal return DATABASE_FIELD_TYPE_FLOAT; case 0x01: // tiny case 0x02: // short case 0x03: // long case 0x08: // longlong case 0x09: // int24 case 0x10: // bit case 0x07: // timestamp case 0x0c: // datetime return DATABASE_FIELD_TYPE_INTEGER; case 0x0f: // varchar case 0xfd: // varstring case 0xfe: // string return DATABASE_FIELD_TYPE_TEXT; case 0xfb: // blob return DATABASE_FIELD_TYPE_BLOB; default: return DATABASE_FIELD_TYPE_INVALID; } }
Conclusión
En este artículo, hemos analizado el uso de funciones para trabajar con sockets utilizando como ejemplo la implementación de un conector
MySQL. Esta ha sido la parte teórica. En la segunda parte del artículo, nos espera la práctica: escribiremos un servicio para recopilar las
propiedades de las señales, así como un programa para visualizar los cambios de sus propiedades.
En el directorio adjunto se encuentran los siguientes archivos:
- Ruta Include\MySQL\: códigos fuente del conector
- Archivo Scripts\test_mysql.mq5: ejemplo de uso del conector analizado en el apartado Aplicación.
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/7117
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso