From 697a3adf089c6522ea5cd6d3fc35407a7667d479 Mon Sep 17 00:00:00 2001 From: Pablo2048 Date: Fri, 3 Jan 2025 16:45:58 +0100 Subject: [PATCH] Aktualizace na verzi 3.4.5 --- README.md | 79 +++--- docs/index.md | 81 +++--- examples/Issue162/Issue162.ino | 29 ++- examples/SSE_perftest/SSE_perftest.ino | 257 +++++++++++++++++++ examples/SimpleServer/SimpleServer.ino | 141 +++++++---- library.json_ | 4 +- library.properties | 2 +- platformio.ini | 24 +- src/AsyncEventSource.cpp | 326 ++++++++++++++----------- src/AsyncEventSource.h | 174 +++++++++++-- src/AsyncWebSocket.cpp | 19 +- src/ESPAsyncWebServer.h | 6 +- src/WebRequest.cpp | 57 +++-- src/WebResponseImpl.h | 4 + src/WebResponses.cpp | 31 +++ src/WebServer.cpp | 3 +- src/literals.h | 1 + 17 files changed, 893 insertions(+), 345 deletions(-) create mode 100644 examples/SSE_perftest/SSE_perftest.ino diff --git a/README.md b/README.md index b4a3442..ccfde8b 100644 --- a/README.md +++ b/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 lib_compat_mode = strict lib_ldf_mode = chain -lib_deps = mathieucarbou/ESPAsyncWebServer @ 3.3.23 +lib_deps = mathieucarbou/ESPAsyncWebServer @ 3.4.5 ``` **Dependencies:** -- **ESP32 with AsyncTCP**: `mathieucarbou/AsyncTCP @ 3.2.14` - Arduino IDE: [https://github.com/mathieucarbou/AsyncTCP#v3.2.14](https://github.com/mathieucarbou/AsyncTCP/releases) +- **ESP32 with AsyncTCP**: `mathieucarbou/AsyncTCP @ 3.3.1` + 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` @@ -99,9 +99,9 @@ AsyncTCPSock can be used instead of AsyncTCP by excluding AsyncTCP from the libr lib_compat_mode = strict lib_ldf_mode = chain 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 - mathieucarbou/ESPAsyncWebServer @ 3.3.23 + mathieucarbou/ESPAsyncWebServer @ 3.4.5 lib_ignore = AsyncTCP mathieucarbou/AsyncTCP @@ -109,14 +109,14 @@ lib_ignore = ## Performance -Performance of `mathieucarbou/ESPAsyncWebServer @ 3.3.23`: +Performance of `mathieucarbou/ESPAsyncWebServer @ 3.4.5`: ```bash > brew install autocannon > 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) @@ -133,29 +133,29 @@ Test is running for 20 seconds with 10 connections. ``` // With AsyncTCP, with 10 workers: no message discarded from the queue // -// Total: 1875 events, 468.75000000000000000000 events / second -// Total: 1870 events, 467.50000000000000000000 events / second -// Total: 1871 events, 467.75000000000000000000 events / second -// Total: 1875 events, 468.75000000000000000000 events / second -// Total: 1871 events, 467.75000000000000000000 events / second -// Total: 1805 events, 451.25000000000000000000 events / second -// Total: 1803 events, 450.75000000000000000000 events / second -// Total: 1873 events, 468.25000000000000000000 events / second -// Total: 1872 events, 468.00000000000000000000 events / second -// Total: 1805 events, 451.25000000000000000000 events / second +// Total: 2038 events, 509.50 events / second +// Total: 2120 events, 530.00 events / second +// Total: 2119 events, 529.75 events / second +// Total: 2038 events, 509.50 events / second +// Total: 2037 events, 509.25 events / second +// Total: 2119 events, 529.75 events / second +// Total: 2119 events, 529.75 events / second +// Total: 2120 events, 530.00 events / second +// Total: 2038 events, 509.50 events / second +// Total: 2038 events, 509.50 events / second // // With AsyncTCPSock, with 10 workers: no message discarded from the queue // -// Total: 1242 events, 310.50000000000000000000 events / second -// Total: 1242 events, 310.50000000000000000000 events / second -// Total: 1242 events, 310.50000000000000000000 events / second -// Total: 1242 events, 310.50000000000000000000 events / second -// Total: 1181 events, 295.25000000000000000000 events / second -// Total: 1182 events, 295.50000000000000000000 events / second -// Total: 1240 events, 310.00000000000000000000 events / second -// Total: 1181 events, 295.25000000000000000000 events / second -// Total: 1181 events, 295.25000000000000000000 events / second -// Total: 1183 events, 295.75000000000000000000 events / second +// Total: 2038 events, 509.50 events / second +// Total: 2120 events, 530.00 events / second +// Total: 2119 events, 529.75 events / second +// Total: 2038 events, 509.50 events / second +// Total: 2037 events, 509.25 events / second +// Total: 2119 events, 529.75 events / second +// Total: 2119 events, 529.75 events / second +// Total: 2120 events, 530.00 events / second +// Total: 2038 events, 509.50 events / second +// Total: 2038 events, 509.50 events / second ``` ## 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. 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` -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. +I personally use the following configuration in my projects: ```c++ - -D CONFIG_ASYNC_TCP_MAX_ACK_TIME=3000 - -D CONFIG_ASYNC_TCP_PRIORITY=10 - -D CONFIG_ASYNC_TCP_QUEUE_SIZE=128 - -D CONFIG_ASYNC_TCP_RUNNING_CORE=1 - -D CONFIG_ASYNC_TCP_STACK_SIZE=4096 - -D WS_MAX_QUEUED_MESSAGES=64 + -D CONFIG_ASYNC_TCP_MAX_ACK_TIME=5000 // (keep default) + -D CONFIG_ASYNC_TCP_PRIORITY=10 // (keep default) + -D CONFIG_ASYNC_TCP_QUEUE_SIZE=64 // (keep default) + -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 // reduce the stack size (default is 16K) ``` ## `AsyncWebSocketMessageBuffer` and `makeBuffer()` diff --git a/docs/index.md b/docs/index.md index d47c640..ccfde8b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -75,13 +75,13 @@ So if you need one of these feature, you will have to stick with 3.x or another ```ini lib_compat_mode = strict lib_ldf_mode = chain -lib_deps = mathieucarbou/ESPAsyncWebServer @ 3.3.23 +lib_deps = mathieucarbou/ESPAsyncWebServer @ 3.4.5 ``` **Dependencies:** -- **ESP32 with AsyncTCP**: `mathieucarbou/AsyncTCP @ 3.2.14` - Arduino IDE: [https://github.com/mathieucarbou/AsyncTCP#v3.2.14](https://github.com/mathieucarbou/AsyncTCP/releases) +- **ESP32 with AsyncTCP**: `mathieucarbou/AsyncTCP @ 3.3.1` + 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` @@ -99,9 +99,9 @@ AsyncTCPSock can be used instead of AsyncTCP by excluding AsyncTCP from the libr lib_compat_mode = strict lib_ldf_mode = chain 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 - mathieucarbou/ESPAsyncWebServer @ 3.3.23 + mathieucarbou/ESPAsyncWebServer @ 3.4.5 lib_ignore = AsyncTCP mathieucarbou/AsyncTCP @@ -109,14 +109,14 @@ lib_ignore = ## Performance -Performance of `mathieucarbou/ESPAsyncWebServer @ 3.3.23`: +Performance of `mathieucarbou/ESPAsyncWebServer @ 3.4.5`: ```bash > brew install autocannon > 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) @@ -133,29 +133,29 @@ Test is running for 20 seconds with 10 connections. ``` // With AsyncTCP, with 10 workers: no message discarded from the queue // -// Total: 1875 events, 468.75000000000000000000 events / second -// Total: 1870 events, 467.50000000000000000000 events / second -// Total: 1871 events, 467.75000000000000000000 events / second -// Total: 1875 events, 468.75000000000000000000 events / second -// Total: 1871 events, 467.75000000000000000000 events / second -// Total: 1805 events, 451.25000000000000000000 events / second -// Total: 1803 events, 450.75000000000000000000 events / second -// Total: 1873 events, 468.25000000000000000000 events / second -// Total: 1872 events, 468.00000000000000000000 events / second -// Total: 1805 events, 451.25000000000000000000 events / second +// Total: 2038 events, 509.50 events / second +// Total: 2120 events, 530.00 events / second +// Total: 2119 events, 529.75 events / second +// Total: 2038 events, 509.50 events / second +// Total: 2037 events, 509.25 events / second +// Total: 2119 events, 529.75 events / second +// Total: 2119 events, 529.75 events / second +// Total: 2120 events, 530.00 events / second +// Total: 2038 events, 509.50 events / second +// Total: 2038 events, 509.50 events / second // // With AsyncTCPSock, with 10 workers: no message discarded from the queue // -// Total: 1242 events, 310.50000000000000000000 events / second -// Total: 1242 events, 310.50000000000000000000 events / second -// Total: 1242 events, 310.50000000000000000000 events / second -// Total: 1242 events, 310.50000000000000000000 events / second -// Total: 1181 events, 295.25000000000000000000 events / second -// Total: 1182 events, 295.50000000000000000000 events / second -// Total: 1240 events, 310.00000000000000000000 events / second -// Total: 1181 events, 295.25000000000000000000 events / second -// Total: 1181 events, 295.25000000000000000000 events / second -// Total: 1183 events, 295.75000000000000000000 events / second +// Total: 2038 events, 509.50 events / second +// Total: 2120 events, 530.00 events / second +// Total: 2119 events, 529.75 events / second +// Total: 2038 events, 509.50 events / second +// Total: 2037 events, 509.25 events / second +// Total: 2119 events, 529.75 events / second +// Total: 2119 events, 529.75 events / second +// Total: 2120 events, 530.00 events / second +// Total: 2038 events, 509.50 events / second +// Total: 2038 events, 509.50 events / second ``` ## 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. 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` -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. +I personally use the following configuration in my projects: ```c++ - -D CONFIG_ASYNC_TCP_MAX_ACK_TIME=3000 - -D CONFIG_ASYNC_TCP_PRIORITY=10 - -D CONFIG_ASYNC_TCP_QUEUE_SIZE=128 - -D CONFIG_ASYNC_TCP_RUNNING_CORE=1 - -D CONFIG_ASYNC_TCP_STACK_SIZE=4096 - -D WS_MAX_QUEUED_MESSAGES=64 + -D CONFIG_ASYNC_TCP_MAX_ACK_TIME=5000 // (keep default) + -D CONFIG_ASYNC_TCP_PRIORITY=10 // (keep default) + -D CONFIG_ASYNC_TCP_QUEUE_SIZE=64 // (keep default) + -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 // reduce the stack size (default is 16K) ``` ## `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" AsyncCallbackJsonWebHandler* handler = new AsyncCallbackJsonWebHandler("/rest/endpoint", [](AsyncWebServerRequest *request, JsonVariant &json) { - JsonObject& jsonObj = json.as(); + JsonObject jsonObj = json.as(); // ... }); server.addHandler(handler); diff --git a/examples/Issue162/Issue162.ino b/examples/Issue162/Issue162.ino index 9f7312f..adc82d9 100644 --- a/examples/Issue162/Issue162.ino +++ b/examples/Issue162/Issue162.ino @@ -56,13 +56,23 @@ void setup() { + + +

Open your browser console!

+ + +)"; + +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; + } +} diff --git a/examples/SimpleServer/SimpleServer.ino b/examples/SimpleServer/SimpleServer.ino index 6d2863f..c8d74a5 100644 --- a/examples/SimpleServer/SimpleServer.ino +++ b/examples/SimpleServer/SimpleServer.ino @@ -147,6 +147,8 @@ AsyncMiddlewareFunction complexAuth([](AsyncWebServerRequest* request, ArMiddlew AuthorizationMiddleware authz([](AsyncWebServerRequest* request) { return request->getAttribute("role") == "staff"; }); +int wsClients = 0; + ///////////////////////////////////////////////////////////////////////////////////////////////////// const char* PARAM_MESSAGE PROGMEM = "message"; @@ -407,6 +409,7 @@ void setup() { // PERF TEST: // > brew install autocannon // > 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) { request->send(200, "text/html", htmlContent); }); @@ -503,6 +506,29 @@ void setup() { 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 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) { (void)len; if (type == WS_EVT_CONNECT) { + wsClients++; + ws.textAll("new client connected"); Serial.println("ws connect"); client->setCloseClientOnQueueFull(false); client->ping(); } else if (type == WS_EVT_DISCONNECT) { + wsClients--; + ws.textAll("client disconnected"); Serial.println("ws disconnect"); } else if (type == WS_EVT_ERROR) { Serial.println("ws error"); @@ -651,59 +681,78 @@ void setup() { // // some perf tests: // 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; // - // 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: 727 events, 181.75000000000000000000 events / second - // Total: 1386 events, 346.50000000000000000000 events / second - // Total: 1385 events, 346.25000000000000000000 events / second - // Total: 1276 events, 319.00000000000000000000 events / second - // Total: 1411 events, 352.75000000000000000000 events / second - // Total: 1276 events, 319.00000000000000000000 events / second - // Total: 1333 events, 333.25000000000000000000 events / second - // Total: 1250 events, 312.50000000000000000000 events / second - // Total: 1275 events, 318.75000000000000000000 events / second - // Total: 1271 events, 317.75000000000000000000 events / second - // Total: 1271 events, 317.75000000000000000000 events / second - // Total: 1254 events, 313.50000000000000000000 events / second - // Total: 1251 events, 312.75000000000000000000 events / second - // Total: 1254 events, 313.50000000000000000000 events / second - // Total: 1262 events, 315.50000000000000000000 events / second + // Total: 1711 events, 427.75 events / second + // Total: 1711 events, 427.75 events / second + // Total: 1626 events, 406.50 events / second + // Total: 1562 events, 390.50 events / second + // Total: 1706 events, 426.50 events / second + // Total: 1659 events, 414.75 events / second + // Total: 1624 events, 406.00 events / second + // Total: 1706 events, 426.50 events / second + // Total: 1487 events, 371.75 events / second + // Total: 1573 events, 393.25 events / second + // Total: 1569 events, 392.25 events / second + // Total: 1559 events, 389.75 events / second + // Total: 1560 events, 390.00 events / second + // Total: 1562 events, 390.50 events / second + // Total: 1626 events, 406.50 events / second // // With AsyncTCP, with 10 workers: // - // Total: 1875 events, 468.75000000000000000000 events / second - // Total: 1870 events, 467.50000000000000000000 events / second - // Total: 1871 events, 467.75000000000000000000 events / second - // Total: 1875 events, 468.75000000000000000000 events / second - // Total: 1871 events, 467.75000000000000000000 events / second - // Total: 1805 events, 451.25000000000000000000 events / second - // Total: 1803 events, 450.75000000000000000000 events / second - // Total: 1873 events, 468.25000000000000000000 events / second - // Total: 1872 events, 468.00000000000000000000 events / second - // Total: 1805 events, 451.25000000000000000000 events / second + // Total: 2038 events, 509.50 events / second + // Total: 2120 events, 530.00 events / second + // Total: 2119 events, 529.75 events / second + // Total: 2038 events, 509.50 events / second + // Total: 2037 events, 509.25 events / second + // Total: 2119 events, 529.75 events / second + // Total: 2119 events, 529.75 events / second + // Total: 2120 events, 530.00 events / second + // Total: 2038 events, 509.50 events / second + // Total: 2038 events, 509.50 events / second // // With AsyncTCPSock, with 16 workers: ESP32 CRASH !!! // // With AsyncTCPSock, with 10 workers: // - // Total: 1242 events, 310.50000000000000000000 events / second - // Total: 1242 events, 310.50000000000000000000 events / second - // Total: 1242 events, 310.50000000000000000000 events / second - // Total: 1242 events, 310.50000000000000000000 events / second - // Total: 1181 events, 295.25000000000000000000 events / second - // Total: 1182 events, 295.50000000000000000000 events / second - // Total: 1240 events, 310.00000000000000000000 events / second - // Total: 1181 events, 295.25000000000000000000 events / second - // Total: 1181 events, 295.25000000000000000000 events / second - // Total: 1183 events, 295.75000000000000000000 events / second + // Total: 1242 events, 310.50 events / second + // Total: 1242 events, 310.50 events / second + // Total: 1242 events, 310.50 events / second + // Total: 1242 events, 310.50 events / second + // Total: 1181 events, 295.25 events / second + // Total: 1182 events, 295.50 events / second + // Total: 1240 events, 310.00 events / second + // Total: 1181 events, 295.25 events / second + // Total: 1181 events, 295.25 events / second + // Total: 1183 events, 295.75 events / second // server.addHandler(&events); - // Run: websocat ws://192.168.4.1/ws - server.addHandler(&ws); + // Run in terminal 1: websocat ws://192.168.4.1/ws => stream data + // 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") server.addHandler(jsonHandler); @@ -721,6 +770,8 @@ uint32_t deltaSSE = 10; uint32_t lastWS = 0; uint32_t deltaWS = 100; +uint32_t lastHeap = 0; + void loop() { uint32_t now = millis(); if (now - lastSSE >= deltaSSE) { @@ -729,9 +780,15 @@ void loop() { } if (now - lastWS >= deltaWS) { ws.printfAll("kp%.4f", (10.0 / 3.0)); - for (auto& client : ws.getClients()) { - client.printf("kp%.4f", (10.0 / 3.0)); - } + // for (auto& client : ws.getClients()) { + // client.printf("kp%.4f", (10.0 / 3.0)); + // } lastWS = millis(); } +#ifdef ESP32 + if (now - lastHeap >= 2000) { + Serial.printf("Free heap: %" PRIu32 "\n", ESP.getFreeHeap()); + lastHeap = now; + } +#endif } diff --git a/library.json_ b/library.json_ index f7e219c..3690b5e 100644 --- a/library.json_ +++ b/library.json_ @@ -1,6 +1,6 @@ { "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.", "keywords": "http,async,websocket,webserver", "homepage": "https://github.com/mathieucarbou/ESPAsyncWebServer", @@ -28,7 +28,7 @@ { "owner": "mathieucarbou", "name": "AsyncTCP", - "version": "^3.2.14", + "version": "^3.3.1", "platforms": "espressif32" }, { diff --git a/library.properties b/library.properties index 865e911..31a6e1d 100644 --- a/library.properties +++ b/library.properties @@ -1,6 +1,6 @@ name=ESP Async WebServer includes=ESPAsyncWebServer.h -version=3.3.23 +version=3.4.5 author=Me-No-Dev maintainer=Mathieu Carbou sentence=Asynchronous HTTP and WebSocket Server Library for ESP32, ESP8266 and RP2040 diff --git a/platformio.ini b/platformio.ini index 328f427..cd19455 100644 --- a/platformio.ini +++ b/platformio.ini @@ -2,11 +2,11 @@ default_envs = arduino-2, arduino-3, arduino-310, esp8266, raspberrypi lib_dir = . ; src_dir = examples/CaptivePortal -; src_dir = examples/SimpleServer +src_dir = examples/SimpleServer ; src_dir = examples/StreamFiles ; src_dir = examples/Filters ; src_dir = examples/Issue85 -src_dir = examples/Issue162 +; src_dir = examples/Issue162 [env] framework = arduino @@ -14,11 +14,11 @@ build_flags = -Og -Wall -Wextra -Wno-unused-parameter - -D CONFIG_ARDUHAL_LOG_COLORS + ; -D CONFIG_ARDUHAL_LOG_COLORS -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_QUEUE_SIZE=128 + -D CONFIG_ASYNC_TCP_QUEUE_SIZE=64 -D CONFIG_ASYNC_TCP_RUNNING_CORE=1 -D CONFIG_ASYNC_TCP_STACK_SIZE=4096 upload_protocol = esptool @@ -31,7 +31,7 @@ lib_deps = ; bblanchon/ArduinoJson @ 5.13.4 ; bblanchon/ArduinoJson @ 6.21.5 bblanchon/ArduinoJson @ 7.2.1 - mathieucarbou/AsyncTCP @ 3.2.14 + mathieucarbou/AsyncTCP @ 3.3.1 board = esp32dev board_build.partitions = partitions-4MB.csv board_build.filesystem = littlefs @@ -49,21 +49,21 @@ platform = https://github.com/pioarduino/platform-espressif32/releases/download/ ; board = esp32-s3-devkitc-1 ; board = esp32-c6-devkitc-1 lib_deps = - mathieucarbou/AsyncTCP @ 3.2.14 + mathieucarbou/AsyncTCP @ 3.3.1 [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-c6-devkitc-1 ; board = esp32-h2-devkitm-1 [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} -D PERF_TEST=1 [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 = https://github.com/mathieucarbou/AsyncTCPSock/archive/refs/tags/v1.0.3-dev.zip 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 board = ${sysenv.PIO_BOARD} lib_deps = - mathieucarbou/AsyncTCP @ 3.2.14 + mathieucarbou/AsyncTCP @ 3.3.1 [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} [env:ci-esp8266] diff --git a/src/AsyncEventSource.cpp b/src/AsyncEventSource.cpp index 1c0ff2a..c06cb8f 100644 --- a/src/AsyncEventSource.cpp +++ b/src/AsyncEventSource.cpp @@ -23,116 +23,99 @@ #endif #include "AsyncEventSource.h" +#define ASYNC_SSE_NEW_LINE_CHAR (char)0xa + using namespace asyncsrv; 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) { - ev += T_retry_; - ev += reconnect; - ev += T_rn; + str += T_retry_; + str += reconnect; + str += ASYNC_SSE_NEW_LINE_CHAR; // '\n' } if (id) { - ev += T_id__; - ev += id; - ev += T_rn; + str += T_id__; + str += id; + str += ASYNC_SSE_NEW_LINE_CHAR; // '\n' } if (event != NULL) { - ev += T_event_; - ev += event; - ev += T_rn; + str += T_event_; + str += event; + str += ASYNC_SSE_NEW_LINE_CHAR; // '\n' } - if (message != NULL) { - size_t messageLen = strlen(message); - char* lineStart = (char*)message; - char* lineEnd; - do { - char* nextN = strchr(lineStart, '\n'); - char* nextR = strchr(lineStart, '\r'); - if (nextN == NULL && nextR == NULL) { - size_t llen = ((char*)message + messageLen) - lineStart; - char* ldata = (char*)malloc(llen + 1); - if (ldata != NULL) { - memcpy(ldata, lineStart, llen); - ldata[llen] = 0; - ev += T_data_; - ev += ldata; - ev += T_rnrn; - free(ldata); - } - lineStart = (char*)message + messageLen; + if (!message) + return str; + + size_t messageLen = strlen(message); + char* lineStart = (char*)message; + char* lineEnd; + do { + char* nextN = strchr(lineStart, '\n'); + char* nextR = strchr(lineStart, '\r'); + if (nextN == NULL && nextR == NULL) { + // a message is a single-line string + str += T_data_; + str += message; + str += T_nn; + return str; + } + + // a message is a multi-line string + char* nextLine = NULL; + if (nextN != NULL && nextR != NULL) { // windows line-ending \r\n + if (nextR + 1 == nextN) { + // normal \r\n sequense + lineEnd = nextR; + nextLine = nextN + 1; } else { - char* nextLine = NULL; - if (nextN != NULL && nextR != NULL) { - if (nextR < nextN) { - lineEnd = nextR; - if (nextN == (nextR + 1)) - nextLine = nextN + 1; - else - nextLine = nextR + 1; - } else { - lineEnd = nextN; - if (nextR == (nextN + 1)) - nextLine = nextR + 1; - else - nextLine = nextN + 1; - } - } else if (nextN != NULL) { - lineEnd = nextN; - nextLine = nextN + 1; - } else { - lineEnd = nextR; - nextLine = nextR + 1; - } - - size_t llen = lineEnd - lineStart; - char* ldata = (char*)malloc(llen + 1); - if (ldata != NULL) { - memcpy(ldata, lineStart, llen); - ldata[llen] = 0; - ev += T_data_; - ev += ldata; - ev += T_rn; - free(ldata); - } - lineStart = nextLine; - if (lineStart == ((char*)message + messageLen)) - ev += T_rn; + // some abnormal \n \r mixed sequence + lineEnd = std::min(nextR, nextN); + nextLine = lineEnd + 1; } - } while (lineStart < ((char*)message + messageLen)); - } + } else if (nextN != NULL) { // Unix/Mac OS X LF + lineEnd = nextN; + nextLine = nextN + 1; + } else { // some ancient garbage + lineEnd = nextR; + nextLine = nextR + 1; + } - return ev; + str += T_data_; + str.concat(lineStart, lineEnd - lineStart); + str += ASYNC_SSE_NEW_LINE_CHAR; // \n + + lineStart = nextLine; + } while (lineStart < ((char*)message + messageLen)); + + // append another \n to terminate message + str += ASYNC_SSE_NEW_LINE_CHAR; // '\n' + + return str; } // Message -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) { // 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) - const size_t extra = _acked + len - _len; - _acked = _len; + const size_t extra = _acked + len - _data->length(); + _acked = _data->length(); return extra; } // Return that no extra bytes left. @@ -144,13 +127,25 @@ size_t AsyncEventSourceMessage::write(AsyncClient* client) { if (!client) return 0; - if (_sent >= _len || !client->canSend()) { + if (_sent >= _data->length() || !client->canSend()) { return 0; } - size_t len = min(_len - _sent, client->space()); - size_t sent = client->add((const char*)_data + _sent, len); - _sent += sent; - return sent; + + size_t len = std::min(_data->length() - _sent, client->space()); + /* + 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) { @@ -160,20 +155,19 @@ size_t AsyncEventSourceMessage::send(AsyncClient* client) { // Client -AsyncEventSourceClient::AsyncEventSourceClient(AsyncWebServerRequest* request, AsyncEventSource* server) { - _client = request->client(); - _server = server; - _lastId = 0; +AsyncEventSourceClient::AsyncEventSourceClient(AsyncWebServerRequest* request, AsyncEventSource* server) + : _client(request->client()), _server(server) { + if (request->hasHeader(T_Last_Event_ID)) _lastId = atoi(request->getHeader(T_Last_Event_ID)->value().c_str()); _client->setRxTimeout(0); _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->onPoll([](void* r, AsyncClient* c) { (void)c; ((AsyncEventSourceClient*)(r))->_onPoll(); }, this); + _client->onAck([](void* r, AsyncClient* c, size_t len, uint32_t time) { (void)c; static_cast(r)->_onAck(len, time); }, this); + _client->onPoll([](void* r, AsyncClient* c) { (void)c; static_cast(r)->_onPoll(); }, this); _client->onData(NULL, NULL); - _client->onTimeout([this](void* r, AsyncClient* c __attribute__((unused)), uint32_t time) { ((AsyncEventSourceClient*)(r))->_onTimeout(time); }, this); - _client->onDisconnect([this](void* r, AsyncClient* c) { ((AsyncEventSourceClient*)(r))->_onDisconnect(); delete c; }, this); + _client->onTimeout([this](void* r, AsyncClient* c __attribute__((unused)), uint32_t time) { static_cast(r)->_onTimeout(time); }, this); + _client->onDisconnect([this](void* r, AsyncClient* c) { static_cast(r)->_onDisconnect(); delete c; }, this); _server->_addClient(this); delete request; @@ -190,29 +184,61 @@ AsyncEventSourceClient::~AsyncEventSourceClient() { } 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; + } #ifdef ESP32 // length() is not thread-safe, thus acquiring the lock before this call.. std::lock_guard lock(_lockmq); #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) { #ifdef ESP8266 ets_printf(String(F("ERROR: Too many messages queued\n")).c_str()); #elif defined(ESP32) - log_e("Too many messages queued: deleting message"); + log_e("Event message queue overflow: discard message"); #endif return false; } - _messageQueue.emplace_back(message, len); - // runqueue trigger when new messages added - if (_client->canSend()) { +#ifdef ESP32 + // length() is not thread-safe, thus acquiring the lock before this call.. + std::lock_guard 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(); } - return true; } @@ -221,15 +247,33 @@ void AsyncEventSourceClient::_onAck(size_t len __attribute__((unused)), uint32_t // Same here, acquiring the lock early std::lock_guard lock(_lockmq); #endif - _runQueue(); + + // adjust in-flight len + if (len < _inflight) + _inflight -= len; + else + _inflight = 0; + + // acknowledge as much messages's data as we got confirmed len from a AsyncTCP + while (len && _messageQueue.size()) { + len = _messageQueue.front().ack(len); + if (_messageQueue.front().finished()) { + // now we could release full ack'ed messages, we were keeping it unless send confirmed from AsyncTCP + _messageQueue.pop_front(); + } + } + + // try to send another batch of data + if (_messageQueue.size()) + _runQueue(); } void AsyncEventSourceClient::_onPoll() { -#ifdef ESP32 - // Same here, acquiring the lock early - std::lock_guard lock(_lockmq); -#endif if (_messageQueue.size()) { +#ifdef ESP32 + // Same here, acquiring the lock early + std::lock_guard lock(_lockmq); +#endif _runQueue(); } } @@ -251,50 +295,42 @@ void AsyncEventSourceClient::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) { if (!connected()) return false; - String ev = generateEventMessage(message, event, id, reconnect); - return _queueMessage(ev.c_str(), ev.length()); -} - -size_t AsyncEventSourceClient::packetsWaiting() const { -#ifdef ESP32 - std::lock_guard lock(_lockmq); -#endif - return _messageQueue.size(); + return _queueMessage(std::make_shared(generateEventMessage(message, event, id, reconnect))); } void AsyncEventSourceClient::_runQueue() { if (!_client) 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; for (auto i = _messageQueue.begin(); i != _messageQueue.end(); ++i) { if (!i->sent()) { const size_t bytes_written = i->write(_client); total_bytes_written += bytes_written; - if (bytes_written == 0) + _inflight += bytes_written; + if (bytes_written == 0 || _inflight > _max_inflight) { + // Serial.print("_"); break; + } } } - if (total_bytes_written > 0) + // flush socket + if (total_bytes_written) _client->send(); - - size_t len = total_bytes_written; - while (len && _messageQueue.size()) { - len = _messageQueue.front().ack(len); - if (_messageQueue.front().finished()) { - _messageQueue.pop_front(); - } - } } +void AsyncEventSourceClient::set_max_inflight_bytes(size_t value) { + if (value >= SSE_MIN_INFLIGH && value <= SSE_MAX_INFLIGH) + _max_inflight = value; +} + +/* AsyncEventSource */ + void AsyncEventSource::authorizeConnect(ArAuthorizeConnectHandler cb) { AuthorizationMiddleware* m = new AuthorizationMiddleware(401, cb); m->_freeOnRemoval = true; @@ -310,18 +346,21 @@ void AsyncEventSource::_addClient(AsyncEventSourceClient* client) { _clients.emplace_back(client); if (_connectcb) _connectcb(client); + + _adjust_inflight_window(); } void AsyncEventSource::_handleDisconnect(AsyncEventSourceClient* client) { + if (_disconnectcb) + _disconnectcb(client); #ifdef ESP32 std::lock_guard lock(_client_queue_lock); #endif - if (_disconnectcb) - _disconnectcb(client); for (auto i = _clients.begin(); i != _clients.end(); ++i) { if (i->get() == client) _clients.erase(i); } + _adjust_inflight_window(); } void AsyncEventSource::close() { @@ -358,14 +397,14 @@ size_t AsyncEventSource::avgPacketsWaiting() const { AsyncEventSource::SendStatus AsyncEventSource::send( 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(generateEventMessage(message, event, id, reconnect)); #ifdef ESP32 std::lock_guard lock(_client_queue_lock); #endif size_t hits = 0; size_t miss = 0; for (const auto& c : _clients) { - if (c->write(ev.c_str(), ev.length())) + if (c->write(shared_msg)) ++hits; else ++miss; @@ -393,7 +432,16 @@ void AsyncEventSource::handleRequest(AsyncWebServerRequest* request) { 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) { _server = server; diff --git a/src/AsyncEventSource.h b/src/AsyncEventSource.h index 10d7901..0796fae 100644 --- a/src/AsyncEventSource.h +++ b/src/AsyncEventSource.h @@ -21,22 +21,29 @@ #define ASYNCEVENTSOURCE_H_ #include + #ifdef ESP32 #include #include #ifndef SSE_MAX_QUEUED_MESSAGES #define SSE_MAX_QUEUED_MESSAGES 32 #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) #include #ifndef SSE_MAX_QUEUED_MESSAGES #define SSE_MAX_QUEUED_MESSAGES 8 #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) #include #ifndef SSE_MAX_QUEUED_MESSAGES #define SSE_MAX_QUEUED_MESSAGES 32 #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 #include @@ -53,58 +60,155 @@ class AsyncEventSourceResponse; class AsyncEventSourceClient; using ArEventHandlerFunction = std::function; using ArAuthorizeConnectHandler = ArAuthorizeFunction; +// shared message object container +using AsyncEvent_SharedData_t = std::shared_ptr; +/** + * @brief Async Event Message container with shared message content data + * + */ class AsyncEventSourceMessage { + private: - uint8_t* _data; - size_t _len; - size_t _sent; - // size_t _ack; - size_t _acked; + const AsyncEvent_SharedData_t _data; + size_t _sent{0}; // num of bytes already sent + size_t _acked{0}; // num of bytes acked public: - AsyncEventSourceMessage(const char* data, size_t len); - ~AsyncEventSourceMessage(); + AsyncEventSourceMessage(AsyncEvent_SharedData_t data) : _data(data) {}; +#ifdef ESP32 + AsyncEventSourceMessage(const char* data, size_t len) : _data(std::make_shared(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); + + /** + * @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); + + /** + * @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); - 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 { private: AsyncClient* _client; 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 _messageQueue; #ifdef ESP32 mutable std::mutex _lockmq; #endif bool _queueMessage(const char* message, size_t len); + bool _queueMessage(AsyncEvent_SharedData_t&& msg); void _runQueue(); public: AsyncEventSourceClient(AsyncWebServerRequest* request, AsyncEventSource* server); ~AsyncEventSourceClient(); - AsyncClient* client() { return _client; } - void close(); - bool write(const char* message, size_t len); + /** + * @brief Send an SSE message to client + * 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 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(); } 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 _onPoll(); void _onTimeout(uint32_t time); 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 { private: String _url; @@ -117,6 +221,9 @@ class AsyncEventSource : public AsyncWebHandler { ArEventHandlerFunction _connectcb = 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: typedef enum { DISCARDED = 0, @@ -124,23 +231,47 @@ class AsyncEventSource : public AsyncWebHandler { PARTIALLY_ENQUEUED = 2, } SendStatus; + AsyncEventSource(const char* url) : _url(url) {}; AsyncEventSource(const String& url) : _url(url) {}; ~AsyncEventSource() { close(); }; const char* url() const { return _url.c_str(); } + // close all connected clients 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; } + + /** + * @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 ! void onDisconnect(ArEventHandlerFunction cb) { _disconnectcb = 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); } - SendStatus send(const char* message, const char* event = NULL, uint32_t id = 0, uint32_t reconnect = 0); - // number of clients connected + + // returns number of connected clients size_t count() const; + + // returns average number of messages pending in all client's queues size_t avgPacketsWaiting() const; - // system callbacks (do not call) + // system callbacks (do not call from user code!) void _addClient(AsyncEventSourceClient* client); void _handleDisconnect(AsyncEventSourceClient* client); bool canHandle(AsyncWebServerRequest* request) const override final; @@ -149,7 +280,6 @@ class AsyncEventSource : public AsyncWebHandler { class AsyncEventSourceResponse : public AsyncWebServerResponse { private: - String _content; AsyncEventSource* _server; public: diff --git a/src/AsyncWebSocket.cpp b/src/AsyncWebSocket.cpp index b89c0d5..9deb74a 100644 --- a/src/AsyncWebSocket.cpp +++ b/src/AsyncWebSocket.cpp @@ -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->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); - _server->_handleEvent(this, WS_EVT_CONNECT, request, NULL, 0); delete request; memset(&_pinfo, 0, sizeof(_pinfo)); } @@ -451,6 +450,8 @@ void AsyncWebSocketClient::close(uint16_t code, const char* message) { if (_status != WS_CONNECTED) return; + _status = WS_DISCONNECTING; + if (code) { uint8_t packetLen = 2; if (message != NULL) { @@ -496,30 +497,37 @@ void AsyncWebSocketClient::_onDisconnect() { } void AsyncWebSocketClient::_onData(void* pbuf, size_t plen) { - // Serial.println("onData"); _lastMessageTime = millis(); uint8_t* data = (uint8_t*)pbuf; while (plen > 0) { if (!_pstate) { const uint8_t* fdata = data; + _pinfo.index = 0; _pinfo.final = (fdata[0] & 0x80) != 0; _pinfo.opcode = fdata[0] & 0x0F; _pinfo.masked = (fdata[1] & 0x80) != 0; _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; plen -= 2; - if (_pinfo.len == 126) { + + if (_pinfo.len == 126 && plen >= 2) { _pinfo.len = fdata[3] | (uint16_t)(fdata[2]) << 8; data += 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; data += 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); data += 4; plen -= 4; @@ -772,6 +780,7 @@ void AsyncWebSocket::_handleEvent(AsyncWebSocketClient* client, AwsEventType typ AsyncWebSocketClient* AsyncWebSocket::_newClient(AsyncWebServerRequest* request) { _clients.emplace_back(request, this); + _handleEvent(&_clients.back(), WS_EVT_CONNECT, request, NULL, 0); return &_clients.back(); } diff --git a/src/ESPAsyncWebServer.h b/src/ESPAsyncWebServer.h index e58a155..f9f693f 100644 --- a/src/ESPAsyncWebServer.h +++ b/src/ESPAsyncWebServer.h @@ -48,10 +48,10 @@ #include "literals.h" -#define ASYNCWEBSERVER_VERSION "3.3.23" +#define ASYNCWEBSERVER_VERSION "3.4.5" #define ASYNCWEBSERVER_VERSION_MAJOR 3 -#define ASYNCWEBSERVER_VERSION_MINOR 3 -#define ASYNCWEBSERVER_VERSION_REVISION 23 +#define ASYNCWEBSERVER_VERSION_MINOR 4 +#define ASYNCWEBSERVER_VERSION_REVISION 5 #define ASYNCWEBSERVER_FORK_mathieucarbou #ifdef ASYNCWEBSERVER_REGEX diff --git a/src/WebRequest.cpp b/src/WebRequest.cpp index 09d6102..e96ec5f 100644 --- a/src/WebRequest.cpp +++ b/src/WebRequest.cpp @@ -28,14 +28,14 @@ using namespace asyncsrv; -enum { PARSE_REQ_START, - PARSE_REQ_HEADERS, - PARSE_REQ_BODY, - PARSE_REQ_END, - PARSE_REQ_FAIL }; +enum { PARSE_REQ_START = 0, + PARSE_REQ_HEADERS = 1, + PARSE_REQ_BODY = 2, + PARSE_REQ_END = 3, + PARSE_REQ_FAIL = 4 }; 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->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); @@ -67,6 +67,18 @@ AsyncWebServerRequest::~AsyncWebServerRequest() { } 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; while (true) { @@ -74,6 +86,12 @@ void AsyncWebServerRequest::_onData(void* buf, size_t len) { // Find new line in buf char* str = (char*)buf; 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') { break; } @@ -142,6 +160,8 @@ void AsyncWebServerRequest::_onData(void* buf, size_t len) { if (!_sent) { if (!_response) 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); _response->_respond(this); _sent = true; @@ -246,6 +266,8 @@ bool AsyncWebServerRequest::_parseReqHead() { _method = HTTP_HEAD; } else if (m == T_OPTIONS) { _method = HTTP_OPTIONS; + } else { + return false; } String g; @@ -257,6 +279,9 @@ bool AsyncWebServerRequest::_parseReqHead() { _url = urlDecode(u); _addGetParams(g); + if (!_url.length()) + return false; + if (!_temp.startsWith(T_HTTP_1_0)) _version = 1; @@ -564,10 +589,14 @@ void AsyncWebServerRequest::_parseLine() { if (_parseState == PARSE_REQ_START) { if (!_temp.length()) { _parseState = PARSE_REQ_FAIL; - _client->close(); + _client->abort(); } else { - _parseReqHead(); - _parseState = PARSE_REQ_HEADERS; + if (_parseReqHead()) { + _parseState = PARSE_REQ_HEADERS; + } else { + _parseState = PARSE_REQ_FAIL; + _client->abort(); + } } return; } @@ -589,6 +618,8 @@ void AsyncWebServerRequest::_parseLine() { if (!_sent) { if (!_response) 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); _response->_respond(this); _sent = true; @@ -765,14 +796,6 @@ void AsyncWebServerRequest::send(AsyncWebServerResponse* response) { if (_response) delete _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) { diff --git a/src/WebResponseImpl.h b/src/WebResponseImpl.h index b58c5bb..fa462b6 100644 --- a/src/WebResponseImpl.h +++ b/src/WebResponseImpl.h @@ -47,6 +47,10 @@ class AsyncBasicResponse : public AsyncWebServerResponse { class AsyncAbstractResponse : public AsyncWebServerResponse { 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; // Data is inserted into cache at begin(). // This is inefficient with vector, but if we use some other container, diff --git a/src/WebResponses.cpp b/src/WebResponses.cpp index 7a26e92..bf8235e 100644 --- a/src/WebResponses.cpp +++ b/src/WebResponses.cpp @@ -352,7 +352,21 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest* request, size_t len, u request->client()->close(); 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; + _in_flight -= (_in_flight > len) ? len : _in_flight; + // get the size of available sock space size_t space = request->client()->space(); 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); _head = _head.substring(space); _writtenLength += request->client()->write(out.c_str(), out.length()); + _in_flight += out.length(); + --_in_flight_credit; // take a credit return out.length(); } } 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; if (_chunked) { if (space <= 8) { return 0; } + outLen = space; } else if (!_sendContentLength) { outLen = space; @@ -422,6 +451,8 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest* request, size_t len, u if (outLen) { _writtenLength += request->client()->write((const char*)buf, outLen); + _in_flight += outLen; + --_in_flight_credit; // take a credit } if (_chunked) { diff --git a/src/WebServer.cpp b/src/WebServer.cpp index 588bd01..ebf126a 100644 --- a/src/WebServer.cpp +++ b/src/WebServer.cpp @@ -56,8 +56,7 @@ AsyncWebServer::AsyncWebServer(uint16_t port) c->setRxTimeout(3); AsyncWebServerRequest* r = new AsyncWebServerRequest((AsyncWebServer*)s, c); if (r == NULL) { - c->close(true); - c->free(); + c->abort(); delete c; } }, diff --git a/src/literals.h b/src/literals.h index 6782390..e924483 100644 --- a/src/literals.h +++ b/src/literals.h @@ -65,6 +65,7 @@ namespace asyncsrv { static constexpr const char* T_response = "response"; static constexpr const char* T_retry_ = "retry: "; 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_rnrn = "\r\n\r\n"; static constexpr const char* T_Transfer_Encoding = "transfer-encoding";