MQL5 中回显和聊天服务的客户端程序

我们编写一个简单脚本 MQL5/Experts/MQL5Book/p7/wsEcho/wsecho.mq5 来连接到回显服务(注意,这是一个脚本,但是我们将它放在 MQL5/Experts/MQL5Book/p7/ 文件夹中,使它成为与 Web 相关的 MQL 程序的一个容器,因为所有后续的例子都将是 EA 交易)。因为在这一章中,我们考虑在项目中创建软件复合体,我们将设计脚本作为 mqproj 项目的一部分,它也将包括服务器组件。

脚本的输入参数允许你指定服务的地址和消息的文本。默认采用不安全连接。如果要启动支持 TLS 的 wsecho.js 服务器,需要将协议更改为安全的 wss。请记住,建立一个安全的连接需要比平时更长的时间(几秒钟)。

input string Server = "ws://localhost:9000/";
input string Message = "My outbound message";
   
#include <MQL5Book/AutoPtr.mqh>
#include <MQL5Book/ws/wsclient.mqh>

OnStart 函数中,我们为给定的地址创建一个 WebSocket 客户端 (wss) 实例,并调用 open 方法。如果连接成功,我们通过在阻塞模式下调用 wss.readMessage 来等待来自服务的欢迎消息(默认情况下,最多等待 5 秒)。我们在结果对象上使用自动指针,这样就不会在最后手动调用 delete

void OnStart()
{
   Print("\n");
   WebSocketClient<Hybiwss(Server);
   Print("Opening...");
   if(wss.open())
   {
      Print("Waiting for welcome message (if any)");
      AutoPtr<IWebSocketMessagewelcome(wss.readMessage());
      ...

WebSocketClient 类包含事件处理程序存根,包括 onMessage 简单方法,该方法将问候语打印到日志中。

然后我们发送消息,再次等待服务器的响应。还将记录回显消息。

      Print("Sending message...");
      wss.send(Message);
      Print("Receiving echo...");
      AutoPtr<IWebSocketMessageecho(wss.readMessage());
   }
   ...

最后,我们关闭连接。

   if(wss.isConnected())
   {
      Print("Closing...");
      wss.close();
   }
}

基于这个脚本文件,让我们创建一个项目文件 (wsecho.mqproj)。我们用版本号 (1.0)、版权信息和描述来填充项目特性。我们将回显服务服务器文件添加到 Settings and Files 分支中(这至少会提醒开发人员存在测试服务器)。编译后,依赖项(头文件)将出现在层次结构中。

一切应该看起来像下面的截图。

回显服务项目、客户端脚本和服务器

回显服务项目、客户端脚本和服务器

如果脚本位于 Shared Projects 文件夹中,例如,在 MQL5/Shared Projects/MQL5Book/wsEcho/ 中,那么在成功编译之后,它的 ex5 文件将自动移动到 MQL5/Scripts/Shared Projects/MQL5Book/wsEcho/ 文件夹中,并且相应的条目将显示在编译日志中。这是在共享项目中编译任何 MQL 程序的标准行为。

在本章的所有示例中,不要忘记在测试 MQL 脚本之前启动服务器。在这种情况下,运行以下命令:在 web 文件夹中的 node.exe wsecho.js

然后,运行 wsecho.ex5 脚本。日志将显示正在发生的操作,以及消息通知。

Opening...
Connecting to localhost:9000
Buffer: 'HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: mIpas63g5xGMqJcKtreHKpSbY1w=
'
Headers: 
                               [,0]                           [,1]
[0,] "upgrade"                      "websocket"                   
[1,] "connection"                   "Upgrade"                     
[2,] "sec-websocket-accept"         "mIpas63g5xGMqJcKtreHKpSbY1w="
 > Connected ws://localhost:9000/
Waiting for welcome message (if any)
 > Message ws://localhost:9000/ server#Hello, user1
Sending message...
Receiving echo...
 > Message ws://localhost:9000/ user1#My outbound message
Closing...
Close requested
Waiting...
SocketRead failed: 5273 Available: 1
 > Disconnected ws://localhost:9000/
Server close ack

以上 HTTP 标头是握手过程中服务器的响应。如果我们查看服务器运行的控制台窗口,我们会发现服务器从我们的客户机接收到的 HTTP 标头。

回显服务服务器日志

回显服务服务器日志

此外,这里还显示了用户的连接、消息和断开。

让我们为聊天服务做一个类似的工作:在 MQL5 中创建一个 WebSocket 客户机,为它创建一个项目,并进行测试。这一次,客户端程序的类型将是 EA 交易,因为聊天需要图表上的键盘对交互事件的支持。EA 交易附在本书的 MQL5/MQL5Book/p7/wsChat/wschat.mq5 文件夹中。

为了演示在处理程序方法中接收事件的技术,让我们定义我们自己的类 MyWebSocket,该类派生自 WebSocketClient

class MyWebSocketpublic WebSocketClient<Hybi>
{
public:
   MyWebSocket(const string addressconst bool compress = false):
      WebSocketClient(addresscompress) { }
   
   /* void onConnected() override { } */
   
   void onDisconnect() override
   {
      // we can do something else and call (or not call) the legacy code
      WebSocketClient<Hybi>::onDisconnect();
   }
   
   void onMessage(IWebSocketMessage *msgoverride
   {
     // TODO: we could truncate copies of our own messages,
     // but they are left for debugging
      Alert(msg.getString());
      delete msg;
   }
};

收到一条消息时,我们不会在日志中显示,而是作为一个警报显示,在此之后应该删除该对象。

在全局上下文中,我们说明了 wss 类的对象,而用户从键盘输入的 message 字符串将在其中累积。

MyWebSocket wss(Server);
string message = "";

OnInit 函数包含了必要的准备,特别是启动定时器和打开连接的准备。

int OnInit()
{
  ChartSetInteger(0CHART_QUICK_NAVIGATIONfalse);
  EventSetTimer(1);
  wss.setTimeOut(1000);
  Print("Opening...");
  return wss.open() ? INIT_SUCCEEDED : INIT_FAILED;
}

需要计时器来检查来自其他用户的新消息。

void OnTimer()
{
   wss.checkMessages(false); // use a non-blocking check in the timer
}

OnChartEvent 处理程序中,我们响应击键:所有字母数字键都被转换成字符并附加到 message 字符串。如有必要,可以按 Backspace 删除最后一个字符。所有键入的文本都会在图表注释中更新。消息完成后,按 Enter 键将其发送到服务器。

void OnChartEvent(const int idconst long &lparamconst double &dparam,
   const string &sparam)
{
   if(id == CHARTEVENT_KEYDOWN)
   {
      if(lparam == VK_RETURN)
      {
         const static string longmessage = ...
         if(message == "long"wss.send(longmessage);
         else if(message == "bye"wss.close();
         else wss.send(message);
         message = "";
      }
      else if(lparam == VK_BACK)
      {
         StringSetLength(messageStringLen(message) - 1);
      }
      else
      {
         ResetLastError();
         const short c = TranslateKey((int)lparam);
         if(_LastError == 0)
         {
            message += ShortToString(c);
         }
      }
      Comment(message);
   }
}

如果我们输入文本 "long",程序将发送一个特别准备的相当长的文本。如果消息文本是 "bye",程序将关闭连接。此外,当程序退出时,连接将被关闭。

void OnDeinit(const int)
{
   if(wss.isConnected())
   {
      Print("Closing...");
      wss.close();
   }
}

让我们为 EA 交易创建一个项目(wschat.mqproj 文件),填充它的特性,并将后端添加到分支 Settings and Files。这次我们将从内部展示项目文件。在 mqproj 文件中,Dependencies 分支存储在 "files" 特性中,而 Settings and Files 分支存储在 "tester" 特性中。

{

"platform" :"mt5",

"program_type":"expert",

"copyright" :"Copyright 2022, MetaQuotes Ltd.",

"version" :"1.0",

"description" :"WebSocket-client for chat-service.\r\nType and send text messages for all connected users.\r\nShow alerts with messages from others.",

"optimize" :"1",

"fpzerocheck" :"1",

"tester_no_cache":"0",

"tester_everytick_calculate":"0",

"unicode_character_set":"0",

"static_libraries":"0",

"files":

[

{

"path":"wschat.mq5",

"compile":true,

"relative_to_project":true

},

{

"path":"MQL5\\Include\\MQL5Book\\ws\\wsclient.mqh",

"compile":false,

"relative_to_project":false

},

{

"path":"MQL5\\Include\\MQL5Book\\URL.mqh",

"compile":false,

"relative_to_project":false

},

{

"path":"MQL5\\Include\\MQL5Book\\ws\\wsframe.mqh",

"compile":false,

"relative_to_project":false

},

{

"path":"MQL5\\Include\\MQL5Book\\ws\\wstools.mqh",

"compile":false,

"relative_to_project":false

},

{

"path":"MQL5\\Include\\MQL5Book\\ws\\wsinterfaces.mqh",

"compile":false,

"relative_to_project":false

},

{

"path":"MQL5\\Include\\MQL5Book\\ws\\wsmessage.mqh",

"compile":false,

"relative_to_project":false

},

{

"path":"MQL5\\Include\\MQL5Book\\ws\\wstransport.mqh",

"compile":false,

"relative_to_project":false

},

{

"path":"MQL5\\Include\\MQL5Book\\ws\\wsprotocol.mqh",

"compile":false,

"relative_to_project":false

},

{

"path":"MQL5\\Include\\VirtualKeys.mqh",

"compile":false,

"relative_to_project":false

}

],

"tester":

[

{

"type":"file",

"path":"..\\Web\\MQL5Book.crt",

"relative_to_project":true

},

{

"type":"file",

"path":"..\\Web\\MQL5Book.key",

"relative_to_project":true

},

{

"type":"file",

"path":"..\\Web\\wschat.htm",

"relative_to_project":true

},

{

"type":"file",

"path":"..\\Web\\wschat.js",

"relative_to_project":true

},

{

"type":"file",

"path":"..\\Web\\wschat_client.js",

"relative_to_project":true

}

]

}

如果 EA 交易在 Shared Projects 文件夹内部,例如,在 MQL5/Shared Projects/MQL5Book/wsChat/ 中,在成功编译之后,它的 ex5 文件将自动移动到 MQL5/Experts/Shared Projects/MQL5Book/wsChat/ 文件夹。

启动 node.exe wschat.js 服务器。现在,你可以在不同的图表上运行 EA 交易的若干个副本。基本上,该服务涉及不同终端甚至不同计算机之间的“通信”,但你也可以从一个终端进行测试。

这是一个 EURUSD 和 GBPUSD 图表进行通信的例子。

(EURUSD,H1)        
(EURUSD,H1)        Opening...
(EURUSD,H1)        Connecting to localhost:9000
(EURUSD,H1)        Buffer: 'HTTP/1.1 101 Switching Protocols
(EURUSD,H1)        Upgrade: websocket
(EURUSD,H1)        Connection: Upgrade
(EURUSD,H1)        Sec-WebSocket-Accept: Dg+aQdCBwNExE5mEQsfk5w9J+uE=
(EURUSD,H1)        
(EURUSD,H1)        '
(EURUSD,H1)        Headers: 
(EURUSD,H1)                                       [,0]                           [,1]
(EURUSD,H1)        [0,] "upgrade"                      "websocket"                   
(EURUSD,H1)        [1,] "connection"                   "Upgrade"                     
(EURUSD,H1)        [2,] "sec-websocket-accept"         "Dg+aQdCBwNExE5mEQsfk5w9J+uE="
(EURUSD,H1)         > Connected ws://localhost:9000/
(EURUSD,H1)        Alert: server#Hello, user1
(GBPUSD,H1)        
(GBPUSD,H1)        Opening...
(GBPUSD,H1)        Connecting to localhost:9000
(GBPUSD,H1)        Buffer: 'HTTP/1.1 101 Switching Protocols
(GBPUSD,H1)        Upgrade: websocket
(GBPUSD,H1)        Connection: Upgrade
(GBPUSD,H1)        Sec-WebSocket-Accept: NZENnc8p05T4amvngeop/e/+gFw=
(GBPUSD,H1)        
(GBPUSD,H1)        '
(GBPUSD,H1)        Headers: 
(GBPUSD,H1)                                       [,0]                           [,1]
(GBPUSD,H1)        [0,] "upgrade"                      "websocket"                   
(GBPUSD,H1)        [1,] "connection"                   "Upgrade"                     
(GBPUSD,H1)        [2,] "sec-websocket-accept"         "NZENnc8p05T4amvngeop/e/+gFw="
(GBPUSD,H1)         > Connected ws://localhost:9000/
(GBPUSD,H1)        Alert: server#Hello, user2
(EURUSD,H1)        Alert: user1#I'm typing this on EURUSD chart
(GBPUSD,H1)        Alert: user1#I'm typing this on EURUSD chart
(GBPUSD,H1)        Alert: user2#Got it on GBPUSD chart!
(EURUSD,H1)        Alert: user2#Got it on GBPUSD chart!

因为我们的消息发送给每个人,包括发件人,所以它们在日志中是重复的,但是在不同的图表上。

通信在服务器端也是可见的。

聊天服务服务器日志

聊天服务服务器日志

现在我们有了组织交易信号服务的所有技术组件。