使用命名管道与 MetaTrader 5 进行无 DLL 通信(基础篇)
用命名管道给 MT5 开一条无 DLL 通道
MT5 终端默认只允许通过 DLL 做进程外通信,但在受限环境(如 VPS 禁 DLL)下,命名管道(Named Pipe)是更干净的替代方案:它走 Windows 内核对象,不依赖任何外部库。
核心思路是终端侧用 FileOpen 以 \.\pipe\ 前缀打开一个管道名,外部程序以服务端身份创建同名管道,两边就能互发字节流。这样 EA 和 Python/C# 助手之间可以低延迟交换报价与信号,而策略测试器不会因为加载 DLL 被拦。
实测在 Windows Server 2012 + MT5 build 950 环境,单次 256 字节报文往返约 0.3 ms;若报文超过 4 KB 建议分帧,否则 FileRead 可能截断。外汇与贵金属杠杆高,管道只解决传输,下单前仍要自己校验点差与滑点风险。
别把管道当万能:它不支持跨机器,只能本机进程间用。
需要远端部署就把管道出口再接一层本地 socket 转发,别让 EA 直接裸连网络。
◍ 绕过 DLL 的沙箱通信思路
做 MT5 扩展时,不少人卡在终端沙箱限制上:想让 EA 和外部程序互通,又不想引入不安全 DLL。一个被低估的合法通道是标准命名管道(named pipe),它本质是文件级操作,走的是系统自带进程间通信机制,不触碰沙箱红线。 早前已有文章演示过在 MetaTrader 5 客户端之间用命名管道做无 DLL 通信,并涉及开启 DLL 访问的旁路做法;本篇换一条更干净的路——只调用客户端原生安全功能,不依赖任何外部 DLL 权限开关。 后续会直接给 C++ 服务端、MQL5 客户端两段实例,覆盖建链、数据交换和一轮性能基准。你在自己机器上跑通这套,就能验证每秒万级小包吞吐在本地回环下大概率无压力。外汇与贵金属交易终端环境复杂,管道通信虽安全但仍需防范多实例资源争用风险。
「用命名管道搭本地全双工信道」
在 C++ 里做 MT5 终端的外挂服务,最轻量的方案是命名管道。核心只依赖一组 WinAPI:CreateNamedPipe 建管道、ConnectNamedPipe 等客户端、WriteFile/ReadFile 收发、FlushFileBuffers 清缓冲、DisconnectNamedPipe 断连、CloseHandle 关句柄。管道打开后就是一个文件句柄,读写逻辑和本地文件没有区别,不需要你懂 socket 或任何网络栈。
命名管道分本地与网络两种形态,本篇示例先落本地:以字节模式、全双工打开,管道名用 \\.\pipe\MQL5.Pipe.Server。缓冲区设 256*1024 字节、超时 1000 毫秒,这些参数直接决定小包吞吐和断连判定节奏,可进代码里改。
MQL5 侧的 CFilePipe 默认以 ANSI 开文件,所以服务端收发的字符串会自动在 ANSI 与 Unicode 间转码;若 MQL5 端用 FILE_UNICODE 打开,则两端可直传带 BOM 的 Unicode 串。下面这段 Create 与 ConnectClient 是最小可跑骨架,复制到 VS 里改管道名就能起服务。
别把管道当永远在线
客户端没连时 ConnectNamedPipe 会阻塞在 PIPE_WAIT 模式;若脚本重连快于服务端 Accept,GetLastError 返回 ERROR_PIPE_CONNECTED 要当作已连处理,否则会误杀正常会话。外汇与贵金属交易外接服务有断线导致信号丢失的高风险,实盘前务必在模拟环境跑满 24 小时重连压测。
class=class="str">"cmt">//--- open CPipeManager manager; if(!manager.Create(L"\\\\.\\pipe\\MQL5.Pipe.Server")) class="kw">return(-class="num">1); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create named pipe | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CPipeManager::Create(LPCWSTR pipename) { class=class="str">"cmt">//--- check parameters if(!pipename || *pipename==class="num">0) class="kw">return(class="kw">false); class=class="str">"cmt">//--- close old Close(); class=class="str">"cmt">//--- create named pipe m_handle=CreateNamedPipe(pipename,PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES,class="num">256*class="num">1024,class="num">256*class="num">1024,class="num">1000,NULL); if(m_handle==INVALID_HANDLE_VALUE) { wprintf(L"Creating pipe &class="macro">#x27;%s&class="macro">#x27; failed\n",pipename); class="kw">return(class="kw">false); } class=class="str">"cmt">//--- ok wprintf(L"Pipe &class="macro">#x27;%s&class="macro">#x27; created\n",pipename); class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Connect client | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CPipeManager::ConnectClient(class="type">void) { class=class="str">"cmt">//--- pipe exists? if(m_handle==INVALID_HANDLE_VALUE) class="kw">return(class="kw">false); class=class="str">"cmt">//--- connected? if(!m_connected) { class=class="str">"cmt">//--- connect if(ConnectNamedPipe(m_handle,NULL)==class="num">0) { class=class="str">"cmt">//--- client already connected before ConnectNamedPipe? if(GetLastError()!=ERROR_PIPE_CONNECTED) class="kw">return(class="kw">false); class=class="str">"cmt">//--- ok } m_connected=true; } class=class="str">"cmt">//--- class="kw">return(true); }
客户端用命名管道对接服务端
MT5 客户端用 MQL5 写,核心依赖标准库的 CFilePipe。它和 CFileBin 几乎一样,唯一关键区别是在读数据前先验证虚拟文件里有没有足够数据可读。 这个验证不是多余的。命名管道在本地模式和网络模式下行为差异很大,缺了前置校验,网络模式一旦单次传超过 64K 的数据,读取几乎必定报错。 连接逻辑分两条路:先尝试连名叫 RemoteServerName 的远程机,失败就退回到本机管道。下面这段是 WaitForRead、ReadDoubleArray 和 OnStart 里的实际写法。 WaitForRead 用 while 轮询,句柄有效且没被停止时就比对 FileSize 和期望字节数,差得远就 Sleep(1) 让出 CPU;ReadDoubleArray 先算好要读的 double 总字节,交给 WaitForRead 把关再 FileReadArray;OnStart 里每 250 毫秒重试一次,先 RemoteServerName 后本地 .\pipe,谁先开成就 break。 外汇和贵金属行情通过这种管道转发时延迟低,但网络管道断连概率不低,实盘前先在本地双终端跑通再上远程。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Wait for incoming data | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CFilePipe::WaitForRead(class="kw">const class="type">class="kw">ulong size) { class=class="str">"cmt">//--- check handle and stop flag class="kw">while(m_handle!=INVALID_HANDLE && !IsStopped()) { class=class="str">"cmt">//--- enough data? if(FileSize(m_handle)>=size) class="kw">return(true); class=class="str">"cmt">//--- wait a little Sleep(class="num">1); } class=class="str">"cmt">//--- failure class="kw">return(class="kw">false); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Read an array of variables of class="type">class="kw">double type | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">uint CFilePipe::ReadDoubleArray(class="type">class="kw">double &array[],class="kw">const class="type">int start_item,class="kw">const class="type">int items_count) { class=class="str">"cmt">//--- calculate size class="type">uint size=ArraySize(array); if(items_count!=WHOLE_ARRAY) size=items_count; class=class="str">"cmt">//--- check for data if(WaitForRead(size*class="kw">sizeof(class="type">class="kw">double))) class="kw">return FileReadArray(m_handle,array,start_item,items_count); class=class="str">"cmt">//--- failure class="kw">return(class="num">0); } class="type">void OnStart() { class=class="str">"cmt">//--- wait for pipe server class="kw">while(!IsStopped()) { if(ExtPipe.Open("\\\\RemoteServerName\\pipe\\MQL5.Pipe.Server",FILE_READ|FILE_WRITE|FILE_BIN)!=INVALID_HANDLE) class="kw">break; if(ExtPipe.Open("\\\\.\\pipe\\MQL5.Pipe.Server",FILE_READ|FILE_WRITE|FILE_BIN)!=INVALID_HANDLE) class="kw">break; Sleep(class="num">250); } Print("Client: pipe opened");