MT5无DLL管道通信实战
用命名管道安全打通外部程序与EA
为什么需要无 DLL 的进程间通信
MetaTrader 5 的保护机制将 MQL5 程序限制在沙箱内,禁止其随意调用外部系统 DLL,以避免不受信 EA 给交易员带来安全威胁。但现实开发中,我们经常需要把行情、信号、风控或统计数据从第三方软件(如 C++ 写的服务器、Python 量化框架、C# 桌面程序)送入 EA,或反向控制 EA。传统做法是通过 DLL 突破沙箱,但这会被平台标记且存在安全隐患。命名管道(Named Pipe)是 Windows 提供的标准文件型通信对象,被 MQL5 标准库 CFilePipe 原生支持,无需任何 DLL 即可实现本机或局域网内的双向通信。
命名管道的工作原理与关键 WinAPI
命名管道是 Windows 内核对象,以 \\.\pipe\名称 的路径形式存在。服务端用 CreateNamedPipe 创建管道实例,客户端用文件打开方式连接。它支持字节模式或消息模式、半双工或全双工、阻塞或异步。本文采用全双工字节模式(PIPE_ACCESS_DUPLEX + PIPE_TYPE_BYTE),最简单且兼容文件读写。服务端核心函数包括:CreateNamedPipe 创建管道、ConnectNamedPipe 等待客户端、WriteFile/ReadFile 收发数据、FlushFileBuffers 刷缓冲、DisconnectNamedPipe 断开、CloseHandle 关句柄。管道句柄可像普通文件一样操作,因此 MQL5 的 CFilePipe 能直接复用文件逻辑。
- CreateNamedPipe:创建命名管道并返回句柄
- ConnectNamedPipe:服务端阻塞等待客户端连入
- WriteFile / ReadFile:基于句柄的字节流读写
- FlushFileBuffers:确保缓冲数据落盘发送
- DisconnectNamedPipe / CloseHandle:断开并释放资源
C++ 服务端实现详解
服务端用 C++ 编写,封装为 CPipeManager 类。创建时调用 CreateNamedPipe,指定全双工、字节类型、等待模式、无限实例、256K 收发缓冲。连接阶段用 ConnectNamedPipe 阻塞,直到 MQL5 客户端打开同名管道。数据交换封装为 Send/Read(二进制)和 SendString/ReadString(ANSI 文本)。由于 MQL5 的 CFilePipe 默认以 ANSI 打开,Unicode 字符串会自动转码;若用 FILE_UNICODE 打开则可传带 BOM 的 Unicode。以下为创建与连接的核心片段:
//--- open
CPipeManager manager;
if(!manager.Create(L"\\\\.\\pipe\\MQL5.Pipe.Server"))
return(-1);
//+------------------------------------------------------------------+
//| Create named pipe |
//+------------------------------------------------------------------+
bool CPipeManager::Create(LPCWSTR pipename)
{
//--- check parameters
if(!pipename || *pipename==0) return(false);
//--- close old
Close();
//--- create named pipe
m_handle=CreateNamedPipe(pipename,PIPE_ACCESS_DUPLEX,
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
PIPE_UNLIMITED_INSTANCES,256*1024,256*1024,1000,NULL);
if(m_handle==INVALID_HANDLE_VALUE)
{
wprintf(L"Creating pipe '%s' failed\n",pipename);
return(false);
}
//--- ok
wprintf(L"Pipe '%s' created\n",pipename);
return(true);
}
//+------------------------------------------------------------------+
//| Connect client |
//+------------------------------------------------------------------+
bool CPipeManager::ConnectClient(void)
{
//--- pipe exists?
if(m_handle==INVALID_HANDLE_VALUE) return(false);
//--- connected?
if(!m_connected)
{
//--- connect
if(ConnectNamedPipe(m_handle,NULL)==0)
{
//--- client already connected before ConnectNamedPipe?
if(GetLastError()!=ERROR_PIPE_CONNECTED)
return(false);
//--- ok
}
m_connected=true;
}
//---
return(true);
}
MQL5 客户端连接与数据交换
MQL5 端使用标准库 CFilePipe。它和 CFileBin 几乎一致,但增加了 WaitForRead 方法,在读取前确认虚拟管道中已有足够字节,这对网络模式至关重要。客户端先尝试连远程计算机名,失败再连本地。连接成功后发送欢迎字符串与构建号,服务器回送 "Hello from pipe server" 和整数 1234567890,客户端再发回测试串与整数。以下为连接逻辑与收发示例:
void OnStart()
{
//--- wait for pipe server
while(!IsStopped())
{
if(ExtPipe.Open("\\\\RemoteServerName\\pipe\\MQL5.Pipe.Server",FILE_READ|FILE_WRITE|FILE_BIN)!=INVALID_HANDLE) break;
if(ExtPipe.Open("\\\\.\\pipe\\MQL5.Pipe.Server",FILE_READ|FILE_WRITE|FILE_BIN)!=INVALID_HANDLE) break;
Sleep(250);
}
Print("Client: pipe opened");
//--- send welcome message
if(!ExtPipe.WriteString(__FILE__+" on MQL5 build "+IntegerToString(__MQ5BUILD__)))
{
Print("Client: sending welcome message failed");
return;
}
//--- read data from server
string str;
int value=0;
if(!ExtPipe.ReadString(str))
{
Print("Client: reading string failed");
return;
}
Print("Server: ",str," received");
if(!ExtPipe.ReadInteger(value))
{
Print("Client: reading integer failed");
return;
}
Print("Server: ",value," received");
//--- send data to server
if(!ExtPipe.WriteString("Test string"))
{
Print("Client: sending string failed");
return;
}
if(!ExtPipe.WriteInteger(value))
{
Print("Client: sending integer failed");
return;
}
注意:CFilePipe 的 ReadString/ReadInteger 内部依赖 WaitForRead,因此网络大块传输不会因缓冲未满而报错。若您自己用 FileReadArray,必须显式调用 WaitForRead 或等价轮询,否则网络模式 >64K 必错。
//+------------------------------------------------------------------+
//| Wait for incoming data |
//+------------------------------------------------------------------+
bool CFilePipe::WaitForRead(const ulong size)
{
//--- check handle and stop flag
while(m_handle!=INVALID_HANDLE && !IsStopped())
{
//--- enough data?
if(FileSize(m_handle)>=size)
return(true);
//--- wait a little
Sleep(1);
}
//--- failure
return(false);
}
//+------------------------------------------------------------------+
//| Read an array of variables of double type |
//+------------------------------------------------------------------+
uint CFilePipe::ReadDoubleArray(double &array[],const int start_item,const int items_count)
{
//--- calculate size
uint size=ArraySize(array);
if(items_count!=WHOLE_ARRAY) size=items_count;
//--- check for data
if(WaitForRead(size*sizeof(double)))
return FileReadArray(m_handle,array,start_item,items_count);
//--- failure
return(0);
}
千兆级性能基准测试
为验证管道吞吐,服务端向客户端发送 128 块各 8MB 的双精度数组(共 1GB),每块首末元素写入哨兵值用于校验,结束后客户端回送确认整数 12345,防止单方过早断连丢数据。本地测试速率约 2921 MB/s,局域网千兆环境约 63 MB/s(占带宽 63%)。这说明管道可承载任意量级数据,适合高频 tick 转发或模型推理结果回传。服务端基准核心:
//--- benchmark
double volume=0.0;
double *buffer=new double[1024*1024]; // 8 Mb
wprintf(L"Server: start benchmark\n");
if(buffer)
{
//--- fill the buffer
for(size_t j=0;j<1024*1024;j++)
buffer[j]=j;
//--- send 8 Mb * 128 = 1024 Mb to client
DWORD ticks=GetTickCount();
for(size_t i=0;i<128;i++)
{
//--- setup guard signatures
buffer[0]=i;
buffer[1024*1024-1]=i+1024*1024-1;
//---
if(!manager.Send(buffer,sizeof(double)*1024*1024))
{
wprintf(L"Server: benchmark failed, %d\n",GetLastError());
break;
}
volume+=sizeof(double)*1024*1024;
wprintf(L".");
}
wprintf(L"\n");
//--- read confirmation
if(!manager.Read(&value,sizeof(value)) || value!=12345)
wprintf(L"Server: benchmark confirmation failed\n");
//--- show statistics
ticks=GetTickCount()-ticks;
if(ticks>0)
wprintf(L"Server: %.0lf Mb sent at %.0lf Mb per second\n",volume/1024/1024,volume/1024/ticks);
//---
delete[] buffer;
}
//--- benchmark
double buffer[];
double volume=0.0;
if(ArrayResize(buffer,1024*1024,0)==1024*1024)
{
uint ticks=GetTickCount();
//--- read 8 Mb * 128 = 1024 Mb from server
for(int i=0;i<128;i++)
{
uint items=ExtPipe.ReadDoubleArray(buffer);
if(items!=1024*1024)
{
Print("Client: benchmark failed after ",volume/1024," Kb, ",items," items received");
break;
}
//--- check the data
if(buffer[0]!=i || buffer[1024*1024-1]!=i+1024*1024-1)
{
Print("Client: benchmark invalid content");
break;
}
//---
volume+=sizeof(double)*1024*1024;
}
//--- send confirmation
value=12345;
if(!ExtPipe.WriteInteger(value))
Print("Client: benchmark confirmation failed ");
//--- show statistics
ticks=GetTickCount()-ticks;
if(ticks>0)
printf("Client: %.0lf Mb received at %.0lf Mb per second\n",volume/1024/1024,volume/1024/ticks);
//---
ArrayFree(buffer);
}
- 本地回环:约 2921 MB/s,几乎无瓶颈
- 千兆局域网:约 63 MB/s,达物理带宽 63%
- 必须收确认信号,否则断连丢尾数据
常见坑与权限问题排查
许多用户在 Win10/11 上跑不通示例,客户端 Open 返回 INVALID_HANDLE。根因常是 UAC 权限不一致:PipeServer.exe 用管理员启动,而 MT5 普通权限运行,导致命名管道 ACL 拒绝访问。解决办法:两端同权限启动,或在 CreateNamedPipe 的 SECURITY_ATTRIBUTES 中将 Dacl 置空,允许低权限连接。另需注意管道名格式:本地必须 \\.\pipe\名,远程为 \\机器名\pipe\名,多斜杠易写错。还有人尝试两个 MT5 客户端互连,若都用 CFilePipe 而无独立服务端,会因无人调用 ConnectNamedPipe 而挂起,需参考专门的 MT 间管道文章。
与其他外部通信方案对比
除命名管道外,MQ5 外部通信还有三类常见路:1)DLL 调用,灵活但触沙箱警报、易携毒;2)文件轮询,写 txt/csv 再读,简单但延迟高、并发差;3)Socket(需 DLL 或 MT5 内置 WebRequest 仅限 HTTP 客户端)。命名管道在“免 DLL、低延迟、大吞吐、本机/内网”场景全面胜出;若需跨公网或对接 Web API,则 WebRequest 或自建中转更合适。总结:管道是 EA 与桌面程序紧耦合集成的最优无 DLL 解。