服务

服务是一种具有单个 OnStart处理程序和 #property service 指令的 MQL 程序。

回顾一下,在服务成功编译之后,需要使用 Navigator窗口上下文菜单中的 Add Service 命令来创建和配置其实例(一个或多个)。

作为一个服务示例,我们来解决一个 MQL 程序开发者经常遇到的小应用问题。大多数开发者会将其程序与用户账户号码绑定。这不一定是针对付费产品,也可能涉及在亲友间分发,用于收集统计数据或优化设置。与此同时,除了有效的真实账户之外,用户还可以注册模拟账户。此类账户的有效期通常有限,因此每隔几周就为它们更新绑定信息相当不便。为此,你需要编辑源代码、重新编译并再次发布程序。

不过我们可以改为开发一个服务,将从给定终端成功连接的账户号码注册到全局变量(或文件)中。

绑定技术基于账户号码的成对加密(或者采用哈希方式):即旧的登录账户和新的登录账户。前一个账户必须是主账户(即获得“已签发”条件性授权),这样这对账户的共同签名才能将产品的使用权扩展到新账户。密钥是只有程序内部才知道的机密信息(假定所有程序都是以封闭编译后形式提供)。运算结果将是一个Base64格式的字符串。该实现使用 MQL5 API 函数,其中一些函数我们还未讲解,特别是通过 AccountInfoIntegerCryptEncode 加密函数获取账号。使用 TerminalInfoInteger函数检查与服务器的连接(请参阅 检查网络连接)。

该服务无需了解哪些账户是主账户,哪些是附属账户。它只需以特定方式为任意连续登录的账户对进行“签名”验证。但特定应用程序应补充其“许可证”检查过程:除了将当前账户与主账户进行比较之外,还应重复执行服务算法,即创建一个 [主账户;当前账户] 的账户对,计算该账户对的加密签名,并检查该签名是否在全局变量之中。

只有在交易模式(而非投资者模式)下连接到同一账户时,才有可能通过将许可证转移到另一台计算机上来盗用此类许可证。当然,不诚信的用户可能会为他人创建模拟账户。因此,需要增强保护措施。在当前实现中,全局变量仅设置为临时变量,即在终端会话结束时会被删除,但仍存在被复制的潜在风险。

作为补充措施,例如可以在签名中对其创建时间进行加密,并将权限有效期设置为每日(或其他频率)。另一种选择是在服务启动时生成随机数,并将其与账户号码一起添加到签名信息中。该随机数仅服务内部可知,但可以通过 EventChartCustom 函数将其传输给图表上的相关 MQL 程序。因此,签名将在终端的该实例持续有效,直至会话结束。每个会话都会生成并发送一个新的随机数,因此它不适用于其他终端。最后,最简单且最便捷的选项可能是在签名中加入系统启动时间:(TimeLocal() - GetTickCount() / 1000)或其派生值。

在各类 MQL 程序中,仅部分程序能在账户切换期间持续运行,并支持实施这种保护方案。由于需要以统一方式保护所有类型的 MQL 程序,包括指标和 EA 交易(在账户变更时重新加载),因此将此任务委托给服务是合理的。然后,服务将控制登录并生成授权签名,并且该服务从终端加载开始持续运行,直至终端关闭。

服务的源代码位于文件MQL5/Services/MQL5Book/p5/ServiceAccount.mq5中。输入参数用于指定主账户以及存储签名的全局变量前缀。在实际程序中,主账户列表应在源代码中进行硬编码,而且最好使用 Common文件夹中的文件来替代全局变量,这样也能覆盖测试程序场景。

#property service
   
input long MasterAccount = 123456789;
input string Prefix = "!A_";

服务的主函数按以下方式执行工作:在一个每秒暂停一次的无限循环中,跟踪账户变更并保存最新账号,为账户对创建签名,然后将其写入全局变量。签名由 Cipher函数生成。

void OnStart()
{
   static long account = 0// previous login
   
   for(; !IsStopped(); )
   {
      // require connection, successful login and full access (not investor)
      const bool c = TerminalInfoInteger(TERMINAL_CONNECTED)
                  && AccountInfoInteger(ACCOUNT_TRADE_ALLOWED);
      const long a = c ? AccountInfoInteger(ACCOUNT_LOGIN) : 0;
   
      if(account != a// account changed
      {
         if(a != 0// current account
         {
            if(account != 0// previous account
            {
               // transfer authorization from one to another
               const string signature = Cipher(accounta);
               PrintFormat("Account %I64d registered by %I64d: %s"
                  aaccountsignature);
               // saving a record about the connection of accounts
               if(StringLen(signature) > 0)
               {
                  GlobalVariableTemp(Prefix + signature);
                  GlobalVariableSet(Prefix + signatureaccount);
               }
            }
            else // the first account is authorized, now waiting for the second one
            {
               PrintFormat("New account %I64d detected"a);
            }
            // remember the last active account
            account = a;
         }
      }
      Sleep(1000);
   }
}

Cipher函数使用特殊联合体 ByteOverlay2 将一对账号(long 型)表示为字节数组,并将其传递给 CryptEncode 进行加密(此处选择 CRYPT_DES 加密方法,但如果不需要从“签名”中恢复信息,也可替换为 CRYPT_AES128、CRYPT_AES256 或仅使用 CRYPT_HASH_SHA256 哈希算法(以密钥作为“盐值”))。

template<typename T>
union ByteOverlay2
{
   T values[2];
   uchar bytes[sizeof(T) * 2];
   ByteOverlay2(const T v1const T v2) { values[0] = v1values[1] = v2; }
};
   
string Cipher(const long data1const long data2)
{
   // TODO: replace the secret with your passphrase
   // TODO: CRYPT_AES128/CRYPT_AES256 methods require 16/32 byte arrays
   const static uchar secret[] = {'S', 'E', 'C', 'R', 'E', 'T', '0'};
   ByteOverlay2<longbo(data1data2);
   uchar result[];
   if(CryptEncode(CRYPT_DESbo.bytessecretresult) > 0)
   {
      uchar dummy[], text[];
      if(CryptEncode(CRYPT_BASE64resultdummytext) > 0)
      {
         return CharArrayToString(text);
      }
   }
   return NULL;
}

然后,终端中的任何程序都可以检查全局变量中是否存在当前账户的“许可证”。这可以通过 CheckAccountsIsCurrentAccountAuthorizedByMaster 函数来实现。它们在服务中仅作演示用途。

CheckAccounts函数会对硬编码的所有主账户进行检查,以找到与当前账户匹配的主账户。

bool CheckAccounts()
{
   const long accounts[] = {MasterAccount}; // TODO: to fill array with constants
   for(int i = 0i < ArraySize(accounts); ++i)
   {
      if(IsCurrentAccountAuthorizedByMaster(accounts[i])) return true;
   }
   return false;
}

IsCurrentAccountAuthorizedByMaster 接收一个主账号作为参数,为其与当前账户的配对重新创建“签名”,并分析匹配度。

bool IsCurrentAccountAuthorizedByMaster(const long data)
{
   const long a = AccountInfoInteger(ACCOUNT_LOGIN);
   if(a == datareturn true// direct match
   const string s = Cipher(dataa); // recalculating "signature"
   if(a != 0 && GlobalVariableGet(Prefix + s) == a)
   {
      Print("Sub-License is active: "s);
      return true;
   }
   return false;
}

假设允许程序在账号 123456789 上运行,且该账号当前处于活动状态。启动时,服务将立即响应,并显示日志条目:

New account 123456789 detected

如果随后我们更改账号(例如更改为 5555555),将获得以下签名:

Account 5555555 registered by 123456789: jdVKxUswBiNlZzDAnV3yxw==

如果我们停止并重新启动服务,将看到正在验证账户 5555555(在 OnStart函数开始时调用内置函数 CheckAccounts 进行演示)。

Sub-License is active: jdVKxUswBiNlZzDAnV3yxw==
Account 123456789 registered by 5555555: ZWcwwJ1d8seN1UrFSzAGIw==

新账户的许可证验证通过。如果切换回原账户,会生成一个从当前账户到前一账户的“通行证”(这是因为服务无法区分主账户和临时账户,而实际程序中通常不需要此类“签名”)。

如需间接授权新账户,需先重新登录主账户,再切换至新账户:这将创建一个新的全局变量,其中包含加密后的账户对 [主账户;新账户]。

该版本的服务不会检查主账户是否为真实账户以及从属账户是否为模拟账户。以上各项限制都可以添加。