Verwendung von Netzwerkfunktionen oder MySQL ohne DLL: Teil I - Konnektor
Inhalt
- Einführung
- Sockets
- Wireshark, Datenverkehrsanalyse
- Datenaustausch
- MySQL-Transaktionsklasse
- Anwendung
- Dokumentation
- Schlussfolgerung
Einführung
Vor etwa einem Jahr erhielt MQL5 Netzwerkfunktionen für die Arbeit mit Sockets. Dies eröffnete Programmierern, die Produkte für den Markt entwickeln, große Möglichkeiten. Jetzt können sie Dinge implementieren, für die zuvor dynamische Bibliotheken erforderlich waren. Wir werden in dieser Serie von zwei Artikeln auf eines dieser Beispiele eingehen. Im ersten Artikel werde ich die Prinzipien des MySQL-Konnektors betrachten, während ich im zweiten Artikel die einfachsten Anwendungen entwickeln werde, die den Konnektor verwenden, nämlich den Dienst zum Sammeln von Eigenschaften der im Terminal verfügbaren Signale und das Programm zum Anzeigen ihrer Änderungen im Laufe der Zeit (siehe Abb. 1).
Abb. 1. Das Programm zur Anzeige von Änderungen der Signaleigenschaften im Laufe der Zeit
Sockets
Ein Socket ist eine Softwareschnittstelle zum Austausch von Daten zwischen Prozessen. Die Prozesse können sowohl auf einem einzelnen PC als auch auf verschiedenen, in ein Netzwerk eingebundenen PCs gestartet werden.
MQL5 stellt nur TCP-Client-Sockets zur Verfügung. Das bedeutet, dass wir in der Lage sind, eine Verbindung zu initiieren, aber nicht von außen darauf warten können. Wenn wir also eine Verbindung zwischen MQL5-Programmen über Sockets herstellen müssen, benötigen wir einen Server, der als Vermittler fungieren soll. Der Server wartet auf eine Verbindung am abgehörten Port und führt auf Anfrage des Kunden bestimmte Funktionen aus. Um sich mit dem Server zu verbinden, müssen wir seine IP-Adresse und seinen Port kennen.
Ein Port ist eine Nummer zwischen 0 und 65535. Es gibt drei Portbereiche: Systeme (0 - 1023), Nutzer (1024-49151) und dynamische Ports (49152-65535). Einige Ports sind für die Arbeit mit bestimmten Funktionen vorgesehen. Die Zuweisung erfolgt durch IANA - eine Organisation, die IP-Adresszonen und Top-Level-Domains verwaltet sowie MIME-Datentypen registriert.
Der Port 3306 wird standardmäßig MySQL zugewiesen. Wir werden uns beim Zugriff auf den Server mit ihm verbinden. Bitte beachten Sie, dass dieser Wert geändert werden kann. Daher sollte bei der Entwicklung eines EA der Port in den Eingaben zusammen mit der IP-Adresse gesetzt werden.
Der folgende Ansatz wird bei der Arbeit mit Sockets verwendet:
- Erstellen Sie einen Socket (Sie erhalten ein Handle oder einen Fehler)
- Mit dem Server verbinden
- Datenaustausch
- Schließen des Sockets
Beachten Sie bei der Arbeit mit mehreren Verbindungen die Begrenzung auf 128 gleichzeitig offene Sockets für ein einziges MQL5-Programm.
Wireshark, Datenverkehrsanalyse
Die Verkehrsanalyse erleichtert das Debuggen des Codes eines Programms, das einen Socket verwendet. Ohne ihn ähnelt der gesamte Prozess der Reparatur von Elektronik ohne ein Oszilloskop. Wireshark erfasst Daten von der ausgewählten Netzwerkschnittstelle und zeigt sie in lesbarer Form an. Er verfolgt die Größe der Pakete, den Zeitabstand zwischen ihnen, das Vorhandensein von Rückübertragungen und Verbindungsabbrüchen sowie viele andere nützliche Daten. Er entschlüsselt auch viele Protokolle.
Ich persönlich verwende Wireshark zu diesem Zweck.
Abb. 2. Wireshark, Datenverkehrsanalyse
Abbildung 2 zeigt das Fenster der Verkehrsanalyse mit den erfassten Paketen, wo:
- Die Anzeige der Filterzeile. "tcp.port==3306" bedeutet, dass nur Pakete mit einem lokalen oder fernen TCP-Port von 3306 angezeigt werden (Standard-MySQL-Server-Port).
- Die Pakete. Hier sehen wir den Verbindungsaufbau, die Serverbegrüßung, die Autorisierungsanfrage und den anschließenden Datenaustausch.
- Ausgewählter Paketinhalt in hexadezimaler Form. In diesem Fall können wir den Inhalt des Begrüßungspakets des MySQL-Servers sehen.
- Transport-Ebene (TCP). Hier befinden wir uns, wenn wir Funktionen zur Arbeit mit Sockets verwenden.
- Anwendungsebene (MySQL). Das ist das, was wir in diesem Artikel besprechen werden.
Der Anzeigefilter schränkt die Paketerfassung nicht ein. Dies ist in der Statuszeile deutlich zu erkennen, die besagt, dass 35 erfasste Pakete von 2623 im Speicher befindlichen Paketen derzeit verarbeitet werden. Um die Belastung des PCs zu verringern, sollten wir den Erfassungsfilter bei der Auswahl der Netzwerkschnittstelle einstellen, wie in Abb. 3 dargestellt. Dies sollte nur dann geschehen, wenn alle anderen Pakete wirklich nicht nützlich sind.
Abb. 3. Paket-Erfassungsfilter
Um sich mit der Verkehrsanalyse vertraut zu machen, versuchen wir, eine Verbindung mit dem Server "google.com" herzustellen, und verfolgen den Prozess. Um dies zu tun, schreiben wir ein kleines Skript.
void OnStart() { //--- Get socket handle int socket=SocketCreate(); if(socket==INVALID_HANDLE) return; //--- Establish connection if(SocketConnect(socket,"google.com",80,2000)==false) { return; } Sleep(5000); //--- Close connection SocketClose(socket); }
Zuerst erzeugen wir also einen Socket und erhalten sein Handle mit der Funktion SocketCreate(). Die Referenz besagt, dass Sie in diesem Fall in zwei Fällen einen Fehler erhalten können, was fast unmöglich ist:
- Der Fehler ERR_NETSOCKET_TOO_MANY_OPENED signalisiert, dass mehr als 128 Sockets offen sind.
- Der Fehler ERR_FUNCTION_NOT_ALLOWED erscheint beim Versuch, eine Socket-Erstellung von einem Indikator aus aufzurufen, in dem diese Funktion deaktiviert ist.
Versuchen wir nach dem Erhalt des Handle, die Verbindung herzustellen. In diesem Beispiel verbinden wir uns mit dem Server "google.com" (vergessen Sie nicht, ihn zu den erlaubten Adressen in den Terminal-Einstellungen hinzuzufügen), und zwar zu Port 80 mit dem Timeout von 2 000 Millisekunden. Nach dem Verbindungsaufbau warten wir 5 Sekunden und schließen ihn wieder. Nun wollen wir sehen, wie es im Fenster der Verkehrsanalyse aussieht.
Abb. 4. Herstellen und Schließen einer Verbindung
In Abbildung 4 sehen wir den Datenaustausch zwischen unserem Skript und dem Server "google.com" mit der IP-Adresse "172.217.16.14". DNS-Abfragen werden hier nicht angezeigt, da die Filterzeile den Ausdruck "tcp.port==80" enthält.
Die ersten drei Pakete bauen eine Verbindung auf, die letzten drei Pakete schließen sie. Die Spalte Time zeigt die Zeit zwischen den Paketen an, und wir können eine Ausfallzeit von 5 Sekunden erkennen. Bitte beachten Sie, dass die Pakete im Gegensatz zu denen in Abbildung 2 grün gefärbt sind. Das liegt daran, dass im vorherigen Fall die Analyse das MySQL-Protokoll einen Datenaustausch erkannt hat. Im aktuellen Fall wurden keine Daten übergeben und die Analyse hat die Pakete mit der Standard-TCP-Farbe hervorgehoben.
Datenaustausch
Dem Protokoll zufolge sollte der MySQL-Server nach dem Verbindungsaufbau eine Begrüßung senden. Als Antwort darauf sendet der Client eine Autorisierungsanfrage. Dieser Mechanismus wird ausführlich im Abschnitt Verbindungsphase auf der Website dev.mysql.com beschrieben. Wenn die Begrüßung nicht empfangen wird, ist die IP-Adresse ungültig oder der Server lauscht auf einem anderen Port. In jedem Fall bedeutet dies, dass wir uns mit etwas verbunden haben, das definitiv kein MySQL-Server ist. In einer normalen Situation müssen wir Daten empfangen (aus dem Socket lesen) und untersuchen.
Empfangen
In der Klasse CMySQLTransaction (die etwas später beschrieben werden soll) wurde der Datenempfang wie folgt implementiert:
//+------------------------------------------------------------------+ //| Data receipt | //+------------------------------------------------------------------+ bool CMySQLTransaction::ReceiveData(ushort error_code=0) { char buf[]; uint timeout_check=GetTickCount()+m_timeout; do { //--- Get the amount of data that can be read from the socket uint len=SocketIsReadable(m_socket); if(len) { //--- Read data from the socket to the buffer int rsp_len=SocketRead(m_socket,buf,len,m_timeout); m_rx_counter+= rsp_len; //--- Send the buffer for handling ENUM_TRANSACTION_STATE res = Incoming(buf,rsp_len); //--- Get the result the following actions will depend on if(res==MYSQL_TRANSACTION_COMPLETE) // server response fully accepted return true; // exit (successful) else if(res==MYSQL_TRANSACTION_ERROR) // error { if(m_packet.error.code) SetUserError(MYSQL_ERR_SERVER_ERROR); else SetUserError(MYSQL_ERR_INTERNAL_ERROR); return false; // exit (error) } //--- In case of another result, continue waiting for data in the loop } } while(GetTickCount()<timeout_check && !IsStopped()); //--- If waiting for the completion of the server response receipt took longer than m_timeout, //--- exit with the error SetUserError(error_code); return false; }Hier ist m_socket ein Socket-Handle, das vorher bei seiner Erzeugung erhalten wurde, während m_timeout die Zeitüberschreitung beim Lesen von Daten ist, die als SocketRead() Funktionsargument für die Annahme eines Datenfragments sowie in Form der Zeitüberschreitung beim Empfang der gesamten Daten verwendet wird. Bevor wir in die Schleife eintreten, setzen wir einen Zeitstempel. Das Erreichen dieses Zeitstempels gilt als Timeout für den Datenempfang:
uint timeout_check=GetTickCount()+m_timeout;
Lesen wir als Nächstes das Ergebnis der Funktion SocketIsReadable() in einer Schleife und warten, bis sie einen Wert ungleich Null zurückgibt. Danach lesen wir die Daten in den Puffer und übergeben sie an die Verarbeitung.
uint len=SocketIsReadable(m_socket); if(len) { //--- Read data from the socket to the buffer int rsp_len=SocketRead(m_socket,buf,len,m_timeout); m_rx_counter+= rsp_len; //--- Send the buffer for handling ENUM_TRANSACTION_STATE res = Incoming(buf,rsp_len); ... }
Wir können uns nicht darauf verlassen, dass das gesamte Paket akzeptiert wird, wenn Daten im Socket vorhanden sind. Es gibt eine Reihe von Situationen, in denen Daten in kleinen Portionen ankommen können. Zum Beispiel kann es sich um eine schlechte Verbindung über ein 4G-Modem mit einer großen Anzahl von Rückübertragungen handeln. Daher sollte unser Handler in der Lage sein, Daten in einige unteilbare Gruppen zu sammeln, mit denen es möglich ist, zu arbeiten. Lassen Sie uns dafür MySQL-Pakete benutzen.
Die Methode CMySQLTransaction::Incoming() wird verwendet, um Daten zu sammeln und zu verarbeiten:
//--- Handle received data ENUM_TRANSACTION_STATE Incoming(uchar &data[], uint len);
Das Ergebnis, das sie zurückgibt, lässt uns wissen, was als Nächstes zu tun ist — den Prozess des Datenempfangs fortzusetzen, abzuschließen oder zu unterbrechen:
enum ENUM_TRANSACTION_STATE { MYSQL_TRANSACTION_ERROR=-1, // Error MYSQL_TRANSACTION_IN_PROGRESS=0, // In progress MYSQL_TRANSACTION_COMPLETE, // Fully completed MYSQL_TRANSACTION_SUBQUERY_COMPLETE // Partially completed };
Im Falle eines internen Fehlers sowie beim Auftreten eines Serverfehlers oder beim Abschluss des Datenempfangs sollte das Lesen von Daten aus dem Socket gestoppt werden. In allen anderen Fällen sollte es fortgesetzt werden. Der Wert MYSQL_TRANSACTION_SUBQUERY_COMPLETE zeigt an, dass eine der Server-Antworten auf eine Mehrfachabfrage eines Clients akzeptiert wurde. Er ist äquivalent zu MYSQL_TRANSACTION_IN_PROGRESS für den Lesealgorithmus.
Abb. 5. MySQL-Paket
Das MySQL-Paketformat ist in Abb. 5 dargestellt. Die ersten drei Bytes definieren die Größe der Nutzdaten im Paket, während das nächste Byte die Seriennummer des Pakets in der Sequenz bedeutet und von Daten gefolgt wird. Die Seriennummer wird zu Beginn jedes Datenaustauschs auf Null gesetzt. Zum Beispiel ist das Begrüßungspaket 0, die Autorisierungsanfrage des Kunden — 1, die Antwort des Servers — 2 (Ende der Verbindungsphase). Beim Senden einer Client-Abfrage wird der Wert der Sequenznummer dann wieder auf Null gesetzt und in jedem Server-Antwortpaket erhöht. Wenn die Anzahl der Pakete 255 überschreitet, geht der Wert der Sequenznummer über Null hinaus.
Das einfachste Paket (MySQL-Ping) sieht in der Verkehrsanalyse wie folgt aus:
Abb. 6. Ping-Paket in der Verkehrsanalyse
Das Ping-Paket enthält ein Datenbyte mit dem Wert 14 (oder 0x0E in hexadezimaler Form).
Betrachten wir die Methode CMySQLTransaction::Incoming(), die die Daten zu Paketen sammelt und sie an den Handler übergibt. Ihr verkürzter Quellcode ist unten angegeben.
ENUM_TRANSACTION_STATE CMySQLTransaction::Incoming(uchar &data[], uint len) { int ptr=0; // index of the current byte in the 'data' buffer ENUM_TRANSACTION_STATE result=MYSQL_TRANSACTION_IN_PROGRESS; // result of handling accepted data while(len>0) { if(m_packet.total_length==0) { //--- If the amount of data in the packet is unknown while(m_rcv_len<4 && len>0) { m_hdr[m_rcv_len] = data[ptr]; m_rcv_len++; ptr++; len--; } //--- Received the amount of data in the packet if(m_rcv_len==4) { //--- Reset error codes etc. m_packet.Reset(); m_packet.total_length = reader.TotalLength(m_hdr); m_packet.number = m_hdr[3]; //--- Length received, reset the counter of length bytes m_rcv_len = 0; //--- Highlight the buffer of a specified size if(ArrayResize(m_packet.data,m_packet.total_length)!=m_packet.total_length) return MYSQL_TRANSACTION_ERROR; // internal error } else // if the amount of data is still not accepted return MYSQL_TRANSACTION_IN_PROGRESS; } //--- Collect packet data while(len>0 && m_rcv_len<m_packet.total_length) { m_packet.data[m_rcv_len] = data[ptr]; m_rcv_len++; ptr++; len--; } //--- Make sure the package has been collected already if(m_rcv_len<m_packet.total_length) return MYSQL_TRANSACTION_IN_PROGRESS; //--- Handle received MySQL packet //... //--- m_rcv_len = 0; m_packet.total_length = 0; } return result; }
Der erste Schritt besteht darin, den Header des Pakets zu sammeln — die ersten 4 Bytes, die die Datenlänge und die Seriennummer in der Sequenz enthalten. Um den Header zu sammeln, verwenden wir den Puffer m_hdr und den Byte-Zähler m_rcv_len. Wenn 4 Bytes gesammelt sind, ermitteln wir deren Länge und ändern den darauf basierenden Puffer m_packet.data. Empfangene Paketdaten werden dorthin kopiert. Wenn das Paket fertig ist, übergeben wir es an den Handler.
Wenn die Länge len der empfangenen Daten nach dem Empfang des Pakets immer noch nicht Null ist, bedeutet dies, dass wir mehrere Pakete erhalten haben. Bei einem Aufruf der Methode Incoming() können mehrere Pakete verarbeitet werden und nicht nur ein einziges als Ganzes (eben auch teilweise).
Die Pakettypen sind unten angegeben:
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 };
Jede von ihnen hat ihren eigenen Handler, der ihre Abfolge und ihren Inhalt entsprechend dem Protokoll untersucht. Die während des Parsens empfangenen Werte werden den Mitgliedern der entsprechenden Klassen zugewiesen. In der aktuellen Konnektor-Implementierung werden alle in Paketen empfangenen Daten untersucht. Dies mag etwas redundant erscheinen, da die Eigenschaften der Felder "Tabelle" und "Originaltabelle" oft übereinstimmen. Außerdem werden die Werte einiger Flags selten benötigt (siehe Abb. 7). Das Vorhandensein dieser Eigenschaften erlaubt es jedoch, die Logik der Interaktion mit dem MySQL-Server auf der Anwendungsschicht des Programms flexibel aufzubauen.
Abb. 7. Feldbeschreibungen der Pakete
Senden
Das Senden von Daten ist etwas einfacher.
//+------------------------------------------------------------------+ //| Form and send ping | //+------------------------------------------------------------------+ bool CMySQLTransaction::ping(void) { if(reset_rbuf()==false) { SetUserError(MYSQL_ERR_INTERNAL_ERROR); return false; } //--- Prepare the output buffer m_tx_buf.Reset(); //--- Reserve a place for the packet header m_tx_buf.Add(0x00,4); //--- Place the command code m_tx_buf+=uchar(0x0E); //--- Form a header m_tx_buf.AddHeader(0); uint len = m_tx_buf.Size(); //--- Send a packet if(SocketSend(m_socket,m_tx_buf.Buf,len)!=len) return false; m_tx_counter+= len; return true; }
Der Quellcode für das Senden eines Pings ist oben angegeben. Kopieren wir die Daten in den vorbereiteten Puffer. Im Falle des Ping ist der Code des Befehls 0x0E. Bilden wir jetzt den Header unter Berücksichtigung der Datenmenge und der Paket-Seriennummer. Bei einem Ping ist die Seriennummer immer gleich Null. Versuchen wir danach, das zusammengesetzte Paket mit der Funktion SocketSend() zu senden.
Die Methode zum Senden einer Abfrage (Query) ist ähnlich wie das Senden eines Pings:
//+------------------------------------------------------------------+ //| Form and send a query | //+------------------------------------------------------------------+ bool CMySQLTransaction::query(string s) { if(reset_rbuf()==false) { SetUserError(MYSQL_ERR_INTERNAL_ERROR); return false; } //--- Prepare the output buffer m_tx_buf.Reset(); //--- Reserve a place for the packet header m_tx_buf.Add(0x00,4); //--- Place the command code m_tx_buf+=uchar(0x03); //--- Add the query string m_tx_buf+=s; //--- Form a header m_tx_buf.AddHeader(0); uint len = m_tx_buf.Size(); //--- Send a packet if(SocketSend(m_socket,m_tx_buf.Buf,len)!=len) return false; m_tx_counter+= len; return true; }
Der einzige Unterschied besteht darin, dass die 'Nutzlast' aus dem Befehlscode (0x03) und der Zeichenkette der Abfrage besteht.
Nach dem Senden von Daten folgt immer die Empfangsmethode CMySQLTransaction::ReceiveData(), die wir zuvor besprochen haben. Wenn sie keinen Fehler auswirft, gilt die Transaktion als erfolgreich.
MySQL-Transaktionsklasse
Es ist nun an der Zeit, die Klasse CMySQLTransaction genauer zu betrachten.
//+------------------------------------------------------------------+ //| MySQL transaction class | //+------------------------------------------------------------------+ class CMySQLTransaction { private: //--- Authorization data string m_host; // MySQL server IP address uint m_port; // TCP port string m_user; // User name string m_password; // Password //--- Timeouts uint m_timeout; // timeout of waiting for TCP data (ms) uint m_timeout_conn; // timeout of establishing a server connection //--- Keep Alive uint m_keep_alive_tout; // time(ms), after which the connection is closed; the value of 0 - Keep Alive is not used uint m_ping_period; // period of sending ping (in ms) in the Keep Alive mode bool m_ping_before_query; // send 'ping' before 'query' (this is reasonable in case of large ping sending periods) //--- Network int m_socket; // socket handle ulong m_rx_counter; // counter of bytes received ulong m_tx_counter; // counter of bytes passed //--- Timestamps ulong m_dT; // last query time uint m_last_resp_timestamp; // last response time uint m_last_ping_timestamp; // last ping time //--- Server response CMySQLPacket m_packet; // accepted packet uchar m_hdr[4]; // packet header uint m_rcv_len; // counter of packet header bytes //--- Transfer buffer CData m_tx_buf; //--- Authorization request class CMySQLLoginRequest m_auth; //--- Server response buffer and its size CMySQLResponse m_rbuf[]; uint m_responses; //--- Waiting and accepting data from the socket bool ReceiveData(ushort error_code); //--- Handle received data ENUM_TRANSACTION_STATE Incoming(uchar &data[], uint len); //--- Packet handlers for each type ENUM_TRANSACTION_STATE PacketOkHandler(CMySQLPacket *p); ENUM_TRANSACTION_STATE PacketGreetingHandler(CMySQLPacket *p); ENUM_TRANSACTION_STATE PacketDataHandler(CMySQLPacket *p); ENUM_TRANSACTION_STATE PacketEOFHandler(CMySQLPacket *p); ENUM_TRANSACTION_STATE PacketErrorHandler(CMySQLPacket *p); //--- Miscellaneous bool ping(void); // send ping bool query(string s); // send a query bool reset_rbuf(void); // initialize the server response buffer uint tick_diff(uint prev_ts); // get the timestamp difference //--- Parser class CMySQLPacketReader reader; public: CMySQLTransaction(); ~CMySQLTransaction(); //--- Set connection parameters bool Config(string host,uint port,string user,string password,uint keep_alive_tout); //--- Keep Alive mode void KeepAliveTimeout(uint tout); // set timeout void PingPeriod(uint period) {m_ping_period=period;} // set ping period in seconds void PingBeforeQuery(bool st) {m_ping_before_query=st;} // enable/disable ping before a query //--- Handle timer events (relevant when using Keep Alive) void OnTimer(void); //--- Get the pointer to the class for working with authorization CMySQLLoginRequest *Handshake(void) {return &m_auth;} //--- Send a request bool Query(string q); //--- Get the number of server responses uint Responses(void) {return m_responses;} //--- Get the pointer to the server response by index CMySQLResponse *Response(uint idx); CMySQLResponse *Response(void) {return Response(0);} //--- Get the server error structure MySQLServerError GetServerError(void) {return m_packet.error;} //--- Options ulong RequestDuration(void) {return m_dT;} // get the last transaction duration ulong RxBytesTotal(void) {return m_rx_counter;} // get the number of received bytes ulong TxBytesTotal(void) {return m_tx_counter;} // get the number of passed bytes void ResetBytesCounters(void) {m_rx_counter=0; m_tx_counter=0;} // reset the counters of received and passed bytes };
Schauen wir uns die folgenden privaten Mitglieder näher an:
- m_packet vom Typ CMySQLPacket - Klasse des aktuell behandelten MySQL-Pakets (Quellcode mit Kommentaren in der Datei MySQLPacket.mqh)
- m_tx_buf vom Typ CData - Klasse des Übertragungspuffers, der für die bequeme Erzeugung einer Abfrage erstellt wurde (Datei Data.mqh)
- m_auth vom Typ CMySQLLoginRequest - Klasse für die Arbeit mit Autorisierung (Passwortverschlüsselung, Speicherung der erhaltenen Serverparameter und spezifizierten Clientparameter, der Quellcode befindet sich in MySQLLoginRequest.mqh)
- m_rbuf vom Typ CMySQLResponse - Server-Reaktionspuffer; die Antwort ist hier das Paket vom Typ "Ok" oder "Daten" (MySQLResponse.mqh)
- reader vom Typ CMySQLPacketReader - Klasse zum Parsen der MySQL-Pakete
Die öffentlichen Methoden sind in der Dokumentation ausführlich beschrieben.
Für die Anwendungsschicht sieht die Transaktionsklasse wie in Abbildung 8 dargestellt aus.
Abb. 8. Struktur der Klassen von CMySQLTransaction
wobei:
- CMySQLLoginRequest — sollte vor dem Aufbau einer Verbindung konfiguriert werden, wenn dei Parameter des Clients angegeben werden, deren Werte sich von den vordefinierten unterscheiden (optional);
- CMySQLResponse — Server-Antwort, wenn eine Transaktion ohne Fehler abgeschlossen wird;
- CMySQLField — Feldbeschreibung;
- CMySQLRow — Zeile (Puffer von Feldwerten in Textform);
- MySQLServerError — Fehlerbeschreibungsstruktur für den Fall, dass eine Transaktion fehlgeschlagen ist;
Es gibt keine öffentlichen Methoden, die für den Aufbau und das Schließen einer Verbindung verantwortlich sind. Dies geschieht automatisch beim Aufruf der Methode CMySQLTransaction::Query(). Wenn der konstante Verbindungsmodus verwendet wird, wird er beim ersten Aufruf von CMySQLTransaction::Query() aufgebaut und nach der definierten Zeitüberschreitung geschlossen.
Wichtig: Im konstanten Verbindungsmodus sollte die Ereignisbehandlung durch OnTimer den Aufruf der Methode CMySQLTransaction::OnTimer() empfangen. In diesem Fall sollte der Zeitabstand des Timers kleiner als die für Ping und Timeout sein.
Die Parameter der Verbindung, das Nutzerkontos sowie spezielle Parameterwerte des Clienten sollten vor dem Aufruf von CMySQLTransaction::Query() festgelegt werden.
Im Allgemeinen wird die Interaktion mit der Transaktionsklasse nach folgendem Prinzip durchgeführt:
Abb. 9. Arbeiten mit der Klasse CMySQLTransaction
Anwendung
Betrachten wir das einfachste Beispiel für die Anwendung des Konnektors. Dazu schreiben wir ein Skript, das die SELECT-Abfrage an die Welttest-Datenbank sendet.
//--- input parameters input string inp_server = "127.0.0.1"; // MySQL server address input uint inp_port = 3306; // TCP port input string inp_login = "admin"; // Login input string inp_password = "12345"; // Password input string inp_db = "world"; // Database name //--- Connect MySQL transaction class #include <MySQL\MySQLTransaction.mqh> CMySQLTransaction mysqlt; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- Configure MySQL transaction class mysqlt.Config(inp_server,inp_port,inp_login,inp_password); //--- Make a query string q = "select `Name`,`SurfaceArea` "+ "from `"+inp_db+"`.`country` "+ "where `Continent`='Oceania' "+ "order by `SurfaceArea` desc limit 10"; if(mysqlt.Query(q)==true) { if(mysqlt.Responses()!=1) return; CMySQLResponse *r = mysqlt.Response(); if(r==NULL) return; Print("Name: ","Surface Area"); uint rows = r.Rows(); for(uint i=0; i<rows; i++) { double area; if(r.Row(i).Double("SurfaceArea",area)==false) break; PrintFormat("%s: %.2f",r.Row(i)["Name"],area); } } else if(GetLastError()==(ERR_USER_ERROR_FIRST+MYSQL_ERR_SERVER_ERROR)) { // in case of a server error Print("MySQL Server Error: ",mysqlt.GetServerError().code," (",mysqlt.GetServerError().message,")"); } else { if(GetLastError()>=ERR_USER_ERROR_FIRST) Print("Transaction Error: ",EnumToString(ENUM_TRANSACTION_ERROR(GetLastError()-ERR_USER_ERROR_FIRST))); else Print("Error: ",GetLastError()); } }
Nehmen wir an, dass unsere Aufgabe darin besteht, eine Liste von Ländern mit dem Kontinentalwert "Ozeanien" zu erhalten, sortiert nach der Fläche vom größten bis zum kleinsten mit maximal 10 Punkten in der Liste. Lassen Sie uns die folgenden Aktionen durchführen:
- Deklarieren wir eine Instanz der Transaktionsklasse mysqlt
- Einstellen der Verbindungsparameter
- Erstellen der entsprechenden Anfrage
- Wenn die Transaktion erfolgreich ist, vergewissern Sie sich, dass die Anzahl der Antworten dem erwarteten Wert entspricht
- Holen wir uns den Zeiger auf die Server-Antwortklasse
- Wir rrufen der Anzahl der Zeilen in der Antwort ab
- Anzeigen der Werte der Zeilen
Die Transaktion kann aus einem von drei Gründen scheitern:
- Ein Serverfehler — wir erhalten seine Beschreibung mit der Methode CMySQLTransaction::GetServerError()
- Ein interner Fehler — wir verwenden die Funktion EnumToString(), um dessen Beschreibung zu erhalten
- Andernfalls erhalten wir den Fehlercode mit LetzterFehlerholen()
Wenn die Eingaben korrekt angegeben werden, sieht das Ergebnis der Skriptoperation wie folgt aus:
Abb. 10. Ergebnis des Aufrufs des Testskript
Komplexere Beispiele für die Anwendung von Mehrfachabfragen und den konstanten Verbindungsmodus werden im zweiten Teil beschrieben.
Dokumentation
Inhalt
- Die Klasse CMySQLLoginRequest für die Authentifizierung
- Die Klasse CMySQLResponse für die Serverantworten
- Die Struktur MySQLServerError für die Fehlernachrichten des Servers
- Die Feldklasse CMySQLField
- Die Zeilenklasse CMySQLRow
Transaktionsklasse CMySQLTransaction
Liste der Methoden der Klasse CMySQLTransaction
Methode |
Aktion |
---|---|
Config |
Einstellung der Verbindungsparameter |
KeepAliveTimeout |
Einstellung des Timeout für den Keep-Alive-Modus in Sekunden |
PingPeriod |
Einstellen einer Ping-Periode für den Keep-Alive-Modus in Sekunden |
PingBeforeQuery |
Aktivieren/Deaktivieren von Ping vor einer Abfrage |
OnTimer |
Handhabung von Timer-Ereignissen (relevant bei Verwendung von Keep Alive) |
Handshake |
Abrufen des Zeigers auf die Klasse zum Arbeiten mit der Authentifizierung |
Query |
Senden einer Anfrage |
Responses |
Ermitteln der Anzahl der Server-Antworten |
Response |
Abrufen des Zeigers auf die Server-Antwortklasse |
GetServerError |
Abrufen der Server-Fehlerstruktur |
RequestDuration |
Transaktionsdauer in Mikrosekunden |
RxBytesTotal |
Der Zähler der akzeptierten Bytes seit dem Programmstart |
TxBytesTotal |
Der Zähler der seit dem Programmstart gesendeten Bytes |
ResetBytesCounters |
Zurücksetzen der Zähler der akzeptierten und gesendeten Bytes |
Unten eine Kurzbeschreibung von jeder Methode.
Config
Sets connection parameters.bool Config( string host, // server name uint port, // port string user, // user name string password, // password string base, // database name uint keep_alive_tout // constant connection timeout (0 - not used) );
Rückgabewert: true wenn erfolgreich, sonst false (ungültige Symbole in String-Argumenten).
KeepAliveTimeout
Aktiviert den konstanten Verbindungsmodus und stellt dessen Zeitüberschreitung ein. Der Wert des Timeout ist eine Zeit in Sekunden ab dem Zeitpunkt des Sendens der letzten Abfrage, nach der die Verbindung geschlossen wird. Wenn Abfragen innerhalb des als Timeout definierten Zeitraums wiederholt werden, wird die Verbindung nicht geschlossen.
void KeepAliveTimeout( uint tout // set the constant connection timeout in seconds (0 - disable) );
PingPeriod
Legt den Zeitraum für das Senden von 'Ping'-Paketen im konstanten Verbindungsmodus fest. Das verhindert, dass der Server die Verbindung schließt. Der Ping wird nach der angegebenen Zeit nach der letzten Abfrage oder dem vorherigen Ping gesendet.
void PingPeriod( uint period // set the ping period in seconds (for the constant connection mode) );
Rückgabewert: keiner.
PingBeforeQuery
Ermöglicht das Senden des 'Ping'-Pakets vor einer Abfrage. Im konstanten Verbindungsmodus kann die Verbindung in Zeitintervallen zwischen Abfragen aus irgendeinem Grund geschlossen oder beendet werden. In diesem Fall ist es möglich, vor dem Senden einer Anfrage einen Ping an den MySQL-Server zu senden, um sicherzustellen, dass die Verbindung aktiv ist.
void PingBeforeQuery( bool st // enable (true)/disable (false) ping before a query );
Rückgabewert: keiner.
OnTimer
Wird im konstanten Verbindungsmodus verwendet. Die Methode sollte von der Ereignisbehandlung durch OnTimer aufgerufen werden. Die Timer-Periode sollte den Mindestwert der Zeiträume für KeepAliveTimeout und PingPeriod nicht überschreiten.
void OnTimer(void);
Rückgabewert: keiner.
Handshake
Ruft den Zeiger auf die Klasse für die Arbeit mit der Authentifizierung ab. Sie kann verwendet werden, um die Flags der Client-Fähigkeiten und die maximale Paketgröße zu setzen, bevor eine Verbindung zum Server hergestellt wird. Nach der Autorisierung erlaubt sie den Empfang der Version und der Flags der Server-Fähigkeiten.
CMySQLLoginRequest *Handshake(void);
Rückgabewert: Zeiger auf die Klasse CMySQLLoginRequest für die Arbeit mit der Autorisierung.
Query
Sendet eine Anfrage
bool Query( string q // query body );
Rückgabewert: Ausführungsergebnis; erfolgreich - true, Fehler - false.
Responses
Ermittelt die Anzahl der Antworten.
uint Responses(void);
Rückgabewert: Anzahl der Server-Antworten.
Pakete vom Typ "Ok" oder "Data" werden als Antworten betrachtet. Wenn die Abfrage erfolgreich ausgeführt wird, werden eine oder mehrere Antworten (bei mehreren Abfragen) akzeptiert.
Response
Ruft den Zeiger auf die MySQL-Server-Antwortklasse ab.
CMySQLResponse *Response( uint idx // server response index );
Rückgabewert: Zeiger auf die Antwortklasse des Servers CMySQLResponse. Die Übergabe eines ungültigen Wertes als Argument gibt NULL zurück.
Die überladene Methode ohne Angabe eines Index ist äquivalent zu Response(0).
CMySQLResponse *Response(void);
Rückgabewert: Zeiger auf die Antwortklasse des Servers CMySQLResponse. Wenn es keine Antworten gibt, wird NULL zurückgegeben.
GetServerError
Ruft die Struktur zur Speicherung des Codes und der Server-Fehlermeldung ab. Sie kann aufgerufen werden, nachdem die Transaktionsklasse den Fehler MYSQL_ERR_SERVER_ERROR zurückgegeben hat.
MySQLServerError GetServerError(void);
Rückgabewert: MySQLServerError-Fehlerstruktur
RequestDuration
Ruft die Ausführungsdauer der Anfrage ab.
ulong RequestDuration(void);
Rückgabewert: Ausführungsdauer der Anfrage in Mikrosekunden vom Zeitpunkt des Sendens bis zum Ende der Bearbeitung
RxBytesTotal
Ruft die Anzahl der akzeptierten Bytes ab.
ulong RxBytesTotal(void);
Rückgabewert: Anzahl der akzeptierten Bytes (TCP-Ebene) seit dem Programmstart. Die Methode ResetBytesCounters wird für einen Reset verwendet.
TxBytesTotal
Ruft die Anzahl der gesendeten Bytes ab.
ulong TxBytesTotal(void);
Rückgabewert: Anzahl der seit dem Programmstart übergebenen Bytes (TCP-Ebene). Die Methode ResetBytesCounters wird für einen Reset verwendet.
ResetBytesCounters
Setzt die Zähler der akzeptierten und gesendeten Bytes zurück.
void ResetBytesCounters(void);
Die Klasse CMySQLLoginRequest für die Authentifizierung
Die Klassenmethoden von CMySQLLoginRequest
Methode |
Aktion |
---|---|
SetClientCapabilities |
Setzt die Flags der Fähigkeiten des Clients. Vordefinierte Werte: 0x005FA685 |
SetMaxPacketSize |
Setzt die maximal erlaubte Paketgröße in Bytes. Vordefinierter Wert: 16777215 |
SetCharset |
Definiert die Menge der verwendeten Symbole. Vordefinierter Wert: 8 |
Version |
Rückgabe der Serverversion von MySQL. Zum Beispiel: "5.7.21-log". |
ThreadId |
Gibt die aktuelle ID des Verbindungsthreads zurück. Sie entspricht dem Wert CONNECTION_ID. |
ServerCapabilities |
Ruft die Flags der Server-Fähigkeiten ab |
ServerLanguage |
Gibt die Kodierung und die Datenbankrepräsentation zurück ID |
Die Klasse CMySQLResponse für die Serverantworten
Ein Paket vom Typ "Ok" oder "Data" wird als Serverantwort betrachtet. Da sie sich deutlich unterscheiden, verfügt die Klasse über einen separaten Satz von Methoden für die Arbeit mit jedem Pakettyp.
Allgemeine Methoden der Klasse CMySQLResponse:
Methode |
Rückgabewert |
---|---|
Typ |
Antworttypen des Servers: MYSQL_RESPONSE_DATA oder MYSQL_RESPONSE_OK |
Methoden für die Datentypen der Pakete:
Methode |
Rückgabewert |
---|---|
Fields |
Anzahl der Felder |
Field |
Zeiger auf die Felderklasse nach Index (überladene Methode - Abrufen des Feldindexes nach dem Namen) |
Field | Feldindex nach dem Namen |
Rows |
Anzahl der Zeilen in der Antwort des Servers |
Row |
Der Zeiger auf die Klasse der Zeilen nach Index |
Value |
String value by row and field indices |
Wert | Zeichenkette nach Zeilenindex und Feldname |
RColumnToArray | Das Leseergebnis einer Spalte in das Array vom Typ string eingetragen |
RColumnToArray |
Das Leseergebnis einer Spalte in das Array vom Typ int mit einer Typenüberprüfung eingetragen |
RColumnToArray |
Das Leseergebnis einer Spalte in das Array vom Typ long mit einer Typenüberprüfung eingetragen |
RColumnToArray |
Das Leseergebnis einer Spalte in das Array vom Typ double mit einer Typenüberprüfung eingetragen |
Methode |
Rückgabewert |
---|---|
AffectedRows |
Anzahl der von der letzten Operation betroffenen Zeilen |
LastId |
Wert von LAST_INSERT_ID |
ServerStatus |
Flags über den Serverstatus |
Warnings |
Anzahl der Warnungen |
Message |
Textnachrichten des Servers |
Die Struktur MySQLServerError für die Fehlernachrichten des Servers
Elemente der Struktur MySQLServerError
Element |
Typ |
Zweck |
---|---|---|
code |
ushort | Fehlernummer |
sqlstate |
uint | Status |
message | string | Textnachrichten des Servers |
Die Feldklasse CMySQLField
Methoden der Klasse CMySQLField
Methode |
Rückgabewert |
---|---|
Catalog |
Verzeichnisname der Tabelle |
Database |
Name der Datenbank der Tabelle |
Tabelle |
Pseudonym der Tabelle eines zugehörigen Feldes |
OriginalTable |
Originalname der Tabelle eines zugehörigen Feldes |
Name |
Pseudonym des Feldes |
OriginalName |
Originalname des Feldes |
Charset |
Verwendete Textcodierungsnummer |
Length |
Länge des Wertes |
Type |
Werttyp |
Flags |
Flags der Attributwerte |
Decimals |
Erlaubte Dezimalstellen |
MQLType |
Feldtyp in der Form des Wertes ENUM_DATABASE_FIELD_TYPE (außer DATABASE_FIELD_TYPE_NULL) |
Zeilenklasse CMySQLRow
Klassenmethoden von CMySQLRow
Methode |
Aktion |
---|---|
Value |
Gibt den Feldwert nach Nummer als Zeichenkette zurück |
operator[] |
Gibt den Feldwert nach Name als Zeichenkette zurück |
MQLType |
Gibt den Feldtyp nach Nummer als ENUM_DATABASE_FIELD_TYPE zurück |
MQLType |
Gibt den Feldtyp nach Name als ENUM_DATABASE_FIELD_TYPE zurück |
Text |
Ruft den Feldwert nach Nummer als Zeichenkette mit Typüberprüfung ab |
Text |
Ruft den Feldwert nach Namen als Zeichenkette mit Typüberprüfung ab |
Integer |
Ruft den int-Wert nach Feldnamen mit Typüberprüfung ab |
Integer |
Ruft den int-Wert nach Feldnummer mit Typüberprüfung ab |
Long |
Ruft den long-Wert nach Feldnummer mit Typüberprüfung ab |
Long |
Ruft den long Typwert nach Feldnamen mit Typüberprüfung ab |
Double |
Ruft den Typwert double nach Feldnummer mit Typüberprüfung ab |
Double |
Ruft den Typwert double nach Feldnamen mit Typüberprüfung ab |
Blob |
Ruft den Wert in Form des uchar-Arrays nach Feldnummer mit Typüberprüfung ab |
Blob |
Ruft den Wert in Form des uchar array nach Feldnamen mit Typüberprüfung ab |
Hinweis. Typüberprüfung bedeutet, dass das lesbare Feld der Methode, die mit dem Typ int arbeitet, gleich DATABASE_FIELD_TYPE_INTEGER sein sollte. Im Falle einer Nichtübereinstimmung wird kein Wert empfangen und die Methode gibt 'false' zurück. Die Konvertierung von MySQL-Feldtyp-IDs in einen Wert des TypsENUM_DATABASE_FIELD_TYPE wird in der Methode CMySQLField::MQLType() implementiert, deren Quellcode unten angegeben ist.
//+------------------------------------------------------------------+ //| Return the field type as the ENUM_DATABASE_FIELD_TYPE value | //+------------------------------------------------------------------+ ENUM_DATABASE_FIELD_TYPE CMySQLField::MQLType(void) { switch(m_type) { case 0x00: // decimal case 0x04: // float case 0x05: // double case 0xf6: // newdecimal return DATABASE_FIELD_TYPE_FLOAT; case 0x01: // tiny case 0x02: // short case 0x03: // long case 0x08: // longlong case 0x09: // int24 case 0x10: // bit case 0x07: // timestamp case 0x0c: // datetime return DATABASE_FIELD_TYPE_INTEGER; case 0x0f: // varchar case 0xfd: // varstring case 0xfe: // string return DATABASE_FIELD_TYPE_TEXT; case 0xfb: // blob return DATABASE_FIELD_TYPE_BLOB; default: return DATABASE_FIELD_TYPE_INVALID; } }
Schlussfolgerung
In diesem Artikel haben wir die Verwendung von Funktionen für die Arbeit mit Sockets am Beispiel der Implementierung des MySQL-Konnektors untersucht. Dies war bisher Theorie. Der zweite Teil des Artikels soll eher praktischer Natur sein: Wir werden einen Dienst zum Sammeln von Signaleigenschaften und ein Programm zum Anzeigen von Änderungen dieser Signale entwickeln.
Das beigefügte Archiv enthält die folgenden Dateien:
- Pfad Include\MySQL\: Quellcode des Konnektors
- Datei Scripts\test_mysql.mq5: das Beispiel für die Verwendung des im Abschnitt Anwendung betrachteten Konnektors.
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/7117
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.