使用云存储服务来进行终端之间的数据交换·进阶篇
(2/3)·直接互联暴露 IP 与算力瓶颈,免费 15GB 云盘如何成为多终端信号中转的更优解
◍ 把信号写进云盘的创建与更新方法
这段 C# 风格的 EA 辅助类代码,负责把 MT5 跑出的价格行为信号序列化后推到 Google Drive。FileCreate 方法先检查 credential,若为空就调 Authorize(),授权失败直接返回 false,不浪费一次网络请求。 创建文件时 MimeType 硬编码为 text/json,ViewersCanCopyContent 设为 true,意味着任何拿到链接的人都能拷贝内容——在外汇与贵金属策略共享时要注意这层暴露风险,可能泄露你的参数逻辑。 FileUpdate 同样先确保授权,credential 为空返回 false;更新逻辑复用同名 name 与 value,但片段里没贴全上传实现。你在 MT5 里接这套,建议先把 ApplicationName 和 credential 作用域打出来验证,别直接信任默认编码。
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 跳下一个。这样即使某个旧文件权限异常删不掉,也不会卡死整轮同步——外汇与贵金属账户跑自动化时,这种容错能明显降低脚本假死概率。
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 失效这里可能静默返回空值,需要你在日志里单独抓。
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 个线程会全卡在等待连接。外汇与贵金属交易杠杆高,桥接模块崩了可能漏掉信号,先在模拟盘跑通再上实盘。
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 处理,属高风险链路需自行压测。
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 可以通过本地管道让外部进程代为读取远端文件,外汇与贵金属交易涉及高杠杆,管道权限和外网调用须自行评估风险。
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(); }