
HTTP和Connexus(第2部分):理解HTTP架构和库设计
概述
本文是该系列文章的延续,我们将构建一个名为Connexus的库。在第一篇文章中,我们了解到WebRequest函数的基本工作原理,理解了它的每个参数,并且还创建了一个示例代码,展示了该函数的使用及其困难之处。在本文中,我们将继续深入了解HTTP协议,了解URL的工作原理以及构建URL所使用的元素,并创建两个初始类,分别是:
- CQueryParam:用于管理URL中查询参数的类
- CURL:包含URL的所有元素的类,包括CQueryParam的一个实例
什么是HTTP?
HTTP是一种用于在网页上传输数据的通信协议。它工作在TCP/IP协议之上,后者建立了客户端和服务器之间的连接。HTTP是一种无状态协议,这意味着每个请求都是独立的,之前的请求未知。一个HTTP请求和响应由三个主要部分组成:
1. 请求行
请求行包含三个元素:
- HTTP方法:定义要执行的动作(GET、POST、PUT、DELETE等)。
- URL:指定请求的资源。
- HTTP版本:代表所使用的协议版本(HTTP/1.1、HTTP/2等)。
请求和响应示例:
REQUEST
GET /index.html HTTP/1.1
RESPONSE
HTTP/1.1 200 OK
2. 请求头
请求头提供了关于请求的附加信息,比如内容类型、用户代理(浏览器)以及认证信息。例如:
Host: www.exemplo.com User-Agent: Mozilla/5.0 Accept-Language: en-US,en;q=0.5
3. 请求体
并非所有请求都包含请求体,但在像POST和PUT这样的方法中很常见,这些方法会将数据(如表单或文件)发送到服务器。
常见HTTP方法
HTTP方法对于确定客户端向服务器请求的操作类型至关重要。每种方法都定义了特定的用途,例如获取数据、发送信息或修改资源。下面我们深入探讨最常见的HTTP方法、其用途以及最佳实例。
- GET:GET 方法是HTTP协议中最常用的方法。其用于从服务器检索或搜索数据,但不会改变服务器的状态。GET方法的主要特性是幂等的,也就是说,对同一资源发起多次 GET 请求不会改变应用程序的状态。
- 特征
- 无负面影响:仅读取数据。不在服务器上产生任何更改。
- 空请求体:通常,GET 请求没有请求体。
- 可缓存:浏览器支持GET响应缓存以提高性能。
- 使用场景:
- 获取静态数据,如HTML页面、图片、文件或JSON格式的信息。
- 从数据库中检索信息而不修改数据。
- POST:POST方法用于向服务器发送数据,通常用于创建新资源。与GET不同,它会改变服务器的状态。POST不是幂等的,这意味着如果您多次发送相同的请求,可能会创建多个资源。
- 特征
- 改变服务器状态:通常用于创建新资源。
- 包含请求体:包含将发送到服务器的数据。
- 不可缓存:通常,不支持POST请求缓存。
- 使用场景:
- 提交包含数据的表单。
- 创建新资源,如向数据库中添加新条目。
- PUT:PUT方法用于更新服务器上的资源。如果资源不存在,可以使用PUT创建。PUT方法的主要特性是其具有幂等性:使用相同的请求体发起多次 PUT请求将始终产生相同的结果。
- 特征
- 幂等:使用相同请求体的重复请求将具有相同的效果。
- 发送完整资源:请求体通常包含已更新资源的完整表示。
- 可创建或更新:如果资源不存在,可使用PUT创建。
- 使用场景:
- 完全更新或替换现有资源。
- 如果资源不存在,则通过特定URL创建资源。
- DELETE:使用DELETE方法从服务器上删除特定资源。与 PUT 类似,它也是幂等的,这意味着如果您对同一资源发起多次DELETE请求,结果将相同:资源将被删除(或者如果已经被删除,则保持缺失状态)。
- 特征
- 幂等:如果您删除一个已经被删除的资源,服务器将返回成功。
- 无请求体:通常,DELETE请求没有请求体。
- 使用场景:
- 删除特定的服务器资源,例如从数据库中删除数据。
- PATCH:PATCH方法用于对资源进行部分更新。与需要发送资源完整表示的PUT不同,PATCH允许您仅修改需要更新的字段。
- HEAD:与GET类似,但没有响应体。其用于检查有关资源的信息。
- OPTIONS:用于描述与服务器的通信选项,包括支持的方法。
- TRACE:用于诊断,返回客户端发送到服务器的数据。
HTTP状态码
HTTP状态码是服务器发送给客户端的响应,用于告知客户端请求的结果。这些状态码是数字形式的,表明请求是成功还是失败,以及是否存在错误或重定向。它们被分为五个主要类别,每个类别都有特定的数字范围,为请求处理过程中发生的情况提供清晰且详细的信息。
下面是对每个类别以及一些最常用状态码的更深入分析。
1xx:信息性响应:1xx系列的状态码表明服务器已经收到了请求,客户端需要等待更多信息。这些响应在日常实践中很少使用,但在某些场景中可能很重要。
2xx:成功:2xx系列的状态码表明请求成功。这些是我们大多数情况下希望看到的状态码,因为它们表明客户端和服务器之间的交互如预期般顺利进行。
3xx:重定向:3xx系列的代码表明客户端需要采取一些额外的行动来完成请求,通常是一个重定向到另一个URL。
4xx:客户端错误:4xx系列的代码表明客户端发出的请求中存在错误。这些错误可能是由于数据格式不正确、缺少认证,或者是试图访问不存在的资源引起的。
5xx:服务器错误:5xx系列的代码表明服务器在尝试处理请求时发生了内部错误。这些错误通常是后端问题,例如内部服务故障、配置错误或系统过载。
构建URL
URL(统一资源定位符)是我们识别和访问网络上资源的方式。它由几个元素组成,这些元素提供了诸如服务器位置、可请求的资源以及可选的附加参数等基本信息,这些附加参数可用于过滤或定制响应。
下面,我们将详细说明URL的每个组成部分以及查询参数如何用于在HTTP请求中传递附加信息。
URL结构
典型的URL遵循以下格式:
protocol://domain:port/path?query=params#fragment
每个部分都有其特定的作用:
- 协议:表明将用于通信的协议,例如http或https。示例:https://。
- 域名:托管资源的服务器名称。它可以是一个域名(例如example.com)或一个IP地址(例如192.168.1.1)。
- 端口:一个可选的数字,指定用于通信的服务器端口。如果省略,浏览器将使用默认端口,例如,http的80端口和https的443端口。示例:8080。
- 路径:指定服务器上的资源或路由。它可以代表页面、API端点或文件。示例:/api/v1/users。
- 查询参数:用于向服务器传递附加信息。它们跟随问号(?)并且由键值对组成。多个参数由&分隔。示例:?name=John&age=30。
- 碎片:指示资源的特定部分,例如HTML页面内的锚点。跟随井号字符(#)。示例: #section2 。通常,碎片既没用处也不会用于API消费数据。这是因为碎片仅在客户端处理,也就是说,由浏览器或正在消费网页的应用程序的界面处理。服务器不会接收URL的碎片,因此它不能用于发送到服务器的HTTP请求,例如在消费API时。因此,我们的库将不支持碎片。
完整示例
让我们分析以下URL:
https://www.exemplo.com:8080/market/symbols?active=EURUSD&timeframe=h1
此处我们有:
- 协议:https
- 域名:www.example.com
- 端口:8080
- 路径:/market/symbols
- 查询参数:active=EURUSD&timeframe=h1
亲身实战:开始构建Connexus库
为了开始构建Connexus库,我们将专注于负责构建和操作URL以及查询参数的类。我们将构建一个模块,从而动态且程序化地创建URL和添加查询参数。
类结构
我们将首先创建一个CURL类,它将负责构建和操作URL。它将允许用户轻松添加查询参数,构建基础URL,并高效处理URL的不同元素。为了以一种有组织且高效的方式管理URL的查询参数,我们将使用一个名为CJson的类。这个类的目标是将查询参数(通常作为字符串在URL中传递)转换成一个结构化且易于管理的格式:JSON。
什么是JSON?
在深入了解CJson类的功能之前,如果您还不熟悉,那么了解JSON(JavaScript对象表示法)格式是很重要的。JSON是一种在网络上用于表示结构化数据的非常常见的数据格式。它由键值对组成,其中每个键都有一个关联的值。这些键值对用逗号分隔,并用大括号“{}”分组。
JSON对象的示例:
{ "name": "John", "age": 30, "city": "New York" }
此处,我们有一个包含三个键值对“name”、“age”和“city”的JSON对象,以及它们各自的值。在URL的查询参数的情况下,每个参数的工作方式类似:有一个键(参数名称)和一个值(与该键关联的值)。
CJson类的用途
CJson类将用于将URL查询参数组织成JSON格式。这使得在将它们包含在最终URL中之前,更容易操作、读取,甚至验证这些参数。与其处理像?name=John&age=30这样的参数字符串,不如处理一个结构化的对象,使代码更干净、更易于理解。CJson类也将有助于发送和接收数据,这一点将在后续文章中看到。
创建第一个类
我们首先在Metaeditor的includes中创建一个名为Connexus的文件夹。在Connexus文件夹内再创建另一个名为URL的文件夹和另一个名为Data的文件夹,在URL文件夹内创建一个名为URL和QueryParam的文件,我也会将CJson类附加到本文中,该类应添加到Data文件夹中。我不会过多涉及这个类的实现细节,但是请相信我,该类很容易使用。其结构应该看起来如下:
MQL5 |--- include |--- |--- Connexus |--- |--- |--- Data |--- |--- |--- |--- Json.mqh |--- |--- |--- URL |--- |--- |--- |--- QueryParam.mqh |--- |--- |--- |--- URL.mqh
查询参数
让我们先从CQueryParam类开始着手。该类将负责添加、删除、搜索和序列化查询参数,同时提供诸如清理数据和解析查询字符串等辅助方法。我们首先创建这个类,并在其中包含一个私有的CJson类型对象,用于以键值对的形式存储查询参数。
//+------------------------------------------------------------------+ //| class : CQueryParam | //| | //| [PROPERTY] | //| Name : CQueryParam | //| Heritage : No heritage | //| Description : Manages query parameters for HTTP requests | //| | //+------------------------------------------------------------------+ class CQueryParam { private: CJson m_query_param; // Storage for query parameters public: CQueryParam(void); ~CQueryParam(void); //--- Functions to manage query parameters void AddParam(string key, string value); // Add a key-value pair void AddParam(string param); // Add a single key-value parameter void AddParams(const string ¶ms[]); // Add multiple parameters void RemoveParam(const string key); // Remove a parameter by key string GetParam(const string key) const; // Retrieve a parameter value by key bool HasParam(const string key); // Check if parameter exists int ParamSize(void); // Get the number of parameters //--- Auxiliary methods bool Parse(const string query_param); // Parse a query string string Serialize(void); // Serialize parameters into a query string void Clear(void); // Clear all parameters };
现在,让我们探索主要的方法,并了解每种方法是如何为类的功能做出贡献的。
- AddParam(string key, string value):此方法负责向查询参数列表中添加一个新的参数。它接收键和值作为参数,并将它们存储于m_query_param对象中。
- AddParam(string param):此方法添加一个已经以key=value格式化的参数。它会检查字符串中是否有=字符,如果存在,则将字符串拆分为两个值,一个作为键,一个作为值,并将它们存储起来。
- AddParams(const string ¶ms[]):此方法一次性添加多个参数。它接收一个以key=value格式的字符串数组,并为数组中的每个项目调用AddParam方法。
- RemoveParam(const string key):此方法从查询参数列表中移除一个参数。它定位到该键并从m_query_param对象中将其移除。- GetParam(const string key):此方法返回特定参数的值,使用键作为输入。
- HasParam(const string key):此方法检查是否已经添加了给定的参数。
- ParamSize(void):此方法返回存储的查询参数的数量。
- Parse(const string query_param):Parse()方法接收一个查询参数字符串,并将其转换为键值对,存储在m_query_param对象中。它通过&字符(用于分隔参数)和=字符(用于分隔键和值)来拆分字符串。
- Serialize(void):Serialize()方法生成一个包含所有存储的查询参数的格式化字符串。它将参数以key=value格式连接起来,并用&分隔每对参数。
- Clear(void):Clear()方法清除所有存储的参数,重置该对象。
以下是带有已实现函数的代码,记得添加CJSON的导入语句:
//+------------------------------------------------------------------+ //| Include the file CJson class | //+------------------------------------------------------------------+ #include "../Data/Json.mqh" //+------------------------------------------------------------------+ //| class : CQueryParam | //| | //| [PROPERTY] | //| Name : CQueryParam | //| Heritage : No heritage | //| Description : Manages query parameters for HTTP requests | //| | //+------------------------------------------------------------------+ class CQueryParam { private: CJson m_query_param; // Storage for query parameters public: CQueryParam(void); ~CQueryParam(void); //--- Functions to manage query parameters void AddParam(string key, string value); // Add a key-value pair void AddParam(string param); // Add a single key-value parameter void AddParams(const string ¶ms[]); // Add multiple parameters void RemoveParam(const string key); // Remove a parameter by key string GetParam(const string key) const; // Retrieve a parameter value by key bool HasParam(const string key); // Check if parameter exists int ParamSize(void); // Get the number of parameters //--- Auxiliary methods bool Parse(const string query_param); // Parse a query string string Serialize(void); // Serialize parameters into a query string void Clear(void); // Clear all parameters }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CQueryParam::CQueryParam(void) { } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CQueryParam::~CQueryParam(void) { } //+------------------------------------------------------------------+ //| Adds a key-value pair to the query parameters | //+------------------------------------------------------------------+ void CQueryParam::AddParam(string key, string value) { m_query_param[key] = value; } //+------------------------------------------------------------------+ //| Adds a single parameter from a formatted string | //+------------------------------------------------------------------+ void CQueryParam::AddParam(string param) { //--- Check if the input string contains an "=" symbol, which indicates a key-value pair if(StringFind(param,"=") >= 0) { //--- Declare an array to hold the key and value after splitting the string string key_value[]; //--- Split the input string using "=" as the delimiter and store the result in the key_value array int size = StringSplit(param,StringGetCharacter("=",0),key_value); //--- If the size of the split result is exactly 2 (meaning a valid key-value pair was found) if(size == 2) { // Add the key-value pair to the m_query_param map // key_value[0] is the key, key_value[1] is the value m_query_param[key_value[0]] = key_value[1]; } } } //+------------------------------------------------------------------+ //| Adds multiple parameters from an array of formatted strings | //+------------------------------------------------------------------+ void CQueryParam::AddParams(const string ¶ms[]) { //--- Get the size of the input array 'params' int size = ArraySize(params); //--- Loop through each element in the 'params' array. for(int i=0;i<size;i++) { //--- Call the AddParam function to add each parameter to the m_query_param map. this.AddParam(params[i]); } } //+------------------------------------------------------------------+ //| Removes a parameter by key | //+------------------------------------------------------------------+ void CQueryParam::RemoveParam(const string key) { m_query_param.Remove(key); } //+------------------------------------------------------------------+ //| Retrieves a parameter value by key | //+------------------------------------------------------------------+ string CQueryParam::GetParam(const string key) const { return(m_query_param[key].ToString()); } //+------------------------------------------------------------------+ //| Checks if a parameter exists by key | //+------------------------------------------------------------------+ bool CQueryParam::HasParam(const string key) { return(m_query_param.FindKey(key) != NULL); } //+------------------------------------------------------------------+ //| Returns the number of parameters stored | //+------------------------------------------------------------------+ int CQueryParam::ParamSize(void) { return(m_query_param.Size()); } //+------------------------------------------------------------------+ //| Parses a query string into parameters | //| Input: query_param - A string formatted as a query parameter | //| Output: bool - Always returns true, indicating successful parsing| //+------------------------------------------------------------------+ bool CQueryParam::Parse(const string query_param) { //--- Split the input string by '&', separating the individual parameters string params[]; int size = StringSplit(query_param, StringGetCharacter("&",0), params); //--- Iterate through each parameter string for(int i=0; i<size; i++) { //--- Split each parameter string by '=', separating the key and value string key_value[]; StringSplit(params[i], StringGetCharacter("=",0), key_value); //--- Check if the split resulted in exactly two parts: key and value if (ArraySize(key_value) == 2) { //--- Assign the value to the corresponding key in the map m_query_param[key_value[0]] = key_value[1]; } } //--- Return true indicating that parsing was successful return(true); } //+------------------------------------------------------------------+ //| Serializes the stored parameters into a query string | //| Output: string - A string representing the serialized parameters | //+------------------------------------------------------------------+ string CQueryParam::Serialize(void) { //--- Initialize an empty string to build the query parameter string string query_param = ""; //--- Iterate over each key-value pair in the parameter map for(int i=0; i<m_query_param.Size(); i++) { //--- Append a '?' at the beginning to indicate the start of parameters if(i == 0) { query_param = "?"; } //--- Construct each key-value pair as 'key=value' if(i == m_query_param.Size()-1) { //--- If it's the last pair, don't append '&' query_param += m_query_param[i].m_key + "=" + m_query_param[i].ToString(); } else { //--- Otherwise, append '&' after each pair query_param += m_query_param[i].m_key + "=" + m_query_param[i].ToString() + "&"; } } //--- Return the constructed query parameter string return(query_param); } //+------------------------------------------------------------------+ //| Clears all stored parameters | //+------------------------------------------------------------------+ void CQueryParam::Clear(void) { m_query_param.Clear(); } //+------------------------------------------------------------------+
URL
既然我们已经有了一个负责处理查询参数的类,那么就来着手处理将使用协议、主机、端口等用于完成其余工作的CURL类。以下是MQL5中CURL类的初步实现,记得导入CQueryParam类:
//+------------------------------------------------------------------+ //| URL.mqh | //| Copyright 2023, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2023, MetaQuotes Ltd." #property link "https://www.mql5.com" #include "QueryParam.mqh" class CURL { public: CURL(void); ~CURL(void); }; CURL::CURL(void) { } CURL::~CURL(void) { } //+------------------------------------------------------------------+
让我们创建一个枚举(ENUM),其中包含最流行的协议。
//+------------------------------------------------------------------+ //| Enum to represent different URL protocol | //+------------------------------------------------------------------+ enum ENUM_URL_PROTOCOL { URL_PROTOCOL_NULL = 0, // No protocol defined URL_PROTOCOL_HTTP, // HTTP protocol URL_PROTOCOL_HTTPS, // HTTPS protocol URL_PROTOCOL_WS, // WebSocket (WS) protocol URL_PROTOCOL_WSS, // Secure WebSocket (WSS) protocol URL_PROTOCOL_FTP // FTP protocol };
在类的私有字段中,我们将添加一个结构体,用于构成URL的基本元素,以及一个名为m_url的该结构体的实例。
private: //--- Structure to hold components of a URL struct MqlURL { ENUM_URL_PROTOCOL protocol; // URL protocol string host; // Host name or IP uint port; // Port number string path; // Path after the host CQueryParam query_param; // Query parameters as key-value pairs }; MqlURL m_url; // Instance of MqlURL to store the URL details
我们创建了设置器(setters)和获取器(getters),具体实现如下:
//+------------------------------------------------------------------+ //| class : CURL | //| | //| [PROPERTY] | //| Name : CURL | //| Heritage : No heritage | //| Description : Define a class CURL to manage and manipulate URLs | //| | //+------------------------------------------------------------------+ class CURL { public: CURL(void); ~CURL(void); //--- Methods to access and modify URL components ENUM_URL_PROTOCOL Protocol(void) const; // Get the protocol void Protocol(ENUM_URL_PROTOCOL protocol); // Set the protocol string Host(void) const; // Get the host void Host(const string host); // Set the host uint Port(void) const; // Get the port void Port(const uint port); // Set the port string Path(void) const; // Get the path void Path(const string path); // Set the path CQueryParam *QueryParam(void); // Access query parameters }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CURL::CURL(void) { this.Clear(); } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CURL::~CURL(void) { } //+------------------------------------------------------------------+ //| Getter for protocol | //+------------------------------------------------------------------+ ENUM_URL_PROTOCOL CURL::Protocol(void) const { return(m_url.protocol); } //+------------------------------------------------------------------+ //| Setter for protocol | //+------------------------------------------------------------------+ void CURL::Protocol(ENUM_URL_PROTOCOL protocol) { m_url.protocol = protocol; } //+------------------------------------------------------------------+ //| Getter for host | //+------------------------------------------------------------------+ string CURL::Host(void) const { return(m_url.host); } //+------------------------------------------------------------------+ //| Setter for host | //+------------------------------------------------------------------+ void CURL::Host(const string host) { m_url.host = host; } //+------------------------------------------------------------------+ //| Getter for port | //+------------------------------------------------------------------+ uint CURL::Port(void) const { return(m_url.port); } //+------------------------------------------------------------------+ //| Setter for port | //+------------------------------------------------------------------+ void CURL::Port(const uint port) { m_url.port = port; } //+------------------------------------------------------------------+ //| Getter for path | //+------------------------------------------------------------------+ string CURL::Path(void) const { return(m_url.path); } //+------------------------------------------------------------------+ //| Setter for path | //+------------------------------------------------------------------+ void CURL::Path(const string path) { m_url.path = path; } //+------------------------------------------------------------------+ //| Accessor for query parameters (returns a pointer) | //+------------------------------------------------------------------+ CQueryParam *CURL::QueryParam(void) { return(GetPointer(m_url.query_param)); } //+------------------------------------------------------------------+
现在我们将着手为类打造核心功能,添加用于处理这些数据的新函数,它们分别是:
-
Clear(void):Clear()方法负责清除类中存储的所有数据,将其属性重置为空或默认值。该方法在您想要重用类实例来构建一个新的URL时,或者需要确保在新的操作中不会意外包含旧数据时非常有用。换句话说,其“重置”了类,移除了所有之前存储的信息。
其工作原理:
- 根据数据类型,将类的属性设置为空或null(例如,协议、域名等设置为空字符串)。
- 移除所有查询参数,并将路径重置为默认值。
- 调用Clear()之后,类实例将处于初始状态,就像其刚被创建一样。
例如:
如果之前存储过类:
- 协议:https
- 域名:api.example.com
- 路径:/v1/users
- 查询参数:id=123&active=true
在调用Clear()之后,将所有这些值重置为:
- 协议:""(空字符串)
- 域名:""(空字符串)
- 路径:""(空字符串)
- 查询参数:""(空字符串)
这使得类准备好从头开始构建一个新的URL。
-
BaseUrl(void):该方法负责生成并返回URL的基础部分,它由协议(例如http、https)、域名(例如www.example.com)以及可选的端口(例如:8080)组成。该方法确保用于与服务器通信的基本要素是正确的。该方法让您可以灵活地构建动态URL,始终从基础部分开始。当您想要重用URL的基础部分来访问同一服务器上的不同资源时,它就会非常有用。
-
PathAndQuery(void):该方法负责生成资源的路径部分,并将您之前添加的查询参数进行拼接。路径通常指定您想要在服务器上访问的资源,而查询参数允许您提供附加情况,例如过滤条件或分页信息。通过将路径和查询参数与基础URL分开,您可以以更有序的方式构建URL的不同部分。该方法返回一个可以直接用于HTTP请求或其他需要这种结构的方法的字符串。
-
FullUrl(void):这是“编译”URL所有部分并返回完整的、可以直接使用的URL的方法。它将BaseURL()和PathAndQuery()结合起来,形成您可以直接在HTTP请求中使用的最终URL。如果您需要完整的URL来发送HTTP请求,这就是最简单的方式,可以确保URL格式正确。其能够预防诸如忘记将基础部分和查询参数拼接起来的错误。
示例:如果类存储了以下值:
- 协议:https
- 域名:api.example.com
- 路径:/v1/users
- 查询参数:id=123&active=true
当调用Serialize()时,该函数将返回:
https://api.exemplo.com/v1/users?id=123&active=true
-
Parse(const string url):与FullUrl(void)的功能相反。它接收一个完整的URL作为参数,并将其组件以有组织的方式分开。其目标是将一个URL分解为更小的部分(协议、域名、端口、路径、查询参数等),以便程序员可以单独操作这些要素。如果您接收到一个URL并需要了解其细节或通过编程方式进行修改,这样会非常有用。
其工作原理:
- 接收一个包含完整URL的字符串。
- 分析(或“解析”)该字符串,识别URL的每一部分:协议(http、https)、域名、端口(如果有)、路径以及任何查询参数。
- 将这些值分配给类的内部属性,例如protocol、host、path、queryParams。- 正确处理诸如://、/、?、&等分隔符,以便将URL拆分为各个部分。
示例: 给定以下URL:
https://api.example.com:8080/v1/users?id=123&active=true
当调用Parse()时,该函数将分配以下值:
- 协议:https
- 域名:api.example.com
- 端口:8080
- 路径:/v1/users
- 查询参数:id=123,active=true
这使得您可以通过编程方式访问URL的每个部分,从而更容易对其进行操作或解析。
-
UrlProtocolToStr(ENUM_URL_PROTOCOL protocol):将协议以字符串形式返回,这在将ENUM_URL_PROTOCOL转换为简单字符串时显得非常有用,例如:
- URL_PROTOCOL_HTTP → “http”
- URL_PROTOCOL_HTTPS → “httpS”
- URL_PROTOCOL_WSS → “wss”
- 等等…
这些方法在构建和操作URL方面都发挥着重要的作用。凭借这些功能,Connexus库变得高度灵活,能够满足API的动态需求,无论是从头开始创建URL还是解析现有的URL。通过实现这些方法,开发人员可以以编程方式构建URL,避免错误并优化与服务器的通信。以下是包含已实现函数的代码:
//+------------------------------------------------------------------+ //| Define constants for different URL protocols | //+------------------------------------------------------------------+ #define HTTP "http" #define HTTPS "https" #define WS "ws" #define WSS "wss" #define FTP "ftp" //+------------------------------------------------------------------+ //| class : CURL | //| | //| [PROPERTY] | //| Name : CURL | //| Heritage : No heritage | //| Description : Define a class CURL to manage and manipulate URLs | //| | //+------------------------------------------------------------------+ class CURL { private: string UrlProtocolToStr(ENUM_URL_PROTOCOL protocol); // Helper method to convert protocol enum to string public: //--- Methods to parse and serialize the URL void Clear(void); // Clear/reset the URL string BaseUrl(void); // Return the base URL (protocol, host, port) string PathAndQuery(void); // Return the path and query part of the URL string FullUrl(void); // Return the complete URL bool Parse(const string url); // Parse a URL string into components }; //+------------------------------------------------------------------+ //| Convert URL protocol enum to string | //+------------------------------------------------------------------+ string CURL::UrlProtocolToStr(ENUM_URL_PROTOCOL protocol) { if(protocol == URL_PROTOCOL_HTTP) { return(HTTP); } if(protocol == URL_PROTOCOL_HTTPS) { return(HTTPS); } if(protocol == URL_PROTOCOL_WS) { return(WS); } if(protocol == URL_PROTOCOL_WSS) { return(WSS); } if(protocol == URL_PROTOCOL_FTP) { return(FTP); } return(NULL); } //+------------------------------------------------------------------+ //| Clear or reset the URL structure | //+------------------------------------------------------------------+ void CURL::Clear(void) { m_url.protocol = URL_PROTOCOL_NULL; m_url.host = ""; m_url.port = 0; m_url.path = ""; m_url.query_param.Clear(); } //+------------------------------------------------------------------+ //| Construct the base URL from protocol, host, and port | //+------------------------------------------------------------------+ string CURL::BaseUrl(void) { //--- Checks if host is not null or empty if(m_url.host != "" && m_url.host != NULL) { MqlURL url = m_url; //--- Set default protocol if not defined if(url.protocol == URL_PROTOCOL_NULL) { url.protocol = URL_PROTOCOL_HTTPS; } //--- Set default port based on the protocol if(url.port == 0) { url.port = (url.protocol == URL_PROTOCOL_HTTPS) ? 443 : 80; } //--- Construct base URL (protocol + host) string serialized_url = this.UrlProtocolToStr(url.protocol) + "://" + url.host; //--- Include port in URL only if it's not the default port for the protocol if(!(url.protocol == URL_PROTOCOL_HTTP && url.port == 80) && !(url.protocol == URL_PROTOCOL_HTTPS && url.port == 443)) { serialized_url += ":" + IntegerToString(m_url.port); } return(serialized_url); } else { return("Error: Invalid host"); } } //+------------------------------------------------------------------+ //| Construct path and query string from URL components | //+------------------------------------------------------------------+ string CURL::PathAndQuery(void) { MqlURL url = m_url; //--- Ensure path starts with a "/" if(url.path == "") { url.path = "/"; } else if(StringGetCharacter(url.path,0) != '/') { url.path = "/" + url.path; } //--- Remove any double slashes from the path StringReplace(url.path,"//","/"); //--- Check for invalid spaces in the path if(StringFind(url.path," ") >= 0) { return("Error: Invalid characters in path"); } //--- Return the full path and query string return(url.path + url.query_param.Serialize()); } //+------------------------------------------------------------------+ //| Return the complete URL (base URL + path + query) | //+------------------------------------------------------------------+ string CURL::FullUrl(void) { return(this.BaseUrl() + this.PathAndQuery()); } //+------------------------------------------------------------------+ //| Parse a URL string and extract its components | //+------------------------------------------------------------------+ bool CURL::Parse(const string url) { //--- Create an instance of MqlURL to hold the parsed data MqlURL urlObj; //--- Parse protocol from the URL int index_end_protocol = 0; //--- Check if the URL starts with "http://" if(StringFind(url,"http://") >= 0) { urlObj.protocol = URL_PROTOCOL_HTTP; index_end_protocol = 7; } else if(StringFind(url,"https://") >= 0) { urlObj.protocol = URL_PROTOCOL_HTTPS; index_end_protocol = 8; } else if(StringFind(url,"ws://") >= 0) { urlObj.protocol = URL_PROTOCOL_WS; index_end_protocol = 5; } else if(StringFind(url,"wss://") >= 0) { urlObj.protocol = URL_PROTOCOL_WSS; index_end_protocol = 6; } else if(StringFind(url,"ftp://") >= 0) { urlObj.protocol = URL_PROTOCOL_FTP; index_end_protocol = 6; } else { return(false); // Unsupported protocol } //--- Separate the endpoint part after the protocol string endpoint = StringSubstr(url,index_end_protocol); // Get the URL part after the protocol string parts[]; // Array to hold the split components of the URL //--- Split the endpoint by the "/" character to separate path and query components int size = StringSplit(endpoint,StringGetCharacter("/",0),parts); //--- Handle the host and port part of the URL string host_port[]; //--- If the first part (host) contains a colon (":"), split it into host and port if(StringSplit(parts[0],StringGetCharacter(":",0),host_port) > 1) { urlObj.host = host_port[0]; // Set the host urlObj.port = (uint)StringToInteger(host_port[1]); // Convert and set the port } else { urlObj.host = parts[0]; //--- Set default port based on the protocol if(urlObj.protocol == URL_PROTOCOL_HTTP) { urlObj.port = 80; } if(urlObj.protocol == URL_PROTOCOL_HTTPS) { urlObj.port = 443; } } //--- If there's no path, default to "/" if(size == 1) { urlObj.path += "/"; // Add a default root path "/" } //--- Loop through the remaining parts of the URL (after the host) for(int i=1;i<size;i++) { //--- If the path contains an empty part, return false (invalid URL) if(parts[i] == "") { return(false); } //--- If the part contains a "?" (indicating query parameters) else if(StringFind(parts[i],"?") >= 0) { string resource_query[]; //--- Split the part by "?" to separate the resource and query if(StringSplit(parts[i],StringGetCharacter("?",0),resource_query) > 0) { urlObj.path += "/"+resource_query[0]; urlObj.query_param.Parse(resource_query[1]); } } else { //--- Otherwise, add to the path as part of the URL urlObj.path += "/"+parts[i]; } } //--- Assign the parsed URL object to the member variable m_url = urlObj; return(true); } //+------------------------------------------------------------------+
最后,我们将再添加两个新函数来帮助开发人员,它们分别是:
- ShowData(void):分别打印URL的各个要素,帮助我们进行调试并了解类中存储了哪些数据。例如:
https://api.exemplo.com/v1/users?id=123&active=true
函数应返回以下内容:
Protocol: https Host: api.exemplo.com Port: 443 Path: /v1/users Query Param: { "id":123, "active":true }
- Compare(CURL &url):该函数接收另一个CURL类的实例。如果两个实例中存储的URL相同,则应返回true;否则返回false。可以用它来避免比较序列化后的URL,从而节省时间。不使用Compare()函数的示例
// Example without using the Compare() method CURl url1; CURl url2; if(url1.FullUrl() == url2.FullUrl()) { Print("Equals URL"); } // Example with method Compare() CURl url1; CURl url2; if(url1.Compare(url2)) { Print("Equals URL"); }
以下是实现这些函数的代码:
//+------------------------------------------------------------------+ //| class : CURL | //| | //| [PROPERTY] | //| Name : CURL | //| Heritage : No heritage | //| Description : Define a class CURL to manage and manipulate URLs | //| | //+------------------------------------------------------------------+ class CURL { public: //--- Auxiliary methods bool Compare(CURL &url); // Compare two URLs string ShowData(); // Show URL details as a string }; //+------------------------------------------------------------------+ //| Compare the current URL with another URL | //+------------------------------------------------------------------+ bool CURL::Compare(CURL &url) { return (m_url.protocol == url.Protocol() && m_url.host == url.Host() && m_url.port == url.Port() && m_url.path == url.Path() && m_url.query_param.Serialize() == url.QueryParam().Serialize()); } //+------------------------------------------------------------------+ //| Display the components of the URL as a formatted string | //+------------------------------------------------------------------+ string CURL::ShowData(void) { return( "Protocol: "+EnumToString(m_url.protocol)+"\n"+ "Host: "+m_url.host+"\n"+ "Port: "+IntegerToString(m_url.port)+"\n"+ "Path: "+m_url.path+"\n"+ "Query Param: "+m_url.query_param.Serialize()+"\n" ); } //+------------------------------------------------------------------+
我们已经完成了用于处理URL的两个类,接下来要进行测试。
测试
现在初始类已经准备好了,让我们通过这些类来创建URL,同时也进行反向操作,即通过类将URL的要素拆分出来。为了进行测试,我将创建一个名为TestUrl.mq5的文件,路径为Experts/Connexus/TestUrl.mq5。
int OnInit() { //--- Creating URL CURL url; url.Host("example.com"); url.Path("/api/v1/data"); Print("Test1 | # ",url.FullUrl() == "https://example.com/api/v1/data"); //--- Changing parts of the URL url.Host("api.example.com"); Print("Test2 | # ",url.FullUrl() == "https://api.example.com/api/v1/data"); //--- Parse URL url.Clear(); string url_str = "https://api.example.com/api/v1/data"; Print("Test3 | # ",url.Parse(url_str)); Print("Test3 | - Protocol # ",url.Protocol() == URL_PROTOCOL_HTTPS); Print("Test3 | - Host # ",url.Host() == "api.example.com"); Print("Test3 | - Port # ",url.Port() == 443); Print("Test3 | - Path # ",url.Path() == "/api/v1/data"); //--- return(INIT_SUCCEEDED); }
运行EA时,终端中包含以下数据:
Test1 | # true Test2 | # true Test3 | # true Test3 | - Protocol # true Test3 | - Host # true Test3 | - Port # true Test3 | - Path # true
结论
在本文中,我们深入探讨了HTTP协议的工作原理,从基本概念(如HTTP动词GET、POST、PUT、DELETE)到帮助我们解读请求返回结果的响应状态码。为了便于在您在MQL5应用程序中管理URL,我们构建了CQueryParam类,它提供了一种简单高效的方式来操作查询参数。此外,我们还实现了CURL类,它允许动态修改URL的各个部分,使得创建和处理HTTP请求的过程更加灵活和健壮。
凭借这些资源,您已经为应用程序与外部API的集成打下了良好的基础,从而方便代码与网络服务器之间的通信。然而,我们才刚刚开始。在下一篇文章中,我们将继续深入HTTP世界,构建专门用于处理请求的头部和正文的类,从而在与HTTP交互时拥有更多的控制权。
请关注后续文章,因为我们正在构建一个重要的库,它将把您的API集成技能提升到一个新的水平!
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/15897
注意: 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.


有趣的东西,感谢作者。
一小勺酸味。在我看来,CURL 类的名称非常不幸。最好用 CiURL 这样的名字。因为可能会与CURL 混淆。