Hat Dir der Artikel gefallen?
Teile ihn mit den anderen -
poste einen Link zu diesem Beitrag!

Nutze neue Möglichkeiten der Plattform MetaTrader 5

Verwendung von Netzwerkfunktionen oder MySQL ohne DLL: Teil I - Konnektor

3 Mai 2020, 10:31
Serhii Shevchuk
0
310

Inhalt

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).


Das Programm zur Anzeige von Änderungen der Signaleigenschaften innerhalb einer bestimmten Zeit

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.

Verkehrsanalyse

Abb. 2. Wireshark, Datenverkehrsanalyse

Abbildung 2 zeigt das Fenster der Verkehrsanalyse mit den erfassten Paketen, wo:

  1. 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).
  2. Die Pakete. Hier sehen wir den Verbindungsaufbau, die Serverbegrüßung, die Autorisierungsanfrage und den anschließenden Datenaustausch.
  3. Ausgewählter Paketinhalt in hexadezimaler Form. In diesem Fall können wir den Inhalt des Begrüßungspakets des MySQL-Servers sehen.
  4. Transport-Ebene (TCP). Hier befinden wir uns, wenn wir Funktionen zur Arbeit mit Sockets verwenden.
  5. 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.

Paket-Erfassungsfilter

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:

  1. Der Fehler ERR_NETSOCKET_TOO_MANY_OPENED signalisiert, dass mehr als 128 Sockets offen sind.
  2. 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.

Herstellen und Schließen einer Verbindung

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.

MySQL-Paket

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:

Ping-Paket in der Verkehrsanalyse

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.


Paketanalyse in Wireshark

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.

Die Klassen

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:

Arbeiten mit der Transaktionsklasse

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:

Wenn die Eingaben korrekt angegeben werden, sieht das Ergebnis der Skriptoperation wie folgt aus:

Ergebnis des Aufrufs des Testskript

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


    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
    Methoden für die Pakete des Typs "Ok":
    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 Software Corp.
    Originalartikel: https://www.mql5.com/ru/articles/7117

    Beigefügte Dateien |
    MQL5.zip (23.17 KB)
    Wie man 3D-Grafiken mit DirectX in MetaTrader 5 erstellt Wie man 3D-Grafiken mit DirectX in MetaTrader 5 erstellt

    3D-Grafiken sind ein hervorragendes Mittel zur Analyse riesiger Datenmengen, da sie die Visualisierung verborgener Muster ermöglichen. Diese Aufgaben können direkt in MQL5 gelöst werden, während die Funktionen von DireсtX die Erstellung dreidimensionaler Objekte ermöglichen. So ist es sogar möglich, Programme von beliebiger Komplexität zu erstellen, sogar 3D-Spiele für MetaTrader 5. Beginnen Sie mit dem Erlernen der 3D-Grafik, indem Sie einfache dreidimensionale Formen zeichnen.

    Bibliothek für ein leichtes und schnelles Entwickeln vom Programmen für den MetaTrader (Teil XXXIII): Schwebende Handelsanfragen - Schließen von Positionen unter bestimmten Bedingungen Bibliothek für ein leichtes und schnelles Entwickeln vom Programmen für den MetaTrader (Teil XXXIII): Schwebende Handelsanfragen - Schließen von Positionen unter bestimmten Bedingungen

    Wir setzen die Entwicklung der Bibliotheksfunktionalität fort, die den Handel mit schwebenden Anfragen ermöglicht. Wir haben bereits das Senden von bedingten Handelsanfragen für die Eröffnung von Positionen und die Platzierung von Pending Orders implementiert. Im aktuellen Artikel werden wir die bedingte Schließung von Positionen implementieren - vollständig, teilweise und das Schließen durch eine entgegengesetzte Position.

    Bibliothek für ein leichtes und schnelles Entwickeln vom Programmen für den MetaTrader (Teil XXXIII): Schwebende Handelsanfragen - Entfernen und Ändern von Orders und Positionen unter bestimmten Bedingungen Bibliothek für ein leichtes und schnelles Entwickeln vom Programmen für den MetaTrader (Teil XXXIII): Schwebende Handelsanfragen - Entfernen und Ändern von Orders und Positionen unter bestimmten Bedingungen

    In diesem Artikel werden wir die Beschreibung des Konzepts des Handels mit schwebenden Anfragen vervollständigen und die Funktionen zum Entfernen von Pending-Orders sowie zur Änderung von Orders und Positionen unter bestimmten Bedingungen schaffen. Auf diese Weise werden wir über die gesamte Funktionalität verfügen, die es uns ermöglicht, einfache benutzerdefinierte Strategien bzw. EA-Verhaltenslogiken zu entwickeln, die unter benutzerdefinierten Bedingungen aktiviert werden.

    Verwendung von Netzwerkfunktionen oder MySQL ohne DLL: Teil II - Programm zur Überwachung von Änderungen der Signaleigenschaften Verwendung von Netzwerkfunktionen oder MySQL ohne DLL: Teil II - Programm zur Überwachung von Änderungen der Signaleigenschaften

    Im vorherigen Teil haben wir die Implementierung des MySQL-Konnektors besprochen. In diesem Artikel wenden wir uns seiner Anwendung durch die Implementierung eines Dienstes zum Sammeln von Signaleigenschaften und des Programms zum Anzeigen ihrer Änderungen im Laufe der Zeit. Das implementierte Beispiel ist praktisch sinnvoll, wenn Nutzer Änderungen an Eigenschaften beobachten müssen, die auf der Webseite des Signals nicht angezeigt werden.