
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

