
Connexus请求解析(第六部分):创建HTTP请求与响应
概述
本文是我们正在构建的名为Connexus的库的系列文章的一部分。在第一篇文章中,我们介绍了WebRequest函数的基本功能,理解了它的各个参数,并编写了一段示例代码来演示其用法以及与之相关的挑战。在上一篇文章中,我们探讨了什么是HTTP方法以及服务器返回的状态码,这些状态码用于指示请求是否成功处理,或者客户端或服务器端是否出现了错误。
在Connexus库系列文章的第六篇中,我们将聚焦于完整的HTTP请求,涵盖构成请求的各个组件。我们将创建一个表示整个请求的类,这将有助于将之前创建的各个类整合在一起。此外,我们还将开发一个类似的类来处理服务器的响应,该类将包含响应数据、状态码,甚至请求的持续时间。
在HTTP通信中,一个请求由多个组件构成,这些组件共同组成了完整的请求。在之前的文章中,我们已经探讨了所有这些组件,并为请求的每个元素创建了单独的类。下面让我们回顾一下这些元素:
- URL(统一资源定位符):定义了服务器在网络上的地址,由域名、端口、路径等更小的部分组成。在Connexus的第二部分中,我们对此进行了更详细的探讨,并创建了一个类来正确格式化URL。
- 请求头:这些是随请求一起发送的附加数据,用于提供有关请求的详细信息,这些信息不属于请求体或 URL 的一部分。在Connexus的第三部分中,我们对此进行了更详细的探讨,并创建了一个类来负责组织请求头。
- 请求体:指的是实际发送或接收的内容。简而言之,请求体就是存储我们感兴趣的数据的位置,这些数据是我们想要发送到服务器的。在第四部分中,我们对此进行了更详细地探讨,并创建一个类来负责存储请求体,该类支持以不同格式添加请求体,如纯文本(字符串)、JSON 或字符数组(二进制)。
- HTTP方法:客户端使用HTTP方法来告诉服务器想要执行什么操作。在Connexus的第五部分中,我们对此进行了更详细的讨论。
- 超时:超时在之前的文章中并未提及,但简要来说,它是服务器必须响应的时间(以毫秒为单位)。如果请求耗时超过分配的时间,请求将被终止,并产生超时错误。这个值对于避免服务器响应时间过长的情况很有用,因为WebRequest函数是同步的,这意味着在等待服务器响应时,EA会保持“暂停”状态。如果请求耗时远超预期,这可能会成为一个问题。为了避免这种情况,建议使用超时设置。超时值可以根据您的需求进行调整,但对于大多数服务器来说,5000毫秒(5 秒)足以让请求得到正确处理。
这些组件中的每一个在构建正确的HTTP请求中都起着基础性的作用,但值得注意的是,并非所有组件都是必需的。唯一必需的元素是URL和HTTP方法(GET、POST、PUT 等)。
为了帮助我们了解库的进度以及我们已经创建的类,让我们看一下当前库的架构图:
请注意,我们已经创建了用于处理HTTP方法、状态码、带查询参数的URL、请求头和请求体的类。
外观(Facade)设计模式
为了继续构建这个库,我们将实现一种设计模式。如果您对设计模式不熟悉,我建议您阅读由Mohamed Abdelmaaboud撰写的系列文章《软件开发与 MQL5 中的设计模式》。在该系列文章中,作者通过示例代码描述了几种设计模式。对于Connexus库,我们将实现“外观”设计模式,这是编程中一种广为人知的模式。这种模式为更复杂的子系统集合提供了一个简化的接口,隐藏了内部复杂性,并允许客户端以一种更简单的方式与系统进行交互。
让我们想象一个与库相关的实例:您想要创建一个请求。为了做到这一点,您需要为请求的每个元素创建一个实例并进行配置,大致如下:
CURL m_url; m_url.Parse("http://example.com/api"); CHttpBody m_body; m_body.AddString("my text"); CHttpHeader m_headers; m_headers.Add("content-type","text/plain"); CHttpMethod m_method; m_method = HTTP_METHOD_GET; int m_timeout = 5000;
这种方法会使代码变得杂乱无章,需要创建多个实例,占用许多行代码,从而降低了代码的可读性。当处理大量请求时,情况会变得更糟,因为管理诸如头信息、正文和URL等多个对象变得复杂且难以维护。这就是外观设计模式发挥作用之处。回到概念本身:该模式为一组更复杂的子系统提供了一个简化的接口,隐藏了内部复杂性,允许客户端以更简单的方式与系统交互。
在这个上下文中,子系统是请求元素的类,如CHttpBody、CHttpHeaders等,目标是为它们创建一个更直观的接口。该模式通过引入一个作为“外观”的类或接口来访问子系统,从而解决了这个问题。最终的开发者只与这个简化的接口交互。
简言之,外观 架构具有以下优势:
- 简化交互:开发者无需直接处理一系列复杂的类,而是可以使用一个隐藏了内部细节的简化接口。
- 减少耦合:由于客户端没有直接与内部子系统耦合,因此可以在不影响客户端的情况下对这些子系统进行修改。
- 提高可维护性:外观接口与内部子系统之间的清晰分离使得代码更容易维护和扩展,因为任何内部变化都可以通过外观进行抽象。
这种设计模式将如何在库中实现?为了实现这一点,我们将创建一个名为CHttpRequest的新类,它将包含子系统。对于库的用户来说,该类的使用应该如下所示:
CHttpRequest request; request.Method() = HTTP_METHOD_GET; request.Url().Parse("http://example.com/api"); request.Body().AddString("my text"); request.Header().Add("content-type","text/plain");
我们注意到代码已经变得简洁且更易读,这正是这种设计模式背后的理念。外观模式为一组更复杂的子系统(如管理 CHttpBody、CHttpHeader 等实例)提供了一个简化的接口(CHttpRequest),隐藏了内部复杂性,并允许客户端以一种更简单的方式与系统进行交互。
创建 CHttpRequest 类
既然我们已经理解了外观模式的概念,那么我们就可以将这种架构应用到CHttpRequest类中。让我们在一个名为core的新文件夹中创建这个类。文件将被命名为HttpRequest.mqh,最终的路径将是Include/Connexus/Core/HttpRequest.mqh。起初,该文件将如下所示://+------------------------------------------------------------------+ //| HttpRequest.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" //+------------------------------------------------------------------+ //| class : CHttpRequest | //| | //| [PROPERTY] | //| Name : CHttpRequest | //| Heritage : No heritage | //| Description : Gathers elements of an http request such as url, | //| body, header, method and timeout | //| | //+------------------------------------------------------------------+ class CHttpRequest { public: CHttpRequest(void); ~CHttpRequest(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CHttpRequest::CHttpRequest(void) { } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CHttpRequest::~CHttpRequest(void) { } //+------------------------------------------------------------------+
让我们导入之前文件中创建的类:CURL、CHttpBody、CHttpHeader 和 CHttpMethod。我们将为每个类创建一个实例,并添加“*”以告知编译器将手动管理指针。此外,我们还将添加一个名为m_timeout的int类型变量,用于存储请求的超时值。
//+------------------------------------------------------------------+ //| HttpRequest.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "../URL/URL.mqh" #include "../Header/HttpBody.mqh" #include "../Header/HttpHeader.mqh" #include "../Constants/HttpMethod.mqh" //+------------------------------------------------------------------+ //| class : CHttpRequest | //| | //| [PROPERTY] | //| Name : CHttpRequest | //| Heritage : No heritage | //| Description : Gathers elements of an http request such as url, | //| body, header, method and timeout | //| | //+------------------------------------------------------------------+ class CHttpRequest { private: CURL *m_url; CHttpBody *m_body; CHttpHeader *m_headers; CHttpMethod *m_method; int m_timeout; public: CHttpRequest(void); ~CHttpRequest(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CHttpRequest::CHttpRequest(void) { m_url = new CURL(); m_body = new CHttpBody(); m_headers = new CHttpHeader(); m_method = new CHttpMethod(); m_timeout = 5000; // Default timeout (5 seconds) } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CHttpRequest::~CHttpRequest(void) { delete m_url; delete m_body; delete m_headers; delete m_method; } //+------------------------------------------------------------------+
现在,让我们为每个实例的指针添加一些访问方法,以及设置和获取超时值的方法:
- CURL *Url(void):返回指向 URL 的指针
- CHttpBody *Body(void):返回指向请求体的指针
- CHttpHeader *Header(void):返回指向请求头的指针
- CHttpMethod *Method(void):返回指向HTTP方法的指针
- CHttpRequest *Timeout(int timeout):设置超时值
- int Timeout(void):获取超时值
除了这些方法外,我们还将添加了一些辅助方法:
-
void Clear(void):清除所有实例中的数据
-
string FormatString(void):生成一个包含所有请求数据的字符串
格式化请求的示例:
HTTP Request: --------------- Method: GET URL: https://api.example.com/resource?id=123&filter=active Headers: --------------- Authorization: Bearer token Content-Type: application/json User-Agent: MyHttpClient/1.0 Body: --------------- { "key": "value", "data": [1, 2, 3] } ---------------
以下是这些方法的实现过程。
//+------------------------------------------------------------------+ //| HttpRequest.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" //+------------------------------------------------------------------+ //| class : CHttpRequest | //| | //| [PROPERTY] | //| Name : CHttpRequest | //| Heritage : No heritage | //| Description : Gathers elements of an http request such as url, | //| body, header, method and timeout | //| | //+------------------------------------------------------------------+ class CHttpRequest { public: //--- HTTP CURL *Url(void); // Get url object CHttpBody *Body(void); // Get body object CHttpHeader *Header(void); // Get header object CHttpMethod *Method(void); // Get method object //--- Timeout CHttpRequest *Timeout(int timeout); // Set timeout int Timeout(void); // Get timeout //--- Auxiliary methods void Clear(void); // Reset data string FormatString(void); // Format data }; //+------------------------------------------------------------------+ //| Get url object | //+------------------------------------------------------------------+ CURL *CHttpRequest::Url(void) { return(GetPointer(m_url)); } //+------------------------------------------------------------------+ //| Get body object | //+------------------------------------------------------------------+ CHttpBody *CHttpRequest::Body(void) { return(GetPointer(m_body)); } //+------------------------------------------------------------------+ //| Get header object | //+------------------------------------------------------------------+ CHttpHeader *CHttpRequest::Header(void) { return(GetPointer(m_headers)); } //+------------------------------------------------------------------+ //| Get method object | //+------------------------------------------------------------------+ CHttpMethod *CHttpRequest::Method(void) { return(GetPointer(m_method)); } //+------------------------------------------------------------------+ //| Set timeout | //+------------------------------------------------------------------+ CHttpRequest *CHttpRequest::Timeout(int timeout) { m_timeout = timeout; return(GetPointer(this)); } //+------------------------------------------------------------------+ //| Get timeout | //+------------------------------------------------------------------+ int CHttpRequest::Timeout(void) { return(m_timeout); } //+------------------------------------------------------------------+ //| Reset data | //+------------------------------------------------------------------+ void CHttpRequest::Clear(void) { m_url.Clear(); m_body.Clear(); m_headers.Clear(); m_timeout = 5000; } //+------------------------------------------------------------------+ //| Format data | //+------------------------------------------------------------------+ string CHttpRequest::FormatString(void) { return( "HTTP Request:"+ "\n---------------"+ "\nMethod: "+m_method.GetMethodDescription()+ "\nURL: "+m_url.FullUrl()+ "\n"+ "\n---------------"+ "\nHeaders:"+ "\n"+m_headers.Serialize()+ "\n"+ "\n---------------"+ "\nBody:"+ "\n"+m_body.GetAsString()+ "\n---------------" ); } //+------------------------------------------------------------------+
至此,我们已经完成了这个类,它作为外观来访问构成HTTP请求的对象。需要注意的是,我只添加了代码中修改过的部分。完整的文件可以在所附文章的末尾找到。
有了这个新的CHttpRequest类,更新后的库架构图如下所示:
总之,CHttpRequest类充当了一个外观角色,简化了配置和发送HTTP请求的过程。在内部,CHttpHeaders类负责处理请求头的逻辑,而CHttpBody类则负责构建请求体。使用这个库的开发人员无需担心请求头或请求体的处理细节——他们只需使用CHttpRequest类的方法设置值,外观类就会处理其余的事情。
创建CHttpResponse类
遵循同样的理念,让我们创建另一个类,用于表示服务器的响应数据。其结构与CHttpRequest类似。以下是构成响应的元素:
- 请求头:与请求一样,响应也包含一个请求头,它向客户端提供有关请求的元数据。
- 响应体:这将包含服务器的响应体。服务器会将我们想要获取的数据发送到这里,这是消息的核心部分。
- 状态码:包含状态码,我们在该系列的第五部分中会对此进行了更详细地探讨。此状态码是一个三位数,用于告知请求是否成功处理,或者是否遇到了客户端或服务器端的错误。
- 持续时间:存储请求完成所需的时间,以毫秒为单位。
我们将在core文件夹内创建一个名为HttpResponse的新文件,最终的路径将是 Include/Connexus/Core/HttpResponse.mqh。创建的类应如下所示:
//+------------------------------------------------------------------+ //| HttpResponse.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" //+------------------------------------------------------------------+ //| class : CHttpResponse | //| | //| [PROPERTY] | //| Name : CHttpResponse | //| Heritage : No heritage | //| Description : gathers elements of an http response such as body, | //| header, status code and duration | //| | //+------------------------------------------------------------------+ class CHttpResponse { public: CHttpResponse(void); ~CHttpResponse(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CHttpResponse::CHttpResponse(void) { } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CHttpResponse::~CHttpResponse(void) { } //+------------------------------------------------------------------+
我们将导入构成响应的元素,它们分别是:CHttpHeader、CHttpBody和CHttpStatusCode。之后,我们将为这些类分别创建一个实例,并创建一个类型为ulong的私有变量,用于存储请求的持续时间。最终的代码如下所示:
//+------------------------------------------------------------------+ //| HttpResponse.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "../Constants/HttpStatusCode.mqh" #include "../Header/HttpBody.mqh" #include "../Header/HttpHeader.mqh" //+------------------------------------------------------------------+ //| class : CHttpResponse | //| | //| [PROPERTY] | //| Name : CHttpResponse | //| Heritage : No heritage | //| Description : gathers elements of an http response such as body, | //| header, status code and duration | //| | //+------------------------------------------------------------------+ class CHttpResponse { private: CHttpHeader *m_header; CHttpBody *m_body; CHttpStatusCodes *m_status_code; ulong m_duration; public: CHttpResponse(void); ~CHttpResponse(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CHttpResponse::CHttpResponse(void) { m_header = new CHttpHeader(); m_body = new CHttpBody(); m_status_code = new CHttpStatusCodes(); m_duration = 0; // Reset duration } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CHttpResponse::~CHttpResponse(void) { delete m_header; delete m_body; delete m_status_code; } //+------------------------------------------------------------------+
现在,让我们继续为这个类添加方法。我们将从最简单的方法开始,这些方法包括只返回各个类实例的指针、以及设置和获取持续时间:
- CHttpHeader *Header(void):返回指向请求头的指针
- CHttpBody *Body(void):返回指向请求体的指针
- CHttpStatusCodes *StatusCode(void):返回指向状态码的指针。
- void Duration(ulong duration):设置持续时间。
- ulong Duration(void):获取持续时间。
对于该类,我们将为CHttpRequest创建相同的辅助方法:
-
void Clear(void):清除实例中的所有数据
-
string FormatString(void):生成一个包含所有响应数据的字符串。
格式化响应的示例
HTTP Response: --------------- Status Code: 200 OK Duration: 120ms Headers: --------------- Content-Type: application/json Content-Length: 256 Server: Apache/2.4.41 (Ubuntu) Date: Wed, 02 Oct 2024 12:34:56 GMT Body: --------------- { "message": "Success", "data": { "id": 123, "name": "Sample Item", "status": "active" } } ---------------
以下是包含实现这些函数的代码:
//+------------------------------------------------------------------+ //| HttpResponse.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" //+------------------------------------------------------------------+ //| class : CHttpResponse | //| | //| [PROPERTY] | //| Name : CHttpResponse | //| Heritage : No heritage | //| Description : gathers elements of an http response such as body, | //| header, status code and duration | //| | //+------------------------------------------------------------------+ class CHttpResponse { public: //--- HTTP CHttpHeader *Header(void); // Get header object CHttpBody *Body(void); // Get body object CHttpStatusCodes *StatusCode(void); // Get status code object //--- Duration void Duration(ulong duration); // Set duration ulong Duration(void); // Get duration //--- Auxiliary methods void Clear(void); // Reset data string FormatString(void); // Format data }; //+------------------------------------------------------------------+ //| Get header object | //+------------------------------------------------------------------+ CHttpHeader *CHttpResponse::Header(void) { return(GetPointer(m_header)); } //+------------------------------------------------------------------+ //| Get body object | //+------------------------------------------------------------------+ CHttpBody *CHttpResponse::Body(void) { return(GetPointer(m_body)); }; //+------------------------------------------------------------------+ //| Get status code object | //+------------------------------------------------------------------+ CHttpStatusCodes *CHttpResponse::StatusCode(void) { return(GetPointer(m_status_code)); }; //+------------------------------------------------------------------+ //| Set duration | //+------------------------------------------------------------------+ void CHttpResponse::Duration(ulong duration) { m_duration = duration; } //+------------------------------------------------------------------+ //| Get duration | //+------------------------------------------------------------------+ ulong CHttpResponse::Duration(void) { return(m_duration); } //+------------------------------------------------------------------+ //| Reset data | //+------------------------------------------------------------------+ void CHttpResponse::Clear(void) { m_header.Clear(); m_body.Clear(); m_status_code.SetStatusCode(HTTP_STATUS_URL_NOT_ALLOWED); } //+------------------------------------------------------------------+ //| Format data | //+------------------------------------------------------------------+ string CHttpResponse::FormatString(void) { return( "HTTP Response:"+ "\n---------------"+ "\nStatus Code: "+m_status_code.GetStatusCodeFormat()+ "\nDuration: "+IntegerToString(m_duration)+" ms"+ "\n"+ "\n---------------"+ "\nHeaders:"+ "\n"+m_header.Serialize()+ "\n---------------"+ "\nBody:"+ "\n"+m_body.GetAsString()+ "\n---------------" ); } //+------------------------------------------------------------------+
需要提醒一下,这里我只包含了对类的修改部分,以避免文章过于冗长。所有使用的代码都附在文末。
至此,响应类已经完成。这些类相对简单,目标只有一个:将请求或响应的元素组合在一起。这样,我们就可以将请求作为一个单独的对象来处理,从而显著简化HTTP请求和响应的使用。有了这个新的CHttpResponse类,更新后的架构图如下所示:
使用示例
简要来说,我将给出这些类的一些使用示例。从CHttpRequest开始,我将使用这个类来构造一个HTTP请求。
//--- Example 1 - GET CHttpRequest request; request.Method() = HTTP_METHOD_GET; request.Url().Parse("http://example.com/api/symbols"); //--- Example 2 - POST CHttpRequest request; request.Method() = HTTP_METHOD_POST; request.Url().Parse("http://example.com/api/symbols"); request.Body().AddToString("{\"EURUSD\":1.08312}"); request.Header().Add("content-type","application/json"); //--- Example 3 - DELETE CHttpRequest request; request.Method() = HTTP_METHOD_DELETE; request.Url().Parse("http://example.com/api/symbols?symbol=EURUSD");
这里的一大优势在于创建请求、添加请求头和请求体或更改HTTP方法的简便性。
对于响应而言,我们需要的是与之相反的操作,不是轻松地创建一个请求对象,而是轻松地读取一个响应对象。两者以类似的方式工作,这使得创建和读取都变得简单。我将附上一张图片,展示开发者使用Connexus时的屏幕界面:
请注意,我们可以以不同的格式(如字符串、JSON或二进制)访问响应体。这里,我们直接访问了CHttpBody类的指针,然后访问了它所拥有的所有方法。当我们为响应的这些元素分别创建类时,我就考虑到库的这一部分,即我们使用每个类的部分。
结论
在本文中,我们探讨了拥有一个CHttpRequest对象的重要性,该对象将HTTP请求的所有组件组合在一起,确保了代码的清晰性、可重用性和易于维护性。我们还看到了如何应用外观(Facade) 设计模式来简化与复杂组件的交互,隐藏内部复杂性,并提供一个清晰且高效的接口。在Connexus中,将外观模式应用于ChttpRequest类,使得创建完整的HTTP请求变得更加容易,简化了开发者的开发过程。同时,我们还创建了遵循相同格式的CHttpResponse类,这应该有助于访问HTTP响应的数据。
在下一篇文章中,我们将深入探讨传输层,该层必须接收一个CHttpRequest对象,并将数据转换为WebRequest函数(这是库的最后一层)。传输层必须能够处理响应数据,并返回一个类型为CHttpResponse的对象,以便开发者随后可以使用响应数据。敬请期待!
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/16182
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.




