feat(httpd): add support for asynchronous request handling

This commit adds support for handling multiple requests simultaneously by introducing two new functions: `httpd_req_async_handler_begin()` and `httpd_req_async_handler_complete()`. These functions allow creating an asynchronous copy of a request that can be used on a separate thread and marking the asynchronous request as completed, respectively.

Additionally, a new flag `for_async_req` has been added to the `httpd_sess_t` struct to indicate if a socket is being used for an asynchronous request and should not be purged from the LRU cache.

An example have been added to demonstrate the usage of these new functions.

Closes https://github.com/espressif/esp-idf/issues/10594

Signed-off-by: Harshit Malpani <harshit.malpani@espressif.com>
This commit is contained in:
Chip Weinberger 2023-04-13 17:05:10 -07:00 committed by Harshit Malpani
parent 1d25057a24
commit 3824eba04d
No known key found for this signature in database
GPG Key ID: FF1193D150EF75C3
11 changed files with 523 additions and 4 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,2 @@
CONFIG_EXAMPLE_MAX_ASYNC_REQUESTS=2
CONFIG_EXAMPLE_WIFI_SSID_PWD_FROM_STDIN=y

View File

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