Aktualizace na verzi 3.4.5

This commit is contained in:
Pavel Brychta 2025-01-03 16:45:58 +01:00
parent 7c828c70d8
commit 697a3adf08
17 changed files with 893 additions and 345 deletions

View File

@ -75,13 +75,13 @@ So if you need one of these feature, you will have to stick with 3.x or another
```ini ```ini
lib_compat_mode = strict lib_compat_mode = strict
lib_ldf_mode = chain lib_ldf_mode = chain
lib_deps = mathieucarbou/ESPAsyncWebServer @ 3.3.23 lib_deps = mathieucarbou/ESPAsyncWebServer @ 3.4.5
``` ```
**Dependencies:** **Dependencies:**
- **ESP32 with AsyncTCP**: `mathieucarbou/AsyncTCP @ 3.2.14` - **ESP32 with AsyncTCP**: `mathieucarbou/AsyncTCP @ 3.3.1`
Arduino IDE: [https://github.com/mathieucarbou/AsyncTCP#v3.2.14](https://github.com/mathieucarbou/AsyncTCP/releases) Arduino IDE: [https://github.com/mathieucarbou/AsyncTCP#v3.3.1](https://github.com/mathieucarbou/AsyncTCP/releases)
- **ESP32 with AsyncTCPSock**: `https://github.com/mathieucarbou/AsyncTCPSock/archive/refs/tags/v1.0.3-dev.zip` - **ESP32 with AsyncTCPSock**: `https://github.com/mathieucarbou/AsyncTCPSock/archive/refs/tags/v1.0.3-dev.zip`
@ -99,9 +99,9 @@ AsyncTCPSock can be used instead of AsyncTCP by excluding AsyncTCP from the libr
lib_compat_mode = strict lib_compat_mode = strict
lib_ldf_mode = chain lib_ldf_mode = chain
lib_deps = lib_deps =
; mathieucarbou/AsyncTCP @ 3.2.14 ; mathieucarbou/AsyncTCP @ 3.3.1
https://github.com/mathieucarbou/AsyncTCPSock/archive/refs/tags/v1.0.3-dev.zip https://github.com/mathieucarbou/AsyncTCPSock/archive/refs/tags/v1.0.3-dev.zip
mathieucarbou/ESPAsyncWebServer @ 3.3.23 mathieucarbou/ESPAsyncWebServer @ 3.4.5
lib_ignore = lib_ignore =
AsyncTCP AsyncTCP
mathieucarbou/AsyncTCP mathieucarbou/AsyncTCP
@ -109,14 +109,14 @@ lib_ignore =
## Performance ## Performance
Performance of `mathieucarbou/ESPAsyncWebServer @ 3.3.23`: Performance of `mathieucarbou/ESPAsyncWebServer @ 3.4.5`:
```bash ```bash
> brew install autocannon > brew install autocannon
> autocannon -c 10 -w 10 -d 20 http://192.168.4.1 > autocannon -c 10 -w 10 -d 20 http://192.168.4.1
``` ```
With `mathieucarbou/AsyncTCP @ 3.2.14` With `mathieucarbou/AsyncTCP @ 3.3.1`
[![](https://mathieu.carbou.me/ESPAsyncWebServer/perf-c10.png)](https://mathieu.carbou.me/ESPAsyncWebServer/perf-c10.png) [![](https://mathieu.carbou.me/ESPAsyncWebServer/perf-c10.png)](https://mathieu.carbou.me/ESPAsyncWebServer/perf-c10.png)
@ -133,29 +133,29 @@ Test is running for 20 seconds with 10 connections.
``` ```
// With AsyncTCP, with 10 workers: no message discarded from the queue // With AsyncTCP, with 10 workers: no message discarded from the queue
// //
// Total: 1875 events, 468.75000000000000000000 events / second // Total: 2038 events, 509.50 events / second
// Total: 1870 events, 467.50000000000000000000 events / second // Total: 2120 events, 530.00 events / second
// Total: 1871 events, 467.75000000000000000000 events / second // Total: 2119 events, 529.75 events / second
// Total: 1875 events, 468.75000000000000000000 events / second // Total: 2038 events, 509.50 events / second
// Total: 1871 events, 467.75000000000000000000 events / second // Total: 2037 events, 509.25 events / second
// Total: 1805 events, 451.25000000000000000000 events / second // Total: 2119 events, 529.75 events / second
// Total: 1803 events, 450.75000000000000000000 events / second // Total: 2119 events, 529.75 events / second
// Total: 1873 events, 468.25000000000000000000 events / second // Total: 2120 events, 530.00 events / second
// Total: 1872 events, 468.00000000000000000000 events / second // Total: 2038 events, 509.50 events / second
// Total: 1805 events, 451.25000000000000000000 events / second // Total: 2038 events, 509.50 events / second
// //
// With AsyncTCPSock, with 10 workers: no message discarded from the queue // With AsyncTCPSock, with 10 workers: no message discarded from the queue
// //
// Total: 1242 events, 310.50000000000000000000 events / second // Total: 2038 events, 509.50 events / second
// Total: 1242 events, 310.50000000000000000000 events / second // Total: 2120 events, 530.00 events / second
// Total: 1242 events, 310.50000000000000000000 events / second // Total: 2119 events, 529.75 events / second
// Total: 1242 events, 310.50000000000000000000 events / second // Total: 2038 events, 509.50 events / second
// Total: 1181 events, 295.25000000000000000000 events / second // Total: 2037 events, 509.25 events / second
// Total: 1182 events, 295.50000000000000000000 events / second // Total: 2119 events, 529.75 events / second
// Total: 1240 events, 310.00000000000000000000 events / second // Total: 2119 events, 529.75 events / second
// Total: 1181 events, 295.25000000000000000000 events / second // Total: 2120 events, 530.00 events / second
// Total: 1181 events, 295.25000000000000000000 events / second // Total: 2038 events, 509.50 events / second
// Total: 1183 events, 295.75000000000000000000 events / second // Total: 2038 events, 509.50 events / second
``` ```
## Important recommendations ## Important recommendations
@ -163,27 +163,14 @@ Test is running for 20 seconds with 10 connections.
Most of the crashes are caused by improper configuration of the library for the project. Most of the crashes are caused by improper configuration of the library for the project.
Here are some recommendations to avoid them. Here are some recommendations to avoid them.
1. Set the running core to be on the same core of your application (usually core 1) `-D CONFIG_ASYNC_TCP_RUNNING_CORE=1` I personally use the following configuration in my projects:
2. Set the stack size appropriately with `-D CONFIG_ASYNC_TCP_STACK_SIZE=16384`.
The default value of `16384` might be too much for your project.
You can look at the [MycilaTaskMonitor](https://mathieu.carbou.me/MycilaTaskMonitor) project to monitor the stack usage.
3. You can change **if you know what you are doing** the task priority with `-D CONFIG_ASYNC_TCP_PRIORITY=10`.
Default is `10`.
4. You can increase the queue size with `-D CONFIG_ASYNC_TCP_QUEUE_SIZE=128`.
Default is `64`.
5. You can decrease the maximum ack time `-D CONFIG_ASYNC_TCP_MAX_ACK_TIME=3000`.
Default is `5000`.
I personally use the following configuration in my projects because my WS messages can be big (up to 4k).
If you have smaller messages, you can increase `WS_MAX_QUEUED_MESSAGES` to 128.
```c++ ```c++
-D CONFIG_ASYNC_TCP_MAX_ACK_TIME=3000 -D CONFIG_ASYNC_TCP_MAX_ACK_TIME=5000 // (keep default)
-D CONFIG_ASYNC_TCP_PRIORITY=10 -D CONFIG_ASYNC_TCP_PRIORITY=10 // (keep default)
-D CONFIG_ASYNC_TCP_QUEUE_SIZE=128 -D CONFIG_ASYNC_TCP_QUEUE_SIZE=64 // (keep default)
-D CONFIG_ASYNC_TCP_RUNNING_CORE=1 -D CONFIG_ASYNC_TCP_RUNNING_CORE=1 // force async_tcp task to be on same core as the app (default is core 0)
-D CONFIG_ASYNC_TCP_STACK_SIZE=4096 -D CONFIG_ASYNC_TCP_STACK_SIZE=4096 // reduce the stack size (default is 16K)
-D WS_MAX_QUEUED_MESSAGES=64
``` ```
## `AsyncWebSocketMessageBuffer` and `makeBuffer()` ## `AsyncWebSocketMessageBuffer` and `makeBuffer()`

View File

@ -75,13 +75,13 @@ So if you need one of these feature, you will have to stick with 3.x or another
```ini ```ini
lib_compat_mode = strict lib_compat_mode = strict
lib_ldf_mode = chain lib_ldf_mode = chain
lib_deps = mathieucarbou/ESPAsyncWebServer @ 3.3.23 lib_deps = mathieucarbou/ESPAsyncWebServer @ 3.4.5
``` ```
**Dependencies:** **Dependencies:**
- **ESP32 with AsyncTCP**: `mathieucarbou/AsyncTCP @ 3.2.14` - **ESP32 with AsyncTCP**: `mathieucarbou/AsyncTCP @ 3.3.1`
Arduino IDE: [https://github.com/mathieucarbou/AsyncTCP#v3.2.14](https://github.com/mathieucarbou/AsyncTCP/releases) Arduino IDE: [https://github.com/mathieucarbou/AsyncTCP#v3.3.1](https://github.com/mathieucarbou/AsyncTCP/releases)
- **ESP32 with AsyncTCPSock**: `https://github.com/mathieucarbou/AsyncTCPSock/archive/refs/tags/v1.0.3-dev.zip` - **ESP32 with AsyncTCPSock**: `https://github.com/mathieucarbou/AsyncTCPSock/archive/refs/tags/v1.0.3-dev.zip`
@ -99,9 +99,9 @@ AsyncTCPSock can be used instead of AsyncTCP by excluding AsyncTCP from the libr
lib_compat_mode = strict lib_compat_mode = strict
lib_ldf_mode = chain lib_ldf_mode = chain
lib_deps = lib_deps =
; mathieucarbou/AsyncTCP @ 3.2.14 ; mathieucarbou/AsyncTCP @ 3.3.1
https://github.com/mathieucarbou/AsyncTCPSock/archive/refs/tags/v1.0.3-dev.zip https://github.com/mathieucarbou/AsyncTCPSock/archive/refs/tags/v1.0.3-dev.zip
mathieucarbou/ESPAsyncWebServer @ 3.3.23 mathieucarbou/ESPAsyncWebServer @ 3.4.5
lib_ignore = lib_ignore =
AsyncTCP AsyncTCP
mathieucarbou/AsyncTCP mathieucarbou/AsyncTCP
@ -109,14 +109,14 @@ lib_ignore =
## Performance ## Performance
Performance of `mathieucarbou/ESPAsyncWebServer @ 3.3.23`: Performance of `mathieucarbou/ESPAsyncWebServer @ 3.4.5`:
```bash ```bash
> brew install autocannon > brew install autocannon
> autocannon -c 10 -w 10 -d 20 http://192.168.4.1 > autocannon -c 10 -w 10 -d 20 http://192.168.4.1
``` ```
With `mathieucarbou/AsyncTCP @ 3.2.14` With `mathieucarbou/AsyncTCP @ 3.3.1`
[![](https://mathieu.carbou.me/ESPAsyncWebServer/perf-c10.png)](https://mathieu.carbou.me/ESPAsyncWebServer/perf-c10.png) [![](https://mathieu.carbou.me/ESPAsyncWebServer/perf-c10.png)](https://mathieu.carbou.me/ESPAsyncWebServer/perf-c10.png)
@ -133,29 +133,29 @@ Test is running for 20 seconds with 10 connections.
``` ```
// With AsyncTCP, with 10 workers: no message discarded from the queue // With AsyncTCP, with 10 workers: no message discarded from the queue
// //
// Total: 1875 events, 468.75000000000000000000 events / second // Total: 2038 events, 509.50 events / second
// Total: 1870 events, 467.50000000000000000000 events / second // Total: 2120 events, 530.00 events / second
// Total: 1871 events, 467.75000000000000000000 events / second // Total: 2119 events, 529.75 events / second
// Total: 1875 events, 468.75000000000000000000 events / second // Total: 2038 events, 509.50 events / second
// Total: 1871 events, 467.75000000000000000000 events / second // Total: 2037 events, 509.25 events / second
// Total: 1805 events, 451.25000000000000000000 events / second // Total: 2119 events, 529.75 events / second
// Total: 1803 events, 450.75000000000000000000 events / second // Total: 2119 events, 529.75 events / second
// Total: 1873 events, 468.25000000000000000000 events / second // Total: 2120 events, 530.00 events / second
// Total: 1872 events, 468.00000000000000000000 events / second // Total: 2038 events, 509.50 events / second
// Total: 1805 events, 451.25000000000000000000 events / second // Total: 2038 events, 509.50 events / second
// //
// With AsyncTCPSock, with 10 workers: no message discarded from the queue // With AsyncTCPSock, with 10 workers: no message discarded from the queue
// //
// Total: 1242 events, 310.50000000000000000000 events / second // Total: 2038 events, 509.50 events / second
// Total: 1242 events, 310.50000000000000000000 events / second // Total: 2120 events, 530.00 events / second
// Total: 1242 events, 310.50000000000000000000 events / second // Total: 2119 events, 529.75 events / second
// Total: 1242 events, 310.50000000000000000000 events / second // Total: 2038 events, 509.50 events / second
// Total: 1181 events, 295.25000000000000000000 events / second // Total: 2037 events, 509.25 events / second
// Total: 1182 events, 295.50000000000000000000 events / second // Total: 2119 events, 529.75 events / second
// Total: 1240 events, 310.00000000000000000000 events / second // Total: 2119 events, 529.75 events / second
// Total: 1181 events, 295.25000000000000000000 events / second // Total: 2120 events, 530.00 events / second
// Total: 1181 events, 295.25000000000000000000 events / second // Total: 2038 events, 509.50 events / second
// Total: 1183 events, 295.75000000000000000000 events / second // Total: 2038 events, 509.50 events / second
``` ```
## Important recommendations ## Important recommendations
@ -163,27 +163,14 @@ Test is running for 20 seconds with 10 connections.
Most of the crashes are caused by improper configuration of the library for the project. Most of the crashes are caused by improper configuration of the library for the project.
Here are some recommendations to avoid them. Here are some recommendations to avoid them.
1. Set the running core to be on the same core of your application (usually core 1) `-D CONFIG_ASYNC_TCP_RUNNING_CORE=1` I personally use the following configuration in my projects:
2. Set the stack size appropriately with `-D CONFIG_ASYNC_TCP_STACK_SIZE=16384`.
The default value of `16384` might be too much for your project.
You can look at the [MycilaTaskMonitor](https://mathieu.carbou.me/MycilaTaskMonitor) project to monitor the stack usage.
3. You can change **if you know what you are doing** the task priority with `-D CONFIG_ASYNC_TCP_PRIORITY=10`.
Default is `10`.
4. You can increase the queue size with `-D CONFIG_ASYNC_TCP_QUEUE_SIZE=128`.
Default is `64`.
5. You can decrease the maximum ack time `-D CONFIG_ASYNC_TCP_MAX_ACK_TIME=3000`.
Default is `5000`.
I personally use the following configuration in my projects because my WS messages can be big (up to 4k).
If you have smaller messages, you can increase `WS_MAX_QUEUED_MESSAGES` to 128.
```c++ ```c++
-D CONFIG_ASYNC_TCP_MAX_ACK_TIME=3000 -D CONFIG_ASYNC_TCP_MAX_ACK_TIME=5000 // (keep default)
-D CONFIG_ASYNC_TCP_PRIORITY=10 -D CONFIG_ASYNC_TCP_PRIORITY=10 // (keep default)
-D CONFIG_ASYNC_TCP_QUEUE_SIZE=128 -D CONFIG_ASYNC_TCP_QUEUE_SIZE=64 // (keep default)
-D CONFIG_ASYNC_TCP_RUNNING_CORE=1 -D CONFIG_ASYNC_TCP_RUNNING_CORE=1 // force async_tcp task to be on same core as the app (default is core 0)
-D CONFIG_ASYNC_TCP_STACK_SIZE=4096 -D CONFIG_ASYNC_TCP_STACK_SIZE=4096 // reduce the stack size (default is 16K)
-D WS_MAX_QUEUED_MESSAGES=64
``` ```
## `AsyncWebSocketMessageBuffer` and `makeBuffer()` ## `AsyncWebSocketMessageBuffer` and `makeBuffer()`
@ -622,7 +609,7 @@ Endpoints which consume JSON can use a special handler to get ready to use JSON
#include "ArduinoJson.h" #include "ArduinoJson.h"
AsyncCallbackJsonWebHandler* handler = new AsyncCallbackJsonWebHandler("/rest/endpoint", [](AsyncWebServerRequest *request, JsonVariant &json) { AsyncCallbackJsonWebHandler* handler = new AsyncCallbackJsonWebHandler("/rest/endpoint", [](AsyncWebServerRequest *request, JsonVariant &json) {
JsonObject& jsonObj = json.as<JsonObject>(); JsonObject jsonObj = json.as<JsonObject>();
// ... // ...
}); });
server.addHandler(handler); server.addHandler(handler);

View File

@ -57,11 +57,21 @@ void setup() {
<script> <script>
let ws = new WebSocket("ws://" + window.location.host + "/ws"); let ws = new WebSocket("ws://" + window.location.host + "/ws");
document.addEventListener("DOMContentLoaded", function () { ws.addEventListener("open", (e) => {
ws.onopen = function () { console.log("WebSocket connected", e);
console.log("WebSocket connected"); });
};
}); ws.addEventListener("error", (e) => {
console.log("WebSocket error", e);
});
ws.addEventListener("close", (e) => {
console.log("WebSocket close", e);
});
ws.addEventListener("message", (e) => {
console.log("WebSocket message", e);
});
function closeAllWsClients() { function closeAllWsClients() {
fetch("/close_all_ws_clients", { fetch("/close_all_ws_clients", {
@ -79,6 +89,11 @@ void setup() {
server.begin(); server.begin();
} }
uint32_t lastTime = 0;
void loop() { void loop() {
vTaskDelete(NULL); if (millis() - lastTime > 5000) {
lastTime = millis();
Serial.printf("Client count: %u\n", ws.count());
}
ws.cleanupClients();
} }

View File

@ -0,0 +1,257 @@
//
// SSE server with a load generator
// it will auto adjust message push rate to minimize discards across all connected clients
// per second stats is printed to a serial console and also published as SSE ping message
// open /sse URL to start events generator
#include <Arduino.h>
#ifdef ESP32
#include <AsyncTCP.h>
#include <WiFi.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#elif defined(TARGET_RP2040)
#include <WebServer.h>
#include <WiFi.h>
#endif
#include <ESPAsyncWebServer.h>
#if __has_include("ArduinoJson.h")
#include <ArduinoJson.h>
#include <AsyncJson.h>
#include <AsyncMessagePack.h>
#endif
#include <LittleFS.h>
const char* htmlContent PROGMEM = R"(
<!DOCTYPE html>
<html>
<head>
<title>Sample HTML</title>
</head>
<body>
<h1>Hello, World!</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod
rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper
arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit
accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi.
Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo
dapibus elit, id varius sem dui id lacus.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod
rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper
arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit
accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi.
Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo
dapibus elit, id varius sem dui id lacus.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod
rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper
arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit
accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi.
Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo
dapibus elit, id varius sem dui id lacus.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod
rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper
arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit
accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi.
Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo
dapibus elit, id varius sem dui id lacus.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod
rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper
arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit
accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi.
Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo
dapibus elit, id varius sem dui id lacus.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod
rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper
arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit
accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi.
Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo
dapibus elit, id varius sem dui id lacus.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod
rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper
arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit
accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi.
Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo
dapibus elit, id varius sem dui id lacus.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod
rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper
arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit
accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi.
Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo
dapibus elit, id varius sem dui id lacus.</p>
</body>
</html>
)";
const char* staticContent PROGMEM = R"(
<!DOCTYPE html>
<html>
<head>
<title>Sample HTML</title>
</head>
<body>
<h1>Hello, %IP%</h1>
</body>
</html>
)";
AsyncWebServer server(80);
AsyncEventSource events("/events");
/////////////////////////////////////////////////////////////////////////////////////////////////////
const char* PARAM_MESSAGE PROGMEM = "message";
const char* SSE_HTLM PROGMEM = R"(
<!DOCTYPE html>
<html>
<head>
<title>Server-Sent Events</title>
<script>
if (!!window.EventSource) {
var source = new EventSource('/events');
source.addEventListener('open', function(e) {
console.log("Events Connected");
}, false);
source.addEventListener('error', function(e) {
if (e.target.readyState != EventSource.OPEN) {
console.log("Events Disconnected");
}
}, false);
source.addEventListener('message', function(e) {
console.log("message", e);
}, false);
source.addEventListener('heartbeat', function(e) {
console.log("heartbeat", e.data);
}, false);
}
</script>
</head>
<body>
<h1>Open your browser console!</h1>
</body>
</html>
)";
static const char* SSE_MSG = R"(Alice felt that this could not be denied, so she tried another question. 'What sort of people live about here?' 'In THAT direction,' the Cat said, waving its right paw round, 'lives a Hatter: and in THAT direction,' waving the other paw, 'lives a March Hare. Visit either you like: they're both mad.'
'But I don't want to go among mad people,' Alice remarked. 'Oh, you can't help that,' said the Cat: 'we're all mad here. I'm mad. You're mad.' 'How do you know I'm mad?' said Alice.
'You must be,' said the Cat, `or you wouldn't have come here.' Alice didn't think that proved it at all; however, she went on 'And how do you know that you're mad?' 'To begin with,' said the Cat, 'a dog's not mad. You grant that?'
)";
void notFound(AsyncWebServerRequest* request) {
request->send(404, "text/plain", "Not found");
}
static const char characters[] = "0123456789ABCDEF";
static size_t charactersIndex = 0;
void setup() {
Serial.begin(115200);
#ifndef CONFIG_IDF_TARGET_ESP32H2
/*
WiFi.mode(WIFI_STA);
WiFi.begin("SSID", "passwd");
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
Serial.printf("WiFi Failed!\n");
return;
}
Serial.print("IP Address: ");
Serial.println(WiFi.localIP());
*/
WiFi.mode(WIFI_AP);
WiFi.softAP("esp-captive");
#endif
server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) {
request->send(200, "text/html", staticContent);
});
events.onConnect([](AsyncEventSourceClient* client) {
if (client->lastId()) {
Serial.printf("SSE Client reconnected! Last message ID that it gat is: %" PRIu32 "\n", client->lastId());
}
client->send("hello!", NULL, millis(), 1000);
});
server.on("/sse", HTTP_GET, [](AsyncWebServerRequest* request) {
request->send(200, "text/html", SSE_HTLM);
});
// go to http://192.168.4.1/sse
server.addHandler(&events);
server.onNotFound(notFound);
server.begin();
}
uint32_t lastSSE = 0;
uint32_t deltaSSE = 25;
uint32_t messagesSSE = 4; // how many messages to q each time
uint32_t sse_disc{0}, sse_enq{0}, sse_penq{0}, sse_second{0};
AsyncEventSource::SendStatus enqueue() {
AsyncEventSource::SendStatus state = events.send(SSE_MSG, "message");
if (state == AsyncEventSource::SendStatus::DISCARDED)
++sse_disc;
else if (state == AsyncEventSource::SendStatus::ENQUEUED) {
++sse_enq;
} else
++sse_penq;
return state;
}
void loop() {
uint32_t now = millis();
if (now - lastSSE >= deltaSSE) {
// enqueue messages
for (uint32_t i = 0; i != messagesSSE; ++i) {
auto err = enqueue();
if (err == AsyncEventSource::SendStatus::DISCARDED || err == AsyncEventSource::SendStatus::PARTIALLY_ENQUEUED) {
// throttle messaging a bit
lastSSE = now + deltaSSE;
break;
}
}
lastSSE = millis();
}
if (now - sse_second > 1000) {
String s;
s.reserve(100);
s = "Ping:";
s += now / 1000;
s += " clients:";
s += events.count();
s += " disc:";
s += sse_disc;
s += " enq:";
s += sse_enq;
s += " partial:";
s += sse_penq;
s += " avg wait:";
s += events.avgPacketsWaiting();
s += " heap:";
s += ESP.getFreeHeap() / 1024;
events.send(s, "heartbeat", now);
Serial.println();
Serial.println(s);
// if we see discards or partial enqueues, let's decrease message rate, else - increase. So that we can come to a max sustained message rate
if (sse_disc || sse_penq)
++deltaSSE;
else if (deltaSSE > 5)
--deltaSSE;
sse_disc = sse_enq = sse_penq = 0;
sse_second = now;
}
}

View File

@ -147,6 +147,8 @@ AsyncMiddlewareFunction complexAuth([](AsyncWebServerRequest* request, ArMiddlew
AuthorizationMiddleware authz([](AsyncWebServerRequest* request) { return request->getAttribute("role") == "staff"; }); AuthorizationMiddleware authz([](AsyncWebServerRequest* request) { return request->getAttribute("role") == "staff"; });
int wsClients = 0;
///////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////
const char* PARAM_MESSAGE PROGMEM = "message"; const char* PARAM_MESSAGE PROGMEM = "message";
@ -407,6 +409,7 @@ void setup() {
// PERF TEST: // PERF TEST:
// > brew install autocannon // > brew install autocannon
// > autocannon -c 10 -w 10 -d 20 http://192.168.4.1 // > autocannon -c 10 -w 10 -d 20 http://192.168.4.1
// > autocannon -c 16 -w 16 -d 20 http://192.168.4.1
server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) { server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) {
request->send(200, "text/html", htmlContent); request->send(200, "text/html", htmlContent);
}); });
@ -503,6 +506,29 @@ void setup() {
request->send(response); request->send(response);
}); });
// time curl -N -v -G -d 'd=3000' -d 'l=10000' http://192.168.4.1/slow.html --output -
server.on("/slow.html", HTTP_GET, [](AsyncWebServerRequest* request) {
uint32_t d = request->getParam("d")->value().toInt();
uint32_t l = request->getParam("l")->value().toInt();
Serial.printf("d = %" PRIu32 ", l = %" PRIu32 "\n", d, l);
AsyncWebServerResponse* response = request->beginChunkedResponse("text/html", [d, l](uint8_t* buffer, size_t maxLen, size_t index) -> size_t {
Serial.printf("%u\n", index);
// finished ?
if (index >= l)
return 0;
// slow down the task by 2 seconds
// to simulate some heavy processing, like SD card reading
delay(d);
memset(buffer, characters[charactersIndex], 256);
charactersIndex = (charactersIndex + 1) % sizeof(characters);
return 256;
});
request->send(response);
});
/* /*
curl -I -X HEAD http://192.168.4.1/download curl -I -X HEAD http://192.168.4.1/download
HTTP/1.1 200 OK HTTP/1.1 200 OK
@ -622,10 +648,14 @@ void setup() {
ws.onEvent([](AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len) { ws.onEvent([](AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len) {
(void)len; (void)len;
if (type == WS_EVT_CONNECT) { if (type == WS_EVT_CONNECT) {
wsClients++;
ws.textAll("new client connected");
Serial.println("ws connect"); Serial.println("ws connect");
client->setCloseClientOnQueueFull(false); client->setCloseClientOnQueueFull(false);
client->ping(); client->ping();
} else if (type == WS_EVT_DISCONNECT) { } else if (type == WS_EVT_DISCONNECT) {
wsClients--;
ws.textAll("client disconnected");
Serial.println("ws disconnect"); Serial.println("ws disconnect");
} else if (type == WS_EVT_ERROR) { } else if (type == WS_EVT_ERROR) {
Serial.println("ws error"); Serial.println("ws error");
@ -651,59 +681,78 @@ void setup() {
// //
// some perf tests: // some perf tests:
// launch 16 concurrent workers for 30 seconds // launch 16 concurrent workers for 30 seconds
// > for i in {1..10}; do ( count=$(gtimeout 30 curl -s -N -H "Accept: text/event-stream" http://192.168.4.1/events 2>&1 | grep -c "^data:"); echo "Total: $count events, $(echo "$count / 4" | bc -l) events / second" ) & done;
// > for i in {1..16}; do ( count=$(gtimeout 30 curl -s -N -H "Accept: text/event-stream" http://192.168.4.1/events 2>&1 | grep -c "^data:"); echo "Total: $count events, $(echo "$count / 4" | bc -l) events / second" ) & done; // > for i in {1..16}; do ( count=$(gtimeout 30 curl -s -N -H "Accept: text/event-stream" http://192.168.4.1/events 2>&1 | grep -c "^data:"); echo "Total: $count events, $(echo "$count / 4" | bc -l) events / second" ) & done;
// //
// With AsyncTCP, with 16 workers: a lot of Too many messages queued: deleting message // With AsyncTCP, with 16 workers: a lot of "Event message queue overflow: discard message", no crash
// //
// Total: 119 events, 29.75000000000000000000 events / second // Total: 1711 events, 427.75 events / second
// Total: 727 events, 181.75000000000000000000 events / second // Total: 1711 events, 427.75 events / second
// Total: 1386 events, 346.50000000000000000000 events / second // Total: 1626 events, 406.50 events / second
// Total: 1385 events, 346.25000000000000000000 events / second // Total: 1562 events, 390.50 events / second
// Total: 1276 events, 319.00000000000000000000 events / second // Total: 1706 events, 426.50 events / second
// Total: 1411 events, 352.75000000000000000000 events / second // Total: 1659 events, 414.75 events / second
// Total: 1276 events, 319.00000000000000000000 events / second // Total: 1624 events, 406.00 events / second
// Total: 1333 events, 333.25000000000000000000 events / second // Total: 1706 events, 426.50 events / second
// Total: 1250 events, 312.50000000000000000000 events / second // Total: 1487 events, 371.75 events / second
// Total: 1275 events, 318.75000000000000000000 events / second // Total: 1573 events, 393.25 events / second
// Total: 1271 events, 317.75000000000000000000 events / second // Total: 1569 events, 392.25 events / second
// Total: 1271 events, 317.75000000000000000000 events / second // Total: 1559 events, 389.75 events / second
// Total: 1254 events, 313.50000000000000000000 events / second // Total: 1560 events, 390.00 events / second
// Total: 1251 events, 312.75000000000000000000 events / second // Total: 1562 events, 390.50 events / second
// Total: 1254 events, 313.50000000000000000000 events / second // Total: 1626 events, 406.50 events / second
// Total: 1262 events, 315.50000000000000000000 events / second
// //
// With AsyncTCP, with 10 workers: // With AsyncTCP, with 10 workers:
// //
// Total: 1875 events, 468.75000000000000000000 events / second // Total: 2038 events, 509.50 events / second
// Total: 1870 events, 467.50000000000000000000 events / second // Total: 2120 events, 530.00 events / second
// Total: 1871 events, 467.75000000000000000000 events / second // Total: 2119 events, 529.75 events / second
// Total: 1875 events, 468.75000000000000000000 events / second // Total: 2038 events, 509.50 events / second
// Total: 1871 events, 467.75000000000000000000 events / second // Total: 2037 events, 509.25 events / second
// Total: 1805 events, 451.25000000000000000000 events / second // Total: 2119 events, 529.75 events / second
// Total: 1803 events, 450.75000000000000000000 events / second // Total: 2119 events, 529.75 events / second
// Total: 1873 events, 468.25000000000000000000 events / second // Total: 2120 events, 530.00 events / second
// Total: 1872 events, 468.00000000000000000000 events / second // Total: 2038 events, 509.50 events / second
// Total: 1805 events, 451.25000000000000000000 events / second // Total: 2038 events, 509.50 events / second
// //
// With AsyncTCPSock, with 16 workers: ESP32 CRASH !!! // With AsyncTCPSock, with 16 workers: ESP32 CRASH !!!
// //
// With AsyncTCPSock, with 10 workers: // With AsyncTCPSock, with 10 workers:
// //
// Total: 1242 events, 310.50000000000000000000 events / second // Total: 1242 events, 310.50 events / second
// Total: 1242 events, 310.50000000000000000000 events / second // Total: 1242 events, 310.50 events / second
// Total: 1242 events, 310.50000000000000000000 events / second // Total: 1242 events, 310.50 events / second
// Total: 1242 events, 310.50000000000000000000 events / second // Total: 1242 events, 310.50 events / second
// Total: 1181 events, 295.25000000000000000000 events / second // Total: 1181 events, 295.25 events / second
// Total: 1182 events, 295.50000000000000000000 events / second // Total: 1182 events, 295.50 events / second
// Total: 1240 events, 310.00000000000000000000 events / second // Total: 1240 events, 310.00 events / second
// Total: 1181 events, 295.25000000000000000000 events / second // Total: 1181 events, 295.25 events / second
// Total: 1181 events, 295.25000000000000000000 events / second // Total: 1181 events, 295.25 events / second
// Total: 1183 events, 295.75000000000000000000 events / second // Total: 1183 events, 295.75 events / second
// //
server.addHandler(&events); server.addHandler(&events);
// Run: websocat ws://192.168.4.1/ws // Run in terminal 1: websocat ws://192.168.4.1/ws => stream data
server.addHandler(&ws); // Run in terminal 2: websocat ws://192.168.4.1/ws => stream data
// Run in terminal 3: websocat ws://192.168.4.1/ws => should fail:
/*
websocat ws://192.168.4.1/ws
websocat: WebSocketError: WebSocketError: Received unexpected status code (503 Service Unavailable)
websocat: error running
*/
server.addHandler(&ws).addMiddleware([](AsyncWebServerRequest* request, ArMiddlewareNext next) {
if (ws.count() > 2) {
// too many clients - answer back immediately and stop processing next middlewares and handler
request->send(503, "text/plain", "Server is busy");
} else {
// process next middleware and at the end the handler
next();
}
});
// Reset connection on HTTP request:
// for i in {1..20}; do curl -v -X GET https://192.168.4.1:80; done;
// The heap size should not decrease over time.
#if __has_include("ArduinoJson.h") #if __has_include("ArduinoJson.h")
server.addHandler(jsonHandler); server.addHandler(jsonHandler);
@ -721,6 +770,8 @@ uint32_t deltaSSE = 10;
uint32_t lastWS = 0; uint32_t lastWS = 0;
uint32_t deltaWS = 100; uint32_t deltaWS = 100;
uint32_t lastHeap = 0;
void loop() { void loop() {
uint32_t now = millis(); uint32_t now = millis();
if (now - lastSSE >= deltaSSE) { if (now - lastSSE >= deltaSSE) {
@ -729,9 +780,15 @@ void loop() {
} }
if (now - lastWS >= deltaWS) { if (now - lastWS >= deltaWS) {
ws.printfAll("kp%.4f", (10.0 / 3.0)); ws.printfAll("kp%.4f", (10.0 / 3.0));
for (auto& client : ws.getClients()) { // for (auto& client : ws.getClients()) {
client.printf("kp%.4f", (10.0 / 3.0)); // client.printf("kp%.4f", (10.0 / 3.0));
} // }
lastWS = millis(); lastWS = millis();
} }
#ifdef ESP32
if (now - lastHeap >= 2000) {
Serial.printf("Free heap: %" PRIu32 "\n", ESP.getFreeHeap());
lastHeap = now;
}
#endif
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "ESPAsyncWebServer", "name": "ESPAsyncWebServer",
"version": "3.3.23", "version": "3.4.5",
"description": "Asynchronous HTTP and WebSocket Server Library for ESP32, ESP8266 and RP2040. Supports: WebSocket, SSE, Authentication, Arduino Json 7, File Upload, Static File serving, URL Rewrite, URL Redirect, etc.", "description": "Asynchronous HTTP and WebSocket Server Library for ESP32, ESP8266 and RP2040. Supports: WebSocket, SSE, Authentication, Arduino Json 7, File Upload, Static File serving, URL Rewrite, URL Redirect, etc.",
"keywords": "http,async,websocket,webserver", "keywords": "http,async,websocket,webserver",
"homepage": "https://github.com/mathieucarbou/ESPAsyncWebServer", "homepage": "https://github.com/mathieucarbou/ESPAsyncWebServer",
@ -28,7 +28,7 @@
{ {
"owner": "mathieucarbou", "owner": "mathieucarbou",
"name": "AsyncTCP", "name": "AsyncTCP",
"version": "^3.2.14", "version": "^3.3.1",
"platforms": "espressif32" "platforms": "espressif32"
}, },
{ {

View File

@ -1,6 +1,6 @@
name=ESP Async WebServer name=ESP Async WebServer
includes=ESPAsyncWebServer.h includes=ESPAsyncWebServer.h
version=3.3.23 version=3.4.5
author=Me-No-Dev author=Me-No-Dev
maintainer=Mathieu Carbou <mathieu.carbou@gmail.com> maintainer=Mathieu Carbou <mathieu.carbou@gmail.com>
sentence=Asynchronous HTTP and WebSocket Server Library for ESP32, ESP8266 and RP2040 sentence=Asynchronous HTTP and WebSocket Server Library for ESP32, ESP8266 and RP2040

View File

@ -2,11 +2,11 @@
default_envs = arduino-2, arduino-3, arduino-310, esp8266, raspberrypi default_envs = arduino-2, arduino-3, arduino-310, esp8266, raspberrypi
lib_dir = . lib_dir = .
; src_dir = examples/CaptivePortal ; src_dir = examples/CaptivePortal
; src_dir = examples/SimpleServer src_dir = examples/SimpleServer
; src_dir = examples/StreamFiles ; src_dir = examples/StreamFiles
; src_dir = examples/Filters ; src_dir = examples/Filters
; src_dir = examples/Issue85 ; src_dir = examples/Issue85
src_dir = examples/Issue162 ; src_dir = examples/Issue162
[env] [env]
framework = arduino framework = arduino
@ -14,11 +14,11 @@ build_flags =
-Og -Og
-Wall -Wextra -Wall -Wextra
-Wno-unused-parameter -Wno-unused-parameter
-D CONFIG_ARDUHAL_LOG_COLORS ; -D CONFIG_ARDUHAL_LOG_COLORS
-D CORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_VERBOSE -D CORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_VERBOSE
-D CONFIG_ASYNC_TCP_MAX_ACK_TIME=3000 -D CONFIG_ASYNC_TCP_MAX_ACK_TIME=5000
-D CONFIG_ASYNC_TCP_PRIORITY=10 -D CONFIG_ASYNC_TCP_PRIORITY=10
-D CONFIG_ASYNC_TCP_QUEUE_SIZE=128 -D CONFIG_ASYNC_TCP_QUEUE_SIZE=64
-D CONFIG_ASYNC_TCP_RUNNING_CORE=1 -D CONFIG_ASYNC_TCP_RUNNING_CORE=1
-D CONFIG_ASYNC_TCP_STACK_SIZE=4096 -D CONFIG_ASYNC_TCP_STACK_SIZE=4096
upload_protocol = esptool upload_protocol = esptool
@ -31,7 +31,7 @@ lib_deps =
; bblanchon/ArduinoJson @ 5.13.4 ; bblanchon/ArduinoJson @ 5.13.4
; bblanchon/ArduinoJson @ 6.21.5 ; bblanchon/ArduinoJson @ 6.21.5
bblanchon/ArduinoJson @ 7.2.1 bblanchon/ArduinoJson @ 7.2.1
mathieucarbou/AsyncTCP @ 3.2.14 mathieucarbou/AsyncTCP @ 3.3.1
board = esp32dev board = esp32dev
board_build.partitions = partitions-4MB.csv board_build.partitions = partitions-4MB.csv
board_build.filesystem = littlefs board_build.filesystem = littlefs
@ -49,21 +49,21 @@ platform = https://github.com/pioarduino/platform-espressif32/releases/download/
; board = esp32-s3-devkitc-1 ; board = esp32-s3-devkitc-1
; board = esp32-c6-devkitc-1 ; board = esp32-c6-devkitc-1
lib_deps = lib_deps =
mathieucarbou/AsyncTCP @ 3.2.14 mathieucarbou/AsyncTCP @ 3.3.1
[env:arduino-310] [env:arduino-310]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.10-rc3/platform-espressif32.zip platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.10/platform-espressif32.zip
; board = esp32-s3-devkitc-1 ; board = esp32-s3-devkitc-1
; board = esp32-c6-devkitc-1 ; board = esp32-c6-devkitc-1
; board = esp32-h2-devkitm-1 ; board = esp32-h2-devkitm-1
[env:perf-test-AsyncTCP] [env:perf-test-AsyncTCP]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.10-rc3/platform-espressif32.zip platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.10/platform-espressif32.zip
build_flags = ${env.build_flags} build_flags = ${env.build_flags}
-D PERF_TEST=1 -D PERF_TEST=1
[env:perf-test-AsyncTCPSock] [env:perf-test-AsyncTCPSock]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.10-rc3/platform-espressif32.zip platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.10/platform-espressif32.zip
lib_deps = lib_deps =
https://github.com/mathieucarbou/AsyncTCPSock/archive/refs/tags/v1.0.3-dev.zip https://github.com/mathieucarbou/AsyncTCPSock/archive/refs/tags/v1.0.3-dev.zip
build_flags = ${env.build_flags} build_flags = ${env.build_flags}
@ -102,10 +102,10 @@ board = ${sysenv.PIO_BOARD}
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
board = ${sysenv.PIO_BOARD} board = ${sysenv.PIO_BOARD}
lib_deps = lib_deps =
mathieucarbou/AsyncTCP @ 3.2.14 mathieucarbou/AsyncTCP @ 3.3.1
[env:ci-arduino-310] [env:ci-arduino-310]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.10-rc3/platform-espressif32.zip platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.10/platform-espressif32.zip
board = ${sysenv.PIO_BOARD} board = ${sysenv.PIO_BOARD}
[env:ci-esp8266] [env:ci-esp8266]

View File

@ -23,116 +23,99 @@
#endif #endif
#include "AsyncEventSource.h" #include "AsyncEventSource.h"
#define ASYNC_SSE_NEW_LINE_CHAR (char)0xa
using namespace asyncsrv; using namespace asyncsrv;
static String generateEventMessage(const char* message, const char* event, uint32_t id, uint32_t reconnect) { static String generateEventMessage(const char* message, const char* event, uint32_t id, uint32_t reconnect) {
String ev; String str;
size_t len{0};
if (message)
len += strlen(message);
if (event)
len += strlen(event);
len += 42; // give it some overhead
str.reserve(len);
if (reconnect) { if (reconnect) {
ev += T_retry_; str += T_retry_;
ev += reconnect; str += reconnect;
ev += T_rn; str += ASYNC_SSE_NEW_LINE_CHAR; // '\n'
} }
if (id) { if (id) {
ev += T_id__; str += T_id__;
ev += id; str += id;
ev += T_rn; str += ASYNC_SSE_NEW_LINE_CHAR; // '\n'
} }
if (event != NULL) { if (event != NULL) {
ev += T_event_; str += T_event_;
ev += event; str += event;
ev += T_rn; str += ASYNC_SSE_NEW_LINE_CHAR; // '\n'
} }
if (message != NULL) { if (!message)
size_t messageLen = strlen(message); return str;
char* lineStart = (char*)message;
char* lineEnd; size_t messageLen = strlen(message);
do { char* lineStart = (char*)message;
char* nextN = strchr(lineStart, '\n'); char* lineEnd;
char* nextR = strchr(lineStart, '\r'); do {
if (nextN == NULL && nextR == NULL) { char* nextN = strchr(lineStart, '\n');
size_t llen = ((char*)message + messageLen) - lineStart; char* nextR = strchr(lineStart, '\r');
char* ldata = (char*)malloc(llen + 1); if (nextN == NULL && nextR == NULL) {
if (ldata != NULL) { // a message is a single-line string
memcpy(ldata, lineStart, llen); str += T_data_;
ldata[llen] = 0; str += message;
ev += T_data_; str += T_nn;
ev += ldata; return str;
ev += T_rnrn; }
free(ldata);
} // a message is a multi-line string
lineStart = (char*)message + messageLen; char* nextLine = NULL;
if (nextN != NULL && nextR != NULL) { // windows line-ending \r\n
if (nextR + 1 == nextN) {
// normal \r\n sequense
lineEnd = nextR;
nextLine = nextN + 1;
} else { } else {
char* nextLine = NULL; // some abnormal \n \r mixed sequence
if (nextN != NULL && nextR != NULL) { lineEnd = std::min(nextR, nextN);
if (nextR < nextN) { nextLine = lineEnd + 1;
lineEnd = nextR;
if (nextN == (nextR + 1))
nextLine = nextN + 1;
else
nextLine = nextR + 1;
} else {
lineEnd = nextN;
if (nextR == (nextN + 1))
nextLine = nextR + 1;
else
nextLine = nextN + 1;
}
} else if (nextN != NULL) {
lineEnd = nextN;
nextLine = nextN + 1;
} else {
lineEnd = nextR;
nextLine = nextR + 1;
}
size_t llen = lineEnd - lineStart;
char* ldata = (char*)malloc(llen + 1);
if (ldata != NULL) {
memcpy(ldata, lineStart, llen);
ldata[llen] = 0;
ev += T_data_;
ev += ldata;
ev += T_rn;
free(ldata);
}
lineStart = nextLine;
if (lineStart == ((char*)message + messageLen))
ev += T_rn;
} }
} while (lineStart < ((char*)message + messageLen)); } else if (nextN != NULL) { // Unix/Mac OS X LF
} lineEnd = nextN;
nextLine = nextN + 1;
} else { // some ancient garbage
lineEnd = nextR;
nextLine = nextR + 1;
}
return ev; str += T_data_;
str.concat(lineStart, lineEnd - lineStart);
str += ASYNC_SSE_NEW_LINE_CHAR; // \n
lineStart = nextLine;
} while (lineStart < ((char*)message + messageLen));
// append another \n to terminate message
str += ASYNC_SSE_NEW_LINE_CHAR; // '\n'
return str;
} }
// Message // Message
AsyncEventSourceMessage::AsyncEventSourceMessage(const char* data, size_t len)
: _data(nullptr), _len(len), _sent(0), _acked(0) {
_data = (uint8_t*)malloc(_len + 1);
if (_data == nullptr) {
_len = 0;
} else {
memcpy(_data, data, len);
_data[_len] = 0;
}
}
AsyncEventSourceMessage::~AsyncEventSourceMessage() {
if (_data != NULL)
free(_data);
}
size_t AsyncEventSourceMessage::ack(size_t len, __attribute__((unused)) uint32_t time) { size_t AsyncEventSourceMessage::ack(size_t len, __attribute__((unused)) uint32_t time) {
// If the whole message is now acked... // If the whole message is now acked...
if (_acked + len > _len) { if (_acked + len > _data->length()) {
// Return the number of extra bytes acked (they will be carried on to the next message) // Return the number of extra bytes acked (they will be carried on to the next message)
const size_t extra = _acked + len - _len; const size_t extra = _acked + len - _data->length();
_acked = _len; _acked = _data->length();
return extra; return extra;
} }
// Return that no extra bytes left. // Return that no extra bytes left.
@ -144,13 +127,25 @@ size_t AsyncEventSourceMessage::write(AsyncClient* client) {
if (!client) if (!client)
return 0; return 0;
if (_sent >= _len || !client->canSend()) { if (_sent >= _data->length() || !client->canSend()) {
return 0; return 0;
} }
size_t len = min(_len - _sent, client->space());
size_t sent = client->add((const char*)_data + _sent, len); size_t len = std::min(_data->length() - _sent, client->space());
_sent += sent; /*
return sent; add() would call lwip's tcp_write() under the AsyncTCP hood with apiflags argument.
By default apiflags=ASYNC_WRITE_FLAG_COPY
we could have used apiflags with this flag unset to pass data by reference and avoid copy to socket buffer,
but looks like it does not work for Arduino's lwip in ESP32/IDF
it is enforced in https://github.com/espressif/esp-lwip/blob/0606eed9d8b98a797514fdf6eabb4daf1c8c8cd9/src/core/tcp_out.c#L422C5-L422C30
if LWIP_NETIF_TX_SINGLE_PBUF is set, and it is set indeed in IDF
https://github.com/espressif/esp-idf/blob/a0f798cfc4bbd624aab52b2c194d219e242d80c1/components/lwip/port/include/lwipopts.h#L744
So let's just keep it enforced ASYNC_WRITE_FLAG_COPY and keep in mind that there is no zero-copy
*/
size_t written = client->add(_data->c_str() + _sent, len, ASYNC_WRITE_FLAG_COPY); // ASYNC_WRITE_FLAG_MORE
_sent += written;
return written;
} }
size_t AsyncEventSourceMessage::send(AsyncClient* client) { size_t AsyncEventSourceMessage::send(AsyncClient* client) {
@ -160,20 +155,19 @@ size_t AsyncEventSourceMessage::send(AsyncClient* client) {
// Client // Client
AsyncEventSourceClient::AsyncEventSourceClient(AsyncWebServerRequest* request, AsyncEventSource* server) { AsyncEventSourceClient::AsyncEventSourceClient(AsyncWebServerRequest* request, AsyncEventSource* server)
_client = request->client(); : _client(request->client()), _server(server) {
_server = server;
_lastId = 0;
if (request->hasHeader(T_Last_Event_ID)) if (request->hasHeader(T_Last_Event_ID))
_lastId = atoi(request->getHeader(T_Last_Event_ID)->value().c_str()); _lastId = atoi(request->getHeader(T_Last_Event_ID)->value().c_str());
_client->setRxTimeout(0); _client->setRxTimeout(0);
_client->onError(NULL, NULL); _client->onError(NULL, NULL);
_client->onAck([](void* r, AsyncClient* c, size_t len, uint32_t time) { (void)c; ((AsyncEventSourceClient*)(r))->_onAck(len, time); }, this); _client->onAck([](void* r, AsyncClient* c, size_t len, uint32_t time) { (void)c; static_cast<AsyncEventSourceClient*>(r)->_onAck(len, time); }, this);
_client->onPoll([](void* r, AsyncClient* c) { (void)c; ((AsyncEventSourceClient*)(r))->_onPoll(); }, this); _client->onPoll([](void* r, AsyncClient* c) { (void)c; static_cast<AsyncEventSourceClient*>(r)->_onPoll(); }, this);
_client->onData(NULL, NULL); _client->onData(NULL, NULL);
_client->onTimeout([this](void* r, AsyncClient* c __attribute__((unused)), uint32_t time) { ((AsyncEventSourceClient*)(r))->_onTimeout(time); }, this); _client->onTimeout([this](void* r, AsyncClient* c __attribute__((unused)), uint32_t time) { static_cast<AsyncEventSourceClient*>(r)->_onTimeout(time); }, this);
_client->onDisconnect([this](void* r, AsyncClient* c) { ((AsyncEventSourceClient*)(r))->_onDisconnect(); delete c; }, this); _client->onDisconnect([this](void* r, AsyncClient* c) { static_cast<AsyncEventSourceClient*>(r)->_onDisconnect(); delete c; }, this);
_server->_addClient(this); _server->_addClient(this);
delete request; delete request;
@ -190,29 +184,61 @@ AsyncEventSourceClient::~AsyncEventSourceClient() {
} }
bool AsyncEventSourceClient::_queueMessage(const char* message, size_t len) { bool AsyncEventSourceClient::_queueMessage(const char* message, size_t len) {
if (!_client) if (_messageQueue.size() >= SSE_MAX_QUEUED_MESSAGES) {
#ifdef ESP8266
ets_printf(String(F("ERROR: Too many messages queued\n")).c_str());
#elif defined(ESP32)
log_e("Event message queue overflow: discard message");
#endif
return false; return false;
}
#ifdef ESP32 #ifdef ESP32
// length() is not thread-safe, thus acquiring the lock before this call.. // length() is not thread-safe, thus acquiring the lock before this call..
std::lock_guard<std::mutex> lock(_lockmq); std::lock_guard<std::mutex> lock(_lockmq);
#endif #endif
_messageQueue.emplace_back(message, len);
/*
throttle queue run
if Q is filled for >25% then network/CPU is congested, since there is no zero-copy mode for socket buff
forcing Q run will only eat more heap ram and blow the buffer, let's just keep data in our own queue
the queue will be processed at least on each onAck()/onPoll() call from AsyncTCP
*/
if (_messageQueue.size() < SSE_MAX_QUEUED_MESSAGES >> 2 && _client->canSend()) {
_runQueue();
}
return true;
}
bool AsyncEventSourceClient::_queueMessage(AsyncEvent_SharedData_t&& msg) {
if (_messageQueue.size() >= SSE_MAX_QUEUED_MESSAGES) { if (_messageQueue.size() >= SSE_MAX_QUEUED_MESSAGES) {
#ifdef ESP8266 #ifdef ESP8266
ets_printf(String(F("ERROR: Too many messages queued\n")).c_str()); ets_printf(String(F("ERROR: Too many messages queued\n")).c_str());
#elif defined(ESP32) #elif defined(ESP32)
log_e("Too many messages queued: deleting message"); log_e("Event message queue overflow: discard message");
#endif #endif
return false; return false;
} }
_messageQueue.emplace_back(message, len); #ifdef ESP32
// runqueue trigger when new messages added // length() is not thread-safe, thus acquiring the lock before this call..
if (_client->canSend()) { std::lock_guard<std::mutex> lock(_lockmq);
#endif
_messageQueue.emplace_back(std::move(msg));
/*
throttle queue run
if Q is filled for >25% then network/CPU is congested, since there is no zero-copy mode for socket buff
forcing Q run will only eat more heap ram and blow the buffer, let's just keep data in our own queue
the queue will be processed at least on each onAck()/onPoll() call from AsyncTCP
*/
if (_messageQueue.size() < SSE_MAX_QUEUED_MESSAGES >> 2 && _client->canSend()) {
_runQueue(); _runQueue();
} }
return true; return true;
} }
@ -221,15 +247,33 @@ void AsyncEventSourceClient::_onAck(size_t len __attribute__((unused)), uint32_t
// Same here, acquiring the lock early // Same here, acquiring the lock early
std::lock_guard<std::mutex> lock(_lockmq); std::lock_guard<std::mutex> lock(_lockmq);
#endif #endif
_runQueue();
// adjust in-flight len
if (len < _inflight)
_inflight -= len;
else
_inflight = 0;
// acknowledge as much messages's data as we got confirmed len from a AsyncTCP
while (len && _messageQueue.size()) {
len = _messageQueue.front().ack(len);
if (_messageQueue.front().finished()) {
// now we could release full ack'ed messages, we were keeping it unless send confirmed from AsyncTCP
_messageQueue.pop_front();
}
}
// try to send another batch of data
if (_messageQueue.size())
_runQueue();
} }
void AsyncEventSourceClient::_onPoll() { void AsyncEventSourceClient::_onPoll() {
#ifdef ESP32
// Same here, acquiring the lock early
std::lock_guard<std::mutex> lock(_lockmq);
#endif
if (_messageQueue.size()) { if (_messageQueue.size()) {
#ifdef ESP32
// Same here, acquiring the lock early
std::lock_guard<std::mutex> lock(_lockmq);
#endif
_runQueue(); _runQueue();
} }
} }
@ -251,50 +295,42 @@ void AsyncEventSourceClient::close() {
_client->close(); _client->close();
} }
bool AsyncEventSourceClient::write(const char* message, size_t len) {
return connected() && _queueMessage(message, len);
}
bool AsyncEventSourceClient::send(const char* message, const char* event, uint32_t id, uint32_t reconnect) { bool AsyncEventSourceClient::send(const char* message, const char* event, uint32_t id, uint32_t reconnect) {
if (!connected()) if (!connected())
return false; return false;
String ev = generateEventMessage(message, event, id, reconnect); return _queueMessage(std::make_shared<String>(generateEventMessage(message, event, id, reconnect)));
return _queueMessage(ev.c_str(), ev.length());
}
size_t AsyncEventSourceClient::packetsWaiting() const {
#ifdef ESP32
std::lock_guard<std::mutex> lock(_lockmq);
#endif
return _messageQueue.size();
} }
void AsyncEventSourceClient::_runQueue() { void AsyncEventSourceClient::_runQueue() {
if (!_client) if (!_client)
return; return;
// there is no need to lock the mutex here, 'cause all the calls to this method must be already lock'ed
size_t total_bytes_written = 0; size_t total_bytes_written = 0;
for (auto i = _messageQueue.begin(); i != _messageQueue.end(); ++i) { for (auto i = _messageQueue.begin(); i != _messageQueue.end(); ++i) {
if (!i->sent()) { if (!i->sent()) {
const size_t bytes_written = i->write(_client); const size_t bytes_written = i->write(_client);
total_bytes_written += bytes_written; total_bytes_written += bytes_written;
if (bytes_written == 0) _inflight += bytes_written;
if (bytes_written == 0 || _inflight > _max_inflight) {
// Serial.print("_");
break; break;
}
} }
} }
if (total_bytes_written > 0) // flush socket
if (total_bytes_written)
_client->send(); _client->send();
size_t len = total_bytes_written;
while (len && _messageQueue.size()) {
len = _messageQueue.front().ack(len);
if (_messageQueue.front().finished()) {
_messageQueue.pop_front();
}
}
} }
void AsyncEventSourceClient::set_max_inflight_bytes(size_t value) {
if (value >= SSE_MIN_INFLIGH && value <= SSE_MAX_INFLIGH)
_max_inflight = value;
}
/* AsyncEventSource */
void AsyncEventSource::authorizeConnect(ArAuthorizeConnectHandler cb) { void AsyncEventSource::authorizeConnect(ArAuthorizeConnectHandler cb) {
AuthorizationMiddleware* m = new AuthorizationMiddleware(401, cb); AuthorizationMiddleware* m = new AuthorizationMiddleware(401, cb);
m->_freeOnRemoval = true; m->_freeOnRemoval = true;
@ -310,18 +346,21 @@ void AsyncEventSource::_addClient(AsyncEventSourceClient* client) {
_clients.emplace_back(client); _clients.emplace_back(client);
if (_connectcb) if (_connectcb)
_connectcb(client); _connectcb(client);
_adjust_inflight_window();
} }
void AsyncEventSource::_handleDisconnect(AsyncEventSourceClient* client) { void AsyncEventSource::_handleDisconnect(AsyncEventSourceClient* client) {
if (_disconnectcb)
_disconnectcb(client);
#ifdef ESP32 #ifdef ESP32
std::lock_guard<std::mutex> lock(_client_queue_lock); std::lock_guard<std::mutex> lock(_client_queue_lock);
#endif #endif
if (_disconnectcb)
_disconnectcb(client);
for (auto i = _clients.begin(); i != _clients.end(); ++i) { for (auto i = _clients.begin(); i != _clients.end(); ++i) {
if (i->get() == client) if (i->get() == client)
_clients.erase(i); _clients.erase(i);
} }
_adjust_inflight_window();
} }
void AsyncEventSource::close() { void AsyncEventSource::close() {
@ -358,14 +397,14 @@ size_t AsyncEventSource::avgPacketsWaiting() const {
AsyncEventSource::SendStatus AsyncEventSource::send( AsyncEventSource::SendStatus AsyncEventSource::send(
const char* message, const char* event, uint32_t id, uint32_t reconnect) { const char* message, const char* event, uint32_t id, uint32_t reconnect) {
String ev = generateEventMessage(message, event, id, reconnect); AsyncEvent_SharedData_t shared_msg = std::make_shared<String>(generateEventMessage(message, event, id, reconnect));
#ifdef ESP32 #ifdef ESP32
std::lock_guard<std::mutex> lock(_client_queue_lock); std::lock_guard<std::mutex> lock(_client_queue_lock);
#endif #endif
size_t hits = 0; size_t hits = 0;
size_t miss = 0; size_t miss = 0;
for (const auto& c : _clients) { for (const auto& c : _clients) {
if (c->write(ev.c_str(), ev.length())) if (c->write(shared_msg))
++hits; ++hits;
else else
++miss; ++miss;
@ -393,7 +432,16 @@ void AsyncEventSource::handleRequest(AsyncWebServerRequest* request) {
request->send(new AsyncEventSourceResponse(this)); request->send(new AsyncEventSourceResponse(this));
} }
// Response void AsyncEventSource::_adjust_inflight_window() {
if (_clients.size()) {
size_t inflight = SSE_MAX_INFLIGH / _clients.size();
for (const auto& c : _clients)
c->set_max_inflight_bytes(inflight);
// Serial.printf("adjusted inflight to: %u\n", inflight);
}
}
/* Response */
AsyncEventSourceResponse::AsyncEventSourceResponse(AsyncEventSource* server) { AsyncEventSourceResponse::AsyncEventSourceResponse(AsyncEventSource* server) {
_server = server; _server = server;

View File

@ -21,22 +21,29 @@
#define ASYNCEVENTSOURCE_H_ #define ASYNCEVENTSOURCE_H_
#include <Arduino.h> #include <Arduino.h>
#ifdef ESP32 #ifdef ESP32
#include <AsyncTCP.h> #include <AsyncTCP.h>
#include <mutex> #include <mutex>
#ifndef SSE_MAX_QUEUED_MESSAGES #ifndef SSE_MAX_QUEUED_MESSAGES
#define SSE_MAX_QUEUED_MESSAGES 32 #define SSE_MAX_QUEUED_MESSAGES 32
#endif #endif
#define SSE_MIN_INFLIGH 2 * 1460 // allow 2 MSS packets
#define SSE_MAX_INFLIGH 16 * 1024 // but no more than 16k, no need to blow it, since same data is kept in local Q
#elif defined(ESP8266) #elif defined(ESP8266)
#include <ESPAsyncTCP.h> #include <ESPAsyncTCP.h>
#ifndef SSE_MAX_QUEUED_MESSAGES #ifndef SSE_MAX_QUEUED_MESSAGES
#define SSE_MAX_QUEUED_MESSAGES 8 #define SSE_MAX_QUEUED_MESSAGES 8
#endif #endif
#define SSE_MIN_INFLIGH 2 * 1460 // allow 2 MSS packets
#define SSE_MAX_INFLIGH 8 * 1024 // but no more than 8k, no need to blow it, since same data is kept in local Q
#elif defined(TARGET_RP2040) #elif defined(TARGET_RP2040)
#include <AsyncTCP_RP2040W.h> #include <AsyncTCP_RP2040W.h>
#ifndef SSE_MAX_QUEUED_MESSAGES #ifndef SSE_MAX_QUEUED_MESSAGES
#define SSE_MAX_QUEUED_MESSAGES 32 #define SSE_MAX_QUEUED_MESSAGES 32
#endif #endif
#define SSE_MIN_INFLIGH 2 * 1460 // allow 2 MSS packets
#define SSE_MAX_INFLIGH 16 * 1024 // but no more than 16k, no need to blow it, since same data is kept in local Q
#endif #endif
#include <ESPAsyncWebServer.h> #include <ESPAsyncWebServer.h>
@ -53,58 +60,155 @@ class AsyncEventSourceResponse;
class AsyncEventSourceClient; class AsyncEventSourceClient;
using ArEventHandlerFunction = std::function<void(AsyncEventSourceClient* client)>; using ArEventHandlerFunction = std::function<void(AsyncEventSourceClient* client)>;
using ArAuthorizeConnectHandler = ArAuthorizeFunction; using ArAuthorizeConnectHandler = ArAuthorizeFunction;
// shared message object container
using AsyncEvent_SharedData_t = std::shared_ptr<String>;
/**
* @brief Async Event Message container with shared message content data
*
*/
class AsyncEventSourceMessage { class AsyncEventSourceMessage {
private: private:
uint8_t* _data; const AsyncEvent_SharedData_t _data;
size_t _len; size_t _sent{0}; // num of bytes already sent
size_t _sent; size_t _acked{0}; // num of bytes acked
// size_t _ack;
size_t _acked;
public: public:
AsyncEventSourceMessage(const char* data, size_t len); AsyncEventSourceMessage(AsyncEvent_SharedData_t data) : _data(data) {};
~AsyncEventSourceMessage(); #ifdef ESP32
AsyncEventSourceMessage(const char* data, size_t len) : _data(std::make_shared<String>(data, len)) {};
#else
// esp8266's String does not have constructor with data/length arguments. Use a concat method here
AsyncEventSourceMessage(const char* data, size_t len) { _data->concat(data, len); };
#endif
/**
* @brief acknowledge sending len bytes of data
* @note if num of bytes to ack is larger then the unacknowledged message length the number of carried over bytes are returned
*
* @param len bytes to acknowlegde
* @param time
* @return size_t number of extra bytes carried over
*/
size_t ack(size_t len, uint32_t time = 0); size_t ack(size_t len, uint32_t time = 0);
/**
* @brief write message data to client's buffer
* @note this method does NOT call client's send
*
* @param client
* @return size_t number of bytes written
*/
size_t write(AsyncClient* client); size_t write(AsyncClient* client);
/**
* @brief writes message data to client's buffer and calls client's send method
*
* @param client
* @return size_t returns num of bytes the clien was able to send()
*/
size_t send(AsyncClient* client); size_t send(AsyncClient* client);
bool finished() { return _acked == _len; }
bool sent() { return _sent == _len; } // returns true if full message's length were acked
bool finished() { return _acked == _data->length(); }
/**
* @brief returns true if all data has been sent already
*
*/
bool sent() { return _sent == _data->length(); }
}; };
/**
* @brief class holds a sse messages queue for a particular client's connection
*
*/
class AsyncEventSourceClient { class AsyncEventSourceClient {
private: private:
AsyncClient* _client; AsyncClient* _client;
AsyncEventSource* _server; AsyncEventSource* _server;
uint32_t _lastId; uint32_t _lastId{0};
size_t _inflight{0}; // num of unacknowledged bytes that has been written to socket buffer
size_t _max_inflight{SSE_MAX_INFLIGH}; // max num of unacknowledged bytes that could be written to socket buffer
std::list<AsyncEventSourceMessage> _messageQueue; std::list<AsyncEventSourceMessage> _messageQueue;
#ifdef ESP32 #ifdef ESP32
mutable std::mutex _lockmq; mutable std::mutex _lockmq;
#endif #endif
bool _queueMessage(const char* message, size_t len); bool _queueMessage(const char* message, size_t len);
bool _queueMessage(AsyncEvent_SharedData_t&& msg);
void _runQueue(); void _runQueue();
public: public:
AsyncEventSourceClient(AsyncWebServerRequest* request, AsyncEventSource* server); AsyncEventSourceClient(AsyncWebServerRequest* request, AsyncEventSource* server);
~AsyncEventSourceClient(); ~AsyncEventSourceClient();
AsyncClient* client() { return _client; } /**
void close(); * @brief Send an SSE message to client
bool write(const char* message, size_t len); * it will craft an SSE message and place it to client's message queue
*
* @param message body string, could be single or multi-line string sepprated by \n, \r, \r\n
* @param event body string, a sinle line string
* @param id sequence id
* @param reconnect client's reconnect timeout
* @return true if message was placed in a queue
* @return false if queue is full
*/
bool send(const char* message, const char* event = NULL, uint32_t id = 0, uint32_t reconnect = 0);
bool send(const String& message, const String& event, uint32_t id = 0, uint32_t reconnect = 0) { return send(message.c_str(), event.c_str(), id, reconnect); } bool send(const String& message, const String& event, uint32_t id = 0, uint32_t reconnect = 0) { return send(message.c_str(), event.c_str(), id, reconnect); }
bool send(const String& message, const char* event, uint32_t id = 0, uint32_t reconnect = 0) { return send(message.c_str(), event, id, reconnect); } bool send(const String& message, const char* event, uint32_t id = 0, uint32_t reconnect = 0) { return send(message.c_str(), event, id, reconnect); }
bool send(const char* message, const char* event = NULL, uint32_t id = 0, uint32_t reconnect = 0);
/**
* @brief place supplied preformatted SSE message to the message queue
* @note message must a properly formatted SSE string according to https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events
*
* @param message data
* @return true on success
* @return false on queue overflow or no client connected
*/
bool write(AsyncEvent_SharedData_t message) { return connected() && _queueMessage(std::move(message)); };
[[deprecated("Use _write(AsyncEvent_SharedData_t message) instead to share same data with multiple SSE clients")]]
bool write(const char* message, size_t len) { return connected() && _queueMessage(message, len); };
// close client's connection
void close();
// getters
AsyncClient* client() { return _client; }
bool connected() const { return _client && _client->connected(); } bool connected() const { return _client && _client->connected(); }
uint32_t lastId() const { return _lastId; } uint32_t lastId() const { return _lastId; }
size_t packetsWaiting() const; size_t packetsWaiting() const { return _messageQueue.size(); };
// system callbacks (do not call) /**
* @brief Sets max amount of bytes that could be written to client's socket while awaiting delivery acknowledge
* used to throttle message delivery length to tradeoff memory consumption
* @note actual amount of data written could possible be a bit larger but no more than available socket buff space
*
* @param value
*/
void set_max_inflight_bytes(size_t value);
/**
* @brief Get current max inflight bytes value
*
* @return size_t
*/
size_t get_max_inflight_bytes() const { return _max_inflight; }
// system callbacks (do not call if from user code!)
void _onAck(size_t len, uint32_t time); void _onAck(size_t len, uint32_t time);
void _onPoll(); void _onPoll();
void _onTimeout(uint32_t time); void _onTimeout(uint32_t time);
void _onDisconnect(); void _onDisconnect();
}; };
/**
* @brief a class that maintains all connected HTTP clients subscribed to SSE delivery
* dispatches supplied messages to the client's queues
*
*/
class AsyncEventSource : public AsyncWebHandler { class AsyncEventSource : public AsyncWebHandler {
private: private:
String _url; String _url;
@ -117,6 +221,9 @@ class AsyncEventSource : public AsyncWebHandler {
ArEventHandlerFunction _connectcb = nullptr; ArEventHandlerFunction _connectcb = nullptr;
ArEventHandlerFunction _disconnectcb = nullptr; ArEventHandlerFunction _disconnectcb = nullptr;
// this method manipulates in-fligh data size for connected client depending on number of active connections
void _adjust_inflight_window();
public: public:
typedef enum { typedef enum {
DISCARDED = 0, DISCARDED = 0,
@ -124,23 +231,47 @@ class AsyncEventSource : public AsyncWebHandler {
PARTIALLY_ENQUEUED = 2, PARTIALLY_ENQUEUED = 2,
} SendStatus; } SendStatus;
AsyncEventSource(const char* url) : _url(url) {};
AsyncEventSource(const String& url) : _url(url) {}; AsyncEventSource(const String& url) : _url(url) {};
~AsyncEventSource() { close(); }; ~AsyncEventSource() { close(); };
const char* url() const { return _url.c_str(); } const char* url() const { return _url.c_str(); }
// close all connected clients
void close(); void close();
/**
* @brief set on-connect callback for the client
* used to deliver messages to client on first connect
*
* @param cb
*/
void onConnect(ArEventHandlerFunction cb) { _connectcb = cb; } void onConnect(ArEventHandlerFunction cb) { _connectcb = cb; }
/**
* @brief Send an SSE message to client
* it will craft an SSE message and place it to all connected client's message queues
*
* @param message body string, could be single or multi-line string sepprated by \n, \r, \r\n
* @param event body string, a sinle line string
* @param id sequence id
* @param reconnect client's reconnect timeout
* @return SendStatus if message was placed in any/all/part of the client's queues
*/
SendStatus send(const char* message, const char* event = NULL, uint32_t id = 0, uint32_t reconnect = 0);
SendStatus send(const String& message, const String& event, uint32_t id = 0, uint32_t reconnect = 0) { return send(message.c_str(), event.c_str(), id, reconnect); }
SendStatus send(const String& message, const char* event, uint32_t id = 0, uint32_t reconnect = 0) { return send(message.c_str(), event, id, reconnect); }
// The client pointer sent to the callback is only for reference purposes. DO NOT CALL ANY METHOD ON IT ! // The client pointer sent to the callback is only for reference purposes. DO NOT CALL ANY METHOD ON IT !
void onDisconnect(ArEventHandlerFunction cb) { _disconnectcb = cb; } void onDisconnect(ArEventHandlerFunction cb) { _disconnectcb = cb; }
void authorizeConnect(ArAuthorizeConnectHandler cb); void authorizeConnect(ArAuthorizeConnectHandler cb);
SendStatus send(const String& message, const String& event, uint32_t id = 0, uint32_t reconnect = 0) { return send(message.c_str(), event.c_str(), id, reconnect); }
SendStatus send(const String& message, const char* event, uint32_t id = 0, uint32_t reconnect = 0) { return send(message.c_str(), event, id, reconnect); } // returns number of connected clients
SendStatus send(const char* message, const char* event = NULL, uint32_t id = 0, uint32_t reconnect = 0);
// number of clients connected
size_t count() const; size_t count() const;
// returns average number of messages pending in all client's queues
size_t avgPacketsWaiting() const; size_t avgPacketsWaiting() const;
// system callbacks (do not call) // system callbacks (do not call from user code!)
void _addClient(AsyncEventSourceClient* client); void _addClient(AsyncEventSourceClient* client);
void _handleDisconnect(AsyncEventSourceClient* client); void _handleDisconnect(AsyncEventSourceClient* client);
bool canHandle(AsyncWebServerRequest* request) const override final; bool canHandle(AsyncWebServerRequest* request) const override final;
@ -149,7 +280,6 @@ class AsyncEventSource : public AsyncWebHandler {
class AsyncEventSourceResponse : public AsyncWebServerResponse { class AsyncEventSourceResponse : public AsyncWebServerResponse {
private: private:
String _content;
AsyncEventSource* _server; AsyncEventSource* _server;
public: public:

View File

@ -287,7 +287,6 @@ AsyncWebSocketClient::AsyncWebSocketClient(AsyncWebServerRequest* request, Async
_client->onTimeout([](void* r, AsyncClient* c, uint32_t time) { (void)c; ((AsyncWebSocketClient*)(r))->_onTimeout(time); }, this); _client->onTimeout([](void* r, AsyncClient* c, uint32_t time) { (void)c; ((AsyncWebSocketClient*)(r))->_onTimeout(time); }, this);
_client->onData([](void* r, AsyncClient* c, void* buf, size_t len) { (void)c; ((AsyncWebSocketClient*)(r))->_onData(buf, len); }, this); _client->onData([](void* r, AsyncClient* c, void* buf, size_t len) { (void)c; ((AsyncWebSocketClient*)(r))->_onData(buf, len); }, this);
_client->onPoll([](void* r, AsyncClient* c) { (void)c; ((AsyncWebSocketClient*)(r))->_onPoll(); }, this); _client->onPoll([](void* r, AsyncClient* c) { (void)c; ((AsyncWebSocketClient*)(r))->_onPoll(); }, this);
_server->_handleEvent(this, WS_EVT_CONNECT, request, NULL, 0);
delete request; delete request;
memset(&_pinfo, 0, sizeof(_pinfo)); memset(&_pinfo, 0, sizeof(_pinfo));
} }
@ -451,6 +450,8 @@ void AsyncWebSocketClient::close(uint16_t code, const char* message) {
if (_status != WS_CONNECTED) if (_status != WS_CONNECTED)
return; return;
_status = WS_DISCONNECTING;
if (code) { if (code) {
uint8_t packetLen = 2; uint8_t packetLen = 2;
if (message != NULL) { if (message != NULL) {
@ -496,30 +497,37 @@ void AsyncWebSocketClient::_onDisconnect() {
} }
void AsyncWebSocketClient::_onData(void* pbuf, size_t plen) { void AsyncWebSocketClient::_onData(void* pbuf, size_t plen) {
// Serial.println("onData");
_lastMessageTime = millis(); _lastMessageTime = millis();
uint8_t* data = (uint8_t*)pbuf; uint8_t* data = (uint8_t*)pbuf;
while (plen > 0) { while (plen > 0) {
if (!_pstate) { if (!_pstate) {
const uint8_t* fdata = data; const uint8_t* fdata = data;
_pinfo.index = 0; _pinfo.index = 0;
_pinfo.final = (fdata[0] & 0x80) != 0; _pinfo.final = (fdata[0] & 0x80) != 0;
_pinfo.opcode = fdata[0] & 0x0F; _pinfo.opcode = fdata[0] & 0x0F;
_pinfo.masked = (fdata[1] & 0x80) != 0; _pinfo.masked = (fdata[1] & 0x80) != 0;
_pinfo.len = fdata[1] & 0x7F; _pinfo.len = fdata[1] & 0x7F;
// log_d("WS[%" PRIu32 "]: _onData: %" PRIu32, _clientId, plen);
// log_d("WS[%" PRIu32 "]: _status = %" PRIu32, _clientId, _status);
// log_d("WS[%" PRIu32 "]: _pinfo: index: %" PRIu64 ", final: %" PRIu8 ", opcode: %" PRIu8 ", masked: %" PRIu8 ", len: %" PRIu64, _clientId, _pinfo.index, _pinfo.final, _pinfo.opcode, _pinfo.masked, _pinfo.len);
data += 2; data += 2;
plen -= 2; plen -= 2;
if (_pinfo.len == 126) {
if (_pinfo.len == 126 && plen >= 2) {
_pinfo.len = fdata[3] | (uint16_t)(fdata[2]) << 8; _pinfo.len = fdata[3] | (uint16_t)(fdata[2]) << 8;
data += 2; data += 2;
plen -= 2; plen -= 2;
} else if (_pinfo.len == 127) {
} else if (_pinfo.len == 127 && plen >= 8) {
_pinfo.len = fdata[9] | (uint16_t)(fdata[8]) << 8 | (uint32_t)(fdata[7]) << 16 | (uint32_t)(fdata[6]) << 24 | (uint64_t)(fdata[5]) << 32 | (uint64_t)(fdata[4]) << 40 | (uint64_t)(fdata[3]) << 48 | (uint64_t)(fdata[2]) << 56; _pinfo.len = fdata[9] | (uint16_t)(fdata[8]) << 8 | (uint32_t)(fdata[7]) << 16 | (uint32_t)(fdata[6]) << 24 | (uint64_t)(fdata[5]) << 32 | (uint64_t)(fdata[4]) << 40 | (uint64_t)(fdata[3]) << 48 | (uint64_t)(fdata[2]) << 56;
data += 8; data += 8;
plen -= 8; plen -= 8;
} }
if (_pinfo.masked) { if (_pinfo.masked && plen >= 4) { // if ws.close() is called, Safari sends a close frame with plen 2 and masked bit set. We must not decrement plen which is already 0.
memcpy(_pinfo.mask, data, 4); memcpy(_pinfo.mask, data, 4);
data += 4; data += 4;
plen -= 4; plen -= 4;
@ -772,6 +780,7 @@ void AsyncWebSocket::_handleEvent(AsyncWebSocketClient* client, AwsEventType typ
AsyncWebSocketClient* AsyncWebSocket::_newClient(AsyncWebServerRequest* request) { AsyncWebSocketClient* AsyncWebSocket::_newClient(AsyncWebServerRequest* request) {
_clients.emplace_back(request, this); _clients.emplace_back(request, this);
_handleEvent(&_clients.back(), WS_EVT_CONNECT, request, NULL, 0);
return &_clients.back(); return &_clients.back();
} }

View File

@ -48,10 +48,10 @@
#include "literals.h" #include "literals.h"
#define ASYNCWEBSERVER_VERSION "3.3.23" #define ASYNCWEBSERVER_VERSION "3.4.5"
#define ASYNCWEBSERVER_VERSION_MAJOR 3 #define ASYNCWEBSERVER_VERSION_MAJOR 3
#define ASYNCWEBSERVER_VERSION_MINOR 3 #define ASYNCWEBSERVER_VERSION_MINOR 4
#define ASYNCWEBSERVER_VERSION_REVISION 23 #define ASYNCWEBSERVER_VERSION_REVISION 5
#define ASYNCWEBSERVER_FORK_mathieucarbou #define ASYNCWEBSERVER_FORK_mathieucarbou
#ifdef ASYNCWEBSERVER_REGEX #ifdef ASYNCWEBSERVER_REGEX

View File

@ -28,14 +28,14 @@
using namespace asyncsrv; using namespace asyncsrv;
enum { PARSE_REQ_START, enum { PARSE_REQ_START = 0,
PARSE_REQ_HEADERS, PARSE_REQ_HEADERS = 1,
PARSE_REQ_BODY, PARSE_REQ_BODY = 2,
PARSE_REQ_END, PARSE_REQ_END = 3,
PARSE_REQ_FAIL }; PARSE_REQ_FAIL = 4 };
AsyncWebServerRequest::AsyncWebServerRequest(AsyncWebServer* s, AsyncClient* c) AsyncWebServerRequest::AsyncWebServerRequest(AsyncWebServer* s, AsyncClient* c)
: _client(c), _server(s), _handler(NULL), _response(NULL), _temp(), _parseState(0), _version(0), _method(HTTP_ANY), _url(), _host(), _contentType(), _boundary(), _authorization(), _reqconntype(RCT_HTTP), _authMethod(AsyncAuthType::AUTH_NONE), _isMultipart(false), _isPlainPost(false), _expectingContinue(false), _contentLength(0), _parsedLength(0), _multiParseState(0), _boundaryPosition(0), _itemStartIndex(0), _itemSize(0), _itemName(), _itemFilename(), _itemType(), _itemValue(), _itemBuffer(0), _itemBufferIndex(0), _itemIsFile(false), _tempObject(NULL) { : _client(c), _server(s), _handler(NULL), _response(NULL), _temp(), _parseState(PARSE_REQ_START), _version(0), _method(HTTP_ANY), _url(), _host(), _contentType(), _boundary(), _authorization(), _reqconntype(RCT_HTTP), _authMethod(AsyncAuthType::AUTH_NONE), _isMultipart(false), _isPlainPost(false), _expectingContinue(false), _contentLength(0), _parsedLength(0), _multiParseState(0), _boundaryPosition(0), _itemStartIndex(0), _itemSize(0), _itemName(), _itemFilename(), _itemType(), _itemValue(), _itemBuffer(0), _itemBufferIndex(0), _itemIsFile(false), _tempObject(NULL) {
c->onError([](void* r, AsyncClient* c, int8_t error) { (void)c; AsyncWebServerRequest *req = (AsyncWebServerRequest*)r; req->_onError(error); }, this); c->onError([](void* r, AsyncClient* c, int8_t error) { (void)c; AsyncWebServerRequest *req = (AsyncWebServerRequest*)r; req->_onError(error); }, this);
c->onAck([](void* r, AsyncClient* c, size_t len, uint32_t time) { (void)c; AsyncWebServerRequest *req = (AsyncWebServerRequest*)r; req->_onAck(len, time); }, this); c->onAck([](void* r, AsyncClient* c, size_t len, uint32_t time) { (void)c; AsyncWebServerRequest *req = (AsyncWebServerRequest*)r; req->_onAck(len, time); }, this);
c->onDisconnect([](void* r, AsyncClient* c) { AsyncWebServerRequest *req = (AsyncWebServerRequest*)r; req->_onDisconnect(); delete c; }, this); c->onDisconnect([](void* r, AsyncClient* c) { AsyncWebServerRequest *req = (AsyncWebServerRequest*)r; req->_onDisconnect(); delete c; }, this);
@ -67,6 +67,18 @@ AsyncWebServerRequest::~AsyncWebServerRequest() {
} }
void AsyncWebServerRequest::_onData(void* buf, size_t len) { void AsyncWebServerRequest::_onData(void* buf, size_t len) {
// SSL/TLS handshake detection
#ifndef ASYNC_TCP_SSL_ENABLED
if (_parseState == PARSE_REQ_START && len && ((uint8_t*)buf)[0] == 0x16) { // 0x16 indicates a Handshake message (SSL/TLS).
#ifdef ESP32
log_d("SSL/TLS handshake detected: resetting connection");
#endif
_parseState = PARSE_REQ_FAIL;
_client->abort();
return;
}
#endif
size_t i = 0; size_t i = 0;
while (true) { while (true) {
@ -74,6 +86,12 @@ void AsyncWebServerRequest::_onData(void* buf, size_t len) {
// Find new line in buf // Find new line in buf
char* str = (char*)buf; char* str = (char*)buf;
for (i = 0; i < len; i++) { for (i = 0; i < len; i++) {
// Check for null characters in header
if (!str[i]) {
_parseState = PARSE_REQ_FAIL;
_client->abort();
return;
}
if (str[i] == '\n') { if (str[i] == '\n') {
break; break;
} }
@ -142,6 +160,8 @@ void AsyncWebServerRequest::_onData(void* buf, size_t len) {
if (!_sent) { if (!_sent) {
if (!_response) if (!_response)
send(501, T_text_plain, "Handler did not handle the request"); send(501, T_text_plain, "Handler did not handle the request");
else if (!_response->_sourceValid())
send(500, T_text_plain, "Invalid data in handler");
_client->setRxTimeout(0); _client->setRxTimeout(0);
_response->_respond(this); _response->_respond(this);
_sent = true; _sent = true;
@ -246,6 +266,8 @@ bool AsyncWebServerRequest::_parseReqHead() {
_method = HTTP_HEAD; _method = HTTP_HEAD;
} else if (m == T_OPTIONS) { } else if (m == T_OPTIONS) {
_method = HTTP_OPTIONS; _method = HTTP_OPTIONS;
} else {
return false;
} }
String g; String g;
@ -257,6 +279,9 @@ bool AsyncWebServerRequest::_parseReqHead() {
_url = urlDecode(u); _url = urlDecode(u);
_addGetParams(g); _addGetParams(g);
if (!_url.length())
return false;
if (!_temp.startsWith(T_HTTP_1_0)) if (!_temp.startsWith(T_HTTP_1_0))
_version = 1; _version = 1;
@ -564,10 +589,14 @@ void AsyncWebServerRequest::_parseLine() {
if (_parseState == PARSE_REQ_START) { if (_parseState == PARSE_REQ_START) {
if (!_temp.length()) { if (!_temp.length()) {
_parseState = PARSE_REQ_FAIL; _parseState = PARSE_REQ_FAIL;
_client->close(); _client->abort();
} else { } else {
_parseReqHead(); if (_parseReqHead()) {
_parseState = PARSE_REQ_HEADERS; _parseState = PARSE_REQ_HEADERS;
} else {
_parseState = PARSE_REQ_FAIL;
_client->abort();
}
} }
return; return;
} }
@ -589,6 +618,8 @@ void AsyncWebServerRequest::_parseLine() {
if (!_sent) { if (!_sent) {
if (!_response) if (!_response)
send(501, T_text_plain, "Handler did not handle the request"); send(501, T_text_plain, "Handler did not handle the request");
else if (!_response->_sourceValid())
send(500, T_text_plain, "Invalid data in handler");
_client->setRxTimeout(0); _client->setRxTimeout(0);
_response->_respond(this); _response->_respond(this);
_sent = true; _sent = true;
@ -765,14 +796,6 @@ void AsyncWebServerRequest::send(AsyncWebServerResponse* response) {
if (_response) if (_response)
delete _response; delete _response;
_response = response; _response = response;
if (_response == NULL) {
_client->close(true);
_onDisconnect();
_sent = true;
return;
}
if (!_response->_sourceValid())
send(500);
} }
void AsyncWebServerRequest::redirect(const char* url, int code) { void AsyncWebServerRequest::redirect(const char* url, int code) {

View File

@ -47,6 +47,10 @@ class AsyncBasicResponse : public AsyncWebServerResponse {
class AsyncAbstractResponse : public AsyncWebServerResponse { class AsyncAbstractResponse : public AsyncWebServerResponse {
private: private:
// amount of responce data in-flight, i.e. sent, but not acked yet
size_t _in_flight{0};
// in-flight queue credits
size_t _in_flight_credit{2};
String _head; String _head;
// Data is inserted into cache at begin(). // Data is inserted into cache at begin().
// This is inefficient with vector, but if we use some other container, // This is inefficient with vector, but if we use some other container,

View File

@ -352,7 +352,21 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest* request, size_t len, u
request->client()->close(); request->client()->close();
return 0; return 0;
} }
// return a credit for each chunk of acked data (polls does not give any credits)
if (len)
++_in_flight_credit;
// for chunked responses ignore acks if there are no _in_flight_credits left
if (_chunked && !_in_flight_credit) {
#ifdef ESP32
log_d("(chunk) out of in-flight credits");
#endif
return 0;
}
_ackedLength += len; _ackedLength += len;
_in_flight -= (_in_flight > len) ? len : _in_flight;
// get the size of available sock space
size_t space = request->client()->space(); size_t space = request->client()->space();
size_t headLen = _head.length(); size_t headLen = _head.length();
@ -364,16 +378,31 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest* request, size_t len, u
String out = _head.substring(0, space); String out = _head.substring(0, space);
_head = _head.substring(space); _head = _head.substring(space);
_writtenLength += request->client()->write(out.c_str(), out.length()); _writtenLength += request->client()->write(out.c_str(), out.length());
_in_flight += out.length();
--_in_flight_credit; // take a credit
return out.length(); return out.length();
} }
} }
if (_state == RESPONSE_CONTENT) { if (_state == RESPONSE_CONTENT) {
// for response data we need to control the queue and in-flight fragmentation. Sending small chunks could give low latency,
// but flood asynctcp's queue and fragment socket buffer space for large responses.
// Let's ignore polled acks and acks in case when we have more in-flight data then the available socket buff space.
// That way we could balance on having half the buffer in-flight while another half is filling up, while minimizing events in asynctcp q
if (_in_flight > space) {
// log_d("defer user call %u/%u", _in_flight, space);
// take the credit back since we are ignoring this ack and rely on other inflight data
if (len)
--_in_flight_credit;
return 0;
}
size_t outLen; size_t outLen;
if (_chunked) { if (_chunked) {
if (space <= 8) { if (space <= 8) {
return 0; return 0;
} }
outLen = space; outLen = space;
} else if (!_sendContentLength) { } else if (!_sendContentLength) {
outLen = space; outLen = space;
@ -422,6 +451,8 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest* request, size_t len, u
if (outLen) { if (outLen) {
_writtenLength += request->client()->write((const char*)buf, outLen); _writtenLength += request->client()->write((const char*)buf, outLen);
_in_flight += outLen;
--_in_flight_credit; // take a credit
} }
if (_chunked) { if (_chunked) {

View File

@ -56,8 +56,7 @@ AsyncWebServer::AsyncWebServer(uint16_t port)
c->setRxTimeout(3); c->setRxTimeout(3);
AsyncWebServerRequest* r = new AsyncWebServerRequest((AsyncWebServer*)s, c); AsyncWebServerRequest* r = new AsyncWebServerRequest((AsyncWebServer*)s, c);
if (r == NULL) { if (r == NULL) {
c->close(true); c->abort();
c->free();
delete c; delete c;
} }
}, },

View File

@ -65,6 +65,7 @@ namespace asyncsrv {
static constexpr const char* T_response = "response"; static constexpr const char* T_response = "response";
static constexpr const char* T_retry_ = "retry: "; static constexpr const char* T_retry_ = "retry: ";
static constexpr const char* T_retry_after = "retry-after"; static constexpr const char* T_retry_after = "retry-after";
static constexpr const char* T_nn = "\n\n";
static constexpr const char* T_rn = "\r\n"; static constexpr const char* T_rn = "\r\n";
static constexpr const char* T_rnrn = "\r\n\r\n"; static constexpr const char* T_rnrn = "\r\n\r\n";
static constexpr const char* T_Transfer_Encoding = "transfer-encoding"; static constexpr const char* T_Transfer_Encoding = "transfer-encoding";