Aktualizace na verzi 3.4.5
This commit is contained in:
parent
7c828c70d8
commit
697a3adf08
79
README.md
79
README.md
@ -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()`
|
||||||
|
@ -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);
|
||||||
|
@ -57,10 +57,20 @@ 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() {
|
||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
257
examples/SSE_perftest/SSE_perftest.ino
Normal file
257
examples/SSE_perftest/SSE_perftest.ino
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -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
|
||||||
|
@ -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]
|
||||||
|
@ -23,30 +23,44 @@
|
|||||||
#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)
|
||||||
|
return str;
|
||||||
|
|
||||||
size_t messageLen = strlen(message);
|
size_t messageLen = strlen(message);
|
||||||
char* lineStart = (char*)message;
|
char* lineStart = (char*)message;
|
||||||
char* lineEnd;
|
char* lineEnd;
|
||||||
@ -54,85 +68,54 @@ static String generateEventMessage(const char* message, const char* event, uint3
|
|||||||
char* nextN = strchr(lineStart, '\n');
|
char* nextN = strchr(lineStart, '\n');
|
||||||
char* nextR = strchr(lineStart, '\r');
|
char* nextR = strchr(lineStart, '\r');
|
||||||
if (nextN == NULL && nextR == NULL) {
|
if (nextN == NULL && nextR == NULL) {
|
||||||
size_t llen = ((char*)message + messageLen) - lineStart;
|
// a message is a single-line string
|
||||||
char* ldata = (char*)malloc(llen + 1);
|
str += T_data_;
|
||||||
if (ldata != NULL) {
|
str += message;
|
||||||
memcpy(ldata, lineStart, llen);
|
str += T_nn;
|
||||||
ldata[llen] = 0;
|
return str;
|
||||||
ev += T_data_;
|
|
||||||
ev += ldata;
|
|
||||||
ev += T_rnrn;
|
|
||||||
free(ldata);
|
|
||||||
}
|
}
|
||||||
lineStart = (char*)message + messageLen;
|
|
||||||
} else {
|
// a message is a multi-line string
|
||||||
char* nextLine = NULL;
|
char* nextLine = NULL;
|
||||||
if (nextN != NULL && nextR != NULL) {
|
if (nextN != NULL && nextR != NULL) { // windows line-ending \r\n
|
||||||
if (nextR < nextN) {
|
if (nextR + 1 == nextN) {
|
||||||
|
// normal \r\n sequense
|
||||||
lineEnd = nextR;
|
lineEnd = nextR;
|
||||||
if (nextN == (nextR + 1))
|
|
||||||
nextLine = nextN + 1;
|
nextLine = nextN + 1;
|
||||||
else
|
|
||||||
nextLine = nextR + 1;
|
|
||||||
} else {
|
} else {
|
||||||
lineEnd = nextN;
|
// some abnormal \n \r mixed sequence
|
||||||
if (nextR == (nextN + 1))
|
lineEnd = std::min(nextR, nextN);
|
||||||
nextLine = nextR + 1;
|
nextLine = lineEnd + 1;
|
||||||
else
|
|
||||||
nextLine = nextN + 1;
|
|
||||||
}
|
}
|
||||||
} else if (nextN != NULL) {
|
} else if (nextN != NULL) { // Unix/Mac OS X LF
|
||||||
lineEnd = nextN;
|
lineEnd = nextN;
|
||||||
nextLine = nextN + 1;
|
nextLine = nextN + 1;
|
||||||
} else {
|
} else { // some ancient garbage
|
||||||
lineEnd = nextR;
|
lineEnd = nextR;
|
||||||
nextLine = nextR + 1;
|
nextLine = nextR + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t llen = lineEnd - lineStart;
|
str += T_data_;
|
||||||
char* ldata = (char*)malloc(llen + 1);
|
str.concat(lineStart, lineEnd - lineStart);
|
||||||
if (ldata != NULL) {
|
str += ASYNC_SSE_NEW_LINE_CHAR; // \n
|
||||||
memcpy(ldata, lineStart, llen);
|
|
||||||
ldata[llen] = 0;
|
|
||||||
ev += T_data_;
|
|
||||||
ev += ldata;
|
|
||||||
ev += T_rn;
|
|
||||||
free(ldata);
|
|
||||||
}
|
|
||||||
lineStart = nextLine;
|
lineStart = nextLine;
|
||||||
if (lineStart == ((char*)message + messageLen))
|
|
||||||
ev += T_rn;
|
|
||||||
}
|
|
||||||
} while (lineStart < ((char*)message + messageLen));
|
} while (lineStart < ((char*)message + messageLen));
|
||||||
}
|
|
||||||
|
|
||||||
return ev;
|
// 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
|
||||||
|
|
||||||
|
// 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();
|
_runQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AsyncEventSourceClient::_onPoll() {
|
void AsyncEventSourceClient::_onPoll() {
|
||||||
|
if (_messageQueue.size()) {
|
||||||
#ifdef ESP32
|
#ifdef ESP32
|
||||||
// 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
|
||||||
if (_messageQueue.size()) {
|
|
||||||
_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;
|
void AsyncEventSourceClient::set_max_inflight_bytes(size_t value) {
|
||||||
while (len && _messageQueue.size()) {
|
if (value >= SSE_MIN_INFLIGH && value <= SSE_MAX_INFLIGH)
|
||||||
len = _messageQueue.front().ack(len);
|
_max_inflight = value;
|
||||||
if (_messageQueue.front().finished()) {
|
|
||||||
_messageQueue.pop_front();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
@ -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:
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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) {
|
||||||
|
@ -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,
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user