Просмотр исходного кода

Merge branch 'example/asio_async_http_request' into 'master'

Examples/Asio:  Async http request

See merge request espressif/esp-idf!15285
Rocha Euripedes 4 лет назад
Родитель
Сommit
279c8aeb8a

+ 10 - 0
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)

+ 52 - 0
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.

+ 2 - 0
examples/protocols/asio/async_request/main/CMakeLists.txt

@@ -0,0 +1,2 @@
+idf_component_register(SRCS "async_http_request.cpp"
+                    INCLUDE_DIRS ".")

+ 369 - 0
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 <string>
+#include <array>
+#include <asio.hpp>
+#include <memory>
+#include <system_error>
+#include <utility>
+#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<AddressResolution> {
+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<class CompletionToken>
+    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<Connection> {
+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<class CompletionToken>
+    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<class DataType, class CompletionToken>
+    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<class DataBuffer, class CompletionToken>
+    void read_async(DataBuffer &&in_data, CompletionToken &&completion_handler)
+    {
+        asio::async_read(socket, in_data, asio::bind_executor(strand, completion_handler));
+    }
+
+private:
+
+    template<class CompletionToken>
+    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<class DataIt>
+    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<Session> {
+public:
+    explicit Session(std::shared_ptr<Connection> connection_in) : connection(std::move(connection_in))
+    {
+    }
+
+    template<class CompletionToken>
+    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<class CompletionToken>
+    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<char, 2048> 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<asio::const_buffer, 2> send_data;
+    std::shared_ptr<Connection> 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<class CompletionToken>
+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<AddressResolution>(context)->resolve(request.host(), request.service_port(),
+    [&context, &request, completion_handler](std::shared_ptr<AddressResolution> resolver, tcp::resolver::results_type results) {
+        /* After resolution we create a Connection.
+        *  The completion handler gets a shared_ptr<Connection> to receive the connection, once the
+        *  connection process is complete.
+        */
+        std::make_shared<Connection>(context)->start(results,
+        [&request, completion_handler](std::shared_ptr<Connection> connection) {
+            // Now we create a HTTP::Session and inject the necessary connection.
+            std::make_shared<Session>(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<Http::Session> 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());
+}