EXAMPLE/ASIO Async HTTP request

Introduces a new example on ASIO to ilustrates on how to compose
async operation to build network related protocols.
pull/8117/head
Euripedes Rocha 2021-09-20 15:25:02 -03:00 zatwierdzone przez bot
rodzic 07bfc09d0c
commit 2433816b14
4 zmienionych plików z 433 dodań i 0 usunięć

Wyświetl plik

@ -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)

Wyświetl plik

@ -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.

Wyświetl plik

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

Wyświetl plik

@ -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());
}