Browse Source

Merge branch 'contrib/github_pr_11190' into 'master'

[HTTPD] support for multiple simultaneous requests (try 3)  (GitHub PR)

Closes IDFGH-9868 and IDFGH-9204

See merge request espressif/esp-idf!23281
Mahavir Jain 2 years ago
parent
commit
2fc770a1a3

+ 34 - 0
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
  *

+ 1 - 0
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) */

+ 7 - 4
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:

+ 45 - 0
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) {

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

+ 58 - 0
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:<ba-add>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
+```

+ 2 - 0
examples/protocols/http_server/async_handlers/main/CMakeLists.txt

@@ -0,0 +1,2 @@
+idf_component_register(SRCS "main.c"
+                    INCLUDE_DIRS ".")

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

+ 353 - 0
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 <esp_wifi.h>
+#include <esp_event.h>
+#include <esp_log.h>
+#include <esp_system.h>
+#include <nvs_flash.h>
+#include <sys/param.h>
+#include "nvs_flash.h"
+#include "esp_netif.h"
+#include "esp_eth.h"
+#include "protocol_examples_common.h"
+#include "esp_tls_crypto.h"
+#include <esp_http_server.h>
+
+/* 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, &copy);
+    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, "<div> no workers available. server busy.</div>");
+            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), "<div>req: %u</div>\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), "<div>%u</div>\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 = "<div><a href=\"/long\">long</a></div>"
+        "<div><a href=\"/quick\">quick</a></div>";
+    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();
+}

+ 2 - 0
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

+ 1 - 0
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