
Connexus客户端(第七部分):添加客户端层
概述
本文是该系列文章的延续,我们将构建一个名为Connexus的库。在第一篇文章中,我们了解到WebRequest函数的基本工作原理,理解了它的每个参数,并且还创建了一个示例代码,展示了该函数的使用方法及其困难之处。在上一篇文章中,我们了解到什么是HTTP方法,以及服务器返回的状态码,这些状态码可以表明请求是否被成功处理,或者是由客户端还是服务器生成了错误。
在本系列的第七篇文章中,我们将添加整个库中最受期待的部分,将使用WebRequest函数发送请求。不过,我们不会直接对其进行访问,在此过程中会用到一些类和接口。让我们开始吧!
先留意一下目前库的状态,当前的架构图如下:
这里的目标是接收一个CHttpRequest对象,也就是一个已准备就绪、配置好了请求头、请求体、URL、方法和超时时间的HTTP请求,然后使用WebRequest函数实际发送一个HTTP请求。它还必须处理请求,并返回一个CHttpResponse对象,其中包含响应数据,如响应头、响应体、状态码和请求的总时长。
创建CHttpClient类
让我们创建所有人翘首以盼的类,也就是库的最终类,由库用户实例化并使用该类。这个类将被命名为CHttpClient.mqh,位于以下路径:Include/Connexus/Core/CHttpClient.mqh。最初,该文件大致如下,我已经添加了适当的导入语句://+------------------------------------------------------------------+ //| HttpClient.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "HttpRequest.mqh" #include "HttpResponse.mqh" #include "../Constants/HttpMethod.mqh" //+------------------------------------------------------------------+ //| class : CHttpClient | //| | //| [PROPERTY] | //| Name : CHttpClient | //| Heritage : No heritage | //| Description : Class responsible for linking the request and | //| response object with the transport layer. | //| | //+------------------------------------------------------------------+ class CHttpClient { public: CHttpClient(void); ~CHttpClient(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CHttpClient::CHttpClient(void) { } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CHttpClient::~CHttpClient(void) { } //+------------------------------------------------------------------+该类的作用是利用WebRequest函数将一个请求对象(即CHttpRequest)转换为HTTP请求,并返回一个包含相关数据的CHttpResponse对象。让我们来创建这个类。为此,我们将创建一个Send()方法,该方法必须接收两个对象,一个CHttpRequest对象和一个CHttpResponse对象,并返回布尔值。
class CHttpClient { public: CHttpClient(void); ~CHttpClient(void); //--- Basis function bool Send(CHttpRequest &request, CHttpResponse &response); };
现在我们来着手实现该功能。首先,让我们简要回顾一下WebRequest函数的参数。它有两种参数形式,我们暂时先用参数较少的版本:
int WebRequest( const string method, // HTTP method const string url, // URL const string headers, // headers int timeout, // timeout const char &data[], // the array of the HTTP message body char &result[], // an array containing server response data string &result_headers // headers of server response );我们接收到的这些对象中,所有相关参数都已准备就绪并配置完成,所以只需将它们添加进去即可。具体实现过程大致如下:
//+------------------------------------------------------------------+ //| Basis function | //+------------------------------------------------------------------+ bool CHttpClient::Send(CHttpRequest &request, CHttpResponse &response) { //--- 1. Request uchar body_request[]; request.Body().GetAsBinary(body_request); //--- 2. Response uchar body_response[]; string headers_response; //--- 3. Send ulong start = GetMicrosecondCount(); int status_code = WebRequest( request.Method().GetMethodDescription(), // HTTP method request.Url().FullUrl(), // URL request.Header().Serialize(), // Headers request.Timeout(), // Timeout body_request, // The array of the HTTP message body body_response, // An array containing server response data headers_response // Headers of server response ); ulong end = GetMicrosecondCount(); //--- 4. Add data in Response response.Clear(); response.Duration((end-start)/1000); response.StatusCode() = status_code; response.Body().AddBinary(body_response); response.Header().Parse(headers_response); //--- 5. Return is success return(response.StatusCode().IsSuccess()); } //+------------------------------------------------------------------+
我在注释里添加了一些数字,只是为了解释下面的每个步骤,让您理解这里发生的事情非常重要,因为这是该库的核心功能:
- 请求(REQUEST):这里我们创建一个名为body_request的uchar类型数组。我们访问请求体,并通过传递数组body_request来获取二进制格式的请求体内容,这样就能修改该数组,其中会插入请求体数据。
- 响应(RESPONSE):我们创建两个变量,WebRequest函数将使用它们来返回服务器响应的响应体和响应头。
- 发送(SEND):这是一切的核心,我们调用WebRequest函数并传入所有参数,包括URL、超时时间,以及步骤2中创建的用于接收响应体和响应头的变量。这里我们还创建了另外两个辅助变量,它们使用GetMicrosecondCount()函数来存储请求前后的时间。这样我们就能获取请求前后的时间,从而以毫秒为单位计算请求的持续时间。
- 在响应中添加数据(ADD DATA IN RESPONSE):这里,在请求得到响应后,无论请求是否成功,我们都使用Clear()函数重置响应对象的所有值,并使用以下公式添加持续时间:(结束时间 - 开始时间)/1000。我们还定义函数返回的状态码,并将响应体和响应头添加到响应对象中。
- 返回(RETURN):在最后一步,我们检查请求是否返回了任何成功代码(介于200和299之间)。这样,我们就能知道请求是否已完成,要获取更多详细信息,只需查看响应对象(即CHttpResponse)的内容即可。
创建该类后,更新的架构图大致如下:
最终,所有类都集成在HttpClient中。让我们进行一个简单的测试。
首次测试
让我们用目前构建好的所有内容,进行首次也是最简单的测试。我们向 httpbin.org 发起一个GET请求和一个POST请求,httpbin.org是一个免费的在线服务,可用于测试HTTP请求。它是由kennethreitz创建的,是一个开源项目(链接)。我们在之前的文章中已经使用过这个公共API,简要说明一下,httpbin的工作方式就像一面镜子,也就是说,我们发送给服务器的任何内容都会返回给客户端。这对于测试应用程序非常有用,因为我们只需知道请求是否完成,以及服务器接收到了哪些数据。
进入代码编写环节,我们在Experts文件夹内新建一个名为TestRequest.mq5的文件,最终路径为 Experts/Connexus/Test/TestRequest.mq5 。我们只使用该文件中的OnInit函数,其余部分忽略。代码大致如下:
//+------------------------------------------------------------------+ //| TestRequest.mq5 | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Initialize return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
我们导入相关库文件,并使用#include来添加导入语句
//+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include <Connexus/Core/HttpClient.mqh>
现在,我们来创建三个对象:
- CHttpRequest:用于定义URL和HTTP方法。对于GET请求,超时时间(Timeout)、请求体(body)和请求头(header)是可选的,超时时间的默认值为 5000 毫秒(5 秒)。
- CHttpResponse:用于存储请求的结果,包括状态码、状态描述,以及请求的持续时间(以毫秒为单位)。
- CHttpClient:实际执行请求的类
//--- Request object CHttpRequest request; request.Method() = HTTP_METHOD_GET; request.Url().Parse("https://httpbin.org/get"); //--- Response object CHttpResponse response; //--- Client http object CHttpClient client;
最后,我们调用 Send() 函数来发起请求,并将数据打印到终端。完整代码如下:
//+------------------------------------------------------------------+ //| TestRequest.mq5 | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include <Connexus/Core/HttpClient.mqh> //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Request object CHttpRequest request; request.Method() = HTTP_METHOD_GET; request.Url().Parse("https://httpbin.org/get"); //--- Response object CHttpResponse response; //--- Client http object CHttpClient client; client.Send(request,response); Print(response.FormatString()); //--- Initialize return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+执行代码后,我们在终端中会看到如下结果:
HTTP Response: --------------- Status Code: HTTP_STATUS_OK [200] - OK Duration: 845 ms --------------- Headers: Date: Fri, 18 Oct 2024 17:52:35 GMT Content-Type: application/json Content-Length: 380 Connection: keep-alive Server: gunicorn/19.9.0 Access-Control-Allow-Origin: * Access-Control-Allow-Credentials: true --------------- Body: { "args": {}, "headers": { "Accept": "*/*", "Accept-Encoding": "gzip, deflate", "Accept-Language": "pt,en;q=0.5", "Host": "httpbin.org", "User-Agent": "MetaTrader 5 Terminal/5.4620 (Windows NT 11.0.22631; x64)", "X-Amzn-Trace-Id": "Root=1-6712a063-5c19d0e03b85df9903cb0e91" }, "origin": "XXX.XX.XX.XX", "url": "https://httpbin.org/get" } ---------------
这是响应对象中的数据,包含状态码及其描述、请求持续时间(以毫秒为单位)、响应头以及响应体。对于将MQL5程序与外部API 连接的开发人员来说,这样极大地简化了开发工作。我们成功地实现了非常简单的HTTP请求发送方式。为了对比展示这一演进过程,我要强调一下在不使用该库时以及使用该库时分别如何发起请求:
- 不使用Connexus库时:
int OnInit() { //--- Defining variables string method = "GET"; // HTTP verb in string (GET, POST, etc...) string url = "https://httpbin.org/get"; // Destination URL string headers = ""; // Request header int timeout = 5000; // Maximum waiting time 5 seconds char data[]; // Data we will send (body) array of type char char result[]; // Data received as an array of type char string result_headers; // String with response headers string body = "{\"key\":\"value\"}"; StringToCharArray(body,data,0,WHOLE_ARRAY,CP_UTF8); ArrayRemove(data,ArraySize(data)-1); //--- Calling the function and getting the status_code int status_code = WebRequest(method,url,headers,timeout,data,result,result_headers); //--- Print the data Print("Status code: ",status_code); Print("Response: ",CharArrayToString(result)); // We use CharArrayToString to display the response in string form. //--- return(INIT_SUCCEEDED); }
- 使用Connexus库时:
int OnInit() { //--- Request object CHttpRequest request; request.Method() = HTTP_METHOD_GET; request.Url().Parse("https://httpbin.org/get"); request.Body().AddString("{\"key\":\"value\"}"); //--- Response object CHttpResponse response; //--- Client http object CHttpClient client; client.Send(request,response); Print(response.FormatString()); //--- Initialize return(INIT_SUCCEEDED); }
使用该库后,处理请求变得轻松许多——开发者能以不同格式(如字符串、JSON 或二进制)访问响应体,还能利用所有其他可用资源。此外,开发者可以修改请求的某些部分(如 URL、请求体和请求头),并复用同一个对象发起新请求。这种方式让连续发起多个请求变得简单易行。
不过,我们也面临一个小问题:耦合度过高!
虽然目前一切运行正常,但代码仍有改进空间,Connexus库也不例外。那么,问题究竟出在哪里?由于我们在CHttpClient类中直接调用了WebRequest函数,导致代码高度耦合。那么,什么是高度耦合的代码呢?
想象一下:您在用扑克牌搭房子。每张牌必须完美平衡,才能让整个结构屹立不倒。现在,假设您搭建的不是一座轻巧灵活的纸牌屋(可以随意移动某张牌而不倒塌),而是一座像混凝土块一样笨重的建筑。这时,如果您试图移动或抽走一张牌,结果会是怎样?整个建筑必然会崩塌。这就是高度耦合代码的写照。
在编程中,代码耦合度过高意味着各部分紧密关联,修改其中一部分时,往往需要同时修改其他多个部分。就像您不小心把鞋带系成了死结,想要解开时却越弄越乱。修改函数、调整类或添加新功能都会变得异常棘手,因为所有部分都紧密地纠缠在一起。
例如,如果一个函数强烈依赖于另一个函数,且这两个函数无法独立存在,那么这段代码就是高度耦合的。这种耦合性会带来严重问题:随着时间的推移,代码将变得难以维护、扩展新功能,甚至修复漏洞时都可能引发新问题。这正是CHttpClient类目前的问题所在——Send()函数直接依赖WebRequest,两者无法分离。在理想情况下,我们希望代码是解耦的,即各部分能独立运作,就像乐高积木一样:可以轻松拼接和拆卸,而不会影响整体结构。
为了降低代码耦合度,我们可以采用接口和依赖注入技术。其核心思想是:创建一个定义必要操作的接口,让类依赖于这个抽象而非具体实现。这样,CHttpClient就能与任何实现该接口的对象通信(无论是 WebRequest 还是模拟函数FakeWebRequest)。接下来,我们深入探讨接口和依赖注入的概念:
- 接口的作用:接口允许我们定义一个“契约”,其他类可以通过实现该契约来提供具体功能。在我们的场景中,可以创建一个名为IHttpTransport的接口,定义CHttpClient发起HTTP请求所需的方法。这样,CHttpClient将依赖于IHttpTransport接口,而非直接依赖WebRequest函数,从而降低耦合度。
- 依赖注入:为了将CHttpClient与IHttpTransport的具体实现连接起来,我们将使用依赖注入。这种技术通过将依赖项(如WebRequest或FakeWebRequest)作为参数传递给CHttpClient,而非在类内部直接实例化,从而实现解耦。
那么,避免代码高度耦合有哪些好处呢?
- 可维护性: 解耦后的代码就像汽车的可更换部件——无需拆卸整个发动机即可替换某个零件。代码各部分相互独立,修改一处不会影响其他部分。需要修复漏洞或改进功能?放心修改,无需担心破坏系统的其他部分。
- 可复用性:解耦后的代码就像乐高积木,你可以将功能模块提取出来,在其他项目或系统部分中复用,而无需处理复杂的依赖关系。
- 可测试性:通过使用接口,我们可以将WebRequest替换为模拟函数(该函数模拟预期行为),从而在不依赖真实HTTP请求的情况下进行测试。这种模拟技术引出了下一个主题:模拟对象(Mocks)。
什么是模拟对象?
模拟对象是真实对象或函数的仿真版本,用于测试类的行为,而无需依赖外部实现。当我们通过接口将CHttpClient与WebRequest解耦后,就可以传入一个实现相同“契约”的模拟版本(即“模拟对象”)。这样,我们就能在受控环境中测试CHttpClient,并预测其在不同响应场景下的行为,而无需实际发起HTTP调用。
在CHttpClient直接调用WebRequest的系统中,测试受限且依赖于服务器的连接性和行为。然而,通过注入一个返回模拟结果的WebRequest模拟对象,我们可以测试不同场景,并独立验证CHttpClient的响应。这些模拟对象对于确保库在错误情况或意外响应下仍能按预期运行至关重要。
接下来,我们将探讨如何实现接口和模拟对象,以提高CHttpRequest类的灵活性和可测试性。
动手编码
那么,如何在代码中实现这些概念呢?我们将使用一些更高级的语言特性,如类和接口。如果您已经读到此处并看过之前的文章,应该对类有一定了解,让我们再回顾一下基本概念。
- 类:类是创建对象的“模板”,在类中可以定义方法和属性。方法是对象的行为或操作,而属性是对象的特征或数据。以MQL5为例:假设有一个名为CPosition的类。在该类中,您可以定义属性如价格、类型(买入或卖出)、止盈、止损、交易量等。类用于创建对象(实例),每个对象都有自己独特的特征。
- 接口:接口是一种“契约”,仅定义类必须实现的方法,但不规定这些方法的具体实现方式。接口不包含方法的实现,仅包含方法的签名(名称、参数和返回类型)。
基于这一概念,让我们通过一个实例来探讨如何在库中应用接口。我们逐步展开:我们希望访问WebRequest函数,但不想直接调用它(因为这会导致代码高度耦合)。我们希望您能拥有自己的WebRequest函数,并让库能够轻松使用它。为此,我们需要在函数和调用者之间创建层级结构。这个层级结构就是接口。
在直接编写代码之前,我先用一些图表来解释。
正如架构图所示,HttpClient类直接调用 WebRequest函数。现在,我们在类与函数之间引入一个中间层——IHttpTransport接口。
通过这个接口,我们可以创建CHttpTransport类,使用WebRequest函数实现该接口。如下图所示:
现在,我们可以充分利用这一设计:为接口创建多种不同的实现,并将它们传递给CHttpClient类。默认情况下,库将使用基于WebRequest的CHttpTransport实现,但我们也可以根据需要添加任意数量的其他实现,如下图所示:
|--- Connexus |--- |--- Core |--- |--- |--- HttpTransport.mqh |--- |--- Interface |--- |--- |--- IHttpTransport.mqh
首先,我们来编写接口代码,明确规范该函数的输入参数与输出结果:
//+------------------------------------------------------------------+ //| transport interface | //+------------------------------------------------------------------+ interface IHttpTransport { int Request(const string method,const string url,const string cookie,const string referer,int timeout,const char &data[],int data_size,char &result[],string &result_headers); int Request(const string method,const string url,const string headers,int timeout,const char &data[],char &result[],string &result_headers); }; //+------------------------------------------------------------------+注意,这里我只是声明了函数,而没有定义函数的具体实现。我告诉编译器:该函数应返回一个整数,方法名称为Request,并指定了预期的参数列表。要实现函数体,必须通过类来完成。我们需要告知编译器:该类必须实现此接口。实际代码结构如下:
//+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "../Interface/IHttpTransport.mqh" //+------------------------------------------------------------------+ //| class : CHttpTransport | //| | //| [PROPERTY] | //| Name : CHttpTransport | //| Heritage : IHttpTransport | //| Description : class that implements the transport interface, | //| works as an extra layer between the request and | //| the final function and WebRequest. | //| | //+------------------------------------------------------------------+ class CHttpTransport : public IHttpTransport { public: CHttpTransport(void); ~CHttpTransport(void); int Request(const string method,const string url,const string cookie,const string referer,int timeout,const char &data[],int data_size,char &result[],string &result_headers); int Request(const string method,const string url,const string headers,int timeout,const char &data[],char &result[],string &result_headers); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CHttpTransport::CHttpTransport(void) { } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CHttpTransport::~CHttpTransport(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ int CHttpTransport::Request(const string method,const string url,const string cookie,const string referer,int timeout,const char &data[],int data_size,char &result[],string &result_headers) { return(WebRequest(method,url,cookie,referer,timeout,data,data_size,result,result_headers)); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ int CHttpTransport::Request(const string method,const string url,const string headers,int timeout,const char &data[],char &result[],string &result_headers) { return(WebRequest(method,url,headers,timeout,data,result,result_headers)); } //+------------------------------------------------------------------+
通过使用接口和模拟对象,我们将CHttpRequest类与WebRequest函数解耦,构建了一个更灵活、更易维护的库。这种方法为使用Connexus的开发者提供了灵活性:他们可以在不发起真实请求的情况下,测试库在不同场景下的行为,从而实现更顺畅的集成,并快速适应新的需求和功能扩展。
现在,我们只需在CHttpClient类中应用这一设计。为此,我们将导入IHttpTransport接口并创建其实例:
//+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "../Interface/IHttpTransport.mqh" //+------------------------------------------------------------------+ //| class : CHttpClient | //| | //| [PROPERTY] | //| Name : CHttpClient | //| Heritage : No heritage | //| Description : Class responsible for linking the request and | //| response object with the transport layer. | //| | //+------------------------------------------------------------------+ class CHttpClient { private: IHttpTransport *m_transport; // Instance to store the transport implementation public: CHttpClient(void); ~CHttpClient(void); //--- Basis function bool Send(CHttpRequest &request, CHttpResponse &response); }; //+------------------------------------------------------------------+
为实现依赖注入,我们将在类中新增一个构造函数,用于接收将使用的传输层(transport layer)对象,并将其存储在成员变量m_transport中。同时,我们还会修改默认的构造函数,使其在未定义传输层时,自动使用CHttpTransport作为默认实现:
//+------------------------------------------------------------------+ //| class : CHttpClient | //| | //| [PROPERTY] | //| Name : CHttpClient | //| Heritage : No heritage | //| Description : Class responsible for linking the request and | //| response object with the transport layer. | //| | //+------------------------------------------------------------------+ class CHttpClient { private: IHttpTransport *m_transport; // Instance to store the transport implementation public: CHttpClient(IHttpTransport *transport); CHttpClient(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CHttpClient::CHttpClient(IHttpTransport *transport) { m_transport = transport; } //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CHttpClient::CHttpClient(void) { m_transport = new CHttpTransport(); } //+------------------------------------------------------------------+
既然我们已经指定传输层,让我们在“发送”函数中使用它。
//+------------------------------------------------------------------+ //| Basis function | //+------------------------------------------------------------------+ bool CHttpClient::Send(CHttpRequest &request, CHttpResponse &response) { //--- Request uchar body_request[]; request.Body().GetAsBinary(body_request); //--- Response uchar body_response[]; string headers_response; //--- Send ulong start = GetMicrosecondCount(); int status_code = m_transport.Request(request.Method().GetMethodDescription(),request.Url().FullUrl(),request.Header().Serialize(),request.Timeout(),body_request,body_response,headers_response); ulong end = GetMicrosecondCount(); //--- Add data in Response response.Clear(); response.Duration((end-start)/1000); response.StatusCode() = status_code; response.Body().AddBinary(body_response); response.Header().Parse(headers_response); //--- Return is success return(response.StatusCode().IsSuccess()); } //+------------------------------------------------------------------+
值得注意的是,类的使用方式并没有改变,也就是说,我们在本文前面测试部分中使用的代码仍然有效。不同之处在于,现在我们可以改变库的最终功能。
结论
这标志着库的另一个阶段的完成,即创建客户端层。通过使用接口和模拟对象,我们将CHttpClient类与WebRequest函数解耦,从而创建了一个更加灵活且易于维护的库。这种方法为使用Connexus的开发者提供了灵活性:他们可以在不发起真实请求的情况下,测试库在不同场景下的行为,从而实现更顺畅的集成,并快速适应新的需求和功能扩展。
使用接口和模拟对象进行解耦的做法显著提高了代码质量,有助于扩展性、可测试性和长期可维护性。对于库来说,这种方法尤其具有优势,因为开发人员通常需要灵活的测试和模块化的替换,这为Connexus库的所有用户提供了附加的价值。
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/16324


