在MQL中操作套接字,或者如何成为信号提供者·进阶篇
🔌

在MQL中操作套接字,或者如何成为信号提供者·进阶篇

(2/3)· 从理论和结构移植走到可跑的TCP/UDP通信,避开阻塞卡死的坑

新手友好 第 2/3 篇

接上篇铺垫的套接字概念,这一篇直接动手在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 注释掉再跑。

MQL5 / C++
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 判定退出。外汇与贵金属行情波动剧烈、断连概率偏高,实盘跑这套推送前建议先在模拟账户验证套接字回收是否干净,避免句柄泄漏拖慢终端。

MQL5 / C++
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 有断连与安全风险。

MQL5 / C++
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 出的"连接成功"。

MQL5 / C++
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。外汇与贵金属行情通过套接字实时接驳存在断连风险,任何自动切图都可能因网络抖动失效,需自行加心跳重连。

MQL5 / C++
   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("连接关闭");
  }
把重复连接诊断交给小布
这些套接字连通性自检和断线重连逻辑,小布盯盘的AIGC模块已内置到品种页后台,你只管写业务推送,不必每次手写WSAStartup到WSACleanup的全套样板。

常见问题

MQL沿用了WinAPI的结构与函数精神,但类型是ulong句柄,调用前必须WSAStartup初始化,否则返回无效套接字。
多半是没用ioctlsocket设非阻塞模式,recv在等数据时阻塞了主线程,加上非阻塞才能边等边跑EA。
可以,小布盯盘后台持续探测品种页关联的信号端连通性,异常断线会在页面提示,省去你写日志轮询。
UDP没有accept和listen的握手,客户端只需WSAStartup、socket、ioctlsocket后用sendto/recvfrom收发,适合低频广播式信号。
图表布局数据量小但要求完整,倾向用TCP配合recv/send确保不丢包,UDP可能丢帧导致客户端渲染错位。