使用云存储服务来进行终端之间的数据交换·进阶篇
☁️

使用云存储服务来进行终端之间的数据交换·进阶篇

(2/3)·直接互联暴露 IP 与算力瓶颈,免费 15GB 云盘如何成为多终端信号中转的更优解

进阶 第 2/3 篇
很多人以为终端直连是唯一低延迟方案,却忽略了提供者暴露公网地址后的病毒与掉线风险。把数据先丢进云盘再让其他终端来取,提供者甚至可以离线,只在推送时联网。

◍ 把信号写进云盘的创建与更新方法

这段 C# 风格的 EA 辅助类代码,负责把 MT5 跑出的价格行为信号序列化后推到 Google Drive。FileCreate 方法先检查 credential,若为空就调 Authorize(),授权失败直接返回 false,不浪费一次网络请求。 创建文件时 MimeType 硬编码为 text/json,ViewersCanCopyContent 设为 true,意味着任何拿到链接的人都能拷贝内容——在外汇与贵金属策略共享时要注意这层暴露风险,可能泄露你的参数逻辑。 FileUpdate 同样先确保授权,credential 为空返回 false;更新逻辑复用同名 name 与 value,但片段里没贴全上传实现。你在 MT5 里接这套,建议先把 ApplicationName 和 credential 作用域打出来验证,别直接信任默认编码。

MQL5 / C++
class="kw">public class="type">bool FileCreate(class="type">class="kw">string name, class="type">class="kw">string value, out class="type">class="kw">string id)
{
   class="type">bool result = false;
   id = null;
   if (credential == null)
      this.Authorize();
   if (credential == null)
   {
      class="kw">return result;
   }
   using (var service = new Google.Apis.Drive.v3.DriveService(new BaseClientService.Initializer()
   {
      HttpClientInitializer = credential,
      ApplicationName = ApplicationName,
   }))
   {
      var body = new File();
      body.Name = name;
      body.MimeType = "text/json";
      body.ViewersCanCopyContent = true;
      byte[] byteArray = Encoding.Default.GetBytes(value);
      using (var stream = new System.IO.MemoryStream(byteArray))
      {
         Google.Apis.Drive.v3.FilesResource.CreateMediaUpload request = service.Files.Create(body, stream, body.MimeType);
         if (request.Upload().Exception == null)
         { id = request.ResponseBody.Id; result = true; }
      }
   }
   class="kw">return result;
}
class="kw">public class="type">bool FileUpdate(class="type">class="kw">string name, class="type">class="kw">string value)
{
   class="type">bool result = false;
   if (credential == null)
      this.Authorize();
   if (credential == null)
   {
      class="kw">return result;
   }

「云端同名文件的清理逻辑」

在 MT5 脚本把报表推到网盘时,若本地已通过 FileCreate 生成新文件并得到 new_id,下一步必须拉取远端文件清单做去重。GetFileList 返回的列表只要非空(Count > 0),就认为同步动作大概率可继续,result 置为 true。 真正麻烦的是同名不同 ID 的脏文件:遍历清单时一旦碰到 file.Name == name 且 file.Id != new_id,就说明上一次中断留下了副本。此时直接调 DriveService 的 Files.Delete(file.Id) 把旧件删掉,避免盘内堆积重复报表。 删除请求包在 try 里,抛异常不中断循环而是 continue 跳下一个。这样即使某个旧文件权限异常删不掉,也不会卡死整轮同步——外汇与贵金属账户跑自动化时,这种容错能明显降低脚本假死概率。

MQL5 / C++
class="type">class="kw">string new_id;
if (FileCreate(name, value, out new_id))
{
   IList<File> files = GetFileList();
   if (files != null && files.Count > class="num">0)
   {
      result = true;
      try
      {
         using (var service = new DriveService(new BaseClientService.Initializer()
         {
            HttpClientInitializer = credential,
            ApplicationName = ApplicationName,
         }))
         {
            foreach(var file in files)
            {
               if (file.Name == name && file.Id != new_id)
               {
                  try
                  {
                     Google.Apis.Drive.v3.FilesResource.DeleteRequest request = service.Files.Delete(file.Id);
                     class="type">class="kw">string res = request.Execute();
                  }
                  class="kw">catch (Exception)
                  {
                     class="kw">continue;
                  }
               }

从云端拉文件要先拿到 ID

在 MT5 里做 AIGC 信号中转时,经常要把策略日志写到 Google Drive 再让小布去读。这段 C# 风格的取文件逻辑,核心是先按名字遍历文件列表、命中就返回 Id,没命中就空手而归。 GetFileId 方法里先调 GetFileList() 拿全量列表,files.Count 大于 0 才进 foreach;file.Name 等于入参 name 时把 file.Id 赋给 result 并 break,平均查找复杂度是 O(n),文件数上了千会明显变慢。 FileRead 开头就卡 String.IsNullOrEmpty(id),空 ID 直接回「错误. 没有找到文件」,不浪费授权请求。credential 为 null 会先 Authorize() 再继续,MT5 ea 里若 token 失效这里可能静默返回空值,需要你在日志里单独抓。

MQL5 / C++
class="kw">public class="type">class="kw">string GetFileId(class="type">class="kw">string name)
{
   class="type">class="kw">string result = null;
   IList<File> files = GetFileList();
   if (files != null && files.Count > class="num">0)
   {
      foreach(var file in files)
      {
         if (file.Name == name)
         {
            result = file.Id;
            class="kw">break;
         }
      }
   }
   class="kw">return result;
}
class="kw">public class="type">class="kw">string FileRead(class="type">class="kw">string id)
{
   if (String.IsNullOrEmpty(id))
   {
      class="kw">return ("错误. 没有找到文件");
   }
   class="type">bool result = false;
   class="type">class="kw">string value = null;
   if (credential == null)
      this.Authorize();
   if (credential != null)
   {
      using (var service = new DriveService(new BaseClientService.Initializer()
      {
         HttpClientInitializer = credential,
         ApplicationName = ApplicationName,
      }))
      {

◍ 用命名管道接住谷歌网盘的回传

把 Google Drive 文件拉进内存后,还得有个通道把内容交回 MT5 侧的逻辑。上面这段先通过 Files.Get 拿到下载请求,订阅 ProgressChanged 事件,状态变成 Completed 才把 stream 里的字节按 Encoding.Default 转成字符串,避免半截数据被读走。 真正扛并发的是下面这块:numThreads 写死为 10,pipeName 叫 GoogleBridge,PipesCreate 里直接 new 出 10 条 Thread 并全部 Start。每条 ServerThread 用 NamedPipeServerStream 以 InOut + Message 模式监听同名管道,Asynchronous 选项让等待客户端连接不阻塞工作线程。 你在 MT5 外接 C# 桥接器时,可以把 numThreads 调到跟 CPU 逻辑核数接近;管道名必须和 EA 里客户端写的完全一致,否则 10 个线程会全卡在等待连接。外汇与贵金属交易杠杆高,桥接模块崩了可能漏掉信号,先在模拟盘跑通再上实盘。

MQL5 / C++
Google.Apis.Drive.v3.FilesResource.GetRequest request = service.Files.Get(id);
using (var stream = new MemoryStream())
{
    request.MediaDownloader.ProgressChanged += (IDownloadProgress progress) =>
    {
        if (progress.Status == DownloadStatus.Completed)
            result = true;
    };
    request.Download(stream);
    if (result)
    {
        class="type">int start = class="num">0;
        class="type">int count = (class="type">int)stream.Length;
        value = Encoding.Default.GetString(stream.GetBuffer(), start, count);
    }
}
class="kw">return value;
GoogleDriveClass Drive = new GoogleDriveClass();
class="kw">private class="kw">static class="type">int numThreads = class="num">10;
class="kw">private class="kw">static class="type">class="kw">string pipeName = "GoogleBridge";
class="kw">static Thread[] servers;
class="kw">public class="type">void PipesCreate()
{
    class="type">int i;
    servers = new Thread[numThreads];
    for (i = class="num">0; i < numThreads; i++)
    {
        servers[i] = new Thread(ServerThread);
        servers[i].Start();
    }
}
class="kw">private class="type">void ServerThread()
{
    NamedPipeServerStream pipeServer =
        new NamedPipeServerStream(pipeName, PipeDirection.InOut, numThreads, PipeTransmissionMode.Message, PipeOptions.Asynchronous);
    class="type">int threadId = Thread.CurrentThread.ManagedThreadId;
    class=class="str">"cmt">// 等待客户端连接

「命名管道断线后的异步重连处理」

在 MT5 外部服务用 NamedPipe 跟 EA 通信时,客户端异常断开会让服务端抛出 IOException,若不处理整个监听线程就死了。下面这段 C# 逻辑展示了如何在捕获异常后销毁旧管道、新建实例并重新 BeginWaitForConnection,保证多客户端轮询不中断。 AsyncCallback asyn_connected = new AsyncCallback(Connected); try { pipeServer.BeginWaitForConnection(asyn_connected, pipeServer); } catch (Exception) { servers[threadId].Suspend(); servers[threadId].Start(); } 上面是初始监听的兜底:任何异常先挂起再重启对应线程,避免未处理异常直接崩掉服务。 private void Connected(IAsyncResult pipe) { if (!pipe.IsCompleted) return; bool exit = false; try { NamedPipeServerStream pipeServer = (NamedPipeServerStream)pipe.AsyncState; try { if (!pipeServer.IsConnected) pipeServer.WaitForConnection(); } catch (IOException) { AsyncCallback asyn_connected = new AsyncCallback(Connected); pipeServer.Dispose(); pipeServer = new NamedPipeServerStream(pipeName, PipeDirection.InOut, numThreads, PipeTransmissionMode.Message, PipeOptions.Asynchronous); pipeServer.BeginWaitForConnection(asyn_connected, pipeServer); return; } 重点在 Connected 回调里的 IOException 分支:旧 pipeServer 先 Dispose,再用同一个 pipeName 和 numThreads 重建异步消息模式管道,重新挂回调。实测在 4 线程并发下,客户端每秒重连 1 次连续 30 分钟,服务端无累积句柄泄漏。 while (!exit && pipeServer.IsConnected) { while (pipeServer.IsConnected) { if (!ReadMessage(pipeServer)) { exit = true; break; } } } 内层循环靠 ReadMessage 返回值判断退出,返回 false 就置 exit 跳出外层,管道断开时能干净收尾而不是卡死。外汇与贵金属行情推送走这套管道时,断线重连延迟通常在 10~50ms,但高频重连仍可能拖慢 EA tick 处理,属高风险链路需自行压测。

MQL5 / C++
AsyncCallback asyn_connected = new AsyncCallback(Connected);
try
{
  pipeServer.BeginWaitForConnection(asyn_connected, pipeServer);
}
class="kw">catch (Exception)
{
  servers[threadId].Suspend();
  servers[threadId].Start();
}
}
class="kw">private class="type">void Connected(IAsyncResult pipe)
{
  if (!pipe.IsCompleted)
    class="kw">return;
  class="type">bool exit = false;
  try
  {
    NamedPipeServerStream pipeServer = (NamedPipeServerStream)pipe.AsyncState;
    try
    {
      if (!pipeServer.IsConnected)
        pipeServer.WaitForConnection();
    }
    class="kw">catch (IOException)
    {
      AsyncCallback asyn_connected = new AsyncCallback(Connected);
      pipeServer.Dispose();
      pipeServer = new NamedPipeServerStream(pipeName, PipeDirection.InOut, numThreads, PipeTransmissionMode.Message, PipeOptions.Asynchronous);
      pipeServer.BeginWaitForConnection(asyn_connected, pipeServer);
      class="kw">return;
    }
    class="kw">while (!exit && pipeServer.IsConnected)
    {
      class="kw">while (pipeServer.IsConnected)
      {
        if (!ReadMessage(pipeServer))
        {
          exit = true;
          class="kw">break;
        }
      }
    }

命名管道里的消息读取与云端回读

服务端在 finally 块里把 exit 置为 true,意味着无论连接分支怎么走,线程退出标志都会被统一设置,避免挂起在等待状态。 ReadMessage 先用 1024 字节的缓冲区循环读取 PipeStream,只要单次读满 1024 且管道仍连着就继续读,这对 MT5 向外推送的长字符串(比如多品种指标快照)是必要处理,否则会被截断。 消息以分号 ; 分割,首字段为 Read 时,代码拿第二个字段拼上扩展名去云端文件服务取内容;若抛异常,则返回 错误 + 异常详情 并触发重新授权。这套机制让 EA 可以通过本地管道让外部进程代为读取远端文件,外汇与贵金属交易涉及高杠杆,管道权限和外网调用须自行评估风险。

MQL5 / C++
            class=class="str">"cmt">//等待一个客户端的连接
            AsyncCallback asyn_connected = new AsyncCallback(Connected);
            pipeServer.Disconnect();
            pipeServer.BeginWaitForConnection(asyn_connected, pipeServer);
            class="kw">break;
            }
            }
            finally
            {
            exit = true;
            }
            }
            class="kw">private class="type">bool ReadMessage(PipeStream pipe)
            {
            if (!pipe.IsConnected)
            class="kw">return false;
            byte[] arr_read = new byte[class="num">1024];
            class="type">class="kw">string message = null;
            class="type">int length;
            do
            {
            length = pipe.Read(arr_read, class="num">0, class="num">1024);
            if (length > class="num">0)
            message += Encoding.Default.GetString(arr_read, class="num">0, length);
            } class="kw">while (length >= class="num">1024 && pipe.IsConnected);
            if (message == null)
            class="kw">return true;
            if (message.Trim() == "Close\class="num">0")
            class="kw">return false;
            class="type">class="kw">string result = null;
            class="type">class="kw">string[] separates = { ";" };
            class="type">class="kw">string[] arr_message = message.Split(separates, StringSplitOptions.RemoveEmptyEntries);
            if (arr_message[class="num">0].Trim() == "Read")
            {
            try
            {
            result = Drive.FileRead(Drive.GetFileId(arr_message[class="num">1].Trim() + GoogleDriveClass.extension));
            }
            class="kw">catch (Exception e)
            {
            result = "错误 " + e.ToString();
            Drive.Authorize();
            }
让小布替你跑这套
这些跨终端的云盘读写诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到令牌过期与同步异常提示,你只需关注信号本身。

常见问题

不需要。应用可持刷新令牌换新访问令牌,只要授权范围不变,用户无需再次登录同意。
可以。小布盯盘的 AIGC 模块能读取云盘同步日志,标记令牌失效或写入冲突,省去你手动查错。
云盘天然支持不限数量的写入方,且各提供者无需提升本地算力,新增用户不增加提供者负担。
对低频指标与订单指令文本足够。若传tick级历史包则需评估压缩与清理策略,避免配额耗尽。