
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




