请 [注册] 或 [登录]  | 返回主站

量化交易吧 /  量化平台 帖子:3195027 新帖:232

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

2020来了发表于:5 月 2 日 22:00回复(1)

内容

  • 概述
  • 套接字
  • Wireshark 流量分析器
  • 数据交换
    • 接收
    • 发送
  • MySQL 业务类
    • 应用系统
      • 文档
        • 结束语

        概述

        大约一年前,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 业务类的实例
        • 设置连通参数
        • 完成相应的查询
        • 如果业务成功,则确保响应数量等于期望值
        • 获取指向服务器响应类的指针
        • 获取响应中的数据行数量
        • 显示数据行的值

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

        • 服务器错误 - 利用 CMySQLTransaction::GetServerError() 方法获取其描述
        • 内部错误 - 利用 EnumToString() 函数获取描述
        • 否则,利用 GetLastError() 获取错误代码。

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

        测试脚本操作结果

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

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


        文档

        内容

        • CMySQLTransaction 业务类
          •     Config
          •     KeepAliveTimeout
          •     PingPeriod
          •     PingBeforeQuery
          •     OnTimer
          •     Handshake
          •     Query
          •     Responses
          •     Response
          •     GetServerError
          •     RequestDuration
          •     RxBytesTotal
          •     TxBytesTotal
          •     ResetBytesCounters
        • CMySQLLoginRequest 授权管理类
          • CMySQLResponse 服务器响应类
            • MySQLServerError 服务器错误结构
            • CMySQLField 字段类
            • CMySQLRow 数据行类   


              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 文件: 在应用系统章节里用到的连通器示例。

              全部回复

              0/140

              量化课程

                移动端课程