在MQL中操作套接字,或者如何成为信号提供者·进阶篇
(2/3)· 从理论和结构移植走到可跑的TCP/UDP通信,避开阻塞卡死的坑
接上篇铺垫的套接字概念,这一篇直接动手在MQL里把WinAPI那套网络和终端打通。很多人在MQ中试socket时一连接就卡死终端,其实是漏了非阻塞模式的设置。本文把客户端、服务器和图表数据推送的实例一步步拆给你看。
◍ 本地搭一套 C/S 套接字的最小实验环境
做 MQL5 套接字实验,我建议先用函数式写法而不是类。线性流程没有分支跳转,调试时一眼能看清 connect、bind、accept 的调用顺序;等逻辑跑通了,再裹成类做 OOP 重构也来得及。不少开发者反馈,从裸函数入手更容易理解 socket 的生命周期。 有个坑必须提前说:服务器 EA 退出时,操作系统不会自动释放已绑定的端口。重复创建 socket 再调 bind,很可能直接报 'Address already in use'。临时解法是对 socket 设 SO_REUSEADDR 选项,或者干脆重启 MT5 终端。用 TCPView 这类监控工具能实时看到本机 socket 状态,确认端口到底有没有被占。 在同一台电脑上跑客户端连本机服务器完全可行,前提是服务器不能藏在 NAT 后面,端口也没被系统或路由器掐掉。想验证多客户端并发,服务器至少得丢到一台有公网 IP 的 VPS 上,并开放一个对外端口——本地回环只能测通联逻辑,测不出真实网络下的连接拒绝现象。
用一次性 socket 把图表模板推给客户端
从服务器往客户端单向丢一个 tpl 文件,是最轻量的 socket 实战:连上、发一次、断线,两端都不用维护 send/recv 循环。服务器端 EA 挂在独立图表上,用 OnTimer 每秒巡检——侦听连接、Send 一次模板、立刻关套接字;若 server socket 掉了就重建。 客户端更简单,做成脚本即可:OnStart 里 Recv 一次就收工,不需要常驻 EA。用户只要在约定时间跑一下脚本,就能把别人服务器上的指标布局和图形对象套到自己的图上,用来接收交易信号或目标位提示。外汇与贵金属波动剧烈,这类推送只解决布局同步,不替代风控。 注意 MT5 的沙盒限制:已存的 tpl 不在文件沙盒内,得借 WinAPI 从 Profiles\Templates 读出来,细节全在下方代码里。另外有人用 WebRequest 换掉客户端 socket,只要补 HTTP 头并在终端设置里放行服务器 URL 就能试。 踩坑点:某些终端版本调 WSACleanup 会顺手关掉 MT5 自身连接。实验时若遇此怪象,把代码里的 WSAStartup / WSACleanup 注释掉再跑。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TplServer | class=class="str">"cmt">//| programming & development - Alexey Sergeev | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "© class="num">2006-class="num">2016 Alexey Sergeev" class="macro">#class="kw">property link "profy.mql@gmail.com" class="macro">#class="kw">property version "class="num">1.00" class="macro">#include "SocketLib.mqh" class="kw">input class="type">class="kw">string Host="class="num">0.0.class="num">0.0"; class="kw">input class="type">class="kw">ushort Port=class="num">8080; class="type">uchar tpl[]; class="type">int iCnt=class="num">0; class="type">class="kw">string exname=""; SOCKET server=INVALID_SOCKET; class=class="str">"cmt">//------------------------------------------------------------------ OnInit class="type">int OnInit() { EventSetTimer(class="num">1); exname=MQLInfoString(MQL_PROGRAM_NAME)+".ex5"; class="kw">return class="num">0; } class=class="str">"cmt">//------------------------------------------------------------------ OnDeinit class="type">void OnDeinit(class="kw">const class="type">int reason) { EventKillTimer(); CloseClean(); } class=class="str">"cmt">//------------------------------------------------------------------ OnChartEvent class="type">void OnChartEvent(class="kw">const class="type">int id,class="kw">const class="type">long &lparam,class="kw">const class="type">class="kw">double &dparam,class="kw">const class="type">class="kw">string &sparam) { if(iCnt==class="num">0) class=class="str">"cmt">// 创建模板文件的限制 - 每秒不能超过一次 { Print("创建 TPL"); class="type">uchar buf[]; CreateTpl(buf); class="type">uchar smb[]; StringToCharArray(Symbol(),smb); ArrayResize(smb,class="num">10); class="type">uchar tf[]; StringToCharArray(IntegerToString(Period()),tf); ArrayResize(tf,class="num">10); class=class="str">"cmt">// 创建用于发送的数据 ArrayCopy(tpl,smb, ArraySize(tpl)); class=class="str">"cmt">// 加上交易品种名称 ArrayCopy(tpl, tf, ArraySize(tpl)); class=class="str">"cmt">// 加上时段数值 ArrayCopy(tpl,buf, ArraySize(tpl)); class=class="str">"cmt">// 加上模板本身 } iCnt++; } class=class="str">"cmt">//------------------------------------------------------------------ OnTimer
「用定时器把图表模板推给连进来的客户端」
在 MT5 里做模板广播,核心是把 OnTimer 当成一个轻量轮询器:每次触发先把 iCnt 归零,再判断 server 套接字是否还活着,死了就调 StartServer 重绑地址端口,活着就进循环挨个 accept 客户端并把 tpl 数组整体 send 出去。 下面这段 OnTimer 的逻辑里,send 的返回值 res 会被直接 printf 成「已发送 %d 字节,共 %d 字节」,你在日志里能看到实际传输长度;若 res 等于 SOCKET_ERROR 则打印 WSAErrorDescript(WSAGetLastError()),方便定位是连接断了还是缓冲区满了。 StartServer 这边先把 WSAStartup(MAKEWORD(2,2),...) 跑通,再用 socket(AF_INET,SOCK_STREAM,IPPROTO_TCP) 建 TCP 套接字,bind 之后立刻 ioctlsocket 设成非阻塞(non_block=1),最后 listen(server,SOMAXCONN)。非阻塞模式意味着 OnTimer 不会被 accept 卡死,但代价是 WSAEISCONN 之外的错误都得自己 CloseClean 收尾。 AcceptClient 只做一件事:从 server 摘一个 new_sock 出来,失败就返回 INVALID_SOCKET,由上层 do-while 判定退出。外汇与贵金属行情波动剧烈、断连概率偏高,实盘跑这套推送前建议先在模拟账户验证套接字回收是否干净,避免句柄泄漏拖慢终端。
class="type">void OnTimer() { iCnt=class="num">0; class=class="str">"cmt">// 重置模板创建计数器 if(server==INVALID_SOCKET) StartServer(Host,Port); else { class=class="str">"cmt">// 在循环中取得所有的客户端并把当前图表模板发送给每个客户端 SOCKET client=INVALID_SOCKET; do { client=AcceptClient(); class=class="str">"cmt">// 接受一个客户端的套接字 if(client==INVALID_SOCKET) class="kw">return; class="type">int slen=ArraySize(tpl); class="type">int res=send(client,tpl,slen,class="num">0); if(res==SOCKET_ERROR) Print("-发送失败,错误: "+WSAErrorDescript(WSAGetLastError())); else printf("已发送 %d 字节,共 %d 字节",res,slen); if(shutdown(client,SD_BOTH)==SOCKET_ERROR) Print("-关闭失败,错误: "+WSAErrorDescript(WSAGetLastError())); closesocket(client); } class="kw">while(client!=INVALID_SOCKET); } } class=class="str">"cmt">//------------------------------------------------------------------ StartServer class="type">void StartServer(class="type">class="kw">string addr,class="type">class="kw">ushort port) { class=class="str">"cmt">// 初始化库 class="type">char wsaData[]; ArrayResize(wsaData,class="kw">sizeof(WSAData)); class="type">int res=WSAStartup(MAKEWORD(class="num">2,class="num">2), wsaData); if(res!=class="num">0) { Print("-WSAStartup 失败,错误: "+class="type">class="kw">string(res)); class="kw">return; } class=class="str">"cmt">// 创建一个套接字 server=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); if(server==INVALID_SOCKET) { Print("-创建失败,错误: "+WSAErrorDescript(WSAGetLastError())); CloseClean(); class="kw">return; } class=class="str">"cmt">// 绑定地址和端口 Print("尝试绑定..."+addr+":"+class="type">class="kw">string(port)); class="type">char ch[]; StringToCharArray(addr,ch); sockaddr_in addrin; addrin.sin_family=AF_INET; addrin.sin_addr=inet_addr(ch); addrin.sin_port=htons(port); ref_sockaddr ref=(ref_sockaddr)addrin; if(bind(server,ref.ref,class="kw">sizeof(addrin))==SOCKET_ERROR) { class="type">int err=WSAGetLastError(); if(err!=WSAEISCONN) { Print("-连接失败,错误: "+WSAErrorDescript(err)+". 清除套接字"); CloseClean(); class="kw">return; } } class=class="str">"cmt">// 设置为非阻塞模式 class="type">int non_block=class="num">1; res=ioctlsocket(server,(class="type">int)FIONBIO,non_block); if(res!=NO_ERROR) { Print("ioctlsocket 失败,错误: "+class="type">class="kw">string(res)); CloseClean(); class="kw">return; } class=class="str">"cmt">// 侦听端口,接受客户端的连接 if(listen(server,SOMAXCONN)==SOCKET_ERROR) { Print("侦听失败,错误: ",WSAErrorDescript(WSAGetLastError())); CloseClean(); class="kw">return; } Print("启动服务器成功"); } class=class="str">"cmt">//------------------------------------------------------------------ Accept SOCKET AcceptClient() class=class="str">"cmt">// 接受一个客户端套接字 { if(server==INVALID_SOCKET) class="kw">return INVALID_SOCKET; ref_sockaddr ch; class="type">int len=class="kw">sizeof(ref_sockaddr); SOCKET new_sock=accept(server,ch.ref,len); class=class="str">"cmt">//sockaddr_in aclient=(sockaddr_in)ch; 转换为结构,如有必要可以获得连接的更多信息 if(new_sock==INVALID_SOCKET) { class="type">int err=WSAGetLastError();
◍ 用 kernel32 把图表模板塞进字节流
EA 在 MT5 里想通过 socket 把当前图表外观发给远端,得先把 .tpl 读成原始字节。上面这段直接调 kernel32.dll 的 CreateFileW / ReadFile,绕开 MQL5 文件函数的编码包装,拿到的是文件本身的 ushort 数组。 关键常量要盯死:GENERIC_READ=0x80000000、OPEN_EXISTING=3、FILE_SHARE_READ_=0x00000001,路径拼在 TerminalInfoString(TERMINAL_PATH)+\\Profiles\\Templates\\ 下面。ChartSaveTemplate(0,"tcpsend.tpl") 先落盘,再读回。 读回来之后有一段去 EA 名的处理:ShortArrayToString 转串,StringReplace(a,exname," ") 把 EA 标识替成空格,再 StringToShortArray 回去。这样远端加载模板时不会因找不到同名 EA 报错。 最后 abuf 按 sz*2 扩容——因为 ushort 是 2 字节,网络发送要的是 uchar 流。外汇贵金属行情波动剧烈,这类自定义传输模块仅供本地验证,实盘接入外部 socket 有断连与安全风险。
if(err==WSAEWOULDBLOCK) Comment("\n等待客户端("+class="type">class="kw">string(TimeCurrent())+")"); else { Print("接受失败,错误: ",WSAErrorDescript(err)); CloseClean(); class="kw">return INVALID_SOCKET; } } class="kw">return new_sock; } class=class="str">"cmt">//------------------------------------------------------------------ CloseClean class="type">void CloseClean() class=class="str">"cmt">// 关闭套接字 { if(server!=INVALID_SOCKET) { closesocket(server); server=INVALID_SOCKET; } WSACleanup(); Print("停止服务器"); } class=class="str">"cmt">//------------------------------------------------------------------ class="macro">#class="kw">import "kernel32.dll" class="type">int CreateFileW(class="type">class="kw">string lpFileName,class="type">uint dwDesiredAccess,class="type">uint dwShareMode,class="type">uint lpSecurityAttributes,class="type">uint dwCreationDisposition,class="type">uint dwFlagsAndAttributes,class="type">int hTemplateFile); class="type">bool ReadFile(class="type">int h,class="type">class="kw">ushort &lpBuffer[],class="type">uint nNumberOfBytesToRead,class="type">uint &lpNumberOfBytesRead,class="type">int lpOverlapped=class="num">0); class="type">uint SetFilePointer(class="type">int h,class="type">int lDistanceToMove,class="type">int,class="type">uint dwMoveMethod); class="type">bool CloseHandle(class="type">int h); class="type">uint GetFileSize(class="type">int h,class="type">int); class="macro">#class="kw">import class="macro">#define FILE_BEGIN class="num">0 class="macro">#define OPEN_EXISTING class="num">3 class="macro">#define GENERIC_READ 0x80000000 class="macro">#define FILE_ATTRIBUTE_NORMAL 0x00000080 class="macro">#define FILE_SHARE_READ_ 0x00000001 class=class="str">"cmt">//------------------------------------------------------------------ LoadTpl class="type">bool CreateTpl(class="type">uchar &abuf[]) { class="type">class="kw">string path=TerminalInfoString(TERMINAL_PATH); class="type">class="kw">string name="tcpsend.tpl"; class=class="str">"cmt">// 创建模板 ChartSaveTemplate(class="num">0,name); class=class="str">"cmt">// 把模板读取到数组中 path+="\\Profiles\\Templates\\"+name; class="type">int h=CreateFileW(path, GENERIC_READ, FILE_SHARE_READ_, class="num">0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, class="num">0); if(h==INVALID_HANDLE) class="kw">return class="kw">false; class="type">uint sz=GetFileSize(h,NULL); class="type">class="kw">ushort rbuf[]; ArrayResize(rbuf,sz); ArrayInitialize(rbuf,class="num">0); SetFilePointer(h,class="num">0,NULL,FILE_BEGIN); class=class="str">"cmt">// 移动到文件顶端 class="type">int r; ReadFile(h,rbuf,sz,r,NULL); CloseHandle(h); class=class="str">"cmt">// 从模板中删除EA的名称 class="type">class="kw">string a=ShortArrayToString(rbuf); ArrayResize(rbuf,class="num">0); StringReplace(a,exname," "); StringToShortArray(a,rbuf); class=class="str">"cmt">// 把文件复制到字节数组中 (保持 Unicode) sz=ArraySize(rbuf); ArrayResize(abuf,sz*class="num">2);
MT5 里用原始套接字拉行情流
想在 MT5 里绕开自带图表去拿外部行情或信号,可以直接用 WinSocket 接口写客户端。下面这段是连接本地 127.0.0.1:8080 的 TCP 客户端骨架,初始化 WSA、建套接字、connect 之后切非阻塞,再循环 recv。 关键参数就两个:Host 默认 "127.0.0.1",Port 默认 8080(ushort)。若你本机跑了转发服务,改这两个 input 就能连上;外汇和贵金属行情转发涉及实时杠杆品种,高空头风险,参数错配可能读到脏数据。 非阻塞模式靠 ioctlsocket 设 FIONBIO=1,之后 recv 返回负值时必须用 WSAGetLastError 判 WSAEWOULDBLOCK,否则会误判断线。接收缓冲写死 rbuf[512],单次最多拉 512 字节,想验证可把 Port 改成你服务实际监听口,开 MT5 跑 OnStart 看 Print 出的"连接成功"。
class="macro">#class="kw">property copyright "© class="num">2006-class="num">2016 Alexey Sergeev" class="macro">#class="kw">property link "profy.mql@gmail.com" class="macro">#class="kw">property version "class="num">1.00" class="macro">#include "..\Experts\SocketLib.mqh" class="kw">input class="type">class="kw">string Host="class="num">127.0.class="num">0.1"; class="kw">input class="type">class="kw">ushort Port=class="num">8080; SOCKET client=INVALID_SOCKET; class=class="str">"cmt">//------------------------------------------------------------------ OnStart class="type">void OnStart() { class=class="str">"cmt">// 初始化库 class="type">char wsaData[]; ArrayResize(wsaData,class="kw">sizeof(WSAData)); class="type">int res=WSAStartup(MAKEWORD(class="num">2,class="num">2), wsaData); if(res!=class="num">0) { Print("-WSAStartup 失败,错误: "+class="type">class="kw">string(res)); class="kw">return; } class=class="str">"cmt">// 创建一个套接字 client=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); if(client==INVALID_SOCKET) { Print("-创建套接字失败,错误: "+WSAErrorDescript(WSAGetLastError())); CloseClean(); class="kw">return; } class=class="str">"cmt">// 连接到服务器 class="type">char ch[]; StringToCharArray(Host,ch); sockaddr_in addrin; addrin.sin_family=AF_INET; addrin.sin_addr=inet_addr(ch); addrin.sin_port=htons(Port); ref_sockaddr ref=(ref_sockaddr)addrin; res=connect(client,ref.ref,class="kw">sizeof(addrin)); if(res==SOCKET_ERROR) { class="type">int err=WSAGetLastError(); if(err!=WSAEISCONN) { Print("-连接失败,错误: "+WSAErrorDescript(err)); CloseClean(); class="kw">return; } } class=class="str">"cmt">// 设置为非阻塞模式 class="type">int non_block=class="num">1; res=ioctlsocket(client,(class="type">int)FIONBIO,non_block); if(res!=NO_ERROR) { Print("ioctlsocket 失败,错误: "+class="type">class="kw">string(res)); CloseClean(); class="kw">return; } Print("连接成功"); class=class="str">"cmt">// 接收数据 class="type">uchar rdata[]; class="type">char rbuf[class="num">512]; class="type">int rlen=class="num">512; class="type">int rall=class="num">0; class="type">bool bNext=class="kw">false; class="kw">while(true) { res=recv(client,rbuf,rlen,class="num">0); if(res<class="num">0) { class="type">int err=WSAGetLastError(); if(err!=WSAEWOULDBLOCK) { Print("-接收失败,错误: "+class="type">class="kw">string(err)+" "+WSAErrorDescript(err)); CloseClean(); class="kw">return; } }
「把收到的字节流还原成图表模板」
服务端推来的数据并非直接可读,前 20 字节按约定拆成了品种与周期:0–9 位是交易品种字符串,10–19 位是时段字符串,其余才是模板二进制。用 CharArrayToString 各取 10 字节,就能拿到 smb 和 tf 两个变量。 接收完成后 Print 会输出「接收了 %d 字节」,这个数等于 ArraySize(rdata),开 MT5 跑一遍就能在日志里核对实际长度,验证协议是否对齐。 模板写入用 FileOpen 建 tcprecv.tpl,从偏移 20 开始把 rdata 剩余部分落盘,再 ChartSetSymbolPeriod 切品种周期、ChartApplyTemplate 套模板。注意 FILE_BIN 模式,文本模式会破坏 tpl 结构。 CloseClean 里先 shutdown(SD_BOTH) 双向断流,失败才打 WSAErrorDescript,然后 closesocket 并把 client 置 INVALID_SOCKET,最后 WSACleanup 释放 Winsock。外汇与贵金属行情通过套接字实时接驳存在断连风险,任何自动切图都可能因网络抖动失效,需自行加心跳重连。
else if(res==class="num">0 && rall==class="num">0) { Print("-接收连接已关闭"); class="kw">break; } else if(res>class="num">0) { rall+=res; ArrayCopy(rdata,rbuf,ArraySize(rdata),class="num">0,res); } if(res>=class="num">0 && res<rlen) class="kw">break; } class=class="str">"cmt">// 关闭套接字 CloseClean(); printf("接收了 %d 字节",ArraySize(rdata)); class=class="str">"cmt">// 从文件中取得交易品种和时段 class="type">class="kw">string smb=CharArrayToString(rdata,class="num">0,class="num">10); class="type">class="kw">string tf=CharArrayToString(rdata,class="num">10,class="num">10); class=class="str">"cmt">// 保存模板文件 class="type">int h=FileOpen("tcprecv.tpl", FILE_WRITE|FILE_SHARE_WRITE|FILE_BIN); if(h<=class="num">0) class="kw">return; FileWriteArray(h,rdata,class="num">20); FileClose(h); class=class="str">"cmt">// 应用到图表 ChartSetSymbolPeriod(class="num">0,smb,(ENUM_TIMEFRAMES)StringToInteger(tf)); ChartApplyTemplate(class="num">0,"\\Files\\tcprecv.tpl"); } class=class="str">"cmt">//------------------------------------------------------------------ CloseClean class="type">void CloseClean() class=class="str">"cmt">// 关闭套接字 { if(client!=INVALID_SOCKET) { if(shutdown(client,SD_BOTH)==SOCKET_ERROR) Print("-关闭失败,错误: "+WSAErrorDescript(WSAGetLastError())); closesocket(client); client=INVALID_SOCKET; } WSACleanup(); Print("连接关闭"); }