应用网络函数,或无需 DLL 的 MySQL:第 I 部分 - 连通器

2 五月 2020, 14:49
Serhii Shevchuk
0
1 965

内容

概述

大约一年前,MQL5 补充了网络函数,从而可以操控套接字(sockets) 了。 这为程序员开发市场所需产品提供了巨大的机遇。 如今,他们能够实现以前需要动态库支持的功能。 在本系列的两篇文章中,我们将研究这样的示例之一。 在第一篇文章中,我将研究 MySQL 连通器原理,而在第二篇文章中,我将利用连通器开发最简单的应用系统,即收集终端里所提供信号属性的服务,和查看它们 随时间流逝而变化的程序。(参见图例 1)。


观察特定时间内信号属性变化的程序

图例 1. 观察信号属性随时间变化的程序


套接字

套接字是软件实现的进程间交换数据的接口。 这些过程既可以在单台 PC 上启动,也可在连接到网络里的不同 PC 上启动。

MQL5 仅提供客户端 TCP 套接字。 这意味着我们能够发起连接,但我们不能等待并响应外部的连接请求。 所以,如果我们需要通过套接字在 MQL5 程序之间提供连通,则需要一个充当中介的服务器。 服务器等待并侦听端口的连接,并响应客户端的请求执行某些功能。 若要连接到服务器,我们需要知道其 IP 地址和端口。

端口是一个介于 0 到 65535 之间的数字。 有三个端口范围:系统保留(0 - 1023),用户(1024 - 49151),和动态端口(49152 - 65535)。 一些端口已分配给某些功能占用。 分配是由 IANA 执行,该组织负责管理 IP 地址区段和顶级域名,并注册 MIME 数据类型。

默认情况下,端口 3306 已分配给 MySQL。 当访问服务器时,我们将与其连接。 请注意,此端口值可以更改。 因此,在开发 EA 时,应在输入中设置端口以及 IP 地址。

在操控套接字时,会用到以下步骤:

  • 创建一个套接字(得到控柄或错误)
  • 连接服务器
  • 交换数据
  • 关闭套接字

当操控多个连接时,请记住单个 MQL5 程序有最多同时打开 128 个套接字的限制。


Wireshark 流量分析器

流量分析器有助于调试应用套接字的程序代码。 缺了它,整个过程就像不用示波器进行电子设备维修一样。 分析器从选定的网络接口捕获数据,并以可读形式显示它们。 它跟踪数据包的大小,它们之间的时间间隔,重传和连接丢失是否存在,以及许多其他有用的数据。 它还解码许多协议。

出于这些目的,我个人选用 Wireshark

流量分析器

图例 2. Wireshark 流量分析器

图例 2 流量分析器窗口展示,其内含有捕获的数据包:

  1. 显示过滤行。 "tcp.port==3306" 表示仅显示本地或远程 TCP 端口 3306 的数据包(默认的 MySQL 服务器端口)。
  2. 数据包。 在此,我们可以看到连接设置过程,服务器应答,授权请求和后续交换。
  3. 所选数据包内容显示为十六进制形式。 在这种情况下,我们可以看到 MySQL 服务器应答数据包的内容。
  4. 传输等级 (TCP)。 当操控套接字函数时,我们定位于此。
  5. 应用系统级别(MySQL)。 这就是我们在本文中要研究的内容。

显示过滤器不会限制数据包捕获。 从状态栏中可清晰看到,此刻正在处理内存中捕获到的 2623 个数据包中的 35 个。 为了降低 PC 的负担,我们应在选择网络接口时设置捕获过滤器,如图例 3 所示。 仅当所有其他数据包确实没有用时,才应这样做。

数据包捕获过滤器

图例 3. 数据包捕获过滤器

为了令我们自己熟悉流量分析器,我们尝试与 “google.com” 服务器建立连接,并跟踪该过程。 为此,编写一个小脚本。

void OnStart()
  {
//--- Get socket handle
   int socket=SocketCreate();
   if(socket==INVALID_HANDLE)
      return;
//--- Establish connection
   if(SocketConnect(socket,"google.com",80,2000)==false)
     {
      return;
     }
   Sleep(5000);
//--- Close connection
   SocketClose(socket);
  }

为此,首先我们创建一个套接字,并利用 SocketCreate() 函数获取其控柄。 在这种情况下,按参考资料的说辞,您可能会得到两种情况的错误,尽管这几乎是不可能的:

  1. ERR_NETSOCKET_TOO_MANY_OPENED 错误信号表示已有多于 128 个以上的套接字被打开。
  2. 出现 ERR_FUNCTION_NOT_ALLOWED 错误则表示尝试从指标里调用套接字创建,而此功能是被禁止的。

得到控柄后,尝试建立连接。 在本示例中,我们连接到 “google.com” 服务器(不要忘记在终端设置中将其添加到允许的服务器地址中),即连接到端口 80,且超时为 2000 毫秒。 建立连接之后,等待 5 秒钟,然后将其关闭。 现在,我们在流量分析器窗口中看看它的样子。

建立和关闭连接

图例 4. 建立和关闭连接

在图例 4 中,我们能看到脚本与 IP 地址为 “172.217.16.14” 的 “google.com” 服务器之间的数据交换。 由于过滤器行里含有 "tcp.port==80" 表达式,因此此处未显示 DNS 查询。

顶部的三个数据包代表建立连接,而底部的三个数据包代表将其关闭。 “Time(时间)” 列显示数据包之间的时间,我们可以看到 5 秒钟的停顿时间。 请注意,数据包的颜色为绿色,与图例 2 中的不同。 这是因为在之前的情况下,分析器在数据交换中检测到 MySQL 协议。 在当前情况下,没有数据经过,分析器使用默认的 TCP 颜色显示数据包。


数据交换

根据协议,MySQL 服务器应在建立连接后发送应答语。 作为响应,客户端发送授权请求。 在 dev.mysql.com 网站上,该机制于连接阶段部分进行了详细阐述。 如果未收到服务端的应答语,则 IP 地址无效,或服务器正在侦听其它端口。 无论何种情况,这意味着我们已连接的绝对不是 MySQL 服务器。 在正常状况下,我们需要接收数据(从套接字读取数据),并解析它。


接收

CMySQLTransaction类中(稍后将详细讲述),数据接收已如下实现:

//+------------------------------------------------------------------+
//| Data receipt                                                     |
//+------------------------------------------------------------------+
bool CMySQLTransaction::ReceiveData(ushort error_code=0)
  {
   char buf[];
   uint   timeout_check=GetTickCount()+m_timeout;
   do
     {
      //--- Get the amount of data that can be read from the socket
      uint len=SocketIsReadable(m_socket);
      if(len)
        {
         //--- Read data from the socket to the buffer
         int rsp_len=SocketRead(m_socket,buf,len,m_timeout);
         m_rx_counter+= rsp_len;
         //--- Send the buffer for handling
         ENUM_TRANSACTION_STATE res = Incoming(buf,rsp_len);
         //--- Get the result the following actions will depend on
         if(res==MYSQL_TRANSACTION_COMPLETE) // server response fully accepted
            return true;   // exit (successful)
         else
            if(res==MYSQL_TRANSACTION_ERROR) // error
              {
               if(m_packet.error.code)
                  SetUserError(MYSQL_ERR_SERVER_ERROR);
               else
                  SetUserError(MYSQL_ERR_INTERNAL_ERROR);
               return false;  // exit (error)
              }
         //--- In case of another result, continue waiting for data in the loop
        }
     }
   while(GetTickCount()<timeout_check && !IsStopped());
//--- If waiting for the completion of the server response receipt took longer than m_timeout,
//--- exit with the error
   SetUserError(error_code);
   return false;
  }
此处的 m_socket 是早前创建的套接字控柄,而 m_timeout 是读取数据的超时,可作为 SocketRead() 函数接受 数据片的参数,以及整体形式接收数据的超时。 进入循环之前,设置时间戳。 若达到它所认定的数据接收超时:
uint   timeout_check=GetTickCount()+m_timeout;

接下来,在循环中读取 SocketIsReadable() 函数结果,并一直等到其返回非零值。 之后,将数据读取到缓冲区,并传递给处理过程。

      uint len=SocketIsReadable(m_socket);
      if(len)
        {
         //--- Read data from the socket to the buffer
         int rsp_len=SocketRead(m_socket,buf,len,m_timeout);
         m_rx_counter+= rsp_len;
         //--- Send the buffer for handling
         ENUM_TRANSACTION_STATE res = Incoming(buf,rsp_len);

        ...

        }

如果套接字中有数据,我们不要指望一次性收到整个数据包的能力。 在大多情况下,数据可能只有一小部分到达。 例如,4G 调制解调器的连接也许很差,并且会有大量重传。 故此,我们的处理程序应该能够将数据收集到一些不可分割的分组中,之后再处理它们。 我们为此使用 MySQL 数据包。

CMySQLTransaction::Incoming() 方法汇集并处理数据:

   //--- Handle received data
   ENUM_TRANSACTION_STATE  Incoming(uchar &data[], uint len);

它返回的结果令我们知道下一步该怎么做 — 继续,完成或中断接收数据的过程:

enum ENUM_TRANSACTION_STATE
  {
   MYSQL_TRANSACTION_ERROR=-1,         // Error
   MYSQL_TRANSACTION_IN_PROGRESS=0,    // In progress
   MYSQL_TRANSACTION_COMPLETE,         // Fully completed
   MYSQL_TRANSACTION_SUBQUERY_COMPLETE // Partially completed
  };

如果发生内部错误,以及出现服务器错误,或完成数据接收时,应停止从套接字读取数据。 在所有其他情况下,则应继续。 MYSQL_TRANSACTION_SUBQUERY_COMPLETE 值表示服务器已接受并响应来自客户端多的个查询之一。 它等效于 MYSQL_TRANSACTION_IN_PROGRESS 读取算法。

MySQL 数据包

图例 5. MySQL 数据包

MySQL数据包格式如图例 5 所示。 前三个字节定义数据包中有用负载的大小,而下一个字节表示序列中数据包的序列号,再后跟随数据。 每次交换开始时,序列号都设置为零。 例如,应答数据包为 0,客户端授权请求 — 1,服务器响应 — 2(连接阶段结束)。 接下来,在发送客户端查询时,序列号的值应再次设置为零,并在每个服务器响应数据包中递增。 如果数据包数量超过 255,则所传递数值归零。

在流量分析器中,最简单的数据包(MySQL ping)如下所示:

流量分析器中的 Ping 数据包

图例 6. 流量分析器中的 Ping 数据包

Ping 数据包只包含一个字节的数据,其值为 14(或十六进制形式的 0x0E)。

我们研究一下 CMySQLTransaction::Incoming() 方法,该方法将数据收集到数据包中,并将其传递给处理程序。 其节略版源代码提供如下。

ENUM_TRANSACTION_STATE CMySQLTransaction::Incoming(uchar &data[], uint len)
  {
   int ptr=0; // index of the current byte in the 'data' buffer
   ENUM_TRANSACTION_STATE result=MYSQL_TRANSACTION_IN_PROGRESS; // result of handling accepted data
   while(len>0)
     {
      if(m_packet.total_length==0)
        {
         //--- If the amount of data in the packet is unknown
         while(m_rcv_len<4 && len>0)
           {
            m_hdr[m_rcv_len] = data[ptr];
            m_rcv_len++;
            ptr++;
            len--;
           }
         //--- Received the amount of data in the packet
         if(m_rcv_len==4)
           {
            //--- Reset error codes etc.
            m_packet.Reset();
            m_packet.total_length = reader.TotalLength(m_hdr);
            m_packet.number = m_hdr[3];
            //--- Length received, reset the counter of length bytes
            m_rcv_len = 0;
            //--- Highlight the buffer of a specified size
            if(ArrayResize(m_packet.data,m_packet.total_length)!=m_packet.total_length)
               return MYSQL_TRANSACTION_ERROR;  // internal error
           }
         else // if the amount of data is still not accepted
            return MYSQL_TRANSACTION_IN_PROGRESS;
        }
      //--- Collect packet data
      while(len>0 && m_rcv_len<m_packet.total_length)
        {
         m_packet.data[m_rcv_len] = data[ptr];
         m_rcv_len++;
         ptr++;
         len--;
        }
      //--- Make sure the package has been collected already
      if(m_rcv_len<m_packet.total_length)
         return MYSQL_TRANSACTION_IN_PROGRESS;

      //--- Handle received MySQL packet
      //...
      //---      

      m_rcv_len = 0;
      m_packet.total_length = 0;
     }
   return result;
  }

第一步是收集数据包头 — 前 4 个字节包含序列中的数据长度和序列号。 为了汇集包头,利用 m_hdr 缓冲区和 m_rcv_len 字节计数器。 再收集到 4 个字节后,获取它们的长度,并基于它更改 m_packet.data 缓冲区。 接收到的数据包数据被复制到其中。 数据包准备好后,将其传递给处理程序。

如果收到数据包后,len 表示的所收长度仍然非零,则表明我们已收到了若干个数据包。 我们可以在单一的 Incoming() 方法调用中处理若干个完整数据包,或几个部分

数据包类型如下:

enum ENUM_PACKET_TYPE
  {
   MYSQL_PACKET_NONE=0,    // None
   MYSQL_PACKET_DATA,      // Data
   MYSQL_PACKET_EOF,       // End of file
   MYSQL_PACKET_OK,        // Ok
   MYSQL_PACKET_GREETING,  // Greeting
   MYSQL_PACKET_ERROR      // Error
  };

它们当中的每一个都有自己的处理程序,该处理程序根据协议解析其序列和内容。 解析期间收到的值将分配给相应的类成员。 在当前的连通器实现中,会解析数据包中接收到的所有数据。 这似乎有些多余,因为 “Table(表格)” 和 “Original table(原始表格)” 字段的属性经常重合。 此外,某些标志的值很少用到(参见图例 7)。 不过,这些属性的可用性能够在程序的应用层灵活地构建与 MySQL 服务器的交互逻辑。


Wireshark 分析器中的数据包

图例 7. 数据包的字段说明


发送

发送数据于此要容易一些。

//+------------------------------------------------------------------+
//| Form and send ping                                               |
//+------------------------------------------------------------------+
bool CMySQLTransaction::ping(void)
  {
   if(reset_rbuf()==false)
     {
      SetUserError(MYSQL_ERR_INTERNAL_ERROR);
      return false;
     }
//--- Prepare the output buffer
   m_tx_buf.Reset();
//--- Reserve a place for the packet header
   m_tx_buf.Add(0x00,4);
//--- Place the command code
   m_tx_buf+=uchar(0x0E);
//--- Form a header
   m_tx_buf.AddHeader(0);
   uint len = m_tx_buf.Size();
//--- Send a packet
   if(SocketSend(m_socket,m_tx_buf.Buf,len)!=len)
      return false;
   m_tx_counter+= len;
   return true;
  }

上面提供了 ping 发送方法的源代码。 将数据复制到准备好的缓冲区。 若执行 ping 操作,这就是 0x0E 命令的代码。 接下来,研究形成包头的数据量和数据包序列号。 对于 ping,序列号始终等于。 之后,尝试利用 SocketSend() 函数发送封装好的数据包。

发送查询(Query)的方法类似于发送 ping:

//+------------------------------------------------------------------+
//| Form and send a query                                            |
//+------------------------------------------------------------------+
bool CMySQLTransaction::query(string s)
  {
   if(reset_rbuf()==false)
     {
      SetUserError(MYSQL_ERR_INTERNAL_ERROR);
      return false;
     }
//--- Prepare the output buffer
   m_tx_buf.Reset();
//--- Reserve a place for the packet header
   m_tx_buf.Add(0x00,4);
//--- Place the command code
   m_tx_buf+=uchar(0x03);
//--- Add the query string
   m_tx_buf+=s;
//--- Form a header
   m_tx_buf.AddHeader(0);
   uint len = m_tx_buf.Size();
//--- Send a packet
   if(SocketSend(m_socket,m_tx_buf.Buf,len)!=len)
      return false;
   m_tx_counter+= len;
   return true;
  }

唯一的区别在于,有用负载由 (0x03)命令码和查询字符串组成。

发送数据后总是跟随我们之前研究过的 CMySQLTransaction::ReceiveData() 接收方法。 如果没有返回错误,则认为业务成功。


MySQL 业务类

现在是时候详细研究 CMySQLTransaction 类了。

//+------------------------------------------------------------------+
//| MySQL transaction class                                          |
//+------------------------------------------------------------------+
class CMySQLTransaction
  {
private:
   //--- Authorization data
   string            m_host;        // MySQL server IP address
   uint              m_port;        // TCP port
   string            m_user;        // User name
   string            m_password;    // Password
   //--- Timeouts
   uint              m_timeout;        // timeout of waiting for TCP data (ms)
   uint              m_timeout_conn;   // timeout of establishing a server connection
   //--- Keep Alive
   uint              m_keep_alive_tout;      // time(ms), after which the connection is closed; the value of 0 - Keep Alive is not used
   uint              m_ping_period;          // period of sending ping (in ms) in the Keep Alive mode
   bool              m_ping_before_query;    // send 'ping' before 'query' (this is reasonable in case of large ping sending periods)
   //--- Network
   int               m_socket;      // socket handle
   ulong             m_rx_counter;  // counter of bytes received
   ulong             m_tx_counter;  // counter of bytes passed
   //--- Timestamps
   ulong             m_dT;                   // last query time
   uint              m_last_resp_timestamp;  // last response time
   uint              m_last_ping_timestamp;  // last ping time
   //--- Server response
   CMySQLPacket      m_packet;      // accepted packet
   uchar             m_hdr[4];      // packet header
   uint              m_rcv_len;     // counter of packet header bytes
   //--- Transfer buffer
   CData             m_tx_buf;
   //--- Authorization request class
   CMySQLLoginRequest m_auth;
   //--- Server response buffer and its size
   CMySQLResponse    m_rbuf[];
   uint              m_responses;
   //--- Waiting and accepting data from the socket
   bool              ReceiveData(ushort error_code);
   //--- Handle received data
   ENUM_TRANSACTION_STATE  Incoming(uchar &data[], uint len);
   //--- Packet handlers for each type
   ENUM_TRANSACTION_STATE  PacketOkHandler(CMySQLPacket *p);
   ENUM_TRANSACTION_STATE  PacketGreetingHandler(CMySQLPacket *p);
   ENUM_TRANSACTION_STATE  PacketDataHandler(CMySQLPacket *p);
   ENUM_TRANSACTION_STATE  PacketEOFHandler(CMySQLPacket *p);
   ENUM_TRANSACTION_STATE  PacketErrorHandler(CMySQLPacket *p);
   //--- Miscellaneous
   bool              ping(void);                // send ping
   bool              query(string s);           // send a query
   bool              reset_rbuf(void);          // initialize the server response buffer
   uint              tick_diff(uint prev_ts);   // get the timestamp difference
   //--- Parser class
   CMySQLPacketReader   reader;
public:
                     CMySQLTransaction();
                    ~CMySQLTransaction();
   //--- Set connection parameters
   bool              Config(string host,uint port,string user,string password,uint keep_alive_tout);
   //--- Keep Alive mode
   void              KeepAliveTimeout(uint tout);                       // set timeout
   void              PingPeriod(uint period) {m_ping_period=period;}    // set ping period in seconds
   void              PingBeforeQuery(bool st) {m_ping_before_query=st;} // enable/disable ping before a query
   //--- Handle timer events (relevant when using Keep Alive)
   void              OnTimer(void);
   //--- Get the pointer to the class for working with authorization
   CMySQLLoginRequest *Handshake(void) {return &m_auth;}
   //--- Send a request
   bool              Query(string q);
   //--- Get the number of server responses
   uint              Responses(void) {return m_responses;}
   //--- Get the pointer to the server response by index
   CMySQLResponse    *Response(uint idx);
   CMySQLResponse    *Response(void) {return Response(0);}
   //--- Get the server error structure
   MySQLServerError  GetServerError(void) {return m_packet.error;}
   //--- Options
   ulong             RequestDuration(void) {return m_dT;}                     // get the last transaction duration
   ulong             RxBytesTotal(void) {return m_rx_counter;}                // get the number of received bytes
   ulong             TxBytesTotal(void) {return m_tx_counter;}                // get the number of passed bytes
   void              ResetBytesCounters(void) {m_rx_counter=0; m_tx_counter=0;} // reset the counters of received and passed bytes
  };

我们仔细看看以下私密成员:

  • CMySQLPacket 类型的 m_packet — 当前正在处理的 MySQL 数据包的类(含有注释的源代码在 MySQLPacket.mqh 文件中)
  • CData 类型的 m_tx_buf — 为方便生成一条查询而创建的传输缓冲区的类(Data.mqh 文件)
  • CMySQLLoginRequest 类型的 m_auth — 处理授权的类(密码加扰,存储获得的服务器参数,和指定的客户端参数,源代码在 MySQLLoginRequest.mqh 中)
  • CMySQLResponse 类型的 m_rbuf — 服务器响应缓冲区;这里的响应是 “Ok” 或 “Data” 类型的数据包(MySQLResponse.mqh)
  • CMySQLPacketReader 类型的 reader — MySQL 数据包解析器类

公开方法已在文档中详细说明。

对于应用层,业务类看起来如图例 8 所示。

类

图例 8. CMySQLTransaction 类的结构

其中:

  • CMySQLLoginRequest — 若客户端参数指定值与预定义值不同时(可选),应在建立连接之前进行配置;
  • CMySQLResponse — 服务器响应(如果业务已完成,且没有错误)
    • CMySQLField — 字段描述;
    • CMySQLRow — 数据行(保存文本形式字段值的缓冲区);
  • MySQLServerError — 在业务失败情况下,错误描述结构。

没有公开方法负责建立和关闭连接。 当调用 CMySQLTransaction::Query() 方法时,这会自动完成。 当使用持续连接模式时,它将在第一次调用 CMySQLTransaction::Query() 时建立,并在定义的超时后关闭。

重要提示:在持续连接模式下,OnTimer 事件处理程序应接收 CMySQLTransaction::OnTimer() 方法的调用。 在这种情况下,计时器周期应小于 ping 和超时周期。

在调用 CMySQLTransaction::Query() 之前,应先设置连接参数,用户帐户,以及特殊的客户端参数值。

通常,与业务类的交互是根据以下原理执行的:

操控业务类

图例 9. 操控 CMySQLTransaction 类



应用系统

我们来研究最简单的连接器应用示例。 为此,编写一个脚本,将 SELECT 查询发送到 world 测试数据库

//--- input parameters
input string   inp_server     = "127.0.0.1";          // MySQL server address
input uint     inp_port       = 3306;                 // TCP port
input string   inp_login      = "admin";              // Login
input string   inp_password   = "12345";              // Password
input string   inp_db         = "world";              // Database name

//--- Connect MySQL transaction class
#include  <MySQL\MySQLTransaction.mqh>
CMySQLTransaction mysqlt;

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- Configure MySQL transaction class
   mysqlt.Config(inp_server,inp_port,inp_login,inp_password);
//--- Make a query
   string q = "select `Name`,`SurfaceArea` "+
              "from `"+inp_db+"`.`country` "+
              "where `Continent`='Oceania' "+
              "order by `SurfaceArea` desc limit 10";
   if(mysqlt.Query(q)==true)
     {
      if(mysqlt.Responses()!=1)
         return;
      CMySQLResponse *r = mysqlt.Response();
      if(r==NULL)
         return;
      Print("Name: ","Surface Area");
      uint rows = r.Rows();
      for(uint i=0; i<rows; i++)
        {
         double area;
         if(r.Row(i).Double("SurfaceArea",area)==false)
            break;
         PrintFormat("%s: %.2f",r.Row(i)["Name"],area);
        }
     }
   else
      if(GetLastError()==(ERR_USER_ERROR_FIRST+MYSQL_ERR_SERVER_ERROR))
        {
         // in case of a server error
         Print("MySQL Server Error: ",mysqlt.GetServerError().code," (",mysqlt.GetServerError().message,")");
        }
      else
        {
         if(GetLastError()>=ERR_USER_ERROR_FIRST)
            Print("Transaction Error: ",EnumToString(ENUM_TRANSACTION_ERROR(GetLastError()-ERR_USER_ERROR_FIRST)));
         else
            Print("Error: ",GetLastError());
        }
  }

假设我们的任务是获取比邻 “Oceania” 大陆值的国家/地区清单,并按国家/地区的面积从最大到最小的顺序排列,清单中最多包含 10 个数据项。 我们执行以下动作:

  • 声明 mysqlt 业务类的实例
  • 设置连通参数
  • 完成相应的查询
  • 如果业务成功,则确保响应数量等于期望值
  • 获取指向服务器响应类的指针
  • 获取响应中的数据行数量
  • 显示数据行的值

业务可能由以下三个原因之一而失败:

如果在输入里正确指定,则脚本操作的结果如下:

测试脚本操作结果

图例 10. 测试脚本操作结果

在第二部分中将探讨更复杂的应用多个查询和持续连接模式的示例。


文档

内容


    CMySQLTransaction 业务类

    CMySQLTransaction 类方法列表

    方法
    动作
    Config
    设置连通参数
    KeepAliveTimeout
    设置以秒为单位的“保持活动”模式的超时
    PingPeriod
    设置以秒为单位的“保持活动”模式的 ping 周期
    PingBeforeQuery
    启用/禁用查询前 ping
    OnTimer
    处理计时器事件(与“保持活动”有关)
    Handshake
    获取指向类的指针从而处理授权
    Query
    发送查询
    Responses
    获取服务器响应数量
    Response
    获取指向服务器响应类的指针
    GetServerError
    获取服务器错误结构
    RequestDuration
    业务持续时间(以微秒为单位)
    RxBytesTotal
    自程序启动以来接收的字节计数
    TxBytesTotal
    自程序启动以来发送的字节计数
    ResetBytesCounters
    重置接收和发送字节的计数器

    以下是每种方法的简要说明。

    Config

    设置连通参数。
    bool  Config(
       string host,         // server name
       uint port,           // port
       string user,         // user name
       string password,     // password
       string base,         // database name
       uint keep_alive_tout // constant connection timeout (0 - not used)
       );
    

    返回值:如果成功,则返回 true;否则返回 false(字符串类型参数中的无效符号)。

    KeepAliveTimeout

    启用持续连接模式,并设置其超时。 超时值是以秒为单位的时间,从发送上一个查询起计算,该时间过后则连接被关闭。 如果重复查询超过了定义的超时值,则不会关闭连接。

    void  KeepAliveTimeout(
       uint tout            // set the constant connection timeout in seconds (0 - disable)
       );
    

    PingPeriod

    设置在持续连接模式下发送 “ping” 数据包的周期。 这样可以防止服务器关闭连接。 在最后一次查询或上次 ping 后的指定时间过后发送 ping。

    void  PingPeriod(
       uint period          // set the ping period in seconds (for the constant connection mode)
       );
    

    返回值:无。

    PingBeforeQuery

    启用在查询之前发送 “ping” 数据包。 在持续连接模式下,查询之间的间隔中,由于某种原因,连接可能会由于某种原因而关闭或中断。 在这种情况下,可以在发送查询之前发送 ping 到 MySQL服务器,从而确保连接处于活动状态。

    void  PingBeforeQuery(
       bool st              // enable (true)/disable (false) ping before a query
       );
    

    返回值:无。

    OnTimer

    在持续连接模式下使用。 该方法应从 OnTimer 事件处理程序中调用。 计时器周期不应超过 KeepAliveTimeout 和 PingPeriod 周期的最小值。

    void  OnTimer(void);

    返回值:无。

    Handshake

    获取指向授权类的指针。 建立与服务器的连接之前,可以用它来设置客户端职能和数据包最大尺寸的标志。 授权后,它接收服务器的职能版本和标志。

    CMySQLLoginRequest *Handshake(void);

    返回值:指向授权 CMySQLLoginRequest 类的指针。

    Query

    发送查询。

    bool  Query(
       string q             // query body
       );
    

    返回值:执行结果; 成功 - true,错误 - false。

    Responses

    获取响应数量。

    uint  Responses(void);

    返回值:服务器响应的数量。

    "Ok" 或 "Data" 类型的数据包被视为响应。 如果查询成功执行,则接受一个或多个响应(对于多个查询)。

    Response

    获取指向 MySQL 服务器响应类的指针

    CMySQLResponse  *Response(
       uint idx                     // server response index
       );
    

    返回值:指向 CMySQLResponse 服务器响应类的指针。 若传递无效值作为参数,则返回 NULL。

    未指定索引的重载方法等效于 Response()。

    CMySQLResponse  *Response(void);

    返回值:指向 CMySQLResponse 服务器响应类的指针。 如果没有响应,则返回 NULL。

    GetServerError

    获取存储代码和服务器错误消息的结构。 若业务类返回 MYSQL_ERR_SERVER_ERROR 错误,则可调用它。

    MySQLServerError  GetServerError(void);

    返回值:MySQLServerError 错误结构

    RequestDuration

    获取请求执行的持续时间。

    ulong  RequestDuration(void);

    返回值:自发送时刻到处理结束的查询持续时间(以微秒为单位)

    RxBytesTotal

    获取接受的字节数。

    ulong  RxBytesTotal(void);

    返回值:自程序启动以来接收的字节数(TCP 级别)。 ResetBytesCounters 方法用于重置。

    TxBytesTotal

    获取发送的字节数。

    ulong  TxBytesTotal(void);

    返回值:自程序启动以来传递的字节数(TCP 级别)。 ResetBytesCounters 方法用于重置。

    ResetBytesCounters

    重置已接受和已发送字节的计数器。

    void  ResetBytesCounters(void);


    CMySQLLoginRequest 授权管理类

    CMySQLLoginRequest 类的方法

    方法
    动作
    SetClientCapabilities
    设置客户端职能标志。 预定义值:0x005FA685
    SetMaxPacketSize
    设置最大允许的数据包大小(以字节为单位)。 预定义值: 16777215
    SetCharset
    定义用到的字符集。 预定义值: 8
    Version
    返回 MySQL 服务器版本。 例如: "5.7.21-log".
    ThreadId
    返回当前的连接线程 ID。 它对应于 CONNECTION_ID 值。
    ServerCapabilities
    获取服务器职能的标志
    ServerLanguage
    返回编码和数据库表述 ID

    CMySQLResponse 服务器响应类

    “Ok” 或 “Data” 类型的数据包被视为服务器响应。 考虑到它们之间的显著差异,该类有一套单独的方法来处理每种类型的数据包。

    General CMySQLResponse 类的方法:

    方法
    返回值
    类型
    服务器响应类型: MYSQL_RESPONSE_DATA 或 MYSQL_RESPONSE_OK

    “Data” 类型数据包方法:

    方法
    返回值
    Fields
    字段数量
    Field
    按索引指向 field 类的指针(重载方法 - 按名称获取字段索引)
    Field 按名称的字段索引
    Rows
    服务器响应中的数据行数量
    Row
    按索引指向数据行类的指针
    Value
    按数据行和字段索引的字符串值
    Value 按数据行和字段名称的字符串值
    ColumnToArray 将数据列读取到 string 型数组的结果
    ColumnToArray
    将数据列读取到 int 型数组的结果,并经类型验证
    ColumnToArray
    将数据列读取到 long 型数组的结果,并经类型验证
    ColumnToArray
    将数据列读取到 double 型数组的结果,并经类型验证
    “Ok” 类型数据包方法:
    方法
    返回值
    AffectedRows
    由上次操作影响的数据行数量
    LastId
    LAST_INSERT_ID
    ServerStatus
    服务器状态 标志
    Warnings
    警告次数
    Message
    服务器文本消息

    MySQLServerError 服务器错误结构

    MySQLServerError 结构元素

    元素
    类型
    目的
    code
     ushort 错误代码
    sqlstate
     uint 状态
    message  string 服务器文本消息


    CMySQLField 字段类

    CMySQLField 类的方法

    方法
     返回值
    Catalog
    数据表所属目录的名称
    Database
    数据表所属数据库的名称
    Table
    字段所属数据表的别名
    OriginalTable
    字段所属数据表的原名称
    Name
    字段别名
    OriginalName
    原字段名称
    Charset
    实施的编码代号
    Length
    数值长度
    类型
    数值类型
    Flags
    定义值属性的标志
    Decimals
    允许的小数位
    MQLType
    字段类型为 ENUM_DATABASE_FIELD_TYPE 值的形式(DATABASE_FIELD_TYPE_NULL 除外)


    CMySQLRow 数据行类

    CMySQLRow 类的方法

    方法
    动作
    Value
    按编号返回字段值的字符串形式
    operator[]
    按名称返回字段值的字符串
    MQLType
    按编号返回字段类型为 ENUM_DATABASE_FIELD_TYPE
    MQLType
    按名称返回字段类型为 ENUM_DATABASE_FIELD_TYPE
    Text
    按编号返回经类型验证的字段值的字符串
    Text
    按名称返回经类型验证的字段值的字符串
    Integer
    按字段编号返回经类型验证的 int 型数值
    Integer
    按字段名称返回经类型验证的 int 型数值
    Long
    按字段编号返回经类型验证的 long 型数值
    Long
    按字段名称返回经类型验证的 long 型数值
    Double
    按字段编号返回经类型验证的 double 型数值
    Double
    按字段名称返回经类型验证的 double 型数值
    Blob
    按字段编号返回经类型验证的 uchar 数组 形式的数值
    Blob
    按字段名称返回经类型验证的 uchar 数组 形式的数值

    请注意: 类型验证意味着处理 int 类型的方法的可读字段应等于 DATABASE_FIELD_TYPE_INTEGER。 如果不匹配,则不会收到任何值,且该方法将返回 “false”。 将 MySQL 字段类型 ID 转换为 ENUM_DATABASE_FIELD_TYPE 类型值是在 CMySQLField::MQLType() 方法中实现的,其源代码如下所供。

    //+------------------------------------------------------------------+
    //| Return the field type as the ENUM_DATABASE_FIELD_TYPE value      |
    //+------------------------------------------------------------------+
    ENUM_DATABASE_FIELD_TYPE CMySQLField::MQLType(void)
      {
       switch(m_type)
         {
          case 0x00:  // decimal
          case 0x04:  // float
          case 0x05:  // double
          case 0xf6:  // newdecimal
             return DATABASE_FIELD_TYPE_FLOAT;
          case 0x01:  // tiny
          case 0x02:  // short
          case 0x03:  // long
          case 0x08:  // longlong
          case 0x09:  // int24
          case 0x10:  // bit
          case 0x07:  // timestamp
          case 0x0c:  // datetime
             return DATABASE_FIELD_TYPE_INTEGER;
          case 0x0f:  // varchar
          case 0xfd:  // varstring
          case 0xfe:  // string
             return DATABASE_FIELD_TYPE_TEXT;
          case 0xfb:  // blob
             return DATABASE_FIELD_TYPE_BLOB;
          default:
             return DATABASE_FIELD_TYPE_INVALID;
         }
      }
    


    结束语

    在本文中,我们以 MySQL 连通器的实现为例,探讨了利用函数操控套接字的实验。 这已成为一条理论。 本文的第二部分会更加实用:我们将开发一个收集信号属性的服务,和一个查看其变化的程序。

    随附的存档包含以下文件:

    • Include\MySQL\ 路径: 连通器源代码
    • Scripts\test_mysql.mq5 文件: 在应用系统章节里用到的连通器示例。

    本文译自 MetaQuotes Software Corp. 撰写的俄文原文
    原文地址: https://www.mql5.com/ru/articles/7117

    附加的文件 |
    MQL5.zip (23.17 KB)
    如何在 MetaTrader 5 中利用 DirectX 创建 3D 图形 如何在 MetaTrader 5 中利用 DirectX 创建 3D 图形

    3D 图形为大数据分析提供了完美的方案,它可以直观透视隐藏的形态。 这些任务能以 MQL5 直接解决,而 DireсtX 函数允许创建三维物体。 故其能够为 MetaTrader 5 创建任意复杂度的程序,甚至 3D 游戏。 学习 3D 图形,从绘制简单的三维形状开始。

    轻松快捷开发 MetaTrader 程序的函数库(第 三十三部分):延后交易请求 - 在特定条件下平仓 轻松快捷开发 MetaTrader 程序的函数库(第 三十三部分):延后交易请求 - 在特定条件下平仓

    我们继续开发利用延后请求进行交易的函数库功能。 我们已实现了发送开仓和下挂单的条件交易请求。 在本文中,我们将实现条件平仓 – 全部、部分和由逆向仓位平仓。

    轻松快捷开发 MetaTrader 程序的函数库(第 三十四部分):延后交易请求 - 在特定条件下删除和修改订单与持仓 轻松快捷开发 MetaTrader 程序的函数库(第 三十四部分):延后交易请求 - 在特定条件下删除和修改订单与持仓

    在本文中,我们将完成延后请求交易概念的论述,并创建删除挂单,以及在特定条件下修改挂单和持仓的功能。 由此,我们将拥有完整的功能,令我们能够开发简单的自定义策略,或者根据用户定义的条件激活 EA 行为逻辑。

    应用网络函数,或无需 DLL 的 MySQL:第 II 部分 - 监视信号属性变化的程序 应用网络函数,或无需 DLL 的 MySQL:第 II 部分 - 监视信号属性变化的程序

    在前一部分当中,我们研究了 MySQL 连通器的实现。 在本文中,我们将研究如何实现收集信号属性的服务应用,和观察其随时间变化的程序。 如果用户需要观察并未显示在信号网页上的属性变化,则所实现的示例具有重大实际意义。