
HTTP и Connexus (Часть 2): Понимание архитектуры HTTP и дизайна библиотеки
Введение
Настоящая статья является продолжением серии статей, в которых мы будем создавать библиотеку под названием Connexus. В первой статье мы разобрались с основами работы функции WebRequest, разобрались с каждым из ее параметров, а также создали пример кода, который демонстрирует использование данной функции и ее трудности. В настоящей статье мы продолжим разбираться, чтобы понимать несколько больше о протоколе HTTP, о том, как работает 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 не имеет тела.
- Когда использовать?
- Удаление определенных ресурсов сервера, например, удаление данных из базы данных.
- PATCH: Метод PATCH используется для частичного обновления ресурса. В отличие от PUT, где надо отправить полное представление ресурса, PATCH позволяет изменять только те поля, которые нуждаются в обновлении.
- HEAD: Аналогично GET, но без тела ответа. Используется для проверки информации о ресурсе.
- OPTIONS: Используется для описания параметров взаимодействия с сервером, включая поддерживаемые методы.
- TRACE: Используется для диагностики, возвращает то, что было отправлено клиентом на сервер.
Коды состояния HTTP
Коды состояния HTTP - это ответы, которые сервер отправляет клиенту, чтобы проинформировать его о результатах запроса. Эти коды являются числовыми и указывают на то, был ли запрос выполнен успешно или неудачно, а также на ошибки или перенаправления. Они разделены на пять основных категорий, каждая из которых имеет определенный диапазон чисел, предоставляющих четкую и подробную информацию о том, что произошло во время обработки запроса.
Вот более подробный обзор каждой категории и некоторых наиболее часто используемых кодов.
1xx: Информативные ответы: Коды состояния в серии 1xx указывают на то, что сервер получил запрос и что клиенту следует дождаться получения дополнительной информации. Эти ответы редко используются в повседневной практике, но могут быть важны в определенных сценариях.
2xx: Успешно: Коды состояния в серии **2xx** указывают на то, что запрос был **выполнен успешно**. Это те коды, которые мы хотим видеть чаще всего, поскольку они указывают на то, что взаимодействие между клиентом и сервером прошло так, как ожидалось.
3xx: Переадресации: Коды в серии 3xx указывают на то, что клиенту необходимо выполнить некоторые дополнительные действия для выполнения запроса, обычно это перенаправление на другой URL.
4xx: Ошибки клиента: Коды серии 4xx указывают на то, что в запросе, сделанном клиентом, была допущена ошибка. Эти ошибки могут быть вызваны неправильным форматом данных, отсутствием аутентификации или попытками доступа к несуществующим ресурсам.
5xx: Ошибки сервера: Коды в серии 5xx указывают на то, что при попытке обработать запрос на сервере произошла внутренняя ошибка. Эти ошибки обычно являются внутренними проблемами, такими как сбои внутренних служб, ошибки конфигурации или перегрузка системы.
Создание URL-адреса
URL-адрес (единый указатель ресурсов (Uniform Resource Locator)) - это способ идентификации ресурсов в Интернете и доступа к ним. Он состоит из нескольких элементов, предоставляющих важную информацию, такую как местоположение сервера, запрашиваемый ресурс и, при необходимости, дополнительные параметры, которые можно использовать для фильтрации или настройки ответов.
Ниже мы подробно рассмотрим каждый компонент URL-адреса и то, как параметры запроса используются для передачи дополнительной информации в HTTP-запросе.
Структура URL
Типичный URL-адрес соответствует такому формату:
protocol://domain:port/path?query=params#fragment
Каждая часть играет определенную роль:
- Протокол: Указывает, какой протокол будет использоваться для связи, например http или https. Пример: https:// .
- Домен: Имя сервера, на котором размещен ресурс. Это может быть доменное имя (например, example.com ) или IP-адрес (например, 192.168.1.1 ).
- Порт: Опциональный номер, указывающий порт сервера, который следует использовать для связи. Если этот параметр не указан, браузер использует порты по умолчанию, такие как 80 для http и 443 для https. Пример: :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 Object Notation), если вы еще с ним не знакомы. JSON - это очень распространенный формат данных, используемый в Интернете для представления структурированных данных. Он состоит из пар ключей, где каждый ключ имеет соответствующее значение. Эти пары разделяются запятыми и группируются в фигурные скобки “{}”
Пример объекта JSON:
{ "name": "John", "age": 30, "city": "New York" }
Здесь у нас есть объект JSON, который содержит три пары ключей “имя”, “возраст” и “город” с соответствующими значениями. В случае параметров запроса URL-адреса каждый параметр работает аналогичным образом: есть ключ (имя параметра) и значение (значение, связанное с этим ключом).
Назначение класса CJson
Класс CJson будет использоваться для организации параметров URL-запроса в формате JSON. Это упрощает манипулирование, чтение и даже проверку этих параметров перед включением их в окончательный URL-адрес. Вместо того чтобы иметь дело со строкой параметров типа ?name=John&age=30, вы можете иметь дело со структурированным объектом, что делает код более чистым и понятным. Класс Json также будет полезен для отправки и получения данных, о чем мы поговорим в следующей статье.
Создание первых классов
Мы начинаем с создания папки под названием Connexus, включенную в Metaeditor. Внутри папки Connexus создаём еще одну папку с именем URL и другую с именем Data. Создаем файл с именем URL и QueryParam внутри папки URL, и я также оставлю прикрепленным к статье класс CJson, который должен быть добавлен в папку Data. Я не буду вдаваться в подробности о реализации этого класса, но он прост в использовании, поверьте мне. Структура должна выглядеть примерно так
MQL5 |--- include |--- |--- Connexus |--- |--- |--- Data |--- |--- |--- |--- Json.mqh |--- |--- |--- URL |--- |--- |--- |--- QueryParam.mqh |--- |--- |--- |--- URL.mqh
QueryParam
Давайте начнем с работы с классом 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[]): Данный метод добавляет сразу множество параметров. Он получает массив строк в формате ключ=значение и вызывает метод 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() генерирует отформатированную строку, содержащую все сохраненные параметры запроса. Он объединяет параметры в формате ключ=значение и разделяет каждую пару символом & .
- 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, который сделает все остальное, используя протокол, хост, порт и т.д. Вот начальная реализация класса CURL в MQL5, не забудьте импортировать класс 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 };
В поле private класса добавим структуру, формирующую базовые элементы 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
Мы создаем методы-установщики и методы-получатели, а также их реализации
//+------------------------------------------------------------------+ //| 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-адреса или когда надо убедиться, что старые данные случайно не будут включены в новую операцию. Другими словами, он "сбрасывает" класс, удаляя всю ранее сохраненную информацию.
Как это работает:
- Устанавливает для атрибутов класса значение empty или 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() для формирования окончательного URL-адреса, который можно использовать непосредственно в HTTP-запросе. Если вам нужен полный 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
Функция должна возвращать следующее:
Протокол: https Хост: api.exemplo.com Порт: 443 Путь: /v1/users Query Param: { "id":123, "active":true }
- Compare(CURL &url): Эта функция получает другой экземпляр класса CURL. Она должна возвращать значение true, если URL-адреса, сохраненные в обоих экземплярах, совпадают, в противном случае она должна возвращать значение 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); }
При запуске советника у нас в терминале отображаются следующие данные:
Test1 | # true Test2 | # true Test3 | # true Test3 | - Protocol # true Test3 | - Host # true Test3 | - Port # true Test3 | - Path # true
Заключение
В настоящей статье мы подробно рассмотрели, как работает протокол HTTP, начиная с базовых понятий, таких как HTTP-глаголы (GET, POST, PUT, DELETE), и заканчивая кодами статуса ответа, которые помогают нам интерпретировать возвращаемые запросы. С целью облегчить управление URL-адресами в ваших MQL5-приложениях, мы создали класс `CQueryParam`, предлагающий простой и эффективный способ манипулирования параметрами запросов. Кроме того, мы реализовали класс `CURL`, который позволяет динамически изменять части URL-адреса, делая процесс создания и обработки HTTP-запросов более гибким и надежным.
Имея в своем распоряжении эти ресурсы, вы уже имеете хорошую основу для интеграции ваших приложений с внешними API, облегчая взаимодействие между вашим кодом и веб-серверами. Однако мы только начинаем. В следующей статье мы продолжим наше путешествие в мир HTTP, где создадим специальные классы для работы с **заголовками** и **телом** запросов, что позволит еще больше контролировать HTTP-взаимодействия.
Следите за новостями в следующих публикациях, поскольку мы создаем незаменимую библиотеку, которая поднимет ваши навыки интеграции API на новый уровень!
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/15897





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Интересный материал, автору спасибо.
Небольшая ложка дёгтя. Имхо, выбрано крайне неудачное название класса CURL. Лучше бы взять что-то вроде CiURL. Ибо может возникнуть путаница с CURL.