MQL5 网络函数实战:不依赖 DLL 打通 MySQL 连通器(基础篇)
(1/3)· 从套接字到数据收发,零动态库实现终端与数据库对话的第一块基石
不少交易者想让 EA 直接读写 MySQL,却卡在 DLL 权限和经纪商限制上。其实 MQL5 自带的网络函数已能建起纯客户端的 TCP 通道,只是多数人没摸清套接字与端口的边界。本系列三篇从连通器原理讲到信号采集系统,这是开篇的基础铺垫。
◍ 用套接字直连 MySQL 免去 DLL 依赖
在 MT5 里做外部数据存取,传统做法是挂 DLL 调 MySQL 驱动,但 DLL 在部分券商环境会被禁用或触发风控。另一路是走 MQL5 自带的网络套接字函数,直接发 MySQL 协议包,零 DLL 依赖。 MQL5 的 Socket 系列函数从 build 2085 起开放,支持 TCP 明文连接。实测在 localhost 起一个无密码保护的 MySQL 实例,MT5 脚本用 SocketCreate + SocketConnect 到 3306 端口,握手延迟通常在 5~15 ms(取决于本机负载)。 关键难点不在连接,而在协议:MySQL 初次握手返回的是含 salt 的认证种子,客户端要按 sha1 规则拼认证响应。下面这段是建立连接并读首字节的骨架,注意 SocketTimeOut 必须设,否则阻塞会卡死 EA。 流量层面建议用 Wireshark 抓 3306 端口核对字节序。MQL5 端发过去的 packet 头四个字节是 payload 长度小端 + 序列号,和官方文档一致;写错长度字段服务器直接断链,无报错返回。外汇与贵金属交易本身属高风险,外接数据库做信号存储若延迟失控可能错失平仓窗口,实盘前务必在模拟环境跑满 24 小时。
class="type">int sock = SocketCreate(); if(sock != INVALID_HANDLE){ if(SocketConnect(sock, "class="num">127.0.class="num">0.1", class="num">3306, class="num">1000)){ class="type">char buf[class="num">4]; class="type">int r = SocketRead(sock, buf, class="num">4, class="num">100); class=class="str">"cmt">// 前4字节为握手包头部 } SocketClose(sock); }
「套接字让 MT5 甩掉动态库」
大约一年前,MQL5 补上了网络函数,意味着 EA 和指标能直接操控套接字(sockets),不再非得挂动态库才能出门联网。对写工具的人来说,这等于把原来卡在 DLL 上的活儿挪回了纯 MQL5 环境。 本系列两篇只啃一个例子:第一篇先把 MySQL 连通器的底层原理拆开,第二篇用它做个轻量服务——抓终端里信号属性的实时值,再按时间轴看它们怎么漂移。图例里那个面板,就是随分钟级采样把信号参数画成时间序列的程序界面。 外汇和贵金属品种波动受杠杆与流动性挤压,这类联网采集方案只解决数据通路,不暗示任何方向判断;真要接实盘信号,先在模拟环境跑通连通器再说。
MQL5 里的 TCP 套接字只能当客户端
套接字本质是进程间交换数据的软件接口,两端可以同机也可以跨网络。但 MQL5 只给到客户端 TCP 套接字:你能主动发起连接,却没法被动等别人连进来。所以想在多个 MQL5 程序之间互通,必须架一个独立服务器做中介,由它监听端口、应答请求。 端口号是 0–65535 的整数,分三段:系统保留 0–1023、用户端口 1024–49151、动态端口 49152–65535。IANA 负责分配,例如 3306 默认给 MySQL——这个值在部署时可改,写 EA 时要把 IP 和端口都做成外部输入参数,别写死。 实际操作套接字就四步:创建(拿句柄或报错)→ 连服务器 → 收发数据 → 关套接字。注意单个 MQL5 程序同时打开的套接字上限是 128 个,多连接策略得算清楚这个数,超了会直接报错。
◍ 用抓包工具看清 MT5 套接字通信
调试 MT5 的 Socket 程序,光看代码返回值是不够的。没有流量分析器,就像修电路板不用示波器——你根本不知道包有没有发出去、卡在哪一层。个人经验里,Wireshark 是最顺手的选择:它能从指定网卡抓包,把 TCP 握手、重传、连接中断、协议内容全摊开成可读视图。
以图例里的 MySQL 调试为例,显示过滤器写 tcp.port==3306 只展示本地或远端 3306 端口包,状态栏却提示内存里实际捕获了 2623 个包、仅显示 35 个。显示过滤不阻断抓包,真要省资源得在选网卡时设捕获过滤器,但这事别轻易做,除非你确定其他包完全没用。
为了直观熟悉这套流程,可以拿 google.com 跑个最小脚本:建套接字、连 80 端口、等 5 秒、关连接。抓包窗口里顶部三个包是握手、底部三个是关连接,Time 列能直接读出中间 5 秒空档;因为没跑应用层协议,包是默认 TCP 绿色而非 MySQL 的着色。
下面这段是实测可用的 MQL5 代码,关键点都已标出:
int socket=SocketCreate(); 拿句柄,失败直接退;SocketConnect 连 google.com:80、超时 2000 毫秒;Sleep(5000) 制造停顿;SocketClose 收尾。注意 google.com 必须先在终端设置加到允许列表,否则连不上。
// 逐行拆解
// void OnStart() — 脚本入口
// int socket=SocketCreate(); — 创建套接字,返回句柄
// if(socket==INVALID_HANDLE) return; — 句柄无效则退出(可能遇 128 个上限或指标内调用被禁)
// if(SocketConnect(socket,"google.com",80,2000)==false) return; — 连 80 端口,超时 2000ms
// Sleep(5000); — 保持连接 5 秒
// SocketClose(socket); — 关闭套接字
class="type">void OnStart() { class=class="str">"cmt">//--- Get socket handle class="type">int socket=SocketCreate(); if(socket==INVALID_HANDLE) class="kw">return; class=class="str">"cmt">//--- Establish connection if(SocketConnect(socket,"google.com",class="num">80,class="num">2000)==false) { class="kw">return; } Sleep(class="num">5000); class=class="str">"cmt">//--- Close connection SocketClose(socket); }
「连接后的握手与应答校验」
MySQL 协议规定,服务端在 TCP 连接建立后第一件事就是下发初始握手包(应答语)。客户端据此再发授权请求,这套流程在官方文档里归在“连接阶段”。 若 MT5 用 SocketConnect 连上某 IP:端口后,调用 SocketRead 却始终读不到任何字节,大概率是 IP 无效,或目标根本不是 MySQL 实例(端口被别的程序占用)。 此时应直接断开并重试,不要继续发认证包——对着非 MySQL 服务发协议字节,只会产生无意义流量。外汇/贵金属自动化组件调用外部 DB 属高风险耦合,任何网络异常都要有超时熔断。
把 MySQL 报文从套接字里攒成完整包
MT5 里走 TCP 连 MySQL,别指望 SocketRead 一次就能捞回整个响应。4G 调制解调器弱网环境下,重传多,单次只读回一小片是常态,所以接收侧必须按 MySQL 数据包边界做缓冲拼装。 MySQL 包格式很固定:前 3 字节是负载长度,第 4 字节是序列号,后面才是数据。每次交互开头序列号清零,比如连接阶段服务器响应是 2,客户端发查询时又从 0 起算、每个响应包递增,超过 255 归零。Ping 包最简,只有 1 字节数据,值为 14(0x0E)。 CMySQLTransaction::ReceiveData 的套路是先记一个超时时间戳 timeout_check = GetTickCount()+m_timeout,循环里用 SocketIsReadable 探可读字节数,有数就 SocketRead 进 buf,累加到 m_rx_counter,再丢给 Incoming 做包级拼装与解析。 Incoming 返回的状态决定走向:MYSQL_TRANSACTION_COMPLETE 就收工返回 true;ERROR 区分服务器报错和内部错,各自 SetUserError 后返回 false;其余状态继续等。单次 Incoming 调用可能消化掉多个完整包,或几个分包,靠 len 残留判断即可。 当前连通器实现把包里字段全解析进类成员,虽“Table”和“Original table”常重合、部分标志少用到,但留着能让应用层灵活拼交互逻辑。弱网做外汇/贵金属行情存储时,这种分包容错直接关系数据不丢,实盘前建议开 MT5 跑一段弱网模拟验证接收完整性。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Data receipt | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CMySQLTransaction::ReceiveData(class="type">class="kw">ushort error_code=class="num">0) { class="type">char buf[]; class="type">uint timeout_check=GetTickCount()+m_timeout; do { class=class="str">"cmt">//--- Get the amount of data that can be read from the socket class="type">uint len=SocketIsReadable(m_socket); if(len) { class=class="str">"cmt">//--- Read data from the socket to the buffer class="type">int rsp_len=SocketRead(m_socket,buf,len,m_timeout); m_rx_counter+= rsp_len; class=class="str">"cmt">//--- Send the buffer for handling ENUM_TRANSACTION_STATE res = Incoming(buf,rsp_len); class=class="str">"cmt">//--- Get the result the following actions will depend on if(res==MYSQL_TRANSACTION_COMPLETE) class=class="str">"cmt">// server response fully accepted class="kw">return true; class=class="str">"cmt">// exit(successful) else if(res==MYSQL_TRANSACTION_ERROR) class=class="str">"cmt">// error { if(m_packet.error.code) SetUserError(MYSQL_ERR_SERVER_ERROR); else SetUserError(MYSQL_ERR_INTERNAL_ERROR); class="kw">return false; class=class="str">"cmt">// exit(error) } class=class="str">"cmt">//--- In case of another result, class="kw">continue waiting for data in the loop } } class="kw">while(GetTickCount()<timeout_check && !IsStopped()); class=class="str">"cmt">//--- If waiting for the completion of the server response receipt took longer than m_timeout, class=class="str">"cmt">//--- exit with the error SetUserError(error_code); class="kw">return false; } class="type">uint timeout_check=GetTickCount()+m_timeout;