一个使用命名管道在 MetaTrader 5 客户端之间进行通信的无 DLL 解决方案(基础篇)
◍ 用命名管道让 MT5 客户端互聊
在 MT5 里做多终端协同,很多人第一反应是调 DLL,其实系统自带的命名管道(Named Pipe)就能绕开第三方依赖。它走的是 Windows 内核的进程间通信机制,MT5 用 FileOpen / FileWrite 配合特殊路径前缀即可读写同一管道,省掉 DLL 签名和白名单的麻烦。 这套方案最早在 2013-12-09 的示例里被提出,实测帖子浏览量 5324、跟评 25 条,说明社区对无 DLL 跨终端通信确实有硬需求。贵金属与外汇跨账号对冲、信号分发时,用它比 socket 更轻,但也要注意:管道读写阻塞会拖慢 EA 的 OnTick,属于高风险环境下的工程取舍。 要验证很简单,开两个 MT5 终端,一个写管道一个读管道,看报价是否实时透传。下面这段是管道服务端建连的核心写法。
class="type">int pipeHandle = FileOpen("\\\\.\\pipe\\mt5link", FILE_READ|FILE_WRITE|FILE_BIN); if(pipeHandle != INVALID_HANDLE) FileWrite(pipeHandle, Ask);
「为什么选命名管道而非写文件」
想在多个 MT5 客户端之间互通报价,最先想到的是用硬盘文件做中转:一个端写、另一个端读。但实测这种方式在面对连续流式报价时效率偏低,单次消息落盘尚可,持续推送就会频繁 IO 阻塞。 后来看到有人用 WCF 把报价导出到 .NET,也有文章走类似思路,但都依赖外部 DLL 或特定框架。我要的是纯 MQL5、无 DLL、一端当服务器一端当客户端的方案。 查 MSDN 的进程间通信规范后发现,命名管道(Named Pipes)支持本机或内网多机通信,不强制 DLL,正好契合需求。本文后续的 CNamedPipes 类就是基于它实现的,并在 MT5 客户端间测了价格变动指标的流推送与整体吞吐。
用命名管道打通 MT5 与外部进程
管道在操作系统里就是一段连接两个进程、专门搬运数据的通道。匿名管道受限明显:既不能走网络,又要求两端必须是父子进程关系;命名管道没有这些束缚,适合把 MT5 客户端和独立写的辅助程序连起来。 服务器进程要用一个约定好的名字建管道,字符串格式为 \\servername\pipe\pipename。本机通信时 servername 直接写成点号,变成 \\.\pipe\pipename,MT5 和外部程序都在同一台机器上跑时就用这种。 客户端必须提前知道管道名才能连进来。实践中我按账号区分,命名成 \\.\pipe\mt[account_number],比如账号 123456 对应 \\.\pipe\mt123456;这只是一个可随意换的约定,你也可以用策略名或符号名做前缀。打开 MT5 后,用任意支持命名管道的语言起一个服务端,就能让行情或信号跨进程流动,外汇和贵金属波动剧烈、杠杆高危,管道只解决传输,不替你过滤风险。
◍ 用 CNamedPipes 类绕开底层管道调用
Windows 下命名管道的底层函数都藏在 kernel32.dll 里。服务器先调 CreateNamedPipe() 建实例,再调 ConnectNamedPipe() 等客户端连;若后者返回 0 且 GetLastError() 报 535(0x217,ERROR_PIPE_CONNECTED),说明客户端抢在 Connect 之前就连上了,这不算失败。读写则复用文件级 ReadFile / WriteFile,逻辑和文件 IO 一致。 自己封装后我把这套脏活收进了 CNamedPipes 类。只要把 CNamedPipes.mqh 丢进终端的 /include,源文件里 include 一下、声明一个 CNamedPipe 对象就能用,不用再碰 WinAPI 原型。 类暴露的方法分三拨:管道生命周期用 Create()、Connect()、Disconnect()、Open()、Close();字符串通信用 WriteUnicode()/ReadUnicode() 和 WriteANSI()/ReadANSI();行情对象走 WriteTick()/ReadTick()。Create() 的 account 参数填了就用指定帐号拼管道名,不填则取当前 AccountInfoInteger(ACCOUNT_LOGIN),建成功返 true。 Open() 是客户端专用,尝试连已建管道,若 5 秒内没连上或打开失败直接返 false。ReadTick() 在管道空时返 0,否则返 MqlTick 的字节数——这个返回值能拿来判空,比盲目读省事。 下方代码里 Create() 用 CreateNamedPipeW 建双向管道(PIPE_ACCESS_DUPLEX),缓冲区按 ushort 长度算;Connect() 里专门接住了 ERROR_PIPE_CONNECTED 那种‘提前连上’的边界情况,直接转成 true。
BOOL WINAPI ReadFile( __in HANDLE hFile, __out LPVOID lpBuffer, __in DWORD nNumberOfBytesToRead, __out_opt LPDWORD lpNumberOfBytesRead, __inout_opt LPOVERLAPPED lpOverlapped ); BOOL WINAPI WriteFile( __in HANDLE hFile, __in LPCVOID lpBuffer, __in DWORD nNumberOfBytesToWrite, __out_opt LPDWORD lpNumberOfBytesWritten, __inout_opt LPOVERLAPPED lpOverlapped ); class=class="str">"cmt">//+------------------------------------------------------------------ class=class="str">"cmt">/// Create() : 尝试创建命名管道的实例 class=class="str">"cmt">/// \参数 account - 源终端帐号 class=class="str">"cmt">/// \返回 true - 如果创建成功, class="kw">false 否则 class=class="str">"cmt">//+------------------------------------------------------------------ class="type">bool CNamedPipe::Create(class="type">int account=class="num">0) { if(account==class="num">0) pipeNumber=IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN)); else pipeNumber=IntegerToString(account); class="type">class="kw">string fullPipeName=pipeNamePrefix+pipeNumber; hPipe=CreateNamedPipeW(fullPipeName, (class="type">int)GENERIC_READ|GENERIC_WRITE|(ENUM_PIPE_ACCESS)PIPE_ACCESS_DUPLEX, (ENUM_PIPE_MODE)PIPE_TYPE_RW_BYTE,PIPE_UNLIMITED_INSTANCES, BufferSize*class="kw">sizeof(class="type">class="kw">ushort),BufferSize*class="kw">sizeof(class="type">class="kw">ushort),class="num">0,NULL); if(hPipe==INVALID_HANDLE_VALUE) class="kw">return class="kw">false; else class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------ class=class="str">"cmt">/// Connect() : 等待客户端连接管道 class=class="str">"cmt">/// \返回 true - 如果连接, class="kw">false 否则. class=class="str">"cmt">//+------------------------------------------------------------------ class="type">bool CNamedPipe::Connect(class="type">void) { if(ConnectNamedPipe(hPipe,NULL)==class="kw">false) class="kw">return(kernel32::GetLastError()==ERROR_PIPE_CONNECTED); else class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------ class=class="str">"cmt">/// Disconnect(): 从通道断开 class=class="str">"cmt">/// \返回 true - 如果断开, class="kw">false 否则 class=class="str">"cmt">//+------------------------------------------------------------------
「管道断开与跨终端读写的具体实现」
命名管道在 MT5 之间传数据,断开动作其实极薄:直接调 DisconnectNamedPipe 包一层句柄即可,没有额外状态清理。 Open() 的实战细节在账号参数:传 0 时自动取当前终端 ACCOUNT_LOGIN 转字符串当管道名,否则用指定 account;前缀拼完后先 WaitNamedPipeW 等 5000 毫秒,超时即打印不可用并返回 false,这一步决定了多终端握手不会无限卡死。 写 Unicode 时先 StringToShortArray 拿到 ushort 数组,分两次 WriteFile——先写 int 长度再写实际字符,读端对称先读长度再读数组,用 ShortArrayToString 还原;STR_SIZE*sizeof(uint) 的栈数组大小是读端上限,超长消息会被截断。 外汇与贵金属 EA 走这套管道做跨账号信号同步时,管道句柄失效不会触发行情异常,但可能让信号延迟或丢失,属于高风险链路,建议每次 Open 失败都重连而非忽略。
class="type">bool CNamedPipe::Disconnect(class="type">void) { class="kw">return DisconnectNamedPipe(hPipe); } class=class="str">"cmt">//+------------------------------------------------------------------ class=class="str">"cmt">/// Open() : 试图打开之前创建的管道 class=class="str">"cmt">/// \参数 account - 源终端帐号 class=class="str">"cmt">/// \返回 true - 如果成功, class="kw">false 否则. class=class="str">"cmt">//+------------------------------------------------------------------ class="type">bool CNamedPipe::Open(class="type">int account=class="num">0) { if(account==class="num">0) pipeName=IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN)); else pipeName=IntegerToString(account); class="type">class="kw">string fullPipeName=pipeNamePrefix+pipeName; if(hPipe==INVALID_HANDLE_VALUE) { if(WaitNamedPipeW(fullPipeName,class="num">5000)==class="num">0) { Print("管道 "+fullPipeName+" 不可用..."); class="kw">return class="kw">false; } hPipe=CreateFileW(fullPipeName,GENERIC_READ|GENERIC_WRITE,class="num">0,NULL,OPEN_EXISTING,class="num">0,NULL); if(hPipe==INVALID_HANDLE_VALUE) { Print("管道打开失败"); class="kw">return class="kw">false; } } class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------ class=class="str">"cmt">/// Close() : 关闭管道句柄 class=class="str">"cmt">/// \返回 class="num">0 如果成功, 非-class="num">0 否则 class=class="str">"cmt">//+------------------------------------------------------------------ class="type">int CNamedPipe::Close(class="type">void) { class="kw">return CloseHandle(hPipe); } class=class="str">"cmt">//+------------------------------------------------------------------ class=class="str">"cmt">/// WriteUnicode() : 写 Unicode 字符串至管道 class=class="str">"cmt">/// \参数 message - 发送的字符串 class=class="str">"cmt">/// \返回写入管道的字节数 class=class="str">"cmt">//+------------------------------------------------------------------ class="type">int CNamedPipe::WriteUnicode(class="type">class="kw">string message) { class="type">int ushortsToWrite, bytesWritten; class="type">class="kw">ushort UNICODEarray[]; ushortsToWrite = StringToShortArray(message, UNICODEarray); WriteFile(hPipe,ushortsToWrite,class="kw">sizeof(class="type">int),bytesWritten,class="num">0); WriteFile(hPipe,UNICODEarray,ushortsToWrite*class="kw">sizeof(class="type">class="kw">ushort),bytesWritten,class="num">0); class="kw">return bytesWritten; } class=class="str">"cmt">//+------------------------------------------------------------------ class=class="str">"cmt">/// ReadUnicode(): 从管道读 Unicode 字符串 class=class="str">"cmt">/// \返回 Unicode 字符串 (MQL5 字符串) class=class="str">"cmt">//+------------------------------------------------------------------ class="type">class="kw">string CNamedPipe::ReadUnicode(class="type">void) { class="type">class="kw">string ret; class="type">class="kw">ushort UNICODEarray[STR_SIZE*class="kw">sizeof(class="type">uint)]; class="type">int bytesRead, ushortsToRead; ReadFile(hPipe,ushortsToRead,class="kw">sizeof(class="type">int),bytesRead,class="num">0); ReadFile(hPipe,UNICODEarray,ushortsToRead*class="kw">sizeof(class="type">class="kw">ushort),bytesRead,class="num">0); if(bytesRead!=class="num">0) ret = ShortArrayToString(UNICODEarray); class="kw">return ret; }