diff --git a/components/esp_http_server/include/esp_http_server.h b/components/esp_http_server/include/esp_http_server.h index 39c2a82a31..a96256fa63 100644 --- a/components/esp_http_server/include/esp_http_server.h +++ b/components/esp_http_server/include/esp_http_server.h @@ -812,6 +812,40 @@ esp_err_t httpd_sess_set_send_override(httpd_handle_t hd, int sockfd, httpd_send */ esp_err_t httpd_sess_set_pending_override(httpd_handle_t hd, int sockfd, httpd_pending_func_t pending_func); +/** + * @brief Start an asynchronous request. This function can be called + * in a request handler to get a request copy that can be used on a async thread. + * + * @note + * - This function is necessary in order to handle multiple requests simultaneously. + * See examples/async_requests for example usage. + * - You must call httpd_req_async_handler_complete() when you are done with the request. + * + * @param[in] r The request to create an async copy of + * @param[out] out A newly allocated request which can be used on an async thread + * + * @return + * - ESP_OK : async request object created + */ +esp_err_t httpd_req_async_handler_begin(httpd_req_t *r, httpd_req_t **out); + +/** + * @brief Mark an asynchronous request as completed. This will + * - free the request memory + * - relinquish ownership of the underlying socket, so it can be reused. + * - allow the http server to close our socket if needed (lru_purge_enable) + * + * @note If async requests are not marked completed, eventually the server + * will no longer accept incoming connections. The server will log a + * "httpd_accept_conn: error in accept (23)" message if this happens. + * + * @param[in] r The request to mark async work as completed + * + * @return + * - ESP_OK : async request was marked completed + */ +esp_err_t httpd_req_async_handler_complete(httpd_req_t *r); + /** * @brief Get the Socket Descriptor from the HTTP request * diff --git a/components/esp_http_server/src/esp_httpd_priv.h b/components/esp_http_server/src/esp_httpd_priv.h index b84905eefe..386339e02c 100644 --- a/components/esp_http_server/src/esp_httpd_priv.h +++ b/components/esp_http_server/src/esp_httpd_priv.h @@ -72,6 +72,7 @@ struct sock_db { bool lru_socket; /*!< Flag indicating LRU socket */ char pending_data[PARSER_BLOCK_SIZE]; /*!< Buffer for pending data to be received */ size_t pending_len; /*!< Length of pending data to be received */ + bool for_async_req; /*!< If true, the socket will not be LRU purged */ #ifdef CONFIG_HTTPD_WS_SUPPORT bool ws_handshake_done; /*!< True if it has done WebSocket handshake (if this socket is a valid WS) */ bool ws_close; /*!< Set to true to close the socket later (when WS Close frame received) */ diff --git a/components/esp_http_server/src/httpd_sess.c b/components/esp_http_server/src/httpd_sess.c index e7ee862e7b..a10c62fb1a 100644 --- a/components/esp_http_server/src/httpd_sess.c +++ b/components/esp_http_server/src/httpd_sess.c @@ -107,10 +107,13 @@ static int enum_function(struct sock_db *session, void *context) if (session->fd == -1) { return 0; } - // Check/update lowest lru - if (session->lru_counter < ctx->lru_counter) { - ctx->lru_counter = session->lru_counter; - ctx->session = session; + // Only close sockets that are not in use + if (session->for_async_req == false) { + // Check/update lowest lru + if (session->lru_counter < ctx->lru_counter) { + ctx->lru_counter = session->lru_counter; + ctx->session = session; + } } break; case HTTPD_TASK_CLOSE: diff --git a/components/esp_http_server/src/httpd_txrx.c b/components/esp_http_server/src/httpd_txrx.c index b7e6ee6f78..a3c24a10f6 100644 --- a/components/esp_http_server/src/httpd_txrx.c +++ b/components/esp_http_server/src/httpd_txrx.c @@ -554,6 +554,51 @@ int httpd_req_recv(httpd_req_t *r, char *buf, size_t buf_len) return ret; } +esp_err_t httpd_req_async_handler_begin(httpd_req_t *r, httpd_req_t **out) +{ + if (r == NULL || out == NULL) { + return ESP_ERR_INVALID_ARG; + } + + // alloc async req + httpd_req_t *async = malloc(sizeof(httpd_req_t)); + if (async == NULL) { + return ESP_ERR_NO_MEM; + } + memcpy(async, r, sizeof(httpd_req_t)); + + // alloc async aux + async->aux = malloc(sizeof(struct httpd_req_aux)); + if (async->aux == NULL) { + free(async); + return ESP_ERR_NO_MEM; + } + memcpy(async->aux, r->aux, sizeof(struct httpd_req_aux)); + + // mark socket as "in use" + struct httpd_req_aux *ra = r->aux; + ra->sd->for_async_req = true; + + *out = async; + + return ESP_OK; +} + +esp_err_t httpd_req_async_handler_complete(httpd_req_t *r) +{ + if (r == NULL) { + return ESP_ERR_INVALID_ARG; + } + + struct httpd_req_aux *ra = r->aux; + ra->sd->for_async_req = false; + + free(r->aux); + free(r); + + return ESP_OK; +} + int httpd_req_to_sockfd(httpd_req_t *r) { if (r == NULL) { diff --git a/examples/protocols/http_server/async_handlers/CMakeLists.txt b/examples/protocols/http_server/async_handlers/CMakeLists.txt new file mode 100644 index 0000000000..62cb350dd4 --- /dev/null +++ b/examples/protocols/http_server/async_handlers/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.16) + +# (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(simple) diff --git a/examples/protocols/http_server/async_handlers/README.md b/examples/protocols/http_server/async_handlers/README.md new file mode 100644 index 0000000000..b2bed5a90c --- /dev/null +++ b/examples/protocols/http_server/async_handlers/README.md @@ -0,0 +1,58 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C6 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | + +# Async Requests Handlers HTTPD Server Example + +The Example demonstrates how to handle multiple long running simultaneous requests +within the HTTPD server. It has the following URIs: + + 1. URI \long for demonstrating async requests running in the background + 2. URI \quick for demonstrating that quick requests are still responsive + 2. URI \ index page + +## How to use example + +### Hardware Required + +* An ESP-Dev-Board (e.g., ESP32-DevKitC, ESP-WROVER-KIT, etc.) +* A USB cable for power supply and programming + +### Configure the project + +``` +idf.py menuconfig +``` +* Open the project configuration menu (`idf.py menuconfig`) to configure Wi-Fi or Ethernet. See "Establishing Wi-Fi or Ethernet Connection" section in [examples/protocols/README.md](../../README.md) for more details. + +### Build and Flash + +Build the project and flash it to the board, then run monitor tool to view serial output: + +``` +idf.py -p PORT flash monitor +``` + +(Replace PORT with the name of the serial port to use.) + +(To exit the serial monitor, type ``Ctrl-]``.) + +See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects. + +## Example Output + +``` +I (14257) example_common: Connected to example_netif_sta +I (14267) example_common: - IPv4 address: 192.168.20.85, +I (14277) example_common: - IPv6 address: fe80:0000:0000:0000:7edf:a1ff:fea4:3454, type: ESP_IP6_ADDR_IS_LINK_LOCAL +I (14287) example: starting async req task worker +I (14287) example: starting async req task worker +I (14297) example: Starting server on port: '80' +I (14307) example: Registering URI handlers +I (14307) main_task: Returned from app_main() +I (15547) wifi:idx:1 (ifx:0, 68:d7:9a:81:26:1e), tid:0, ssn:0, winSize:64 +I (19627) example: uri: / +I (25877) example: uri: /quick +I (33247) example: uri: /long +I (33247) example: invoking /long +I (33247) example: uri: /long +``` diff --git a/examples/protocols/http_server/async_handlers/main/CMakeLists.txt b/examples/protocols/http_server/async_handlers/main/CMakeLists.txt new file mode 100644 index 0000000000..cf2c455cb5 --- /dev/null +++ b/examples/protocols/http_server/async_handlers/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "main.c" + INCLUDE_DIRS ".") diff --git a/examples/protocols/http_server/async_handlers/main/Kconfig.projbuild b/examples/protocols/http_server/async_handlers/main/Kconfig.projbuild new file mode 100644 index 0000000000..263ec76bff --- /dev/null +++ b/examples/protocols/http_server/async_handlers/main/Kconfig.projbuild @@ -0,0 +1,10 @@ +menu "Example Configuration" + + config EXAMPLE_MAX_ASYNC_REQUESTS + int "Max Simultaneous Requests" + default 2 + help + The maximum number of simultaneous async requests that the + web server can handle. + +endmenu diff --git a/examples/protocols/http_server/async_handlers/main/main.c b/examples/protocols/http_server/async_handlers/main/main.c new file mode 100644 index 0000000000..96fb13e671 --- /dev/null +++ b/examples/protocols/http_server/async_handlers/main/main.c @@ -0,0 +1,353 @@ +/* Async Request Handlers HTTP Server Example + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include +#include +#include +#include +#include +#include +#include "nvs_flash.h" +#include "esp_netif.h" +#include "esp_eth.h" +#include "protocol_examples_common.h" +#include "esp_tls_crypto.h" +#include + +/* An example that demonstrates multiple + long running http requests running in parallel. + + In this example, multiple long http request can run at + the same time. (uri: /long) + + While these long requests are running, the server can still + respond to other incoming synchronous requests. (uri: /quick) + */ + +#define ASYNC_WORKER_TASK_PRIORITY 5 +#define ASYNC_WORKER_TASK_STACK_SIZE 2048 + +static const char *TAG = "example"; + +// Async reqeusts are queued here while they wait to +// be processed by the workers +static QueueHandle_t async_req_queue; + +// Track the number of free workers at any given time +static SemaphoreHandle_t worker_ready_count; + +// Each worker has its own thread +static TaskHandle_t worker_handles[CONFIG_EXAMPLE_MAX_ASYNC_REQUESTS]; + +typedef esp_err_t (*httpd_req_handler_t)(httpd_req_t *req); + +typedef struct { + httpd_req_t* req; + httpd_req_handler_t handler; +} httpd_async_req_t; + + +static bool is_on_async_worker_thread(void) +{ + // is our handle one of the known async handles? + TaskHandle_t handle = xTaskGetCurrentTaskHandle(); + for (int i = 0; i < CONFIG_EXAMPLE_MAX_ASYNC_REQUESTS; i++) { + if (worker_handles[i] == handle) { + return true; + } + } + return false; +} + + +// Submit an HTTP req to the async worker queue +static esp_err_t submit_async_req(httpd_req_t *req, httpd_req_handler_t handler) +{ + // must create a copy of the request that we own + httpd_req_t* copy = NULL; + esp_err_t err = httpd_req_async_handler_begin(req, ©); + if (err != ESP_OK) { + return err; + } + + httpd_async_req_t async_req = { + .req = copy, + .handler = handler, + }; + + // How should we handle resource exhaustion? + // In this example, we immediately respond with an + // http error if no workers are available. + int ticks = 0; + + // counting semaphore: if success, we know 1 or + // more asyncReqTaskWorkers are available. + if (xSemaphoreTake(worker_ready_count, ticks) == false) { + ESP_LOGE(TAG, "No workers are available"); + httpd_req_async_handler_complete(copy); // cleanup + return ESP_FAIL; + } + + // Since worker_ready_count > 0 the queue should already have space. + // But lets wait up to 100ms just to be safe. + if (xQueueSend(async_req_queue, &async_req, pdMS_TO_TICKS(100)) == false) { + ESP_LOGE(TAG, "worker queue is full"); + httpd_req_async_handler_complete(copy); // cleanup + return ESP_FAIL; + } + + return ESP_OK; +} + + +/* A long running HTTP GET handler */ +static esp_err_t long_async_handler(httpd_req_t *req) +{ + ESP_LOGI(TAG, "uri: /long"); + // This handler is first invoked on the httpd thread. + // In order to free the httpd thread to handle other requests, + // we must resubmit our request to be handled on an async worker thread. + if (is_on_async_worker_thread() == false) { + + // submit + if (submit_async_req(req, long_async_handler) == ESP_OK) { + return ESP_OK; + } else { + httpd_resp_set_status(req, "503 Busy"); + httpd_resp_sendstr(req, "
no workers available. server busy.
"); + return ESP_OK; + } + } + + // track the number of long requests + static uint8_t req_count = 0; + req_count++; + + // send a request count + char s[100]; + snprintf(s, sizeof(s), "
req: %u
\n", req_count); + httpd_resp_sendstr_chunk(req, s); + + // then every second, send a "tick" + for (int i = 0; i < 60; i++) { + + // This delay makes this a "long running task". + // In a real application, this may be a long calculation, + // or some IO dependent code for instance. + vTaskDelay(pdMS_TO_TICKS(1000)); + + // send a tick + snprintf(s, sizeof(s), "
%u
\n", i); + httpd_resp_sendstr_chunk(req, s); + } + + // send "complete" + httpd_resp_sendstr_chunk(req, NULL); + + return ESP_OK; +} + +static void async_req_worker_task(void *p) +{ + ESP_LOGI(TAG, "starting async req task worker"); + + while (true) { + + // counting semaphore - this signals that a worker + // is ready to accept work + xSemaphoreGive(worker_ready_count); + + // wait for a request + httpd_async_req_t async_req; + if (xQueueReceive(async_req_queue, &async_req, portMAX_DELAY)) { + + ESP_LOGI(TAG, "invoking %s", async_req.req->uri); + + // call the handler + async_req.handler(async_req.req); + + // Inform the server that it can purge the socket used for + // this request, if needed. + if (httpd_req_async_handler_complete(async_req.req) != ESP_OK) { + ESP_LOGE(TAG, "failed to complete async req"); + } + } + } + + ESP_LOGW(TAG, "worker stopped"); + vTaskDelete(NULL); +} + +static void start_async_req_workers(void) +{ + + // counting semaphore keeps track of available workers + worker_ready_count = xSemaphoreCreateCounting( + CONFIG_EXAMPLE_MAX_ASYNC_REQUESTS, // Max Count + 0); // Initial Count + if (worker_ready_count == NULL) { + ESP_LOGE(TAG, "Failed to create workers counting Semaphore"); + return; + } + + // create queue + async_req_queue = xQueueCreate(1, sizeof(httpd_async_req_t)); + if (async_req_queue == NULL){ + ESP_LOGE(TAG, "Failed to create async_req_queue"); + vSemaphoreDelete(worker_ready_count); + return; + } + + // start worker tasks + for (int i = 0; i < CONFIG_EXAMPLE_MAX_ASYNC_REQUESTS; i++) { + + bool success = xTaskCreate(async_req_worker_task, "async_req_worker", + ASYNC_WORKER_TASK_STACK_SIZE, // stack size + (void *)0, // argument + ASYNC_WORKER_TASK_PRIORITY, // priority + &worker_handles[i]); + + if (!success) { + ESP_LOGE(TAG, "Failed to start asyncReqWorker"); + continue; + } + } +} + + +/* A quick HTTP GET handler, which does not + use any asynchronous features */ +static esp_err_t quick_handler(httpd_req_t *req) +{ + ESP_LOGI(TAG, "uri: /quick"); + char s[100]; + snprintf(s, sizeof(s), "random: %u\n", rand()); + httpd_resp_sendstr(req, s); + return ESP_OK; +} + +static esp_err_t index_handler(httpd_req_t *req) +{ + ESP_LOGI(TAG, "uri: /"); + const char* html = "" + ""; + httpd_resp_sendstr(req, html); + return ESP_OK; +} + +static httpd_handle_t start_webserver(void) +{ + httpd_handle_t server = NULL; + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.lru_purge_enable = true; + + // It is advisable that httpd_config_t->max_open_sockets > MAX_ASYNC_REQUESTS + // Why? This leaves at least one socket still available to handle + // quick synchronous requests. Otherwise, all the sockets will + // get taken by the long async handlers, and your server will no + // longer be responsive. + config.max_open_sockets = CONFIG_EXAMPLE_MAX_ASYNC_REQUESTS + 1; + + // Start the httpd server + ESP_LOGI(TAG, "Starting server on port: '%d'", config.server_port); + if (httpd_start(&server, &config) != ESP_OK) { + ESP_LOGI(TAG, "Error starting server!"); + return NULL; + } + + const httpd_uri_t index_uri = { + .uri = "/", + .method = HTTP_GET, + .handler = index_handler, + }; + + const httpd_uri_t long_uri = { + .uri = "/long", + .method = HTTP_GET, + .handler = long_async_handler, + }; + + const httpd_uri_t quick_uri = { + .uri = "/quick", + .method = HTTP_GET, + .handler = quick_handler, + }; + + // Set URI handlers + ESP_LOGI(TAG, "Registering URI handlers"); + httpd_register_uri_handler(server, &index_uri); + httpd_register_uri_handler(server, &long_uri); + httpd_register_uri_handler(server, &quick_uri); + + return server; +} + +static esp_err_t stop_webserver(httpd_handle_t server) +{ + // Stop the httpd server + return httpd_stop(server); +} + +static void disconnect_handler(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) +{ + httpd_handle_t* server = (httpd_handle_t*) arg; + if (*server) { + ESP_LOGI(TAG, "Stopping webserver"); + if (stop_webserver(*server) == ESP_OK) { + *server = NULL; + } else { + ESP_LOGE(TAG, "Failed to stop http server"); + } + } +} + +static void connect_handler(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) +{ + httpd_handle_t* server = (httpd_handle_t*) arg; + if (*server == NULL) { + ESP_LOGI(TAG, "Starting webserver"); + *server = start_webserver(); + } +} + +void app_main(void) +{ + static httpd_handle_t server = NULL; + + ESP_ERROR_CHECK(nvs_flash_init()); + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + /* 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()); + + /* Register event handlers to stop the server when Wi-Fi or Ethernet is disconnected, + * and re-start it upon connection. + */ +#ifdef CONFIG_EXAMPLE_CONNECT_WIFI + ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &connect_handler, &server)); + ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &disconnect_handler, &server)); +#endif // CONFIG_EXAMPLE_CONNECT_WIFI +#ifdef CONFIG_EXAMPLE_CONNECT_ETHERNET + ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &connect_handler, &server)); + ESP_ERROR_CHECK(esp_event_handler_register(ETH_EVENT, ETHERNET_EVENT_DISCONNECTED, &disconnect_handler, &server)); +#endif // CONFIG_EXAMPLE_CONNECT_ETHERNET + + // start workers + start_async_req_workers(); + + /* Start the server for the first time */ + server = start_webserver(); +} diff --git a/examples/protocols/http_server/async_handlers/sdkconfig.ci b/examples/protocols/http_server/async_handlers/sdkconfig.ci new file mode 100644 index 0000000000..5d82a55bd8 --- /dev/null +++ b/examples/protocols/http_server/async_handlers/sdkconfig.ci @@ -0,0 +1,2 @@ +CONFIG_EXAMPLE_MAX_ASYNC_REQUESTS=2 +CONFIG_EXAMPLE_WIFI_SSID_PWD_FROM_STDIN=y diff --git a/tools/ci/check_copyright_ignore.txt b/tools/ci/check_copyright_ignore.txt index 930414d325..3efe7da1cf 100644 --- a/tools/ci/check_copyright_ignore.txt +++ b/tools/ci/check_copyright_ignore.txt @@ -1421,6 +1421,7 @@ examples/protocols/http_server/advanced_tests/http_server_advanced_test.py examples/protocols/http_server/advanced_tests/main/include/tests.h examples/protocols/http_server/advanced_tests/main/main.c examples/protocols/http_server/advanced_tests/main/tests.c +examples/protocols/http_server/async_handlers/main/main.c examples/protocols/http_server/captive_portal/example_test.py examples/protocols/http_server/captive_portal/main/dns_server.c examples/protocols/http_server/captive_portal/main/include/dns_server.h