交易信号服务和测试 Web 页

交易信号服务在技术上与聊天服务相同,但是它的用户(或者说是客户连接)必须扮演两个角色之一:

  • 消息提供者
  • 消息消费者

此外,信息不应该对每个人都可用,而是根据一些订阅方案运作。

为了确保这一点,当连接到服务时,用户将被要求提供特定的标识信息,这些信息因角色而异。

提供者必须指定一个在所有信号中唯一的公共信号标识符 (PUB_ID)。基本上,同一个人可能会产生不止一个信号,因此应该能够获得多个识别标志。在这个意义上,我们不会通过引入单独的提供者标识符(作为特定的人)和其信号的标识符来使服务复杂化。相反,只支持信号标识符。对于一个真正的信号服务,这个问题需要和授权一起解决,这一点我们在本书之外已经讨论过了。

该标识符将用于发布该信号,或仅需将其传递给有意订阅此信号的人员然而,不应仅凭公开标识符就让任何获知者都具备访问该信号的能力。在最简单的情况下,开账户监控可采用此方式,但我们将专门演示信号场景中的访问限制方案。

为此,提供者必须向服务器提供一个只有他们自己知道而公众不知道的秘密密钥 (PUB_KEY)。生成特定订户的访问密钥将需要该密钥。

消费者(订阅者)还必须有一个唯一标识符(SUB_ID,这里我们也是省略了授权)。用户订阅需要的信号时,须向信号提供者提交标识符(实际操作中需同步完成确认付款,该流程通常由服务器自动化处理)提供者形成一个快照,其中包含提供者的标识符、订户的标识符和秘密密钥。在我们的服务中,这将通过从 PUB_ID:PUB_KEY:SUB_ID 字符串计算 SHA256 散列来完成,之后,结果字节被转换为十六进制格式的字符串。这将是特定用户的特定提供商的信号的访问密钥(子密钥或访问密钥)。提供者(在真实系统中,由服务器本身自动完成)将这个密钥转发给订户。

因此,当连接到服务时,订户将必须指定订户标识符 (SUB_ID)、期望信号的标识符 (PUB_ID)和访问密钥 (SUB_KEY)。因为服务器知道提供者的秘密密钥,所以它可以为给定的 PUB_ID 和 SUB_ID 的组合重新计算访问密钥,并将其与提供的 SUB_KEY 进行比较。匹配意味着正常的消息传递过程继续。这种差异将导致错误消息,并断开伪订户与服务的连接。

值得注意的是,在我们的演示中,为了简单起见,不存在正常注册的用户和信号,因此标识符的选择是任意的。对我们来说,重要的是跟踪标识符的唯一性,以便知道在线发送信息的发送方和接收方。因此,我们的服务不保证标识符(例如,“超级趋势”)昨天、今天和明天属于同一个用户。根据“先到先得”原则来预留名称。只要提供商用给定的标识符持续连接,信号就被传递。如果提供者断开连接,那么标识符在任何下一个连接中变得可供选择。

唯一会一直忙碌的标识符是 "Server":服务器用它来发送它的连接状态消息。

为了在服务器文件夹中生成访问密钥,使用了简单的 JavaScriptaccess.js。在命令行上运行该 JavaScript 文件时,需要将上面类型的 PUB_ID:PUB_KEY:SUB_ID(标识符和它们之间的密钥,由 ':' 符号连接)作为唯一的参数传递

如果未指定该参数,脚本将为一些演示标识符 (PUB_ID_001,SUB_ID_100) 和一个秘密 (PUB_KEY_FFF) 生成一个访问密钥。

// JavaScript
const args = process.argv.slice(2);
const input = args.length > 0 ? args[0] : 'PUB_ID_001:PUB_KEY_FFF:SUB_ID_100';
console.log('Hashing "', input, '"');
const crypto = require('crypto');
console.log(crypto.createHash('sha256').update(input).digest('hex'));

使用以下命令运行脚本:

node access.js PUB_ID_001:PUB_KEY_FFF:SUB_ID_100

我们得到这样的结果:

fd3f7a105eae8c2d9afce0a7a4e11bf267a40f04b7c216dd01cf78c7165a2a5a

顺便说一下,你可以使用 CryptEncode 函数在纯 MQL5 环境中检查和重复该算法。

分析了概念部分之后,让我们继续实际的实现。

信号服务的服务器脚本将放在 MQL5/Experts/MQL5Book/p7/Web/wspubsub.js 文件中。在其中设置服务器与我们之前所做的一样。但是,除此之外,你还需要连接 access.js 中使用的相同的 "crypto" 模块。主页被称为 wspubsub.htm

// JavaScript
const crypto = require('crypto');
...
http1.createServer(optionsfunction (reqres)
{
   ...
   if(req.url == '/')
   {
      req.url = "wspubsub.htm";
   }
   ...
});

我们将为信号提供商和消费者分别定义两个映射,而不是一个连接客户端的映射。

// JavaScript
const publishers = new Map();
const subscribers = new Map();

在这两个映射中,键均为提供者 ID,但是第一个存储提供者的对象,第二个存储订阅每个提供者的订阅者的对象(对象数组)。

为了在握手过程中传输标识符和密钥,我们将使用 WebSocket 规范允许的特殊标头,即 Sec-Websocket-Protocol。我们约定:用符号 '-' 将标识符和键拼接:提供者应生成类似 X-MQL5-publisher-PUB_ID-PUB_KEY 的字符串,订阅者应生成类似 X-MQL5-subscriber-SUB_ID-PUB_ID-SUB_KEY 的字符串。

如果在没有 Sec-Websocket-ProtocolX-MQL5-... 标头的情况下尝试连接到我们服务,则会立即停止并关闭。

在新的客户端对象中(在 "connection" 事件处理程序参数 onConnect(client) 中),这个标题很容易从 client.protocol 特性中提取。

让我们以简化形式展示注册和发送信号提供者消息的过程,省略了错误处理(附上完整代码)。值得注意的是,消息文本是以 JSON 格式生成的(我们将在下一节详细讨论)。特别是,消息的发送者在 "origin" 特性中传递(此外,当消息由服务本身发送时,该字段包含字符串 "Server"),来自提供者的应用程序数据放在 "msg" 特性中,这可能不仅仅是文本,还可以是任何内容的嵌套结构。

// JavaScript
const wsServer = new WebSocket.Server({ server });
wsServer.on('connection', function onConnect(client)
{
   console.log('New user:', ++countclient.protocol);
   if(client.protocol.startsWith('X-MQL5-publisher'))
   {
      const parts = client.protocol.split('-');
      client.id = parts[3];
      client.key = parts[4];
      publishers.set(client.idclient);
      client.send('{"origin":"Server", "msg":"Hello, publisher ' + client.id + '"}');
      client.on('message', function(message)
      {
         console.log('%s : %s', client.idmessage);
         
         if(subscribers.get(client.id))
            subscribers.get(client.id).forEach(function(elem)
         {
            elem.send('{"origin":"publisher ' + client.id + '", "msg":'
               + message + '}');
         });
      });
      client.on('close', function()
      {
         console.log('Publisher disconnected:', client.id);
         if(subscribers.get(client.id))
            subscribers.get(client.id).forEach(function(elem)
         {
            elem.close();
         });
         publishers.delete(client.id);
      });
   }
   ...

订阅者的算法前半部分与此类似,但新增了访问密钥的计算环节,以及它与连接客户端传输的内容的比较。

// JavaScript
   else if(client.protocol.startsWith('X-MQL5-subscriber'))
   {
      const parts = client.protocol.split('-');
      client.id = parts[3];
      client.pub_id = parts[4];
      client.access = parts[5];
      const id = client.pub_id;
      var p = publishers.get(id);
      if(p)
      {
         const check = crypto.createHash('sha256').update(id + ':' + p.key + ':'
            + client.id).digest('hex');
         if(check != client.access)
         {
            console.log(`Bad credentials: '${client.access}' vs '${check}'`);
            client.send('{"origin":"Server", "msg":"Bad credentials, subscriber '
               + client.id + '"}');
            client.close();
            return;
         }
   
         var list = subscribers.get(id);
         if(list == undefined)
         {
            list = [];
         }
         list.push(client);
         subscribers.set(idlist);
         client.send('{"origin":"Server", "msg":"Hello, subscriber '
            + client.id + '"}');
         p.send('{"origin":"Server", "msg":"New subscriber ' + client.id + '"}');
      }
      
      client.on('close', function()
      {
         console.log('Subscriber disconnected:', client.id);
         const list = subscribers.get(client.pub_id);
         if(list)
         {
            if(list.length > 1)
            {
               const filtered = list.filter(function(el) { return el !== client; });
               subscribers.set(client.pub_idfiltered);
            }
            else
            {
               subscribers.delete(client.pub_id);
            }
         }
      });
   }

wspubsub.htm 客户端页面上的用户界面邀请你点击一个链接,该链接指向两个页面中的一个,这两个页面都包含了针对提供者 (wspublisher.htm + wspublisher_client.js) 或订阅者 (wssubscriber.htm + wssubscriber_client.js) 的表单。

信号服务测试客户端 Web 页面

信号服务测试客户端 Web 页面

它们的实现继承了之前考虑的 JavaScript 客户端的特性,但是在 Sec-Websocket-ProtocolX-MQL5- 标头定制方面还有一处细节优化。

一直到现在,我们都是交换简单的短信。但是对于信令服务,你需要传输大量的结构化信息,而 JSON 更适合这一点。因此,客户端可以解析 JSON,尽管它们并没有将它用于预期的目的,因为即使在 JSON 中找到了买卖给定数量的特定股票的命令,浏览器也不知道如何处理。

我们需要在 MQL5 中为我们的信号服务客户端添加 JSON 支持。同时,你可以在 wspubsub.js 服务器上运行,并根据信号提供者和使用者指定的细节测试它们的选择性连接。出于你自己利益考虑,我们建议你自行完成。