From 2433816b14374dffd5c4a2b5a4234841fdcec3b9 Mon Sep 17 00:00:00 2001 From: Euripedes Rocha Date: Mon, 20 Sep 2021 15:25:02 -0300 Subject: [PATCH] EXAMPLE/ASIO Async HTTP request Introduces a new example on ASIO to ilustrates on how to compose async operation to build network related protocols. --- .../asio/async_request/CMakeLists.txt | 10 + .../protocols/asio/async_request/README.md | 52 +++ .../asio/async_request/main/CMakeLists.txt | 2 + .../async_request/main/async_http_request.cpp | 369 ++++++++++++++++++ 4 files changed, 433 insertions(+) create mode 100644 examples/protocols/asio/async_request/CMakeLists.txt create mode 100644 examples/protocols/asio/async_request/README.md create mode 100644 examples/protocols/asio/async_request/main/CMakeLists.txt create mode 100644 examples/protocols/asio/async_request/main/async_http_request.cpp diff --git a/examples/protocols/asio/async_request/CMakeLists.txt b/examples/protocols/asio/async_request/CMakeLists.txt new file mode 100644 index 0000000000..fc7e3c944d --- /dev/null +++ b/examples/protocols/asio/async_request/CMakeLists.txt @@ -0,0 +1,10 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) + +# (Not part of the boilerplate) +# This example uses an extra component for common functions such as Wi-Fi and Ethernet connection. +set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/protocol_examples_common) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(async_http_request) diff --git a/examples/protocols/asio/async_request/README.md b/examples/protocols/asio/async_request/README.md new file mode 100644 index 0000000000..c2b114511c --- /dev/null +++ b/examples/protocols/asio/async_request/README.md @@ -0,0 +1,52 @@ +| Supported Targets | ESP32 | +| ----------------- | ----- | + +# Async request using ASIO + +(See the README.md file in the upper level 'examples' directory for more information about examples.) + +The application aims to show how to compose async operations using ASIO to build network protocols and operations. + +# Configure and Building example + +This example doesn't require any configuration, just build it with + +``` +idf.py build +``` + +# Async operations composition and automatic lifetime control + +On this example we compose the operation by starting the next step in the chain inside the completion handler of the +previous operation. Also we pass the `Connection` class itself as the parameter of its final handler to be owned by +the following operation. This is possible due to the control of lifetime by the usage of `std::shared_ptr`. + +The control of lifetime of the class, done by `std::shared_ptr` usage, guarantee that the data will be available for +async operations until it's not needed any more. This makes necessary that all of the async operation class must start +its lifetime as a `std::shared_ptr` due to the usage of `std::enable_shared_from_this`. + + + User creates a shared_ptr──┐ + of AddressResolution and │ + ask for resolve. │ + The handler for the ┌▼─────────────────────┐ + complete operation is sent│ AddressResolution │ In the completion of resolve a connection is created. + └─────────────────┬────┘ AddressResolution is automaticly destroyed since it's + │ no longer needed + ┌─▼────────────────────────────────────┐ + │ Connection │ + └──────┬───────────────────────────────┘ + Http::Session is created once we have a Connection. │ + Connection is passed to Http::Session that holds it │ + avoiding it's destruction. │ + ┌─▼───────────────────────────────┐ + │ Http::Session │ + └────────┬────────────────────────┘ + After the HTTP request is │ + sent the completion handler │ + is called. │ + └────►Completion Handler() + + +The previous diagram shows the process and the life span of each of the tasks in this examples. At each stage the +object responsible for the last action inject itself to the completion handler of the next stage for reuse. diff --git a/examples/protocols/asio/async_request/main/CMakeLists.txt b/examples/protocols/asio/async_request/main/CMakeLists.txt new file mode 100644 index 0000000000..018c22a0af --- /dev/null +++ b/examples/protocols/asio/async_request/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "async_http_request.cpp" + INCLUDE_DIRS ".") diff --git a/examples/protocols/asio/async_request/main/async_http_request.cpp b/examples/protocols/asio/async_request/main/async_http_request.cpp new file mode 100644 index 0000000000..579a61fe28 --- /dev/null +++ b/examples/protocols/asio/async_request/main/async_http_request.cpp @@ -0,0 +1,369 @@ +/* + * SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + * + * ASIO HTTP request example +*/ + +#include +#include +#include +#include +#include +#include +#include "esp_log.h" +#include "nvs_flash.h" +#include "esp_event.h" +#include "protocol_examples_common.h" + +constexpr auto TAG = "async_request"; +using asio::ip::tcp; + +namespace { + +void esp_init() +{ + ESP_ERROR_CHECK(nvs_flash_init()); + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + esp_log_level_set("async_request", ESP_LOG_DEBUG); + + /* This helper function configures Wi-Fi or Ethernet, as selected in menuconfig. + * Read "Establishing Wi-Fi or Ethernet Connection" section in + * examples/protocols/README.md for more information about this function. + */ + ESP_ERROR_CHECK(example_connect()); +} + +/** + * @brief Simple class to add the resolver to a chain of actions + * + */ +class AddressResolution : public std::enable_shared_from_this { +public: + explicit AddressResolution(asio::io_context &context) : ctx(context), resolver(ctx) {} + + /** + * @brief Initiator function for the address resolution + * + * @tparam CompletionToken callable responsible to use the results. + * + * @param host Host address + * @param port Port for the target, must be number due to a limitation on lwip. + */ + template + void resolve(const std::string &host, const std::string &port, CompletionToken &&completion_handler) + { + auto self(shared_from_this()); + resolver.async_resolve(host, port, [self, completion_handler](const asio::error_code & error, tcp::resolver::results_type results) { + if (error) { + ESP_LOGE(TAG, "Failed to resolve: %s", error.message().c_str()); + return; + } + completion_handler(self, results); + }); + } + +private: + asio::io_context &ctx; + tcp::resolver resolver; + +}; + +/** + * @brief Connection class + * + * The lowest level dependency on our asynchronous task, Connection provide an interface to TCP sockets. + * A similar class could be provided for a TLS connection. + * + * @note: All read and write operations are written on an explicit strand, even though an implicit strand + * occurs in this example since we run the io context in a single task. + * + */ +class Connection : public std::enable_shared_from_this { +public: + explicit Connection(asio::io_context &context) : ctx(context), strand(context), socket(ctx) {} + + /** + * @brief Start the connection + * + * Async operation to start a connection. As the final act of the process the Connection class pass a + * std::shared_ptr of itself to the completion_handler. + * Since it uses std::shared_ptr as an automatic control of its lifetime this class must be created + * through a std::make_shared call. + * + * @tparam completion_handler A callable to act as the final handler for the process. + * @param host host address + * @param port port number - due to a limitation on lwip implementation this should be the number not the + * service name tipically seen in ASIO examples. + * + * @note The class could be modified to store the completion handler, as a member variable, instead of + * pass it along asynchronous calls to allow the process to run again completely. + * + */ + template + void start(tcp::resolver::results_type results, CompletionToken &&completion_handler) + { + connect(results, completion_handler); + } + + /** + * @brief Start an async write on the socket + * + * @tparam data + * @tparam completion_handler A callable to act as the final handler for the process. + * + */ + template + void write_async(const DataType &data, CompletionToken &&completion_handler) + { + asio::async_write(socket, data, asio::bind_executor(strand, completion_handler)); + } + + /** + * @brief Start an async read on the socket + * + * @tparam data + * @tparam completion_handler A callable to act as the final handler for the process. + * + */ + template + void read_async(DataBuffer &&in_data, CompletionToken &&completion_handler) + { + asio::async_read(socket, in_data, asio::bind_executor(strand, completion_handler)); + } + +private: + + template + void connect(tcp::resolver::results_type results, CompletionToken &&completion_handler) + { + auto self(shared_from_this()); + asio::async_connect(socket, results, [self, completion_handler](const asio::error_code & error, [[maybe_unused]] const tcp::endpoint & endpoint) { + if (error) { + ESP_LOGE(TAG, "Failed to connect: %s", error.message().c_str()); + return; + } + completion_handler(self); + }); + } + asio::io_context &ctx; + asio::io_context::strand strand; + tcp::socket socket; +}; + +} // namespace +namespace Http { +enum class Method { GET }; + +/** + * @brief Simple HTTP request class + * + * The user needs to write the request information direct to header and body fields. + * + * Only GET verb is provided. + * + */ +class Request { +public: + Request(Method method, std::string host, std::string port, const std::string &target) : host_data(std::move(host)), port_data(std::move(port)) + { + header_data.append("GET "); + header_data.append(target); + header_data.append(" HTTP/1.1"); + header_data.append("\r\n"); + header_data.append("Host: "); + header_data.append(host_data); + header_data.append("\r\n"); + header_data.append("\r\n"); + }; + + void set_header_field(std::string const &field) + { + header_data.append(field); + } + + void append_to_body(std::string const &data) + { + body_data.append(data); + }; + + const std::string &host() const + { + return host_data; + } + + const std::string &service_port() const + { + return port_data; + } + + const std::string &header() const + { + return header_data; + } + + const std::string &body() const + { + return body_data; + } + +private: + std::string host_data; + std::string port_data; + std::string header_data; + std::string body_data; +}; + +/** + * @brief Simple HTTP response class + * + * The response is built from received data and only parsed to split header and body. + * + * A copy of the received data is kept. + * + */ +struct Response { + /** + * @brief Construct a response from a contiguous buffer. + * + * Simple http parsing. + * + */ + template + explicit Response(DataIt data, size_t size) + { + raw_response = std::string(data, size); + + auto header_last = raw_response.find("\r\n\r\n"); + if (header_last != std::string::npos) { + header = raw_response.substr(0, header_last); + } + body = raw_response.substr(header_last + 3); + } + /** + * @brief Print response content. + */ + void print() + { + ESP_LOGI(TAG, "Header :\n %s", header.c_str()); + ESP_LOGI(TAG, "Body : \n %s", body.c_str()); + } + + std::string raw_response; + std::string header; + std::string body; +}; + +/** @brief HTTP Session + * + * Session class to handle HTTP protocol implementation. + * + */ +class Session : public std::enable_shared_from_this { +public: + explicit Session(std::shared_ptr connection_in) : connection(std::move(connection_in)) + { + } + + template + void send_request(const Request &request, CompletionToken &&completion_handler) + { + auto self = shared_from_this(); + send_data = { asio::buffer(request.header()), asio::buffer(request.body()) }; + connection->write_async(send_data, [self, &completion_handler](std::error_code error, std::size_t bytes_transfered) { + if (error) { + ESP_LOGE(TAG, "Request write error: %s", error.message().c_str()); + return; + } + ESP_LOGD(TAG, "Bytes Transfered: %d", bytes_transfered); + self->get_response(completion_handler); + }); + } + +private: + template + void get_response(CompletionToken &&completion_handler) + { + auto self = shared_from_this(); + connection->read_async(asio::buffer(receive_buffer), [self, &completion_handler](std::error_code error, std::size_t bytes_received) { + if (error and error.value() != asio::error::eof) { + return; + } + ESP_LOGD(TAG, "Bytes Received: %d", bytes_received); + if (bytes_received == 0) { + return; + } + Response response(std::begin(self->receive_buffer), bytes_received); + + completion_handler(self, response); + }); + } + /* + * For this example we assumed 2048 to be enough for the receive_buffer + */ + std::array receive_buffer; + /* + * The hardcoded 2 below is related to the type we receive the data to send. We gather the parts from Request, header + * and body, to send avoiding the copy. + */ + std::array send_data; + std::shared_ptr connection; +}; + +/** @brief Execute a fully async HTTP request + * + * @tparam completion_handler + * @param ctx io context + * @param request + * + * @note : We build this function as a simpler interface to compose the operations of connecting to + * the address and running the HTTP session. The Http::Session class is injected to the completion handler + * for further use. + */ +template +void request_async(asio::io_context &context, const Request &request, CompletionToken &&completion_handler) +{ + /* + * The first step is to resolve the address we want to connect to. + * The AddressResolution itself is injected to the completion handler. + * + * This shared_ptr is destroyed by the end of the scope. Pay attention that this is a non blocking function + * the lifetime of the object is extended by the resolve call + */ + std::make_shared(context)->resolve(request.host(), request.service_port(), + [&context, &request, completion_handler](std::shared_ptr resolver, tcp::resolver::results_type results) { + /* After resolution we create a Connection. + * The completion handler gets a shared_ptr to receive the connection, once the + * connection process is complete. + */ + std::make_shared(context)->start(results, + [&request, completion_handler](std::shared_ptr connection) { + // Now we create a HTTP::Session and inject the necessary connection. + std::make_shared(connection)->send_request(request, completion_handler); + }); + }); +} +}// namespace Http + +extern "C" void app_main(void) +{ + // Basic initialization of ESP system + esp_init(); + + asio::io_context io_context; + Http::Request request(Http::Method::GET, "www.httpbin.org", "80", "/get"); + Http::request_async(io_context, request, [](std::shared_ptr session, Http::Response response) { + /* + * We only print the response here but could reuse session for other requests. + */ + response.print(); + }); + + // io_context.run will block until all the tasks on the context are done. + io_context.run(); + ESP_LOGI(TAG, "Context run done"); + + ESP_ERROR_CHECK(example_disconnect()); +}