async_http_request.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. /*
  2. * SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
  3. *
  4. * SPDX-License-Identifier: CC0-1.0
  5. *
  6. * ASIO HTTP request example
  7. */
  8. #include <string>
  9. #include <array>
  10. #include <asio.hpp>
  11. #include <memory>
  12. #include <system_error>
  13. #include <utility>
  14. #include "esp_log.h"
  15. #include "nvs_flash.h"
  16. #include "esp_event.h"
  17. #include "protocol_examples_common.h"
  18. constexpr auto TAG = "async_request";
  19. using asio::ip::tcp;
  20. namespace {
  21. void esp_init()
  22. {
  23. ESP_ERROR_CHECK(nvs_flash_init());
  24. ESP_ERROR_CHECK(esp_netif_init());
  25. ESP_ERROR_CHECK(esp_event_loop_create_default());
  26. esp_log_level_set("async_request", ESP_LOG_DEBUG);
  27. /* This helper function configures Wi-Fi or Ethernet, as selected in menuconfig.
  28. * Read "Establishing Wi-Fi or Ethernet Connection" section in
  29. * examples/protocols/README.md for more information about this function.
  30. */
  31. ESP_ERROR_CHECK(example_connect());
  32. }
  33. /**
  34. * @brief Simple class to add the resolver to a chain of actions
  35. *
  36. */
  37. class AddressResolution : public std::enable_shared_from_this<AddressResolution> {
  38. public:
  39. explicit AddressResolution(asio::io_context &context) : ctx(context), resolver(ctx) {}
  40. /**
  41. * @brief Initiator function for the address resolution
  42. *
  43. * @tparam CompletionToken callable responsible to use the results.
  44. *
  45. * @param host Host address
  46. * @param port Port for the target, must be number due to a limitation on lwip.
  47. */
  48. template<class CompletionToken>
  49. void resolve(const std::string &host, const std::string &port, CompletionToken &&completion_handler)
  50. {
  51. auto self(shared_from_this());
  52. resolver.async_resolve(host, port, [self, completion_handler](const asio::error_code & error, tcp::resolver::results_type results) {
  53. if (error) {
  54. ESP_LOGE(TAG, "Failed to resolve: %s", error.message().c_str());
  55. return;
  56. }
  57. completion_handler(self, results);
  58. });
  59. }
  60. private:
  61. asio::io_context &ctx;
  62. tcp::resolver resolver;
  63. };
  64. /**
  65. * @brief Connection class
  66. *
  67. * The lowest level dependency on our asynchronous task, Connection provide an interface to TCP sockets.
  68. * A similar class could be provided for a TLS connection.
  69. *
  70. * @note: All read and write operations are written on an explicit strand, even though an implicit strand
  71. * occurs in this example since we run the io context in a single task.
  72. *
  73. */
  74. class Connection : public std::enable_shared_from_this<Connection> {
  75. public:
  76. explicit Connection(asio::io_context &context) : ctx(context), strand(context), socket(ctx) {}
  77. /**
  78. * @brief Start the connection
  79. *
  80. * Async operation to start a connection. As the final act of the process the Connection class pass a
  81. * std::shared_ptr of itself to the completion_handler.
  82. * Since it uses std::shared_ptr as an automatic control of its lifetime this class must be created
  83. * through a std::make_shared call.
  84. *
  85. * @tparam completion_handler A callable to act as the final handler for the process.
  86. * @param host host address
  87. * @param port port number - due to a limitation on lwip implementation this should be the number not the
  88. * service name typically seen in ASIO examples.
  89. *
  90. * @note The class could be modified to store the completion handler, as a member variable, instead of
  91. * pass it along asynchronous calls to allow the process to run again completely.
  92. *
  93. */
  94. template<class CompletionToken>
  95. void start(tcp::resolver::results_type results, CompletionToken &&completion_handler)
  96. {
  97. connect(results, completion_handler);
  98. }
  99. /**
  100. * @brief Start an async write on the socket
  101. *
  102. * @tparam data
  103. * @tparam completion_handler A callable to act as the final handler for the process.
  104. *
  105. */
  106. template<class DataType, class CompletionToken>
  107. void write_async(const DataType &data, CompletionToken &&completion_handler)
  108. {
  109. asio::async_write(socket, data, asio::bind_executor(strand, completion_handler));
  110. }
  111. /**
  112. * @brief Start an async read on the socket
  113. *
  114. * @tparam data
  115. * @tparam completion_handler A callable to act as the final handler for the process.
  116. *
  117. */
  118. template<class DataBuffer, class CompletionToken>
  119. void read_async(DataBuffer &&in_data, CompletionToken &&completion_handler)
  120. {
  121. asio::async_read(socket, in_data, asio::bind_executor(strand, completion_handler));
  122. }
  123. private:
  124. template<class CompletionToken>
  125. void connect(tcp::resolver::results_type results, CompletionToken &&completion_handler)
  126. {
  127. auto self(shared_from_this());
  128. asio::async_connect(socket, results, [self, completion_handler](const asio::error_code & error, [[maybe_unused]] const tcp::endpoint & endpoint) {
  129. if (error) {
  130. ESP_LOGE(TAG, "Failed to connect: %s", error.message().c_str());
  131. return;
  132. }
  133. completion_handler(self);
  134. });
  135. }
  136. asio::io_context &ctx;
  137. asio::io_context::strand strand;
  138. tcp::socket socket;
  139. };
  140. } // namespace
  141. namespace Http {
  142. enum class Method { GET };
  143. /**
  144. * @brief Simple HTTP request class
  145. *
  146. * The user needs to write the request information direct to header and body fields.
  147. *
  148. * Only GET verb is provided.
  149. *
  150. */
  151. class Request {
  152. public:
  153. Request(Method method, std::string host, std::string port, const std::string &target) : host_data(std::move(host)), port_data(std::move(port))
  154. {
  155. header_data.append("GET ");
  156. header_data.append(target);
  157. header_data.append(" HTTP/1.1");
  158. header_data.append("\r\n");
  159. header_data.append("Host: ");
  160. header_data.append(host_data);
  161. header_data.append("\r\n");
  162. header_data.append("\r\n");
  163. };
  164. void set_header_field(std::string const &field)
  165. {
  166. header_data.append(field);
  167. }
  168. void append_to_body(std::string const &data)
  169. {
  170. body_data.append(data);
  171. };
  172. const std::string &host() const
  173. {
  174. return host_data;
  175. }
  176. const std::string &service_port() const
  177. {
  178. return port_data;
  179. }
  180. const std::string &header() const
  181. {
  182. return header_data;
  183. }
  184. const std::string &body() const
  185. {
  186. return body_data;
  187. }
  188. private:
  189. std::string host_data;
  190. std::string port_data;
  191. std::string header_data;
  192. std::string body_data;
  193. };
  194. /**
  195. * @brief Simple HTTP response class
  196. *
  197. * The response is built from received data and only parsed to split header and body.
  198. *
  199. * A copy of the received data is kept.
  200. *
  201. */
  202. struct Response {
  203. /**
  204. * @brief Construct a response from a contiguous buffer.
  205. *
  206. * Simple http parsing.
  207. *
  208. */
  209. template<class DataIt>
  210. explicit Response(DataIt data, size_t size)
  211. {
  212. raw_response = std::string(data, size);
  213. auto header_last = raw_response.find("\r\n\r\n");
  214. if (header_last != std::string::npos) {
  215. header = raw_response.substr(0, header_last);
  216. }
  217. body = raw_response.substr(header_last + 3);
  218. }
  219. /**
  220. * @brief Print response content.
  221. */
  222. void print()
  223. {
  224. ESP_LOGI(TAG, "Header :\n %s", header.c_str());
  225. ESP_LOGI(TAG, "Body : \n %s", body.c_str());
  226. }
  227. std::string raw_response;
  228. std::string header;
  229. std::string body;
  230. };
  231. /** @brief HTTP Session
  232. *
  233. * Session class to handle HTTP protocol implementation.
  234. *
  235. */
  236. class Session : public std::enable_shared_from_this<Session> {
  237. public:
  238. explicit Session(std::shared_ptr<Connection> connection_in) : connection(std::move(connection_in))
  239. {
  240. }
  241. template<class CompletionToken>
  242. void send_request(const Request &request, CompletionToken &&completion_handler)
  243. {
  244. auto self = shared_from_this();
  245. send_data = { asio::buffer(request.header()), asio::buffer(request.body()) };
  246. connection->write_async(send_data, [self, &completion_handler](std::error_code error, std::size_t bytes_transfered) {
  247. if (error) {
  248. ESP_LOGE(TAG, "Request write error: %s", error.message().c_str());
  249. return;
  250. }
  251. ESP_LOGD(TAG, "Bytes Transfered: %d", bytes_transfered);
  252. self->get_response(completion_handler);
  253. });
  254. }
  255. private:
  256. template<class CompletionToken>
  257. void get_response(CompletionToken &&completion_handler)
  258. {
  259. auto self = shared_from_this();
  260. connection->read_async(asio::buffer(receive_buffer), [self, &completion_handler](std::error_code error, std::size_t bytes_received) {
  261. if (error and error.value() != asio::error::eof) {
  262. return;
  263. }
  264. ESP_LOGD(TAG, "Bytes Received: %d", bytes_received);
  265. if (bytes_received == 0) {
  266. return;
  267. }
  268. Response response(std::begin(self->receive_buffer), bytes_received);
  269. completion_handler(self, response);
  270. });
  271. }
  272. /*
  273. * For this example we assumed 2048 to be enough for the receive_buffer
  274. */
  275. std::array<char, 2048> receive_buffer;
  276. /*
  277. * The hardcoded 2 below is related to the type we receive the data to send. We gather the parts from Request, header
  278. * and body, to send avoiding the copy.
  279. */
  280. std::array<asio::const_buffer, 2> send_data;
  281. std::shared_ptr<Connection> connection;
  282. };
  283. /** @brief Execute a fully async HTTP request
  284. *
  285. * @tparam completion_handler
  286. * @param ctx io context
  287. * @param request
  288. *
  289. * @note : We build this function as a simpler interface to compose the operations of connecting to
  290. * the address and running the HTTP session. The Http::Session class is injected to the completion handler
  291. * for further use.
  292. */
  293. template<class CompletionToken>
  294. void request_async(asio::io_context &context, const Request &request, CompletionToken &&completion_handler)
  295. {
  296. /*
  297. * The first step is to resolve the address we want to connect to.
  298. * The AddressResolution itself is injected to the completion handler.
  299. *
  300. * This shared_ptr is destroyed by the end of the scope. Pay attention that this is a non blocking function
  301. * the lifetime of the object is extended by the resolve call
  302. */
  303. std::make_shared<AddressResolution>(context)->resolve(request.host(), request.service_port(),
  304. [&context, &request, completion_handler](std::shared_ptr<AddressResolution> resolver, tcp::resolver::results_type results) {
  305. /* After resolution we create a Connection.
  306. * The completion handler gets a shared_ptr<Connection> to receive the connection, once the
  307. * connection process is complete.
  308. */
  309. std::make_shared<Connection>(context)->start(results,
  310. [&request, completion_handler](std::shared_ptr<Connection> connection) {
  311. // Now we create a HTTP::Session and inject the necessary connection.
  312. std::make_shared<Session>(connection)->send_request(request, completion_handler);
  313. });
  314. });
  315. }
  316. }// namespace Http
  317. extern "C" void app_main(void)
  318. {
  319. // Basic initialization of ESP system
  320. esp_init();
  321. asio::io_context io_context;
  322. Http::Request request(Http::Method::GET, "www.httpbin.org", "80", "/get");
  323. Http::request_async(io_context, request, [](std::shared_ptr<Http::Session> session, Http::Response response) {
  324. /*
  325. * We only print the response here but could reuse session for other requests.
  326. */
  327. response.print();
  328. });
  329. // io_context.run will block until all the tasks on the context are done.
  330. io_context.run();
  331. ESP_LOGI(TAG, "Context run done");
  332. ESP_ERROR_CHECK(example_disconnect());
  333. }