
Websockets für MetaTrader 5
Einführung
MetaTrader 5 ist im Laufe der Jahre sehr ausgereift und bietet eine Vielzahl von Funktionen für Händler. Ein herausragendes Merkmal ist seine Fähigkeit, sich trotz der Verwendung einer proprietären Programmiersprache in verschiedene Systeme und Plattformen zu integrieren. Diese Fähigkeit ist sehr wichtig, da sie Händlern viele Optionen bietet, wenn es darum geht, potenziell profitable Handelsstrategien zu erforschen.Der Schlüssel zu dieser Integration wird die Fähigkeit sein, die Vorteile moderner Netzwerkprotokolle zu nutzen, die effizienter und einfacher zu implementieren sind. In diesem Sinne werden wir die Implementierung eines Websocket-Clients für MetaTrader 5-Anwendungen ohne den Einsatz einer Dynamic Link Library untersuchen.
Zum Einstieg eine kurze Einführung in das Websocket-Netzwerkprotokoll.
Einführung in Websockets
Das Websocket-Protokoll ist eine Kommunikationsmethode, die einen bidirektionalen Informationsfluss zwischen einem Server und einem Client ermöglicht, ohne dass mehrere Hypertext-Transfer-Protokoll-basierte Anfragen gestellt werden müssen. Browser und die meisten Webschnittstellenanwendungen verwenden das Websocket-Protokoll, um verschiedene Dienste wie Instant Messaging, dynamische Webinhalte und Online-Multiplayer-Spiele bereitzustellen.
Warum der Bedarf an Websockets
Bevor es das Websocket-Protokoll gab, mussten Entwickler ineffiziente und kostspielige Techniken einsetzen, um eine asynchrone Kommunikation zwischen einem Server und einem Client zu erreichen.
Dazu gehörten:
- Polling — dies ist eine inhärent synchrone Methode, bei der ständig Anfragen gestellt werden, obwohl möglicherweise keine Daten zu übertragen sind, was zu einer Verschwendung von Rechenressourcen führt.
- Long Polling — ähnlich wie beim Polling, ist der Unterschied bei dieser Technik bereits im Namen enthalten. Anstatt häufige Anfragen zu stellen, stellt ein Client relativ wenige Anfragen an einen Server, der daraufhin eine Verbindung öffnet und aktiv hält, bis es zu einem Austausch kommt oder ein Timeout in Kraft tritt.
- Streaming — Bei dieser Methode muss ein Client eine Anfrage nach Daten stellen, und der Server hält die Verbindung dann unbegrenzt am Leben. Der größte Nachteil ist hier die umfangreiche Verwendung von HTTP-Headern, die die Größe der abgerufenen Daten erhöhen.
- Ajax - in erster Linie eine Browsertechnologie, führten asynchrones Javascript und Xml zu asynchronen Webinhalten. Man konnte einen Beitrag auf einer Website machen und den Inhalt fast sofort auf der Webseite erscheinen lassen, ohne die gesamte Webseite aktualisieren zu müssen.
Alle oben beschriebenen Methoden ermöglichten einen mehr oder weniger starken bidirektionalen Datenaustausch zwischen Client und Server, haben aber im Vergleich zu Websockets aus drei Hauptgründen Nachteile:
- Wie bereits erwähnt, bieten die oben genannten Techniken einen unterschiedlichen Grad an asynchroner Übertragung, tatsächlich kann die Art der Kommunikation bestenfalls als halbduplex beschrieben werden. Das bedeutet, dass jeder Teilnehmer bei einen Datenaustausch warten muss, bis der andere fertig ist, bevor eine Antwort angeboten werden kann.
- Bei den oben genannten Methoden werden die http-Header ausgiebig genutzt. Kombiniert man dies mit der Häufigkeit der http-Anfragen, die einige Male benötigt werden, führt dies zu einem relativ exzessiven Datenverbrauch. Das kann ein Nachteil sein, wenn eine effiziente Bandbreitennutzung wichtig ist.
- Ebenfalls mit der Effizienz verbunden sind die Kosten. Das Aufrechterhalten von Serververbindungen für lange Zeiträume, in denen sie nicht benötigt werden, oder das Senden von Datenübertragungen an Clients, die vielleicht schon weggefahren sind, ist eine Verschwendung für große Unternehmen, da es die Kosten für den Betrieb der Server in die Höhe treibt.
Eigenschaften von Websockets
Ein Websocket ist ein TCP-basiertes Protokoll, das weiter ausgebaut werden kann, um andere Anwendungen oder industriedefinierte Unterprotokolle zu unterstützen. Da es auf TCP basiert, kann es über die Standard-HTTP-Ports 80 und 443 arbeiten und hat ein ähnliches Universal Resource Locator-Schema. Websocket-Serveradressen werden mit ws oder wss vorangestellt, im Gegensatz zu http, folgen aber der gleichen URL-Adressstruktur wie eine Http-Webadresse. Zum Beispiel:
ws(s)://websocketexampleurl.com:80/hello.php
Websockets verstehen
Um zu verstehen, wie ein Websocket-Client in Mql5 implementiert werden kann, ist es notwendig, mit den Grundlagen der allgemeinen Computervernetzung vertraut zu sein. Das Websocket-Protokoll ähnelt dem Hypertext-Transfer-Protokoll, wobei Header in Client-Anfragen an den Server verwendet werden. Genau wie das Hypertext-Transfer-Protokoll erfordert auch der Aufbau einer über Websockets definierten Verbindung die Verwendung von Headern. Der Hauptunterschied bei Websockets besteht darin, dass eine solche Anfrage nur zum Aufbau oder zur Initialisierung des Websockets benötigt wird. Ein Client stellt eine Anfrage, die wie eine normale Hypertext-Transfer-Protokoll-Anfrage aussieht, dann wechseln die verwendeten Protokolle von der Verwendung des Hypertext-Transfer-Protokolls zum Websocket-Protokoll.
Dieser Vorgang wird als Websocket-Handshake bezeichnet. Die Umschaltung der Protokolle erfolgt nur, wenn die anfängliche Hypertext-Transfer-Protokoll-Anfrage an den Server einen bestimmten Header oder bestimmte Header enthält. Der Server muss dann entsprechend antworten, indem er den Wunsch nach dem Aufbau einer Websocket-Verbindung bekräftigt. Informationen über die Art der speziellen Header und wie der Server antworten darf, sind alle in RFC 6455 dokumentiert.
Sobald das Websocket aufgebaut ist, gibt es keine Notwendigkeit mehr, Hypertext-Transfer-Protokoll-ähnliche Anfragen zu verwenden, hier weichen die Protokolle in ihrer Funktionsweise voneinander ab. Beim Austausch von Daten über das Websocket-Protokoll wird ein anderes Format verwendet. Dieses Format ist stromlinienförmiger und verwendet viel weniger Rohbits im Vergleich zu einer Hypertext-Transfer-Protokoll-Anfrage. Das verwendete Format wird als Framing-Protokoll bezeichnet, wobei die in einer Transaktion zwischen Hosts ausgetauschten Daten als Frame bezeichnet werden.
Jeder Frame ist eine Folge von Bits, die auf eine bestimmte Art und Weise angeordnet sind und dem Framing-Protokoll entsprechen, wie es in RFC 6455 festgelegt ist. Jeder Websocket-Frame enthält Bits, die einen Opcode, die Größe der Nutzlast und die eigentliche Nutzlast selbst definieren. Das Protokoll definiert auch, wie diese Bits angeordnet und schließlich im Frame verpackt werden. Ein Opcode ist einfach ein reservierter numerischer Wert, der zur Klassifizierung eines Frames verwendet wird.
Für Websockets sind die Basis-Opcodes wie folgt definiert:
0 — continuation frame: dieser Wert bezeichnet Nutzdaten, die unvollständig sind, daher sollten mehr Frames erwartet werden. Diese Funktion ermöglicht die Fragmentierung von Frames. Sie ermöglicht die Aufteilung von Daten in Stücke, die in verschiedene Frames verpackt werden.
1 — text frame: bei diesem Wert sind die Nutzdaten in Textform.
2 — binary frame: mit diesem Wert sind die Daten in binärer Form.
8 — close frame: dieser Wert bezeichnet einen speziellen Frame-Typ, der gesendet wird, wenn einer der beiden Endpunkte beabsichtigt, eine bestehende Websocket-Verbindung zu schließen, es handelt sich um einen Frame-Typ, der als Kontroll-Frame bezeichnet wird. Kontrollframes haben bereits eine besondere Bedeutung, so dass sie nicht immer Nutzdaten enthalten, d.h. die Nutzdaten sind optional.
9 — ping frame: ein weiterer Kontroll-Frame, der verwendet wird, um festzustellen, ob ein Endpunkt noch verbunden ist oder nicht
10 — pong frame: der pong frame wird als Antwort verwendet, wenn ein Endpunkt einen ping frame empfängt. In einer solchen Situation muss der Empfänger so schnell wie möglich mit einem entsprechenden Pong-Frame antworten. Normalerweise reicht es aus, die im Ping-Frame enthaltene Daten zu zurückzuschicken.
Dies sind die einzigen Basis-Opcodes, die von jedem Websocket unterstützt werden sollten. Das Protokoll erlaubt Websocket-basierten APIs oder Websocket-Unterprotokollen, diese reservierten Werte zu erweitern.
Der letzte wichtige Aspekt bei Frames ist die Maskierung. RFC 6455 verlangt, dass alle Frames, die von einem Client an einen Server gesendet werden, maskiert werden. Die Maskierung dient als eine grundlegende Sicherheitsmaßnahme für das Websocket-Protokoll. Dabei wird die Nutzlast mit einem zufällig generierten 4-Byte-Wert, dem sogenannten Schlüssel, unter Verwendung eines vordefinierten Algorithmus verschlüsselt. Man kann es sich als eine Art Datenverschleierung vorstellen. Der Algorithmus ist in dem Dokument RFC 6455 dokumentiert. Jeder vom Client gesendete Frame muss einen frisch generierten, zufälligen Schlüssel verwenden, auch bei fragmentierten Frames. In diesem Abschnitt wurden die wichtigsten Eigenschaften des Websocket-Protokolls kurz erläutert. Für tiefergehende Informationen können alle Details in der RFC6455-Dokumentation nachgelesen werden. Bewaffnet mit diesem Wissen denke ich, dass das Verständnis der Code-Implementierung viel einfacher sein wird. Mql5 Websocket-Client - Übersicht über die Bibliothek Zu Beginn wird der Code in drei Klassen aufgeteilt Jeder vom Client gesendete Frame muss einen frisch generierten, zufälligen Schlüssel verwenden, auch bei fragmentierten Frames.
In diesem Abschnitt wurden die wichtigsten Eigenschaften des Websocket-Protokolls kurz erläutert. Für tiefergehende Informationen können alle Details in der RFC6455-Dokumentation nachgelesen werden. Bewaffnet mit diesem Wissen denke ich, dass das Verständnis der Code-Implementierung viel einfacher sein wird.
Mql5 Websocket-Client — Übersicht über die Bibliothek
Zu Beginn wird der Code in drei Klassen aufgeteilt
CSocket — kapselt die Netzwerkfunktionen der Mql5 App.
CFrame — die Frame-Klasse repräsentiert einen Websocket-Frame und wird in erster Linie verwendet, um von einem Server empfangene Frames zu dekodieren.
CWebSocketClient — repräsentiert den Websocket-Client selbst.
CSocket
//+------------------------------------------------------------------+ //| structs | //+------------------------------------------------------------------+ struct CERT { string cert_subject; string cert_issuer; string cert_serial; string cert_thumbprint; datetime cert_expiry; }; //+------------------------------------------------------------------+ //| Class CSocket. | //| Purpose: Base class of socket operations. | //| | //+------------------------------------------------------------------+ class CSocket { private: static int m_usedsockets; // tracks number of sockets in use in single program bool m_log; // logging state bool m_usetls; // tls state uint m_tx_timeout; // send system socket timeout in milliseconds uint m_rx_timeout; // receive system socket timeout in milliseconds int m_socket; // socket handle string m_address; // server address uint m_port; // port CERT m_cert; // Server certificate info public: CSocket(); ~CSocket(); //--- methods to get private properties int SocketID(void) const { return(m_socket); } string Address(void) const { return(m_address); } uint Port(void) const { return(m_port); } bool IsSecure(void) const { return(m_usetls); } uint RxTimeout(void) const { return(m_rx_timeout); } uint TxTimeout(void) const { return(m_tx_timeout); } bool ServerCertificate(CERT& certificate); //--- methods to set private properties bool SetTimeouts(uint tx_timeout, uint rx_timeout); //--- general methods for working sockets void Log(const string custom_message,const int line,const string func); static uint SocketsInUse(void) { return(m_usedsockets); } bool Open(const string server,uint port,uint timeout,bool use_tls=false,bool enablelog=false); bool Close(void); uint Readable(void); bool Writable(void); bool IsConnected(void); int Read(uchar& out[],uint out_len,uint ms_timeout,bool read_available); int Send(uchar& in[],uint in_len); }; int CSocket::m_usedsockets=0; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CSocket::CSocket():m_socket(INVALID_HANDLE), m_address(""), m_port(0), m_usetls(false), m_log(false), m_rx_timeout(150), m_tx_timeout(150) { ZeroMemory(m_cert); } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CSocket::~CSocket() { //--- check handle if(m_socket!=INVALID_HANDLE) Close(); } //+------------------------------------------------------------------+ //| set system socket timeouts | //+------------------------------------------------------------------+ bool CSocket::SetTimeouts(uint tx_timeout,uint rx_timeout) { if(m_socket==INVALID_HANDLE) { Log("Invalid socket",__LINE__,__FUNCTION__); return(false); } if(SocketTimeouts(m_socket,tx_timeout,rx_timeout)) { m_tx_timeout=tx_timeout; m_rx_timeout=rx_timeout; Log("Socket Timeouts set",__LINE__,__FUNCTION__); return(true); } return(false); } //+------------------------------------------------------------------+ //| certificate | //+------------------------------------------------------------------+ bool CSocket::ServerCertificate(CERT& certificate) { if(m_socket==INVALID_HANDLE) { Log("Invalid socket",__LINE__,__FUNCTION__); return(false); } if(SocketTlsCertificate(m_socket,m_cert.cert_subject,m_cert.cert_issuer,m_cert.cert_serial,m_cert.cert_thumbprint,m_cert.cert_expiry)) { certificate=m_cert; Log("Server certificate retrieved",__LINE__,__FUNCTION__); return(true); } return(false); } //+------------------------------------------------------------------+ //|connect() | //+------------------------------------------------------------------+ bool CSocket::Open(const string server,uint port,uint timeout,bool use_tls=false,bool enablelog=false) { if(m_socket!=INVALID_HANDLE) Close(); if(m_usedsockets>=128) { Log("Too many sockets open",__LINE__,__FUNCTION__); return(false); } m_usetls=use_tls; m_log=enablelog; m_socket=SocketCreate(); if(m_socket==INVALID_HANDLE) { Log("Invalid socket",__LINE__,__FUNCTION__); return(false); } ++m_usedsockets; m_address=server; if(port==0) { if(m_usetls) m_port=443; else m_port=80; } else m_port=port; //--- if(!m_usetls && m_port==443) m_usetls=true; //--- Log("Connecting to "+m_address,__LINE__,__FUNCTION__); //--- if(m_usetls) { if(m_port!=443) { if(SocketConnect(m_socket,server,port,timeout)) return(SocketTlsHandshake(m_socket,server)); } else { return(SocketConnect(m_socket,server,port,timeout)); } } return(SocketConnect(m_socket,server,port,timeout)); } //+------------------------------------------------------------------+ //|close() | //+------------------------------------------------------------------+ bool CSocket::Close(void) { //--- if(m_socket==INVALID_HANDLE) { Log("Socket Disconnected",__LINE__,__FUNCTION__); return(true); } //--- if(SocketClose(m_socket)) { m_socket=INVALID_HANDLE; --m_usedsockets; Log("Socket Disconnected from "+m_address,__LINE__,__FUNCTION__); m_address=""; ZeroMemory(m_cert); return(true); } //--- Log("",__LINE__,__FUNCTION__); return(false); } //+------------------------------------------------------------------+ //|readable() | //+------------------------------------------------------------------+ uint CSocket::Readable(void) { if(m_socket==INVALID_HANDLE) { Log("Invalid socket",__LINE__,__FUNCTION__); return(0); } //--- Log("Is Socket Readable ",__LINE__,__FUNCTION__); //--- return(SocketIsReadable(m_socket)); } //+------------------------------------------------------------------+ //|writable() | //+------------------------------------------------------------------+ bool CSocket::Writable(void) { if(m_socket==INVALID_HANDLE) { Log("Invalid socket",__LINE__,__FUNCTION__); return(false); } //--- Log("Is Socket Writable ",__LINE__,__FUNCTION__); //--- return(SocketIsWritable(m_socket)); } //+------------------------------------------------------------------+ //|isconnected() | //+------------------------------------------------------------------+ bool CSocket::IsConnected(void) { if(m_socket==INVALID_HANDLE) { Log("Invalid socket",__LINE__,__FUNCTION__); return(false); } //--- Log("Is Socket Connected ",__LINE__,__FUNCTION__); //--- return(SocketIsConnected(m_socket)); } //+------------------------------------------------------------------+ //|read() | //+------------------------------------------------------------------+ int CSocket::Read(uchar& out[],uint out_len,uint ms_timeout,bool read_available=false) { if(m_socket==INVALID_HANDLE) { Log("Invalid socket",__LINE__,__FUNCTION__); return(-1); } //--- Log("Reading from "+m_address,__LINE__,__FUNCTION__); if(m_usetls) { if(read_available) return(SocketTlsReadAvailable(m_socket,out,out_len)); else return(SocketTlsRead(m_socket,out,out_len)); } else return(SocketRead(m_socket,out,out_len,ms_timeout)); return(-1); } //+------------------------------------------------------------------+ //|send() | //+------------------------------------------------------------------+ int CSocket::Send(uchar& in[],uint in_len) { if(m_socket==INVALID_HANDLE) { Log("Invalid socket",__LINE__,__FUNCTION__); return(-1); } //--- Log("Sending to "+m_address,__LINE__,__FUNCTION__); //--- if(m_usetls) return(SocketTlsSend(m_socket,in,in_len)); else return(SocketSend(m_socket,in,in_len)); //--- return(-1); } //+------------------------------------------------------------------+ //|log() | //+------------------------------------------------------------------+ void CSocket::Log(const string custom_message,const int line,const string func) { if(m_log) { //--- int eid=GetLastError(); //--- if(eid!=0) { PrintFormat("[MQL error ID: %d][%s][Line: %d][Function: %s]",eid,custom_message,line,func); ResetLastError(); return; } if(custom_message!="") PrintFormat("[%s][Line: %d][Function: %s]",custom_message,line,func); } //--- } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Die Klasse socket definiert struct CERT, das die Server-Zertifikatsinformationen kapselt.
- cert_subject — Name des Zertifikatsinhabers
- cert_issuer — Name des Zertifikatsausstellers
- cert_serial — Seriennummer des Zertifikats
- cert_thumbprint — SHA-1-Hash des Zertifikats
- cert_expiry — Ablaufdatum des Zertifikats
Methoden zum Abrufen privater Eigenschaften:
SocketID — gibt das Socket-Handle für einen erfolgreich erstellten Socket zurück.
Address — gibt die Remote-Adresse, mit der der Socket verbunden ist, als String zurück
Port — gibt den entfernten Port zurück, mit dem ein aktiver Socket verbunden ist
IsSecure — gibt true oder false zurück, je nachdem, ob der Socket TLS-Sicherheit aktiviert hat oder nicht.
RxTimeout — gibt den eingestellten Timeout in Millisekunden für das Lesen von einem Socket zurück.
TxTimeout — gibt den eingestellten Timeout in Millisekunden für das Schreiben in einen Socket zurück.
ServerCertificate — gibt die Server-Zertifikatsinformationen für einen Server zurück, mit dem der Socket verbunden ist.
SocketsInUse — gibt die Gesamtzahl der Sockets zurück, die derzeit in einem einzelnen Programm verwendet werden.
Methoden zum Setzen von privaten Eigenschaften.
SetTimeouts — setzt die Timeouts in Millisekunden für das Lesen und Schreiben auf einen Socket.
allgemeine Methoden zum Arbeiten mit Sockets
Log — Utility-Methode zum Protokollieren der Aktivitäten eines Sockets. Um Meldungen in das Journal des Terminals auszugeben, muss das Logging beim Initialisieren eines Sockets mit der Open-Methode eingestellt werden.
Open — Methode zum Aufbau einer Verbindung zu einem entfernten Server, wobei ein neuer Socket erzeugt wird. Die Methode
Close — Methode zum Trennen der Verbindung zu einem entfernten Server und Deinitialisieren eines Sockets.
Readable — gibt die Anzahl der Bytes zurück, die auf einem Socket zum Lesen zur Verfügung stehen
Writable — fragt ab, ob ein Socket für Sendevorgänge verfügbar ist.
IsConnected — prüft, ob eine Socket-Verbindung noch aktiv ist.
Read — Liest Daten von einem Socket
Send — Methode zur Durchführung von Sendeoperationen an einem aktiven Socket.
CFrame
//+------------------------------------------------------------------+ //| enums | //+------------------------------------------------------------------+ enum ENUM_FRAME_TYPE // type of websocket frames (ie, message types) { CONTINUATION_FRAME=0x0, TEXT_FRAME=0x1, BINARY_FRAME= 0x2, CLOSE_FRAME = 8, PING_FRAME = 9, PONG_FRAME = 0xa, }; //+------------------------------------------------------------------+ //| class frame | //| represents a websocket message frame | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class CFrame { private: uchar m_array[]; uchar m_isfinal; ENUM_FRAME_TYPE m_msgtype; int Resize(int size) {return(ArrayResize(m_array,size,size));} public: CFrame():m_isfinal(0),m_msgtype(0) { } ~CFrame() { } int Size(void) {return(ArraySize(m_array));} bool Add(const uchar value); bool Fill(const uchar &array[],const int src_start,const int count); void Reset(void); uchar operator[](int index); string ToString(void); ENUM_FRAME_TYPE MessageType(void) { return(m_msgtype);} bool IsFinal(void) { return(m_isfinal==1);} void SetMessageType(ENUM_FRAME_TYPE mtype) { m_msgtype=mtype;} void SetFinal(void) { m_isfinal=1;} }; //+------------------------------------------------------------------+ //| Receiving an element by index | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ uchar CFrame::operator[](int index) { static uchar invalid_value; //--- int max=ArraySize(m_array)-1; if(index<0 || index>=ArraySize(m_array)) { PrintFormat("%s index %d is not in range (0-%d)!",__FUNCTION__,index,max); return(invalid_value); } //--- return(m_array[index]); } //+------------------------------------------------------------------+ //| Adding element | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool CFrame::Fill(const uchar &array[],const int src_start,const int count) { int p_size=Size(); //--- int size=Resize(p_size+count); //--- if(size>0) return(ArrayCopy(m_array,array,p_size,src_start,count)==count); else return(false); //--- } //+------------------------------------------------------------------+ //| Assigning for the array | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool CFrame::Add(const uchar value) { int size=Resize(Size()+1); //--- if(size>0) m_array[size-1]=value; else return(false); //--- return(true); //--- } //+------------------------------------------------------------------+ //| Reset | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void CFrame::Reset(void) { if(Size()) ArrayFree(m_array); //--- m_isfinal=0; m_msgtype=0; } //+------------------------------------------------------------------+ //|converting array to string | //+------------------------------------------------------------------+ string CFrame::ToString(void) { if(Size()) if(m_msgtype==CLOSE_FRAME) return(CharArrayToString(m_array,2,WHOLE_ARRAY,CP_UTF8)); else return(CharArrayToString(m_array,0,WHOLE_ARRAY,CP_UTF8)); else return(NULL); }
Die Klasse Frame definiert die Enumeration ENUM_FRAME_TYPE, die die verschiedenen Frame-Typen beschreibt, wie sie im Websocket-Protokoll dokumentiert sind.
Die Instanzen der Klasse CFrame repräsentieren einen einzelnen Frame, der vom Server empfangen wurde. Das bedeutet, dass eine komplette Nachricht aus einer Kollektion von Frames bestehen kann. Die Klasse ermöglicht Funktionen zur Abfrage der verschiedenen Eigenschaften jedes Frames, einschließlich der einzelnen Byte-Werte, aus denen sich ein Frame zusammensetzt.Die Methode Size gibt die Größe eines Frames in Bytes zurück. Da die Klasse ein Array vom Typ unsigned character als Container für einen Frame verwendet. Diese Methode gibt einfach die Größe des zugrunde liegenden Arrays zurück. Die Methode MessageType gibt den Typ des Frames als Typ ENUM_FRAME_TYPE zurück Die Methode IsFinal prüft, ob es sich bei dem Frame um den letzten bzw. finalen Frame handelt, d.h. es wird davon ausgegangen, dass die empfangenen Daten vollständig sind, so dass man zwischen einer fragmentierten und damit unvollständigen Nachricht und einer vollständigen unterscheiden kann. operator[] - die Überladung mit dem tiefgestellten Operator ermöglicht den individuellen Abruf eines beliebigen Elements im Frame im Array-Format. Die Klasse CFrame wird im Websocket-Client verwendet, da dieser von einem CSocket-Objekt liest. Die Methoden, die zum Füllen eines Frames verwendet werden, sind Add und Fill. Diese erlauben das Füllen eines Frames entweder durch ein einzelnes Element oder durch ein entsprechendes Array. Mit der Utility-Methode Reset kann ein Frame geleert und seine Eigenschaften zurückgesetzt werden, während die Methode ToString ein praktisches Werkzeug ist, um den Frame-Inhalt in einen bekannten String-Wert zu konvertieren. CWebSocketClient Die Klasse hat Konstanten, die als #defines implementiert sind. Die vorangestellten HEADER-Symbole sind mit den http-Header-Feldern verknüpft, die zum Erstellen des Eröffnungs-Handshakes benötigt werden. GUID ist ein global eindeutiger Bezeichner, der vom Websocket-Protokoll auf der Server-Seite verwendet wird, wenn ein Teil der Antwort-Header generiert wird . Unsere Klasse verwendet sie, um die Korrektheit des Handshake-Prozesses zu bestätigen und auch zu demonstrieren, aber im Wesentlichen ist sie unnötig, der Client muss nur auf die Existenz des |Sec-WebSocket-Accept| Header-Feldes prüfen, um einen erfolgreichen Handshake zu bestätigen.
Die Methode Size gibt die Größe eines Frames in Bytes zurück. Da die Klasse ein Array vom Typ unsigned character als Container für einen Frame verwendet. Diese Methode gibt einfach die Größe des zugrunde liegenden Arrays zurück.
Die Methode MessageType gibt den Typ des Frames als Typ ENUM_FRAME_TYPE zurück
Die Methode IsFinal prüft, ob es sich bei dem Frame um den letzten bzw. finalen Frame handelt, d.h. es wird davon ausgegangen, dass die empfangenen Daten vollständig sind, so dass man zwischen einer fragmentierten und damit unvollständigen Nachricht und einer vollständigen unterscheiden kann.
operator[] - die Überladung mit dem tiefgestellten Operator ermöglicht den individuellen Abruf eines beliebigen Elements im Frame im Array-Format.
Die Klasse CFrame wird im Websocket-Client verwendet, da dieser von einem CSocket-Objekt liest. Die Methoden, die zum Füllen eines Frames verwendet werden, sind Add und Fill. Diese erlauben das Füllen eines Frames entweder durch ein einzelnes Element oder durch ein entsprechendes Array.
Mit der Utility-Methode Reset kann ein Frame geleert und seine Eigenschaften zurückgesetzt werden, während die Methode ToString ein praktisches Werkzeug ist, um den Frame-Inhalt in einen bekannten String-Wert zu konvertieren.
CWebSocketClient
Die Klasse hat Konstanten, die über #defines implementiert sind. Die vorangestellten HEADER-Symbole sind mit den http-Header-Feldern verknüpft, die zum Erstellen des Eröffnungs-Handshakes benötigt werden. GUID ist ein global eindeutiger Bezeichner, der vom Websocket-Protokoll auf der Server-Seite verwendet wird, wenn ein Teil der Antwort-Header generiert wird. Unsere Klasse verwendet sie, um die Korrektheit des Handshake-Prozesses zu bestätigen und auch zu demonstrieren, aber im Wesentlichen ist sie unnötig, der Client muss nur auf die Existenz des |Sec-WebSocket-Accept| Header-Feldes prüfen, um einen erfolgreichen Handshake zu bestätigen.
#include <Socket.mqh> #include <Frame.mqh> //+------------------------------------------------------------------+ //| defines | //+------------------------------------------------------------------+ #define SH1 "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" #define HEADER_EOL "\r\n" #define HEADER_GET "GET /" #define HEADER_HOST "Host: " #define HEADER_UPGRADE "Upgrade: websocket"+HEADER_EOL #define HEADER_CONNECTION "Connection: Upgrade"+HEADER_EOL #define HEADER_KEY "Sec-WebSocket-Key: " #define HEADER_WS_VERSION "Sec-WebSocket-Version: 13"+HEADER_EOL+HEADER_EOL #define HEADER_HTTP " HTTP/1.1"
Der Enum-Typ ENUM_STATUS_CLOSE_CODE listet die Close-Codes auf, die man zusammen mit einem Close-Frame senden oder empfangen kann. Während das Enum ENUM_WEBSOCKET_CLIENT_STATE die verschiedenen Zustände symbolisiert, die der Websocket einnehmen kann.
Geschlossen ist der Ausgangszustand, bevor ein Socket dem Client zugewiesen wird, oder nachdem ein Client eine Verbindung abgebrochen hat und der zugrunde liegende Socket geschlossen wurde.
Wenn eine anfängliche Verbindung hergestellt wird, bevor der Eröffnungs-Handshake ("Header") gesendet wird, befindet sich der Client in einem Verbindungszustand. Wenn der Eröffnungs-Handshake gesendet wurde und eine Antwort empfangen wird, die die Verwendung des Websocket-Protokolls erlaubt, dann ist der Client verbunden.
Der schließende Zustand tritt ein, wenn entweder der Client zum ersten Mal seit der Client-Initialisierung einen Close-Frame empfängt oder der Client den ersten Close-Frame sendet, um dem Server mitzuteilen, dass er die Verbindung trennt. In einem schließenden Zustand kann der Client nur Close-Frames an den Server senden, jeder Versuch, eine andere Art von Frame zu senden, schlägt fehl. Denken Sie daran, dass der Server in einem schließenden Zustand möglicherweise nicht antwortet, da er nicht verpflichtet ist, den Dienst fortzusetzen, sobald er entweder eine Schließbenachrichtigung gesendet oder empfangen hat.
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ enum ENUM_CLOSE_CODE // possible reasons for disconnecting sent with a close frame { NORMAL_CLOSE = 1000, // normal closure initiated by choice GOING_AWAY_CLOSE, // close code for client navigating away from end point, used in browsers PROTOCOL_ERROR_CLOSE, // close caused by some violation of a protocol, usually application defined FRAME_TYPE_ERROR_CLOSE, // close caused by an endpoint receiving frame type that is not supportted or allowed UNDEFINED_CLOSE_1, // close code is not defined by websocket protocol UNUSED_CLOSE_1, // unused UNUSED_CLOSE_2, // values ENCODING_TYPE_ERROR_CLOSE, // close caused data in message is of wrong encoding type, usually referring to strings APP_POLICY_ERROR_CLOSE, // close caused by violation of user policy MESSAGE_SIZE_ERROR_CLOSE, // close caused by endpoint receiving message that is too large EXTENSION_ERROR_CLOSE, // close caused by non compliance to or no support for specified extension of websocket protocol SERVER_SIDE_ERROR_CLOSE, // close caused by some error that occurred on the server UNUSED_CLOSE_3 = 1015, // unused }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ enum ENUM_WEBSOCKET_STATE { CLOSED=0, CLOSING, CONNECTING, CONNECTED };
Die Methode ClientState ruft die Eigenschaft ab, die den Verbindungsstatus eines beliebigen Websocket-Clients definiert.
//+------------------------------------------------------------------+ //| ClientState() | //+------------------------------------------------------------------+ ENUM_WEBSOCKET_STATE CWebSocketClient::ClientState(void) { if(m_socket.IsConnected()) return(m_wsclient); //--- if(m_wsclient!=CLOSED) { m_socket.Close(); m_wsclient=CLOSED; } //--- return(m_wsclient); }
SetMaxSendSize() wird verwendet, um die Frame-Fragmentierungseigenschaft des Websocket-Clients zu konfigurieren. Diese Methode legt die maximale Größe in Bytes für einen einzelnen Frame fest, der vom Client an den Server gesendet wird. Das macht den Client flexibel für die Verwendung mit jeder API, die Frame-Größenbegrenzungen erzwingt.
void SetMaxSendSize(int maxsend) {if(maxsend>=0) m_maxsendsize=maxsend; else m_maxsendsize=0; }
Mit der Methode Connect wird eine Websocket-Verbindung aufgebaut. Der Parameter secure ist ein Boolean, um das Websocket mit TLS zu konfigurieren oder nicht. Die Methode ruft zunächst die Methode open der Klasse CSocket auf, um eine initiale TCP-Verbindung aufzubauen. Bei Erfolg wechselt der Zustand des Websockets auf Verbinden, woraufhin die Upgrade-Helfer-Methode ins Spiel kommt. Ihre Aufgabe ist die Erstellung des erforderlichen Http-Headers für die Umschaltung auf das Websocket-Protokoll. Schließlich wird der Zustand des Websockets beim Verlassen der Funktion überprüft.
//+------------------------------------------------------------------+ //| Connect(): Used to establish connection to websocket server | //+------------------------------------------------------------------+ bool CWebSocketClient::Connect(const string url,const uint port,const uint timeout,bool use_tls=false,bool enablelog=false) { reset(); //--- m_timeout=timeout; //--- if(!m_socket.Open(url,port,m_timeout,use_tls,enablelog)) { m_socket.Log("Connect error",__LINE__,__FUNCTION__); return(false); } else m_wsclient=CONNECTING; //--- if(!upgrade()) return(false); //--- m_socket.Log("ws client state "+EnumToString(m_wsclient),__LINE__,__FUNCTION__); //--- if(m_wsclient!=CONNECTED) { m_wsclient=CLOSED; m_socket.Close(); reset(); } //--- return(m_wsclient==CONNECTED); }
Zum Schließen oder Trennen einer Verbindung wird die Methode ClientClose verwendet. Sie hat zwei Standardparameter, den Schließcode und einen Nachrichtentext, der als Schließrahmen an den Server gesendet wird. Der Nachrichtentext wird abgeschnitten, wenn er größer als das Limit von 122 Zeichen ist. Gemäß der Websocket-Spezifikation sollte, wenn einer der beiden Endpunkte (Server oder Client) einen Close-Frame empfängt (zum ersten Mal), der Empfänger antworten und der Sender sollte eine Antwort als Bestätigung der Close-Anfrage erwarten. Wie aus dem Code von clientClose ersichtlich ist, wird der zugrunde liegende TCP-Socket geschlossen, sobald der Close-Frame gesendet wurde, ohne auf eine Antwort zu warten, selbst wenn das Schließen vom Client initiiert wurde. Das Warten auf eine Antwort an diesem Punkt des Lebenszyklus des Clients scheint eine Verschwendung von Ressourcen zu sein und wurde daher nicht implementiert.
//+------------------------------------------------------------------+ //| Close() inform server client is disconnecting | //+------------------------------------------------------------------+ bool CWebSocketClient::Close(ENUM_CLOSE_CODE close_code=NORMAL_CLOSE,const string close_reason="") { ClientState(); //--- if(m_wsclient==0) { m_socket.Log("Client Disconnected",__LINE__,__FUNCTION__); //--- return(true); } //--- if(ArraySize(m_txbuf)<=0) { if(close_reason!="") { int len=StringToCharArray(close_reason,m_txbuf,2,120,CP_UTF8)-1; if(len<=0) return(false); else ArrayRemove(m_txbuf,len,1); } else { if(ArrayResize(m_txbuf,2)<=0) { m_socket.Log("array resize error",__LINE__,__FUNCTION__); return(false); } } m_txbuf[0]=(uchar)(close_code>>8) & 0xff; m_txbuf[1]=(uchar)(close_code>>0) & 0xff; //--- } //--- m_msgsize=ArraySize(m_txbuf); m_sent=false; //--- send(CLOSE_FRAME); //--- m_socket.Close(); //--- reset(); //--- return(true); //--- }
Wenn Sie beliebige Daten an einen Server senden, können Sie zwischen zwei Methoden wählen. SendString nimmt einen String und SendData nimmt ein Array als Eingabe.
SendPing und SendPong sind spezielle Methoden zum Senden von Pings und Pongs. Beide erlauben einen optionalen Nachrichtentext, für den das Limit von 122 Zeichen gilt.
Alle öffentlichen Sendemethoden packen ihre jeweiligen Eingaben in das Array m_txbuff. Die private Sendemethode legt den Frame-Typ fest und verwendet filltxbuffer(), um die Fragmentierung der Nachricht abhängig vom Wert der Eigenschaft m_maxsendsize zu aktivieren. FillTxbuffer() bereitet einen einzelnen Frame vor und packt ihn in das Array m_send. Sobald m_send vorbereitet ist, wird es an den Server gesendet. All dies wird in einer Schleife durchgeführt, bis der gesamte Inhalt von m_txbuffer gesendet wurde.
//+------------------------------------------------------------------+ //| Send() sends text data to websocket server | //+------------------------------------------------------------------+ int CWebSocketClient::SendString(const string message) { ClientState(); //--- if(m_wsclient==CLOSED || m_wsclient==CLOSING) { m_socket.Log("invalid ws client handle",__LINE__,__FUNCTION__); return(0); } //--- if(message=="") { m_socket.Log("no message specified",__LINE__,__FUNCTION__); return(0); } //--- int len=StringToCharArray(message,m_txbuf,0,WHOLE_ARRAY,CP_UTF8)-1; if(len<=0) { m_socket.Log("string char array error",__LINE__,__FUNCTION__); return(0); } else ArrayRemove(m_txbuf,len,1); //--- m_msgsize=ArraySize(m_txbuf); m_sent=false; //--- return(send(TEXT_FRAME)); } //+------------------------------------------------------------------+ //| Send() sends user supplied array buffer | //+------------------------------------------------------------------+ int CWebSocketClient::SendData(uchar &message_buffer[]) { ClientState(); //--- if(m_wsclient==CLOSED || m_wsclient==CLOSING) { m_socket.Log("invalid ws client handle",__LINE__,__FUNCTION__); return(0); } //--- if(ArraySize(message_buffer)==0) { m_socket.Log("array is empty",__LINE__,__FUNCTION__); return(0); } //--- if(ArrayResize(m_txbuf,ArraySize(message_buffer))<0) { m_socket.Log("array resize error",__LINE__,__FUNCTION__); return(0); } else ArrayCopy(m_txbuf,message_buffer); //--- m_msgsize=ArraySize(m_txbuf); m_sent=false; //--- return(send(BINARY_FRAME)); } //+------------------------------------------------------------------+ //| SendPong() sends pong response upon receiving ping | //+------------------------------------------------------------------+ int CWebSocketClient::SendPong(const string msg="") { ClientState(); //--- if(m_wsclient==CLOSED || m_wsclient==CLOSING) { m_socket.Log("invalid ws client handle",__LINE__,__FUNCTION__); return(0); } //--- if(ArraySize(m_txbuf)<=0) { if(msg!="") { int len=StringToCharArray(msg,m_txbuf,0,122,CP_UTF8)-1; if(len<=0) { m_socket.Log("string to char array error",__LINE__,__FUNCTION__); return(0); } else ArrayRemove(m_txbuf,len,1); } } //--- m_msgsize=ArraySize(m_txbuf); m_sent=false; //--- return(send(PONG_FRAME)); } //+------------------------------------------------------------------+ //| SendPing() ping the server | //+------------------------------------------------------------------+ int CWebSocketClient::SendPing(const string msg="") { ClientState(); //--- if(m_wsclient==CLOSED || m_wsclient==CLOSING) { m_socket.Log("invalid ws client handle",__LINE__,__FUNCTION__); return(0); } //--- if(ArraySize(m_txbuf)<=0) { if(msg!="") { int len=StringToCharArray(msg,m_txbuf,0,122,CP_UTF8)-1; if(len<=0) { m_socket.Log("string to char array error",__LINE__,__FUNCTION__); return(0); } else ArrayRemove(m_txbuf,len,1); } } //--- m_msgsize=ArraySize(m_txbuf); m_sent=false; //--- return(send(PING_FRAME)); }
//+------------------------------------------------------------------+ //|prepareSendBuffer()prepares array buffer for socket dispatch | //+------------------------------------------------------------------+ bool CWebSocketClient::fillTxBuffer(ENUM_FRAME_TYPE ftype) { uchar header[]; static int it; static int start; uchar masking_key[4]={0}; int maxsend=(m_maxsendsize<7)?m_msgsize:((m_maxsendsize<126)?m_maxsendsize-6:((m_maxsendsize<65536)?m_maxsendsize-8:m_maxsendsize-14)); //--- for(int i=0; i<4; i++) { masking_key[i]=(uchar)(255*MathRand()/32767); } //--- m_socket.Log("[send]max size - "+IntegerToString(maxsend),__LINE__,__FUNCTION__); m_socket.Log("[send]should be max size - "+IntegerToString(m_maxsendsize),__LINE__,__FUNCTION__); int message_size=(((start+maxsend)-1)<=(m_msgsize-1))?maxsend:m_msgsize%maxsend; bool isfinal=((((start+maxsend)-1)==(m_msgsize-1)) || (message_size<maxsend) ||(message_size<=0))?true:false; bool isfirst=(start==0)?true:false; //--- m_socket.Log("[send]message size - "+IntegerToString(message_size),__LINE__,__FUNCTION__); if(isfirst) m_socket.Log("[send]first frame",__LINE__,__FUNCTION__); if(isfinal) m_socket.Log("[send]final frame",__LINE__,__FUNCTION__); //--- if(ArrayResize(header,2+(message_size>=126 ? 2 : 0)+(message_size>=65536 ? 6 : 0)+(4))<0) { m_socket.Log("array resize error",__LINE__,__FUNCTION__); return(false); } //header[0] = (isfinal)? (0x80 | 0x1) :( ); switch(ftype) { case CLOSE_FRAME: header[0]=uchar(0x80|CLOSE_FRAME); m_socket.Log("[building]close frame",__LINE__,__FUNCTION__); break; case PING_FRAME: header[0]=uchar(0x80|PING_FRAME); m_socket.Log("[building]ping frame",__LINE__,__FUNCTION__); break; case PONG_FRAME: header[0]=uchar(0x80|PONG_FRAME); m_socket.Log("[building]pong frame",__LINE__,__FUNCTION__); break; default: header[0]=(isfinal)? 0x80:0x0; m_socket.Log("[building]"+EnumToString(ftype),__LINE__,__FUNCTION__); if(isfirst) header[0]|=uchar(ftype); break; } //--- if(message_size<126) { header[1] = (uchar)(message_size & 0xff) | 0x80; header[2] = masking_key[0]; header[3] = masking_key[1]; header[4] = masking_key[2]; header[5] = masking_key[3]; } else if(message_size<65536) { header[1] = 126 | 0x80; header[2] = (uchar)(message_size >> 8) & 0xff; header[3] = (uchar)(message_size >> 0) & 0xff; header[4] = masking_key[0]; header[5] = masking_key[1]; header[6] = masking_key[2]; header[7] = masking_key[3]; } else { header[1] = 127 | 0x80; header[2] = (uchar)(message_size >> 56) & 0xff; header[3] = (uchar)(message_size >> 48) & 0xff; header[4] = (uchar)(message_size >> 40) & 0xff; header[5] = (uchar)(message_size >> 32) & 0xff; header[6] = (uchar)(message_size >> 24) & 0xff; header[7] = (uchar)(message_size >> 16) & 0xff; header[8] = (uchar)(message_size >> 8) & 0xff; header[9] = (uchar)(message_size >> 0) & 0xff; header[10] = masking_key[0]; header[11] = masking_key[1]; header[12] = masking_key[2]; header[13] = masking_key[3]; } //--- if(ArrayResize(m_send,ArraySize(header),message_size)<0) { m_socket.Log("array resize error",__LINE__,__FUNCTION__); return(false); } //--- ArrayCopy(m_send,header,0,0); //--- if(message_size) { if(ArrayResize(m_send,ArraySize(header)+message_size)<0) { m_socket.Log("array resize error",__LINE__,__FUNCTION__); return(false); } //--- ArrayCopy(m_send,m_txbuf,ArraySize(header),start,message_size); //--- int bufsize=ArraySize(m_send); //--- int message_offset=bufsize-message_size; //--- for(int i=0; i<message_size; i++) { m_send[message_offset+i]^=masking_key[i&0x3]; } } //--- if(isfinal) { it=0; start=0; m_sent=true; ArrayFree(m_txbuf); } else { it++; start=it*maxsend; } //--- return(true); }
//+------------------------------------------------------------------+ //|int sendMessage() helper | //+------------------------------------------------------------------+ int CWebSocketClient::send(ENUM_FRAME_TYPE frame_type) { //--- bool done=false; int bytes_sent=0,sum_sent=0; while(!m_sent) { done=fillTxBuffer(frame_type); if(done && m_socket.Writable()) { bytes_sent=m_socket.Send(m_send,(uint)ArraySize(m_send)); //--- if(bytes_sent<0) break; else { sum_sent+=bytes_sent; ArrayFree(m_send); } //--- } else break; } //--- if(ArraySize(m_send)>0) ArrayFree(m_send); //--- m_socket.Log("",__LINE__,__FUNCTION__); //--- return(sum_sent); }
Alle Daten, die an den Client gesendet werden, werden mit der privaten Methode fillrxbuffer() im Array m_rxbuff gepuffert, wenn die öffentliche Methode Readable() aufgerufen wird. Sie gibt die Größe des Arrays m_rxbuff zurück, was die Verfügbarkeit der abrufbaren Daten bei einem Aufruf der Methode Read() anzeigt.
//+------------------------------------------------------------------+ //| receiver()fills rxbuf with raw message | //+------------------------------------------------------------------+ int CWebSocketClient::fillRxBuffer(void) { uint leng=0; int rsp_len=-1; //--- uint timeout_check=GetTickCount()+m_timeout; //--- do { leng=m_socket.Readable(); if(leng) rsp_len+=m_socket.Read(m_rxbuf,leng,m_timeout); leng=0; } while(GetTickCount()<timeout_check); //--- m_socket.Log("receive size "+IntegerToString(rsp_len),__LINE__,__FUNCTION__); //--- int m_rxsize=ArraySize(m_rxbuf); //--- if(m_rxsize<3) return(0); //--- switch((uint)m_rxbuf[1]) { case 126: if(m_rxsize<4) { m_rxsize=0; } break; case 127: if(m_rxsize<10) { m_rxsize=0; } break; default: break; } //--- return(m_rxsize); }
int Readable(void) { return(fillRxBuffer());}
Die Methode Read() nimmt als Eingabe ein Array vom Typ CFrame entgegen, in das alle Frames geschrieben werden sollen. Die Methode verwendet die private Funktion parse(), um die Byte-Daten zu dekodieren, damit sie für die Lesbarkeit korrekt organisiert werden können. Die Methode parse() trennt die Nutzdaten von den Header-Bytes, die beschreibende Informationen über die gerade empfangenen Frames kodieren.
//+------------------------------------------------------------------+ //| parse() cleans up raw data buffer discarding unnecessary elements| //+------------------------------------------------------------------+ bool CWebSocketClient::parse(CFrame &out[]) { uint i,data_len=0,frames=0; uint s=0; m_total_len=0; //--- int shift=0; for(i=0; i<(uint)ArraySize(m_rxbuf); i+=(data_len+shift)) { ++frames; m_socket.Log("value of frame is "+IntegerToString(frames)+" Value of i is "+IntegerToString(i),__LINE__,__FUNCTION__); switch((uint)m_rxbuf[i+1]) { case 126: data_len=((uint)m_rxbuf[i+2]<<8)+((uint)m_rxbuf[i+3]); shift=4; break; case 127: data_len=((uint)m_rxbuf[i+2]<<56)+((uint)m_rxbuf[i+3]<<48)+((uint)m_rxbuf[i+4]<<40)+ ((uint)m_rxbuf[i+5]<<32)+((uint)m_rxbuf[i+6]<<24)+((uint)m_rxbuf[i+7]<<16)+ ((uint)m_rxbuf[i+8]<<8)+((uint)m_rxbuf[i+9]); shift=10; break; default: data_len=(uint)m_rxbuf[i+1]; shift=2; break; } m_total_len+=data_len; if(data_len>0) { if(ArraySize(out)<(int)frames) { if(ArrayResize(out,frames,1)<=0) { m_socket.Log("array resize error",__LINE__,__FUNCTION__); return(false); } } //--- if(!out[frames-1].Fill(m_rxbuf,i+shift,data_len)) { m_socket.Log("Error adding new frame",__LINE__,__FUNCTION__); return(false); } //--- switch((uchar)m_rxbuf[i]) { case 0x1: if(out[frames-1].MessageType()==0) out[frames-1].SetMessageType(TEXT_FRAME); break; case 0x2: if(out[frames-1].MessageType()==0) out[frames-1].SetMessageType(BINARY_FRAME); break; case 0x80: case 0x81: if(out[frames-1].MessageType()==0) out[frames-1].SetMessageType(TEXT_FRAME); case 0x82: if(out[frames-1].MessageType()==0) out[frames-1].SetMessageType(BINARY_FRAME); m_socket.Log("received last frame",__LINE__,__FUNCTION__); out[frames-1].SetFinal(); break; case 0x88: m_socket.Log("received close frame",__LINE__,__FUNCTION__); out[frames-1].SetMessageType(CLOSE_FRAME); if(m_wsclient==CONNECTED) { ArrayCopy(m_txbuf,m_rxbuf,0,i+shift,data_len); m_wsclient=CLOSING; } break; case 0x89: m_socket.Log("received ping frame",__LINE__,__FUNCTION__); out[frames-1].SetMessageType(PING_FRAME); if(m_wsclient==CONNECTED) ArrayCopy(m_txbuf,m_rxbuf,0,i+shift,data_len); break; case 0x8a: m_socket.Log("received pong frame",__LINE__,__FUNCTION__); out[frames-1].SetMessageType(PONG_FRAME); break; default: break; } } } //--- return(true); }
uint CWebSocketClient::Read(CFrame &out[]) { ClientState(); //--- if(m_wsclient==0) { m_socket.Log("invalid ws client handle",__LINE__,__FUNCTION__); return(0); } //--- int rx_size=ArraySize(m_rxbuf); //--- if(rx_size<=0) { m_socket.Log("receive buffer is empty, Make sure to call Readable first",__LINE__,__FUNCTION__); return(0); } //---clean up rxbuf if(!parse(out)) { ArrayFree(m_rxbuf); return(0); } //--- ArrayFree(m_rxbuf); //--- return(m_total_len); }
Verwenden der Klasse.
Nachdem wir unsere Websocket-Klasse definiert haben, wollen wir uns ansehen, wie man sie in mt5-Programmen verwenden kann. Bevor wir mit der Entwicklung einer Anwendung beginnen, die diese Klasse implementiert, müssen Sie zunächst die Adresse des Remote-Servers, den Sie besuchen möchten, in die Liste der zulässigen Endpunkte in den Terminaleinstellungen eingeben.
Denken Sie daran, WebsocketClient.mqh einzubinden, und führen Sie dann die folgenden Schritte aus:
CWebSocketClient wsc;
- Deklarieren von WebsocketClient-Instanz oder Instanzen
Wenn Sie eine maximale Sendegröße für alle Sendeoperationen im Zusammenhang mit der Verbindung festlegen möchten, ist dies der richtige Zeitpunkt dafür. Bei der Initialisierung der Instanz ist m_maxsendsize gleich 0, was anzeigt, dass es keine Begrenzung der Framegröße gibt.
wsc.SetMaxSendSize(129); // max size in bytes set
- Rufen Sie die connect-Methode mit den entsprechenden Eingabeparametern auf und prüfen Sie das Ergebnis.
if(wsc.Connect(Address,Port,Timeout,usetls,true)) { //// }
Wenn die Verbindung erfolgreich war, können Sie mit dem Senden beginnen oder auf empfangene Nachrichten prüfen. Sie können Daten mit einer der beiden Methoden zum Senden von Daten senden, die für zuvor vorbereitete Arrays verwendet werden.
sent=wsc.SendString("string message");
// or
// prepare and fill arbitrary array[] with data and send
sent=wsc.SendData(array);
Oder wenn Sie nur eine String-Nachricht senden möchten, verwenden Sie die Methode sendstring.
sent=wsc.SendPing("optional message");
Es ist auch möglich, dem Server einen Ping zu senden, der optional von einer Nachricht begleitet werden kann. Wenn nach einem Ping auf eine Antwort gewartet wird, sollte der Antwort-Frame pong das Echo zurückgeben, das mit dem Ping gesendet wurde. Der Client muss dasselbe tun, wenn er einen Ping vom Server empfängt.
if(wsc.Readable()>0) { //read message.... //declare frame object to receive message // and pass it to read method. CFrame msg_frames[]; received=wsc.Read(msg_frames); Print(msg_frames[0].ToString()); if(msg_frames[0].IsFinal()) { Print("\n Final frame received"); }
Prüfen Sie für den Empfang mit der Methode readable, ob Daten aus dem Socket lesbar sind. Wenn die Methode einen lesbaren Socket anzeigt, rufen Sie die Client-Read-Methode mit einem Objekt-Array vom Typ Frame auf. Der Websocket schreibt dann alle Nachrichtenfragmente, die durchgekommen sind, in das Objekt-Array. Hier können Sie die Methoden vom Typ Frame verwenden, um den Inhalt des Frame-Arrays abzufragen. Wie bereits erwähnt, wenn einer der empfangenen Frames ein Ping-Frame ist, wird empfohlen, so schnell wie möglich mit einem Pong zu antworten. Um diese Anforderung zu erfüllen, erstellt der Websocket-Client beim Empfang eines Pings einen Pong-Response-Frame, der Nutzer muss lediglich die Methode zum Senden eines Pings ohne Argumente aufrufen.
Wenn einer der empfangenen Frames ein Close-Frame ist, wechselt der Status des Websocket-Clients in den Closing-Status, was bedeutet, dass der Server eine Close-Anfrage gesendet hat und sich darauf vorbereitet, die Verbindung zu diesem Client zu trennen. Im schließenden Zustand sind die Sendeoperationen eingeschränkt. Der Client kann nur den obligatorischen Close-Response-Frame senden, wiederum genau wie beim Empfang eines Ping-Frames, beim Empfang eines Close-Frames erstellt der Websocket-Client einen Close-Frame, der zum Versand bereit ist.
wsc.Close(NORMAL_CLOSE,"good bye"); // can also be called with out any arguments. // wsc.Close();
Wenn Sie mit dem Websocket-Client fertig sind, rufen Sie die Methode zum Schließen der Verbindung auf. Normalerweise ist es ausreichend, die Methode ohne Angabe von Argumenten aufzurufen, es sei denn, es gibt etwas, worüber Sie den Server informieren möchten. In diesem Fall verwenden Sie einen der Close-Code-Gründe zusammen mit einer kurzen Abschiedsnachricht. Diese Nachricht wird zwangsweise auf 122 Zeichen begrenzt, alles was darüber hinausgeht, wird verworfen.
Ein lokaler Websocket-Server
Zu Testzwecken enthält die diesem Artikel beigefügte Zip-Datei einen Websocket-Server, der einen Echo-Dienst bereitstellt. Der Server wurde mit der libwebsocket-Bibliothek gebaut und der Quellcode steht auf github zum Download bereit. Zum Bauen wird nur Visual Studio benötigt, da alle anderen Abhängigkeiten im github-Repository vorhanden sind.
Ausführen des Servers und Testen der Bibliothek
Um den Echo-Server zu starten, klicken Sie einfach doppelt auf die Anwendung (exe-Datei). Der Server sollte nun funktionieren. Bitte beachten Sie, dass eine installierte Firewall den Server blockieren könnte, geben Sie ihm also einfach die nötigen Rechte. Es ist auch wichtig zu wissen, dass die zugehörigen .dll-Dateien, die im Verzeichnis der Serveranwendung enthalten sind, benötigt werden und der Server ohne sie nicht funktioniert.
Lassen Sie uns schnell unsere WebSocketClient-Klasse testen. Hier ist ein Beispielprogramm.
//+------------------------------------------------------------------+ //| Websocketclient_test.mq5 | //| Copyright 2019, MetaQuotes Software Corp. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2019, MetaQuotes Software Corp." #property link "https://www.mql5.com" #property version "1.00" #property strict #include<WebSocketClient.mqh> input string Address="127.0.0.1"; input int Port =7681; input bool ExtTLS =false; input int MaxSize=256; input int Timeout=5000; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ string _msg="For the mql5-program to operate, it must be compiled (Compile button or F7 key). Compilation should" "pass without errors (some warnings are possible; they should be analyzed). At this process, an" "executable file with the same name and with EX5 extension must be created in the corresponding" "directory, terminal_dir\\MQL5\\Experts, terminal_dir\\MQL5\\indicators or terminal_dir\\MQL5\\scripts." "This file can be run." "Operating features of MQL5 programs are described in the following sections:" "- Program running – order of calling predefined event-handlers." "- Testing trading strategies – operating features of MQL5 programs in the Strategy Tester." "- Client terminal events – description of events, which can be processed in programs." "- Call of imported functions – description order, allowed parameters, search details and call agreement" "for imported functions." "· Runtime errors – getting information about runtime and critical errors." "Expert Advisors, custom indicators and scripts are attached to one of opened charts by Drag'n'Drop" "method from the Navigator window." "For an expert Advisor to stop operating, it should be removed from a chart. To do it select 'Expert'" "'list' in chart context menu, then select an Expert Advisor from list and click 'Remove' button." "Operation of Expert Advisors is also affected by the state of the 'AutoTrading' button." "In order to stop a custom indicator, it should be removed from a chart." "Custom indicators and Expert Advisors work until they are explicitly removed from a chart;" "information about attached Expert Advisors and Indicators is saved between client terminal sessions." "Scripts are executed once and are deleted automatically upon operation completion or change of the" "current chart state, or upon client terminal shutdown. After the restart of the client terminal scripts" "are not started, because the information about them is not saved." "Maximum one Expert Advisor, one script and unlimited number of indicators can operate in one chart." "Services do not require to be bound to a chart to work and are designed to perform auxiliary functions." "For example, in a service, you can create a custom symbol, open its chart, receive data for it in an" "endless loop using the network functions and constantly update it." "Each script, each service and each Expert Advisor runs in its own separate thread. All indicators" "calculated on one symbol, even if they are attached to different charts, work in the same thread." "Thus, all indicators on one symbol share the resources of one thread." "All other actions associated with a symbol, like processing of ticks and history synchronization, are" "also consistently performed in the same thread with indicators. This means that if an infinite action is" "performed in an indicator, all other events associated with its symbol will never be performed." "When running an Expert Advisor, make sure that it has an actual trading environment and can access" "the history of the required symbol and period, and synchronize data between the terminal and the" "server. For all these procedures, the terminal provides a start delay of no more than 5 seconds, after" "which the Expert Advisor will be started with available data. Therefore, in case there is no connection" "to the server, this may lead to a delay in the start of an Expert Advisor."; //--- CWebSocketClient wsc; //--- int sent=-1; uint received=-1; //--- // string subject,issuer,serial,thumbprint; //--- // datetime expiration; //--- //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- create timer EventSetTimer(2); //--- wsc.SetMaxSendSize(MaxSize); //--- if(wsc.Connect(Address,Port,Timeout,ExtTLS,true)) { sent=wsc.SendString(_msg); //-- Print("sent data is "+IntegerToString(sent)); //--- } //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- destroy timer EventKillTimer(); Print("Deinit call"); wsc.Close(); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void OnTimer() { if(wsc.Readable()>0) { CFrame msg_frames[]; received=wsc.Read(msg_frames); if(received>0) { int ll=ArraySize(msg_frames); Print("number of received frames is "+IntegerToString(ll)); for(int i=0; i<ll; i++) { Print(msg_frames[i].ToString()); } if(msg_frames[ll-1].IsFinal()) { Print("\n Final frame received"); wsc.Close(NORMAL_CLOSE,"good bye"); ExpertRemove(); } } } else { Print("\n Nothing readable in socket"); if(wsc.ClientState()!=CONNECTED) { Print("\n Client disconnected"); ExpertRemove(); } } } //+------------------------------------------------------------------+
Dieser Expert Advisor verbindet sich mit dem lokal laufenden Echo-Websocket-Server und versucht sofort, eine ziemlich große Nachricht zu senden. Die EA-Eingaben erlauben es, tls zu aktivieren, zu deaktivieren und die Sendegröße anzupassen, um zu sehen, wie der Mechanismus der Nachrichtenfragmentierung funktioniert. Im Code habe ich die maximale Nachrichtengröße auf 256 gesetzt, so dass jeder Frame diese Größe oder weniger hat.
In der Funktion onTimer prüft der EA, ob Nachrichten vom Server verfügbar sind. Die empfangene Nachricht wird an das mt5-Terminal ausgegeben, dann wird die Websocket-Verbindung beendet. Beim nächsten OnTimer-Ereignis, wenn die Verbindung geschlossen ist, entfernt sich der EA aus dem Chart. Hier ist die Ausgabe im Experten-Log des Mt5.
Hier sehen Sie ein Video, in dem das Programm läuft, während es mit dem Server verbunden ist.
Schlussfolgerung
Dieser Artikel begann mit einem kurzen Überblick über das Websocket-Protokoll. Dann folgte eine detaillierte Beschreibung, wie ein Websocket-Client in Metrader 5 nur mit der Programmiersprache mql5 implementiert werden kann. Als Nächstes haben wir einen Server gebaut, den wir dann zum Testen unseres mt5-Clients verwendet haben. Ich hoffe, dass Sie die hier beschriebenen Tools nützlich finden werden. Der gesamte Quellcode steht unten zum Download bereit.
Inhalt des angehängten Archivs.
Verzeichnis | Inhalt | Beschreibung |
---|---|---|
MT5zip\server | echo_websocket_server.exe, websockets.dll,ssleay32.dll,libeay32.dll | Server-Anwendung zusammen mit den dafür erforderlichen Abhängigkeiten |
MT5zip\Mql5\include | Frame.mqh, Socket.mqh, WebsocketClient.mqh | Include-Dateien, die Code für die CFrame-Klasse, die CSocket-Klasse bzw. die CWebsocket-Klasse enthalten |
MT5zip\Mql5\Experts | Websocketclient_test.mq5 | MetaTrader Expert Advisor zur Demonstration der Verwendung der Klasse CWebsocket |
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/8196





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