From 216a86a69d423551b65aac29cd3e7668a46fc157 Mon Sep 17 00:00:00 2001 From: Emil Muratov Date: Tue, 30 Sep 2025 14:46:55 +0900 Subject: [PATCH 01/15] AsyncWebSocketResponse - replace object pointer with callback function it would allow using AsyncWebSocketResponse with any type of objects that could take ownership of client pointer, not limited to specific class. This is a prereq for newer WSocket implementation --- src/AsyncWebSocket.cpp | 11 +++++------ src/AsyncWebSocket.h | 7 ++++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/AsyncWebSocket.cpp b/src/AsyncWebSocket.cpp index 0cd17493b..8f08c1abd 100644 --- a/src/AsyncWebSocket.cpp +++ b/src/AsyncWebSocket.cpp @@ -816,10 +816,10 @@ void AsyncWebSocket::_handleEvent(AsyncWebSocketClient *client, AwsEventType typ } } -AsyncWebSocketClient *AsyncWebSocket::_newClient(AsyncWebServerRequest *request) { +bool AsyncWebSocket::newClient(AsyncWebServerRequest *request) { _clients.emplace_back(request, this); _handleEvent(&_clients.back(), WS_EVT_CONNECT, request, NULL, 0); - return &_clients.back(); + return true; } void AsyncWebSocket::_handleDisconnect(AsyncWebSocketClient *client) { @@ -1228,7 +1228,7 @@ void AsyncWebSocket::handleRequest(AsyncWebServerRequest *request) { return; } const AsyncWebHeader *key = request->getHeader(WS_STR_KEY); - AsyncWebServerResponse *response = new AsyncWebSocketResponse(key->value(), this); + AsyncWebServerResponse *response = new AsyncWebSocketResponse(key->value(), [this](AsyncWebServerRequest *r){ return newClient(r); }); if (response == NULL) { #ifdef ESP32 log_e("Failed to allocate"); @@ -1257,8 +1257,7 @@ AsyncWebSocketMessageBuffer *AsyncWebSocket::makeBuffer(const uint8_t *data, siz * Authentication code from https://github.com/Links2004/arduinoWebSockets/blob/master/src/WebSockets.cpp#L480 */ -AsyncWebSocketResponse::AsyncWebSocketResponse(const String &key, AsyncWebSocket *server) { - _server = server; +AsyncWebSocketResponse::AsyncWebSocketResponse(const String &key, AwsHandshakeHandler cb) : _callback(cb) { _code = 101; _sendContentLength = false; @@ -1314,7 +1313,7 @@ size_t AsyncWebSocketResponse::_ack(AsyncWebServerRequest *request, size_t len, (void)time; if (len) { - _server->_newClient(request); + _callback(request); } return 0; diff --git a/src/AsyncWebSocket.h b/src/AsyncWebSocket.h index 46cdb5e50..e4bf76163 100644 --- a/src/AsyncWebSocket.h +++ b/src/AsyncWebSocket.h @@ -445,7 +445,8 @@ class AsyncWebSocket : public AsyncWebHandler { uint32_t _getNextId() { return _cNextId++; } - AsyncWebSocketClient *_newClient(AsyncWebServerRequest *request); + // callback function that takes the ownership of the connected client, called from a AsyncWebSocketResponse instance + bool newClient(AsyncWebServerRequest *request); void _handleDisconnect(AsyncWebSocketClient *client); void _handleEvent(AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len); bool canHandle(AsyncWebServerRequest *request) const override final; @@ -464,10 +465,10 @@ class AsyncWebSocket : public AsyncWebHandler { class AsyncWebSocketResponse : public AsyncWebServerResponse { private: String _content; - AsyncWebSocket *_server; + AwsHandshakeHandler _callback; public: - AsyncWebSocketResponse(const String &key, AsyncWebSocket *server); + AsyncWebSocketResponse(const String &key, AwsHandshakeHandler cb); void _respond(AsyncWebServerRequest *request); size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time); bool _sourceValid() const { From d1d2f2abc19029bef4e8207d541411d0f66ab2b6 Mon Sep 17 00:00:00 2001 From: Emil Muratov Date: Sat, 4 Oct 2025 23:34:52 +0900 Subject: [PATCH 02/15] reimplementation of async websockets - PoC a proof of concept - message and buffer are abstracted behind generic class - websocket client reassembles incoming meassage spanning multiple tcp segments - in/out message queues - 8 byte message size support - queue / message cap limit - different event and status calls - two-way ws-close ack --- src/AsyncWSocket.cpp | 741 +++++++++++++++++++++++++++++++++++++++++++ src/AsyncWSocket.h | 591 ++++++++++++++++++++++++++++++++++ 2 files changed, 1332 insertions(+) create mode 100644 src/AsyncWSocket.cpp create mode 100644 src/AsyncWSocket.h diff --git a/src/AsyncWSocket.cpp b/src/AsyncWSocket.cpp new file mode 100644 index 000000000..269d8dc37 --- /dev/null +++ b/src/AsyncWSocket.cpp @@ -0,0 +1,741 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +// A new experimental implementation of Async WebSockets client/server + +#include "AsyncWSocket.h" +#include "literals.h" + +constexpr const char WS_STR_CONNECTION[] = "Connection"; +//constexpr const char WS_STR_UPGRADE[] = "Upgrade"; +//constexpr const char WS_STR_ORIGIN[] = "Origin"; +//constexpr const char WS_STR_COOKIE[] = "Cookie"; +constexpr const char WS_STR_VERSION[] = "Sec-WebSocket-Version"; +constexpr const char WS_STR_KEY[] = "Sec-WebSocket-Key"; +constexpr const char WS_STR_PROTOCOL[] = "Sec-WebSocket-Protocol"; +//constexpr const char WS_STR_ACCEPT[] = "Sec-WebSocket-Accept"; + + +/** + * @brief apply mask key to supplied data byte-per-byte + * this is used for unaligned portions of data buffer where 32 bit ops can't be applied + * this function takes mask offset to rollover the key bytes + * + * @param mask - mask key + * @param mask_offset - offset byte for mask + * @param data - data to apply + * @param length - data block length + */ +inline void wsMaskPayloadPerByte(uint32_t mask, size_t mask_offset, char *data, size_t length) { + for (char* ptr = data; ptr != data + length; ++ptr) { + *ptr ^= reinterpret_cast(&mask)[mask_offset++]; // roll mask bytes + if (mask_offset == sizeof(mask)) + mask_offset = 0; + } + } + +/** + * @brief apply mask key to supplied data using 32 bit XOR + * + * @param mask - mask key + * @param mask_offset - offset byte for mask + * @param data - data to apply + * @param length - data block length + */ +void wsMaskPayload(uint32_t mask, size_t mask_offset, char *data, size_t length) { + /* + we could benefit from 32-bit xor to unmask the data. The great thing of esp32 is that it could do unaligned 32 bit memory access + while some other MCU does not (yes, RP2040, I'm talking about you!) So have to go hard way - do all operations 32-bit aligned to cover all supported MCUs + */ + + // If data size is so small that it does not make sense to use 32 bit aligned calculations, just use byte-by-byte version + if (length < 4 * sizeof(mask)){ + wsMaskPayloadPerByte(mask, mask_offset % 4, data, length); + } else { + // do 32-bit vectored calculations + + // get unaligned part size + const size_t head_remainder = reinterpret_cast(data) % sizeof(mask); + // set aligned head + char* data_aligned_head = head_remainder == 0 ? data : (data + sizeof(mask) - head_remainder); + // set aligned tail + char* const data_end = data + length; + char* const data_aligned_end = data_end - reinterpret_cast(data_end) % sizeof(mask); + + // unmask unaligned part at the begining + if (head_remainder) + wsMaskPayloadPerByte(mask, mask_offset % 4, data, head_remainder); + + // need a derived mask key in a 32 bit var which is rolled over by the appropriate offset for data position, our byte-by-byte function could help here to derive key + uint32_t shifted_mask{0}; + wsMaskPayloadPerByte(mask, (mask_offset + data_aligned_head - data) % sizeof(mask), reinterpret_cast(&shifted_mask), sizeof(mask)); + + // (un)mask the payload + do { + *reinterpret_cast(data_aligned_head) ^= shifted_mask; + data_aligned_head += sizeof(mask); + } while(data_aligned_head != data_aligned_end); + + // unmask the unalined remainder + wsMaskPayloadPerByte(mask, (mask_offset + (data_aligned_end - data)) % sizeof(mask), data_aligned_end, data_end - data_aligned_end); + } +} + +size_t webSocketSendHeader(AsyncClient *client, WSMessageFrame& frame) { + if (!client || !client->canSend()) { + return 0; + } + + size_t headLen = 2; + if (frame.len > 65535){ + headLen += 8; + } else if (frame.len > 125) { + headLen += 2; + } + if (frame.len && frame.mask) { + headLen += 4; + } + + size_t space = client->space(); + if (space < headLen) { + // Serial.println("SF 2"); + return 0; + } + space -= headLen; + + // header buffer + uint8_t buf[headLen]; + + buf[0] = static_cast(frame.msg->type) & 0x0F; + if (frame.msg->final()) { + buf[0] |= 0x80; + } + if (frame.len < 126) { + // 1 byte len + buf[1] = frame.len & 0x7F; + } else if (frame.len > 65535){ + // 8 byte len + buf[1] = 127; + uint32_t lenl = htonl(frame.len & 0xffffffff); + uint32_t lenh = htonl(frame.len >> 32); + memcpy(buf+2, &lenh, sizeof(lenh)); + memcpy(buf+6, &lenl, sizeof(lenl)); + } else { + // 2 byte len + buf[1] = 126; + *(uint16_t*)(buf+2) = htons(frame.len & 0xffff); + } + + if (frame.len && frame.mask) { + buf[1] |= 0x80; + memcpy(buf + (headLen - sizeof(frame.mask)), &frame.mask, sizeof(frame.mask)); + } + + size_t sent = client->add((const char*)buf, headLen); + + if (frame.msg->type == WSFrameType_t::close && frame.msg->getStatusCode()){ + // this is a 'close' message with status code, need to send the code also along with header + uint16_t code = htons (frame.msg->getStatusCode()); + sent += client->add((char*)(&code), 2); + headLen += 2; + } + + // return size of a header added or 0 if any error + return sent == headLen ? sent : 0; +} + + +// ******** WSocket classes implementation ******** + +WSocketClient::WSocketClient(uint32_t id, AsyncWebServerRequest *request, WSocketClient::event_callback_t call_back, size_t msgsize, size_t qcapsize) : + id(id), + _client(request->client()), + _cb(call_back), + _max_msgsize(msgsize), + _max_qcap(qcapsize) +{ + // disable connection timeout + _client->setRxTimeout(0); + _client->setNoDelay(true); + // set AsyncTCP callbacks + _client->onAck( [](void *r, AsyncClient *c, size_t len, uint32_t rtt) { (void)c; reinterpret_cast(r)->_clientSend(len); }, this ); + //_client->onAck( [](void *r, AsyncClient *c, size_t len, uint32_t rtt) { (void)c; reinterpret_cast(r)->_onAck(len, rtt); }, this ); + _client->onDisconnect( [](void *r, AsyncClient *c) { reinterpret_cast(r)->_onDisconnect(c); }, this ); + _client->onTimeout( [](void *r, AsyncClient *c, uint32_t time) { (void)c; reinterpret_cast(r)->_onTimeout(time); }, this ); + _client->onData( [](void *r, AsyncClient *c, void *buf, size_t len) { (void)c; reinterpret_cast(r)->_onData(buf, len); }, this ); + _client->onPoll( [](void *r, AsyncClient *c) { (void)c; reinterpret_cast(r)->_clientSend(); }, this ); + // not implemented yet + //_client->onError( [](void *r, AsyncClient *c, int8_t error) { (void)c; reinterpret_cast(r)->_onError(error); }, this ); + delete request; +} + +WSocketClient::~WSocketClient() { + if (_client){ + delete _client; + _client = nullptr; + } +} + +// ***** AsyncTCP callbacks ***** +//#ifdef NOTHING +// callback acknowledges sending pieces of data for outgoing frame +void WSocketClient::_clientSend(size_t acked_bytes){ + if (!_client || _connection == conn_state_t::disconnected || !_client->space()) + return; + + /* + this method could be called from different threads - AsyncTCP's ack/poll and user thread when enqueing messages, + only AsyncTCP's ack is mandatory to execute since it carries acked data size, others could be ignored completely + if this call is already exucute in progress. Worse case it will catch up later on next poll + */ + + // create lock object but don't actually take the lock yet + std::unique_lock lock{_sendLock, std::defer_lock}; + + if (acked_bytes){ + // if it's the ack call from AsyncTCP - wait for lock! + lock.lock(); + log_d("_clientSend, ack:%u/%u, space:%u", acked_bytes, _in_flight, _client ? _client->space() : 0); + } else { + // if there is no acked data - just quit, we are already sending something + if (!lock.try_lock()) + return; + } + + // for response data we need to control AsyncTCP's event queue and in-flight fragmentation. Sending small chunks could give lower 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 and minimizing events in asynctcp's Q + if (acked_bytes){ + _in_flight -= std::min(acked_bytes, _in_flight); + log_d("infl:%u, state:%u", _in_flight, _connection); + // check if we were waiting to ack our disconnection frame + if (!_in_flight && (_connection == conn_state_t::disconnecting)){ + log_d("closing tcp-conn"); + // we are server, should close connection first as per https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.1 + // here we close from the app side, send TCP-FIN to the party and move to FIN_WAIT_1/2 states + _client->close(); + return; + } + + // return buffer credit on acked data + ++_in_flight_credit; + } + + if (_in_flight > _client->space() || !_in_flight_credit) { + log_d("defer ws send call, in-flight:%u/%u", _in_flight, _client->space()); + return; + } + + // no message in transit, try to evict one from a Q + if (!_outFrame.msg){ + if (_evictOutQueue()){ + // generate header and add to the socket buffer + _in_flight += webSocketSendHeader(_client, _outFrame); + } else + return; // nothing to send now + } + + // if there is a pending _outFrame - send the data from there + while (_outFrame.msg){ + if (_outFrame.index < _outFrame.len){ + size_t payload_pend = _client->add(_outFrame.msg->getCurrentChunk().first + _outFrame.chunk_offset, _outFrame.msg->getCurrentChunk().second - _outFrame.chunk_offset); + // if no data was added to client's buffer then it's something wrong, we can't go on + if (!payload_pend){ + _client->abort(); + return; + } + _outFrame.index += payload_pend; + _outFrame.chunk_offset += payload_pend; + _in_flight += payload_pend; + } + + if (_outFrame.index == _outFrame.len){ + // if we complete writing entire message, send the frame right away + // increment in-flight counter and take the credit + if (!_client->send()) + _client->abort(); + --_in_flight_credit; + + if (_outFrame.msg->type == WSFrameType_t::close){ + // if we just sent close frame, then change client state and purge out queue, we won't transmit anything from now on + // the connection will be terminated once all in-flight data are acked from the other side + _connection = conn_state_t::disconnecting; + _outFrame.msg.reset(); + _messageQueueOut.clear(); + return; + } + + // release _outFrame message here + _outFrame.msg.reset(); + + // execute callback that a message was sent + if (_cb) + _cb(this, event_t::msgSent); + + // if there are free in-flight credits try to pull next msg from Q + if (_in_flight_credit && _evictOutQueue()){ + // generate header and add to the socket buffer + _in_flight += webSocketSendHeader(_client, _outFrame); + continue; + } else { + return; + } + } + + if (!_client->space()){ + // we have exhausted socket buffer, send it and quit + if (!_client->send()) + _client->abort(); + // take in-flight credit + --_in_flight_credit; + return; + } + + // othewise it was chunked message object and current chunk has been complete + if (_outFrame.chunk_offset == _outFrame.msg->getCurrentChunk().second){ + // request a new one + size_t next_chunk_size = _outFrame.msg->getNextChunk(); + if (next_chunk_size == 0){ + // chunk is not ready yet, need to async wait and return for data later, we quit here and reevaluate on next ack or poll event from AsyncTCP + if (!_client->send()) _client->abort(); + // take in-flight credit + --_in_flight_credit; + return; + } else if (next_chunk_size == -1){ + // something is wrong! there would be no more chunked data but the message has not reached it's full size yet, can do nothing but close the coonections + log_e("Chunk data is incomlete!"); + _client->abort(); + return; + } + // go on with a loop + } else { + // can't be there? + } + } +} + +bool WSocketClient::_evictOutQueue(){ + // check if we have something in the Q and enough sock space to send a header at least + if (_messageQueueOut.size() && _client->space() > 16 ){ + { + #ifdef ESP32 + std::unique_lock lockout(_outQlock); + #endif + _outFrame.msg.swap(_messageQueueOut.front()); + _messageQueueOut.pop_front(); + } + _outFrame.chunk_offset = _outFrame.index = 0; + _outFrame.len = _outFrame.msg->getSize(); + return true; + } + + return false; + // this function assumes it's callee will take care of actually sending the header and message body further +} + +void WSocketClient::_onError(int8_t) { + // Serial.println("onErr"); +} + +void WSocketClient::_onTimeout(uint32_t time) { + if (!_client) { + return; + } + // Serial.println("onTime"); + (void)time; + _client->abort(); +} + +void WSocketClient::_onDisconnect(AsyncClient *c) { + Serial.println("TCP client disconencted"); + delete c; + _client = c = nullptr; + _connection = conn_state_t::disconnected; + + #ifdef ESP32 + std::lock_guard lock(_outQlock); + #endif + // clear out Q, we won't be able to send it anyway from now on + _messageQueueOut.clear(); + // execute callback + if (_cb) + _cb(this, event_t::disconnect); +} + +void WSocketClient::_onData(void *pbuf, size_t plen) { + Serial.printf("_onData, len:%u\n", plen); + if (!pbuf || !plen || _connection == conn_state_t::disconnected) return; + char *data = (char *)pbuf; + + while (plen){ + if (!_inFrame.msg){ + // it's a new frame, need to parse header data + size_t framelen; + uint16_t errcode; + std::tie(framelen, errcode) = _mkNewFrame(data, plen, _inFrame); + if (!framelen){ + // was unable to start receiving frame? initiate disconnect exchange + #ifdef ESP32 + std::unique_lock lockout(_outQlock); + #endif + _messageQueueOut.push_front( std::make_shared(errcode) ); + // try sending disconnect message now + _clientSend(); + return; + } + // receiving a new frame from here + data += framelen; + plen -= framelen; + } else { + // continuation of existing frame + size_t payload_len = std::min(static_cast(_inFrame.len - _inFrame.index), plen); + // unmask the payload + if (_inFrame.mask) + wsMaskPayload(_inFrame.mask, _inFrame.index, static_cast(data), payload_len); + + // todo: for now assume object will consume all the payload provided + _inFrame.msg->addChunk(data, payload_len, _inFrame.index); + data += payload_len; + plen -= payload_len; + } + + // if we got whole frame now + if (_inFrame.index == _inFrame.len){ + Serial.printf("_onData, cmplt msg len:%lu\n", _inFrame.len); + + switch (_inFrame.msg->type){ + // received close message + case WSFrameType_t::close : { + if (_connection == conn_state_t::disconnecting){ + log_d("recv close ack"); + // if it was ws-close ack - we can close TCP connection + _connection == conn_state_t::disconnected; + // normally we should call close() here and wait for other side also close tcp connection with TCP-FIN, but + // for various reasons ws clients could linger connection when received TCP-FIN not closing it from the app side (even after + // two side ws-close exchange, i.e. websocat, websocket-client) + // This would make server side TCP to stay in FIN_WAIT_2 state quite long time, let's call abort() here instead of close(), + // it is harsh but let other side know that nobody would talk to it any longer and let it reinstate a new connection if needed + // anyway we won't receive/send anything due to '_connection == conn_state_t::disconnected;' + _client->abort(); + _inFrame.msg.reset(); + return; + } + + // otherwise it's a close request from a peer - echo back close message as per https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.1 + { + log_d("recv client's ws-close req"); + #ifdef ESP32 + std::unique_lock lockin(_inQlock); + std::unique_lock lockout(_outQlock); + #endif + // push message to recv Q, client might use it to understand disconnection reason + _messageQueueIn.push_back(_inFrame.msg); + // purge the out Q and echo recieved frame back to client, once it's tcp-acked from the other side we can close tcp connection + _messageQueueOut.clear(); + _messageQueueOut.push_front(_inFrame.msg); + } + _inFrame.msg.reset(); + if (_cb) + _cb(this, event_t::msgRecv); + + break; + } + + case WSFrameType_t::ping : { + #ifdef ESP32 + std::unique_lock lock(_outQlock); + #endif + // just reply to ping, does user needs this ping message? + _messageQueueOut.emplace_front( std::make_shared>(WSFrameType_t::pong, true, _inFrame.msg->getData()) ); + _inFrame.msg.reset(); + break; + } + + default: { + log_e("other msg"); + // any other messages + { + #ifdef ESP32 + std::unique_lock lock(_inQlock); + #endif + // check in-queue for overflow + if (_messageQueueIn.size() >= _max_qcap){ + log_e("q overflow"); + switch (_overflow_policy){ + case overflow_t::discard : + // discard incoming message + break; + case overflow_t::drophead : + _messageQueueIn.pop_front(); + _messageQueueIn.push_back(_inFrame.msg); + break; + case overflow_t::droptail : + _messageQueueIn.pop_back(); + _messageQueueIn.push_back(_inFrame.msg); + break; + case overflow_t::disconnect : { + #ifdef ESP32 + std::unique_lock lock(_inQlock); + #endif + _messageQueueOut.push_front( std::make_shared(1011, "Server Q overflow") ); + break; + } + default:; + } + + } else { + log_e("push new to Q"); + _messageQueueIn.push_back(_inFrame.msg); + } + } + _inFrame.msg.reset(); + log_e("evt on new msg"); + if (_cb) + _cb(this, event_t::msgRecv); + + break; + } + } + } + } + +} + +std::pair WSocketClient::_mkNewFrame(char* data, size_t len, WSMessageFrame& frame){ + if (len < 2) return {0, 1002}; // return protocol error + uint8_t opcode = data[0] & 0x0F; + bool final = data[0] & 0x80; + bool masked = data[1] & 0x80; + + // read frame size + frame.len = data[1] & 0x7F; + size_t offset = 2; // first 2 bytes + + if (frame.len == 126 && len >= 4) { + frame.len = data[3] | (uint16_t)(data[2]) << 8; + offset += 2; + } else if (frame.len == 127 && len >= 10) { + frame.len = ntohl(*(uint32_t*)(data + 2)); // MSB + frame.len <= 32; + frame.len |= ntohl(*(uint32_t*)(data + 6)); // LSB + offset += 8; + } + + if (frame.len > _max_msgsize) // message too big than we allowed to accept + return {0, 1009}; + + // if ws.close() is called, Safari sends a close frame with plen 2 and masked bit set. We must not try to read mask key from beyond packet size + if (masked && len >= offset + 4) { + // mask bytes order are LSB, so we can copy it as-is + frame.mask = *reinterpret_cast(data + offset); + Serial.printf("mask key at %u, :0x", offset); + Serial.println(frame.mask, HEX); + offset += 4; + } + + frame.index = frame.chunk_offset = 0; + + size_t bodylen = std::min(static_cast(frame.len), len - offset); + // if there is no body in message, then it must a specific control message with no payload + if (!bodylen){ + // I'll make an empty binary container for such messages + _inFrame.msg = std::make_shared>>(static_cast(opcode)); + } else { + // let's unmask payload right in sock buff, later it could be consumed as raw data + if (masked){ + wsMaskPayload(_inFrame.mask, 0, data + offset, bodylen); + } + + // create a new container object that fits best for message type + switch (static_cast(opcode)){ + case WSFrameType_t::text : + // create a text message container consuming as much data as possible from current payload + _inFrame.msg = std::make_shared>(WSFrameType_t::text, final, (char*)(data + offset), bodylen); + break; + + case WSFrameType_t::close : { + uint16_t status_code = ntohs(*(uint16_t*)(data + offset)); + offset += 2; + if (bodylen > 2){ + bodylen -= 2; + // create a text message container consuming as much data as possible from current payload + _inFrame.msg = std::make_shared(status_code, data + offset, bodylen); + } else { + // must be close message w/o body + _inFrame.msg = std::make_shared(status_code); + } + break; + } + + default: + _inFrame.msg = std::make_shared>>(static_cast(opcode), bodylen); + // copy data + memcpy(_inFrame.msg->getData(), data + offset, bodylen); + + } + offset += bodylen; + _inFrame.index = bodylen; + } + + log_e("new msg offset:%u, body:%u", offset, bodylen); + // return the number of consumed data from input buffer + return {offset, 0}; +} + +WSocketClient::err_t WSocketClient::enqueueMessage(std::shared_ptr mptr){ + if (_connection != conn_state_t::connected) + return err_t::disconnected; + + if (_messageQueueOut.size() < _max_qcap){ + #ifdef ESP32 + std::lock_guard lock(_outQlock); + #endif + _messageQueueOut.emplace_back( std::move(mptr) ); + _clientSend(); + return err_t::ok; + } + + return err_t::nospace; +} + +std::shared_ptr WSocketClient::dequeueMessage(){ + #ifdef ESP32 + std::unique_lock lock(_inQlock); + #endif + std::shared_ptr msg; + if (_messageQueueIn.size()){ + msg.swap(_messageQueueIn.front()); + _messageQueueIn.pop_front(); + } + return msg; +} + +WSocketClient::err_t WSocketClient::canSend() const { + if (_connection != conn_state_t::connected) return err_t::disconnected; + if (_messageQueueOut.size() >= _max_qcap ) return err_t::nospace; + return err_t::ok; +} + +WSocketClient::err_t WSocketClient::close(uint16_t code, const char *message){ + if (_connection != conn_state_t::connected) + return err_t::disconnected; + + #ifdef ESP32 + std::lock_guard lock(_outQlock); + #endif + if (message) + _messageQueueOut.emplace_front( std::make_shared(code, message) ); + else + _messageQueueOut.emplace_front( std::make_shared(code) ); + _clientSend(); + return err_t::ok; +} + +// ***** WSocketServer implementation ***** + + +bool WSocketServer::newClient(AsyncWebServerRequest *request){ + // remove expired clients first + _purgeClients(); + { + #ifdef ESP32 + std::lock_guard lock (_lock); + #endif + _clients.emplace_back(getNextId(), request, [this](WSocketClient *c, WSocketClient::event_t e){ _clientEvents(c, e); }); + } + _clientEvents(&_clients.back(), WSocketClient::event_t::connect); + return true; +} + +void WSocketServer::_clientEvents(WSocketClient *client, WSocketClient::event_t event){ + log_d("_clientEvents: %u", event); + if (_eventHandler) + _eventHandler(client, event); +} + +void WSocketServer::handleRequest(AsyncWebServerRequest *request) { + if (!request->hasHeader(WS_STR_VERSION) || !request->hasHeader(WS_STR_KEY)) { + request->send(400); + return; + } + if (_handshakeHandler != nullptr) { + if (!_handshakeHandler(request)) { + request->send(401); + return; + } + } + const AsyncWebHeader *version = request->getHeader(WS_STR_VERSION); + if (version->value().compareTo(asyncsrv::T_13) != 0) { + AsyncWebServerResponse *response = request->beginResponse(400); + response->addHeader(WS_STR_VERSION, asyncsrv::T_13); + request->send(response); + return; + } + const AsyncWebHeader *key = request->getHeader(WS_STR_KEY); + AsyncWebServerResponse *response = new AsyncWebSocketResponse(key->value(), [this](AsyncWebServerRequest *r){ return newClient(r); }); + if (response == NULL) { +#ifdef ESP32 + log_e("Failed to allocate"); +#endif + request->abort(); + return; + } + if (request->hasHeader(WS_STR_PROTOCOL)) { + const AsyncWebHeader *protocol = request->getHeader(WS_STR_PROTOCOL); + // ToDo: check protocol + response->addHeader(WS_STR_PROTOCOL, protocol->value()); + } + request->send(response); +} + +WSocketClient* WSocketServer::_getClient(uint32_t id) { + auto iter = std::find_if(_clients.begin(), _clients.end(), [id](const WSocketClient &c) { return c.id == id; }); + if (iter != std::end(_clients)) + return &(*iter); + else + return nullptr; +} + +WSocketClient const* WSocketServer::_getClient(uint32_t id) const { + const auto iter = std::find_if(_clients.cbegin(), _clients.cend(), [id](const WSocketClient &c) { return c.id == id; }); + if (iter != std::cend(_clients)) + return &(*iter); + else + return nullptr; +} + +WSocketServer::msgall_err_t WSocketServer::canSend() const { + size_t cnt = std::count_if(std::begin(_clients), std::end(_clients), [](const WSocketClient &c) { return c.canSend() == WSocketClient::err_t::ok; }); + if (!cnt) return msgall_err_t::none; + return cnt == _clients.size() ? msgall_err_t::ok : msgall_err_t::partial; +} + +WSocketServer::msgall_err_t WSocketServer::pingAll(const char *data, size_t len){ + size_t cnt{0}; + for (auto &c : _clients) { + if ( c.ping(data, len) == WSocketClient::err_t::ok) + ++cnt; + } + if (!cnt) + return msgall_err_t::none; + return cnt == _clients.size() ? msgall_err_t::ok : msgall_err_t::partial; +} + +WSocketServer::msgall_err_t WSocketServer::messageAll(std::shared_ptr m){ + size_t cnt{0}; + for (auto &c : _clients) { + if ( c.enqueueMessage(m) == WSocketClient::err_t::ok) + ++cnt; + } + if (!cnt) + return msgall_err_t::none; + return cnt == _clients.size() ? msgall_err_t::ok : msgall_err_t::partial; +} + +void WSocketServer::_purgeClients(){ + log_d("purging clients"); + std::lock_guard lock(_lock); + // purge clients that are disconnected and with all messages consumed + std::erase_if(_clients, [](const WSocketClient& c){ return (c.status() == WSocketClient::conn_state_t::disconnected && !c.inQueueSize() ); }); +} diff --git a/src/AsyncWSocket.h b/src/AsyncWSocket.h new file mode 100644 index 000000000..e51fd6029 --- /dev/null +++ b/src/AsyncWSocket.h @@ -0,0 +1,591 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +// A new experimental implementation of Async WebSockets client/server + + +#pragma once + +#include "AsyncWebSocket.h" + +class WSocketServer; + +/** + * @brief WebSocket message type + * + */ +enum class WSFrameType_t : uint8_t { // type field is 4 bits + continuation = 0, // fragment of a previous message + text, + binary, + close = 0x08, + ping, + pong +}; + +/** + * @brief Message transition state + * reflects message transmission state + * + */ +enum class WSMessageStatus_t { + empty = 0, // bare message, not usefull for anything yet + sending, // message in sending state + sent, // message transition has been complete + incomlete, // message is not complete. i.e. partially received + complete, // message is complete, no new data is expected + error // malformed message +}; + + + +/** + * @brief abstract WebSocket message + * + */ +class WSMessageGeneric { + friend class WSocketClient; + /** Is this the last chunk in a fragmented message ?*/ + bool _final; + +public: + const WSFrameType_t type; + + WSMessageGeneric(WSFrameType_t type, bool final = true) : type(type), _final(final) {}; + virtual ~WSMessageGeneric(){}; + + // if this message in final fragment + bool final() const { return _final; } + + /** + * @brief Get the Size of the message + * + * @return size_t + */ + virtual size_t getSize() const = 0; + + /** + * @brief Get access to RO-data + * @note we cast it to char* 'cause of two reasons: + * - the LWIP's _tcp_write() function accepts const char* + * - websocket is mostly text anyway unless compressed + * @note for messages with type 'text' this lib will always include NULL terminator at the end of data, NULL byte is NOT included in total size of the message returned by getSize() + * a care should be taken when accessing the data for messages with type 'binary', it won't have NUL terminator at the end and would be valid up to getSize() in length! + * @note buffer should be writtable so that client class could apply masking on data if needed + * @return char* const + */ + virtual char* getData() = 0; + + /** + * @brief WebSocket message status code as defined in https://datatracker.ietf.org/doc/html/rfc6455#section-7.4 + * + * @return uint16_t + */ + virtual uint16_t getStatusCode() const { return 0; } + +protected: + + /** + * @brief access message buffer in chunks + * this method is used internally by WSocketClient class when sending message. In simple case it just wraps around getDtata() call + * but could also be implemented in derived classes for chunked transfers + * + * @return std::pair + */ + virtual std::pair getCurrentChunk(){ return std::pair(getData(), getSize()); } ; + + /** + * @brief move to the next chunk of message data assuming current chunk has been consumed already + * @note nextChunk call switches the pointer, getCurrentChunk() would return new chunk afterwards + * + * @return int32_t - size of a next chunk of data + * @note if returned size is '-1' then no more data chunks are available + * @note if returned size is '0' then next chunk is not available yet, a call should be repeated later to retreive new chunk + */ + virtual int32_t getNextChunk(){ return -1; }; + + /** + * @brief add a chunk of data to the message + * it will NOT extend total message size, it's to reassemble a message taking several TCP packets + * + * @param data - data chunk pointer + * @param len - size of a chunk + * @param offset - an offset for chunk from begingn of message (@note offset is for calculation reference only, the chunk is always added to the end of current message data) + */ + virtual void addChunk(char* data, size_t len, size_t offset) = 0; + +}; + + +template +class WSMessageContainer : public WSMessageGeneric { +protected: + T container; + +public: + + WSMessageContainer(WSFrameType_t t, bool final = true) : WSMessageGeneric(t, final) {} + + // variadic constructor for anything that T can be made of + template + WSMessageContainer (WSFrameType_t t, bool final, Args&&... args) : WSMessageGeneric(t, final), container(std::forward(args)...) {}; + + /** + * @copydoc WSMessageGeneric::getSize() + */ + size_t getSize() const override { + // specialisation for Arduino String + if constexpr(std::is_same_v>) + return container.length(); + // otherwise we assume either STL container is used, (i.e. st::vector or std::string) or derived class should implement same methods + if constexpr(!std::is_same_v>) + return container.size(); + }; + + /** + * @copydoc WSMessageGeneric::getData() + * @details though casted to const char* the data there is NOT NULL-terminated string! + */ + char* getData() override { + // specialization for Arduino String + if constexpr(std::is_same_v>) + return container.c_str(); + // otherwise we assume either STL container is used, (i.e. st::vector or std::string) or derived class should implement same methods + if constexpr(!std::is_same_v>) + return reinterpret_cast(container.data()); + } + + // access message container object + T& getContainer(){ return container; } + +protected: + + /** + * @copydoc WSMessageGeneric::addChunk(char* data, size_t len, size_t offset) + * @details though casted to const char* the data there is NOT NULL-terminated string! + */ + void addChunk(char* data, size_t len, size_t offset) override { + // specialization for Arduino String + if constexpr(std::is_same_v>){ + container.concat(data, len); + } + + // specialization for std::string + if constexpr(std::is_same_v>){ + container.append(data, len); + } + + // specialization for std::vector + if constexpr(std::is_same_v, std::decay_t>){ + container.resize(len + offset); + memcpy(container.data(), data, len); + } + } + +}; + +/** + * @brief Control message - 'Close' + * + */ +class WSMessageClose : public WSMessageContainer { + const uint16_t _status_code; + +public: + /** + * @brief Construct Close message without the body + * + * @param status close code as defined in https://datatracker.ietf.org/doc/html/rfc6455#section-7.4 + */ + WSMessageClose (uint16_t status = 1000) : WSMessageContainer(WSFrameType_t::close, true), _status_code(status) {}; + // variadic constructor for anything that std::string can be made of + template + WSMessageClose (uint16_t status, Args&&... args) : WSMessageContainer(WSFrameType_t::close, true, std::forward(args)...), _status_code(status) {}; + + uint16_t getStatusCode() const override { return _status_code; } +}; + +/** + * @brief structure that owns the message (or fragment) while sending/receiving by WSocketClient + */ +struct WSMessageFrame { + /** Mask key */ + uint32_t mask; + /** Length of the current message/fragment to be transmitted. This equals the total length of the message if num == 0 && final == true */ + uint64_t len; + /** Offset of the payload data pending to be sent. Note: this is NOT websocket message fragment's size! */ + uint64_t index; + /** offset in the current chunk of data, used to when sending message in chunks (not WS fragments!), for single chunged messages chunk_offset and index are same */ + size_t chunk_offset; + // message object + std::shared_ptr msg; +}; + +/** + * @brief WebSocket client instance + * + */ +class WSocketClient { +public: + // TCP connection state + enum class conn_state_t { + connected, + disconnecting, + disconnected + }; + + /** + * @brief WebSocket Client Events + * + */ + enum class event_t { + connect, + disconnect, + msgRecv, + msgSent + }; + + // error codes + enum class err_t { + ok, // no problem :) + nospace, // message queues are overflowed, can't accept more data + messageTooBig, // can't accept such a large message + disconnected // peer connection is broken + }; + + enum class overflow_t { + disconnect, + discard, + drophead, + droptail + }; + + using event_callback_t = std::function; + + // Client connection ID (increments for each new connection for the given server) + const uint32_t id; + +private: + AsyncClient *_client; + event_callback_t _cb; + // incoming message size limit + size_t _max_msgsize; + // cummulative maximum of the data messages held in message queues, both in and out + size_t _max_qcap; + +public: + + /** + * @brief Construct a new WSocketClient object + * + * @param id - client's id tag + * @param request - AsyncWebServerRequest which is switching the protocol to WS + * @param call_back - event callback handler + * @param msgcap - incoming message size limit, if incoming msg advertizes larger size the connection would be dropped + * @param qcap - in/out queues sizes (in number of messages) + */ + WSocketClient(uint32_t id, AsyncWebServerRequest *request, WSocketClient::event_callback_t call_back, size_t msgsize = 8 * 1024, size_t qcap = 4); + ~WSocketClient(); + + /** + * @brief Enqueue message for sending + * + * @param msg rvalue reference to message object, i.e. WSocketClient will take the ownership of the object + * @return err_t enqueue error status + */ + err_t enqueueMessage(std::shared_ptr mptr); + + /** + * @brief retrieve message from inbout Q + * + * @return std::shared_ptr + * @note if Q is empty, then empty pointer is returned, so it should be validated + */ + std::shared_ptr dequeueMessage(); + + conn_state_t status() const { + return _connection; + } + + AsyncClient *client() { + return _client; + } + + const AsyncClient *client() const { + return _client; + } + + /** + * @brief Set inbound queueu overflow Policy + * + * @param policy inbound queue overflow policy + * + * @note overflow_t::disconnect (default) - disconnect client with respective close message when inbound Q is full. + * This is the default behavior in yubox-node-org, which is not silently discarding messages but instead closes the connection. + * The big issue with this behavior is that is can cause the UI to automatically re-create a new WS connection, which can be filled again, + * and so on, causing a resource exhaustion. + * + * @note overflow_t::discard - silently discard new messages if the queue is full. + * This is the default behavior in the original ESPAsyncWebServer library from me-no-dev. This behavior allows the best performance at the expense of unreliable message delivery in case the queue is full. + * + * @note overflow_t::drophead - drop the oldest message from inbound queue to fit new message + * @note overflow_t::droptail - drop most recent message from inbound queue to fit new message + * + */ + void setOverflowPolicy(overflow_t policy){ _overflow_policy = policy; } + + overflow_t getOverflowPolicy() const { return _overflow_policy; } + + // send control frames + err_t close(uint16_t code = 0, const char *message = NULL); + err_t ping(const char *data = NULL, size_t len = 0); + + size_t inQueueSize() const { return _messageQueueIn.size(); }; + size_t outQueueSize() const { return _messageQueueOut.size(); }; + + // check if client can enqueue and send new messages + err_t canSend() const; + +private: + conn_state_t _connection{conn_state_t::connected}; + // frames in transit + WSMessageFrame _inFrame{}, _outFrame{}; + // message queues + std::deque< std::shared_ptr > _messageQueueIn; + std::deque< std::shared_ptr > _messageQueueOut; + +#ifdef ESP32 + // access mutex'es + std::mutex _sendLock; + mutable std::recursive_mutex _inQlock; + mutable std::recursive_mutex _outQlock; +#endif + + // inbound Q overflow behavior + overflow_t _overflow_policy{overflow_t::disconnect}; + + // amount of sent data in-flight, i.e. copied to socket buffer, but not acked yet from lwip side + size_t _in_flight{0}; + // in-flight data credits + size_t _in_flight_credit{2}; + + + /** + * @brief go through out Q and send message data () + * @note this method will grab a mutex lock on outQ internally + * + */ + void _clientSend(size_t acked_bytes = 0); + + /** + * @brief expell next message from out Q (if any) and start sending it to the peer + * @note this function will generate and add msg header to socket buffer but assumes it's callee will take care of actually sending the header and message body further + * @note this function assumes that it's calle has already set _outFrameLock + * + */ + bool _evictOutQueue(); + + /** + * @brief try to parse ws header and create a new frame message + * + * @return size_t size of the parsed and consumed bytes from input in a new message + */ + std::pair _mkNewFrame(char* data, size_t len, WSMessageFrame& frame); + + // AsyncTCP callbacks + void _onError(int8_t); + void _onTimeout(uint32_t time); + void _onDisconnect(AsyncClient *c); + void _onData(void *pbuf, size_t plen); +}; + + +/** + * @brief WebServer Handler implementation that plays the role of a socket server + * + */ +class WSocketServer : public AsyncWebHandler { +public: + // error enque to all + enum class msgall_err_t { + ok = 0, // message was enqueued for delivering to all clients + partial, // some of clients queueus are full, message was not enqueued there + none // no clients or all outbound queues are full, message discarded + }; + + using WSocketServerEvent_t = std::function; + + + explicit WSocketServer(const char* url, WSocketServerEvent_t handler = {}) : _url(url), _eventHandler(handler) {} + ~WSocketServer() = default; + + const char *url() const { + return _url.c_str(); + } + void enable(bool e) { + _enabled = e; + } + bool enabled() const { + return _enabled; + } + + + /** + * @brief check if client with specified id can accept new message for sending + * + * @param id + * @return WSocketClient::err_t + */ + WSocketClient::err_t canSend(uint32_t id) const { return _getClient(id) ? _getClient(id)->canSend() : WSocketClient::err_t::disconnected; }; + + /** + * @brief check how many clients are available for sending data + * + * @return msgall_err_t + */ + msgall_err_t canSend() const; + + // return number of active clients + //size_t activeClientsCount() const { return std::count_if(std::begin(_clients), std::end(_clients), [](const WSocketClient &c) { return c.status() == WSocketClient::conn_state_t::connected; }); }; + + // find if there is a client with specified id + bool hasClient(uint32_t id) const { + return _getClient(id) != nullptr; + } + + /** + * @brief disconnect client + * + * @param id + * @param code + * @param message + */ + void close(uint32_t id, uint16_t code = 0, const char *message = NULL){ if (WSocketClient *c = _getClient(id)) { c->close(code, message); } } + + /** + * @brief disconnect all clients + * + * @param code + * @param message + */ + void closeAll(uint16_t code = 0, const char *message = NULL){ for (auto &c : _clients) { c.close(code, message); } } + + /** + * @brief sned ping to client + * + * @param id + * @param data + * @param len + * @return true + * @return false + */ + WSocketClient::err_t ping(uint32_t id, const char *data = NULL, size_t len = 0){ if (WSocketClient *c = _getClient(id)) { return c->ping(data, len); } } + + /** + * @brief send ping to all clients + * + * @param data + * @param len + * @return msgall_err_t + */ + msgall_err_t pingAll(const char *data = NULL, size_t len = 0); + + msgall_err_t messageAll(std::shared_ptr m); + + + /* + bool text(uint32_t id, const uint8_t *message, size_t len); + bool text(uint32_t id, const char *message, size_t len); + bool text(uint32_t id, const char *message); + bool text(uint32_t id, const String &message); + bool text(uint32_t id, AsyncWebSocketMessageBuffer *buffer); + bool text(uint32_t id, AsyncWebSocketSharedBuffer buffer); + + enqueue_err_t textAll(const uint8_t *message, size_t len); + enqueue_err_t textAll(const char *message, size_t len); + enqueue_err_t textAll(const char *message); + enqueue_err_t textAll(const String &message); + enqueue_err_t textAll(AsyncWebSocketMessageBuffer *buffer); + enqueue_err_t textAll(AsyncWebSocketSharedBuffer buffer); + + bool binary(uint32_t id, const uint8_t *message, size_t len); + bool binary(uint32_t id, const char *message, size_t len); + bool binary(uint32_t id, const char *message); + bool binary(uint32_t id, const String &message); + bool binary(uint32_t id, AsyncWebSocketMessageBuffer *buffer); + bool binary(uint32_t id, AsyncWebSocketSharedBuffer buffer); + + enqueue_err_t binaryAll(const uint8_t *message, size_t len); + enqueue_err_t binaryAll(const char *message, size_t len); + enqueue_err_t binaryAll(const char *message); + enqueue_err_t binaryAll(const String &message); + enqueue_err_t binaryAll(AsyncWebSocketMessageBuffer *buffer); + enqueue_err_t binaryAll(AsyncWebSocketSharedBuffer buffer); + + size_t printf(uint32_t id, const char *format, ...) __attribute__((format(printf, 3, 4))); + size_t printfAll(const char *format, ...) __attribute__((format(printf, 2, 3))); +*/ + + void handleHandshake(AwsHandshakeHandler handler) { + _handshakeHandler = handler; + } + + /** + * @brief access clients list + * @note manipulating the list of clients without the locking could be dangerous! + * + * @return std::list& + */ + std::list &getClients() { + return _clients; + } + + // return next available client's ID + uint32_t getNextId() { + return ++_cNextId; + } + + /** + * @brief callback for AsyncServe - onboard new ws client + * + * @param request + * @return true + * @return false + */ + bool newClient(AsyncWebServerRequest *request); + +private: + String _url; + WSocketServerEvent_t _eventHandler; + AwsHandshakeHandler _handshakeHandler; + std::list _clients; + uint32_t _cNextId{0}; + bool _enabled{true}; + #ifdef ESP32 + std::mutex _lock; + #endif + + + /** + * @brief Get ptr to client with specified id + * + * @param id + * @return WSocketClient* - nullptr if client not founc + */ + WSocketClient* _getClient(uint32_t id); + WSocketClient const* _getClient(uint32_t id) const; + + /** + * @brief go through clients list and remove those ones that are disconnected and have no messages pending + * + */ + void _purgeClients(); + + // WSocketClient events handler + void _clientEvents(WSocketClient *client, WSocketClient::event_t event); + + // WebServer methods + bool canHandle(AsyncWebServerRequest *request) const override final { return _enabled && request->isWebSocketUpgrade() && request->url().equals(_url); }; + void handleRequest(AsyncWebServerRequest *request) override final; + +}; From e7a60a9cdec965fc91ac93f64918fa705ae85118 Mon Sep 17 00:00:00 2001 From: Emil Muratov Date: Sun, 5 Oct 2025 21:43:26 +0900 Subject: [PATCH 03/15] dummy ws message class - gracefully handle large incoming messages If client sends us a message with size larger that predefined quota we could handle it gracefully. We send to the peer ws close message with proper code and wait for ack to terminate TCP connection. Meanwhile incoming message data is transparently discarded. --- src/AsyncWSocket.cpp | 95 ++++++++++++++++++++++++++++---------------- src/AsyncWSocket.h | 70 ++++++++++++++++++++++---------- 2 files changed, 110 insertions(+), 55 deletions(-) diff --git a/src/AsyncWSocket.cpp b/src/AsyncWSocket.cpp index 269d8dc37..1e40daf69 100644 --- a/src/AsyncWSocket.cpp +++ b/src/AsyncWSocket.cpp @@ -7,13 +7,9 @@ #include "literals.h" constexpr const char WS_STR_CONNECTION[] = "Connection"; -//constexpr const char WS_STR_UPGRADE[] = "Upgrade"; -//constexpr const char WS_STR_ORIGIN[] = "Origin"; -//constexpr const char WS_STR_COOKIE[] = "Cookie"; constexpr const char WS_STR_VERSION[] = "Sec-WebSocket-Version"; constexpr const char WS_STR_KEY[] = "Sec-WebSocket-Key"; constexpr const char WS_STR_PROTOCOL[] = "Sec-WebSocket-Protocol"; -//constexpr const char WS_STR_ACCEPT[] = "Sec-WebSocket-Accept"; /** @@ -132,14 +128,6 @@ size_t webSocketSendHeader(AsyncClient *client, WSMessageFrame& frame) { } size_t sent = client->add((const char*)buf, headLen); - - if (frame.msg->type == WSFrameType_t::close && frame.msg->getStatusCode()){ - // this is a 'close' message with status code, need to send the code also along with header - uint16_t code = htons (frame.msg->getStatusCode()); - sent += client->add((char*)(&code), 2); - headLen += 2; - } - // return size of a header added or 0 if any error return sent == headLen ? sent : 0; } @@ -192,21 +180,15 @@ void WSocketClient::_clientSend(size_t acked_bytes){ // create lock object but don't actually take the lock yet std::unique_lock lock{_sendLock, std::defer_lock}; - if (acked_bytes){ - // if it's the ack call from AsyncTCP - wait for lock! - lock.lock(); - log_d("_clientSend, ack:%u/%u, space:%u", acked_bytes, _in_flight, _client ? _client->space() : 0); - } else { - // if there is no acked data - just quit, we are already sending something - if (!lock.try_lock()) - return; - } - // for response data we need to control AsyncTCP's event queue and in-flight fragmentation. Sending small chunks could give lower 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 and minimizing events in asynctcp's Q if (acked_bytes){ + // if it's the ack call from AsyncTCP - wait for lock! + lock.lock(); + log_d("_clientSend, ack:%u/%u, space:%u", acked_bytes, _in_flight, _client ? _client->space() : 0); + _in_flight -= std::min(acked_bytes, _in_flight); log_d("infl:%u, state:%u", _in_flight, _connection); // check if we were waiting to ack our disconnection frame @@ -220,6 +202,10 @@ void WSocketClient::_clientSend(size_t acked_bytes){ // return buffer credit on acked data ++_in_flight_credit; + } else { + // if there is no acked data - just quit if won't be able to grab a lock, we are already sending something + if (!lock.try_lock()) + return; } if (_in_flight > _client->space() || !_in_flight_credit) { @@ -271,7 +257,7 @@ void WSocketClient::_clientSend(size_t acked_bytes){ // execute callback that a message was sent if (_cb) - _cb(this, event_t::msgSent); + _cb(this, event_t::msgSent); // if there are free in-flight credits try to pull next msg from Q if (_in_flight_credit && _evictOutQueue()){ @@ -374,13 +360,13 @@ void WSocketClient::_onData(void *pbuf, size_t plen) { size_t framelen; uint16_t errcode; std::tie(framelen, errcode) = _mkNewFrame(data, plen, _inFrame); - if (!framelen){ - // was unable to start receiving frame? initiate disconnect exchange + if (framelen < 2 || errcode){ + // got bad length or close code, initiate disconnect procedure #ifdef ESP32 std::unique_lock lockout(_outQlock); #endif _messageQueueOut.push_front( std::make_shared(errcode) ); - // try sending disconnect message now + // send disconnect message now _clientSend(); return; } @@ -404,6 +390,12 @@ void WSocketClient::_onData(void *pbuf, size_t plen) { if (_inFrame.index == _inFrame.len){ Serial.printf("_onData, cmplt msg len:%lu\n", _inFrame.len); + if (_inFrame.msg->getStatusCode() == 1007){ + // this is a dummy/corrupted message, we discard it + _inFrame.msg.reset(); + continue; + } + switch (_inFrame.msg->type){ // received close message case WSFrameType_t::close : { @@ -511,19 +503,32 @@ std::pair WSocketClient::_mkNewFrame(char* data, size_t len, W // read frame size frame.len = data[1] & 0x7F; size_t offset = 2; // first 2 bytes - + log_d("ws hdr: "); + //Serial.println(frame.mask, HEX); + char buffer[10] = {}; // Buffer for hex conversion + char* ptr = data; + for (size_t i = 0; i != 10; ++i ) { + sprintf(buffer, "%02X", *ptr); // Convert to uppercase hex + Serial.print(buffer); + //Serial.print(" "); + ++ptr; + } + Serial.println(); + + // find message size from header if (frame.len == 126 && len >= 4) { - frame.len = data[3] | (uint16_t)(data[2]) << 8; + // two byte + frame.len = (data[2] << 8) | data[3]; offset += 2; } else if (frame.len == 127 && len >= 10) { + // four byte frame.len = ntohl(*(uint32_t*)(data + 2)); // MSB - frame.len <= 32; + frame.len <<= 32; frame.len |= ntohl(*(uint32_t*)(data + 6)); // LSB offset += 8; } - if (frame.len > _max_msgsize) // message too big than we allowed to accept - return {0, 1009}; + log_d("new hdr, sock data:%u, msg body size:%u", len, frame.len); // if ws.close() is called, Safari sends a close frame with plen 2 and masked bit set. We must not try to read mask key from beyond packet size if (masked && len >= offset + 4) { @@ -537,11 +542,18 @@ std::pair WSocketClient::_mkNewFrame(char* data, size_t len, W frame.index = frame.chunk_offset = 0; size_t bodylen = std::min(static_cast(frame.len), len - offset); - // if there is no body in message, then it must a specific control message with no payload if (!bodylen){ - // I'll make an empty binary container for such messages - _inFrame.msg = std::make_shared>>(static_cast(opcode)); + // if there is no body in message, then it must a specific control message with no payload + _inFrame.msg = std::make_shared(static_cast(opcode)); } else { + if (frame.len > _max_msgsize){ + // message is bigger than we are allowed to accept, create a dummy container for it, it will just discard all incoming data + _inFrame.msg = std::make_shared(static_cast(opcode, 1007)); // code 'Invalid frame payload data' + offset += bodylen; + _inFrame.index = bodylen; + return {offset, 1009}; // code 'message too big' + } + // let's unmask payload right in sock buff, later it could be consumed as raw data if (masked){ wsMaskPayload(_inFrame.mask, 0, data + offset, bodylen); @@ -632,8 +644,8 @@ WSocketClient::err_t WSocketClient::close(uint16_t code, const char *message){ return err_t::ok; } -// ***** WSocketServer implementation ***** +// ***** WSocketServer implementation ***** bool WSocketServer::newClient(AsyncWebServerRequest *request){ // remove expired clients first @@ -739,3 +751,16 @@ void WSocketServer::_purgeClients(){ // purge clients that are disconnected and with all messages consumed std::erase_if(_clients, [](const WSocketClient& c){ return (c.status() == WSocketClient::conn_state_t::disconnected && !c.inQueueSize() ); }); } + +size_t WSocketServer::activeClientsCount() const { + return std::count_if(std::begin(_clients), std::end(_clients), [](const WSocketClient &c) { return c.status() == WSocketClient::conn_state_t::connected; }); +}; + + +// ***** WSMessageClose implementation ***** + +WSMessageClose::WSMessageClose (uint16_t status) : WSMessageContainer(WSFrameType_t::close, true), _status_code(status) { + // convert code to message body + uint16_t buff = htons (status); + container.append((char*)(&buff), 2); +}; diff --git a/src/AsyncWSocket.h b/src/AsyncWSocket.h index e51fd6029..a750714b2 100644 --- a/src/AsyncWSocket.h +++ b/src/AsyncWSocket.h @@ -78,8 +78,9 @@ class WSMessageGeneric { /** * @brief WebSocket message status code as defined in https://datatracker.ietf.org/doc/html/rfc6455#section-7.4 + * it could be used as integrity control or derived class features * - * @return uint16_t + * @return uint16_t 0 - default code for correct message */ virtual uint16_t getStatusCode() const { return 0; } @@ -116,7 +117,6 @@ class WSMessageGeneric { }; - template class WSMessageContainer : public WSMessageGeneric { protected: @@ -197,14 +197,36 @@ class WSMessageClose : public WSMessageContainer { * * @param status close code as defined in https://datatracker.ietf.org/doc/html/rfc6455#section-7.4 */ - WSMessageClose (uint16_t status = 1000) : WSMessageContainer(WSFrameType_t::close, true), _status_code(status) {}; + WSMessageClose (uint16_t status = 1000); // variadic constructor for anything that std::string can be made of template - WSMessageClose (uint16_t status, Args&&... args) : WSMessageContainer(WSFrameType_t::close, true, std::forward(args)...), _status_code(status) {}; + WSMessageClose (uint16_t status, Args&&... args) : WSMessageContainer(WSFrameType_t::close, true, std::forward(args)...), _status_code(status) { + // convert code to message body + uint16_t buff = htons (status); + container.append((char*)(&buff), 2); + }; uint16_t getStatusCode() const override { return _status_code; } }; +/** + * @brief Dummy message that does not carry any data + * could be used as a container for bodyless control messages or + * specific cased (to gracefully handle oversised incoming messages) + */ +class WSMessageDummy : public WSMessageGeneric { + const uint16_t _code; +public: + explicit WSMessageDummy(WSFrameType_t type, uint16_t status_code = 0) : WSMessageGeneric(type, true), _code(status_code) {}; + size_t getSize() const override { return 0; }; + char* getData() override { return nullptr; }; + uint16_t getStatusCode() const override { return _code; } + +protected: + void addChunk(char* data, size_t len, size_t offset) override {}; +}; + + /** * @brief structure that owns the message (or fragment) while sending/receiving by WSocketClient */ @@ -229,9 +251,9 @@ class WSocketClient { public: // TCP connection state enum class conn_state_t { - connected, - disconnecting, - disconnected + connected, // connected and exchangin messages + disconnecting, // awaiting close ack + disconnected // ws client is disconnected }; /** @@ -369,7 +391,6 @@ class WSocketClient { // in-flight data credits size_t _in_flight_credit{2}; - /** * @brief go through out Q and send message data () * @note this method will grab a mutex lock on outQ internally @@ -419,17 +440,6 @@ class WSocketServer : public AsyncWebHandler { explicit WSocketServer(const char* url, WSocketServerEvent_t handler = {}) : _url(url), _eventHandler(handler) {} ~WSocketServer() = default; - const char *url() const { - return _url.c_str(); - } - void enable(bool e) { - _enabled = e; - } - bool enabled() const { - return _enabled; - } - - /** * @brief check if client with specified id can accept new message for sending * @@ -446,7 +456,7 @@ class WSocketServer : public AsyncWebHandler { msgall_err_t canSend() const; // return number of active clients - //size_t activeClientsCount() const { return std::count_if(std::begin(_clients), std::end(_clients), [](const WSocketClient &c) { return c.status() == WSocketClient::conn_state_t::connected; }); }; + size_t activeClientsCount() const; // find if there is a client with specified id bool hasClient(uint32_t id) const { @@ -490,6 +500,21 @@ class WSocketServer : public AsyncWebHandler { */ msgall_err_t pingAll(const char *data = NULL, size_t len = 0); + /** + * @brief send message to specific client + * + * @param id + * @param m + * @return WSocketClient::err_t + */ + WSocketClient::err_t message(uint32_t id, std::shared_ptr m){ if (WSocketClient *c = _getClient(id)) { return c->enqueueMessage(std::move(m)); } } + + /** + * @brief send message to all available clients + * + * @param m + * @return msgall_err_t + */ msgall_err_t messageAll(std::shared_ptr m); @@ -545,6 +570,11 @@ class WSocketServer : public AsyncWebHandler { return ++_cNextId; } + // return bound URL + const char *url() const { + return _url.c_str(); + } + /** * @brief callback for AsyncServe - onboard new ws client * From bd4f022ef9829f1123d61d64961b1da46cd04c34 Mon Sep 17 00:00:00 2001 From: Emil Muratov Date: Wed, 8 Oct 2025 22:58:08 +0900 Subject: [PATCH 04/15] WSocketServerWorker implementation WSocketServer with worker thread that handles events from clients. Decouples AsyncTCP thread callbacks from executing user code to handle events and incoming messages. --- src/AsyncWSocket.cpp | 272 ++++++++++++++++++++++++++++++------------- src/AsyncWSocket.h | 225 +++++++++++++++++++++++++---------- 2 files changed, 356 insertions(+), 141 deletions(-) diff --git a/src/AsyncWSocket.cpp b/src/AsyncWSocket.cpp index 1e40daf69..586f7cc99 100644 --- a/src/AsyncWSocket.cpp +++ b/src/AsyncWSocket.cpp @@ -11,6 +11,16 @@ constexpr const char WS_STR_VERSION[] = "Sec-WebSocket-Version"; constexpr const char WS_STR_KEY[] = "Sec-WebSocket-Key"; constexpr const char WS_STR_PROTOCOL[] = "Sec-WebSocket-Protocol"; +// WSockServer worke task +constexpr const char WS_SRV_TASK[] = "WSSrvtask"; + +// cast enum class to uint (for bit set) +template +constexpr std::common_type_t> +enum2uint32(E e) { + return static_cast>>(e); +} + /** * @brief apply mask key to supplied data byte-per-byte @@ -135,7 +145,7 @@ size_t webSocketSendHeader(AsyncClient *client, WSMessageFrame& frame) { // ******** WSocket classes implementation ******** -WSocketClient::WSocketClient(uint32_t id, AsyncWebServerRequest *request, WSocketClient::event_callback_t call_back, size_t msgsize, size_t qcapsize) : +WSocketClient::WSocketClient(uint32_t id, AsyncWebServerRequest *request, WSocketClient::event_cb_t call_back, size_t msgsize, size_t qcapsize) : id(id), _client(request->client()), _cb(call_back), @@ -152,16 +162,21 @@ WSocketClient::WSocketClient(uint32_t id, AsyncWebServerRequest *request, WSocke _client->onTimeout( [](void *r, AsyncClient *c, uint32_t time) { (void)c; reinterpret_cast(r)->_onTimeout(time); }, this ); _client->onData( [](void *r, AsyncClient *c, void *buf, size_t len) { (void)c; reinterpret_cast(r)->_onData(buf, len); }, this ); _client->onPoll( [](void *r, AsyncClient *c) { (void)c; reinterpret_cast(r)->_clientSend(); }, this ); - // not implemented yet - //_client->onError( [](void *r, AsyncClient *c, int8_t error) { (void)c; reinterpret_cast(r)->_onError(error); }, this ); + _client->onError( [](void *r, AsyncClient *c, int8_t error) { (void)c; log_e("err:%d", error); }, this ); delete request; } WSocketClient::~WSocketClient() { if (_client){ + // remove callback here, 'cause _client's destructor will call it again + _client->onDisconnect(nullptr); delete _client; _client = nullptr; } + if (_eventGroup){ + vEventGroupDelete(_eventGroup); + _eventGroup = nullptr; + } } // ***** AsyncTCP callbacks ***** @@ -190,7 +205,9 @@ void WSocketClient::_clientSend(size_t acked_bytes){ log_d("_clientSend, ack:%u/%u, space:%u", acked_bytes, _in_flight, _client ? _client->space() : 0); _in_flight -= std::min(acked_bytes, _in_flight); - log_d("infl:%u, state:%u", _in_flight, _connection); + // return buffer credit on acked data + ++_in_flight_credit; + log_d("infl:%u, credits:%u, conn state:%u", _in_flight, _in_flight_credit, _connection); // check if we were waiting to ack our disconnection frame if (!_in_flight && (_connection == conn_state_t::disconnecting)){ log_d("closing tcp-conn"); @@ -200,8 +217,6 @@ void WSocketClient::_clientSend(size_t acked_bytes){ return; } - // return buffer credit on acked data - ++_in_flight_credit; } else { // if there is no acked data - just quit if won't be able to grab a lock, we are already sending something if (!lock.try_lock()) @@ -209,7 +224,7 @@ void WSocketClient::_clientSend(size_t acked_bytes){ } if (_in_flight > _client->space() || !_in_flight_credit) { - log_d("defer ws send call, in-flight:%u/%u", _in_flight, _client->space()); + log_d("defer ws send call, in-flight:%u/%u, credit:%u", _in_flight, _client->space(), _in_flight_credit); return; } @@ -255,9 +270,8 @@ void WSocketClient::_clientSend(size_t acked_bytes){ // release _outFrame message here _outFrame.msg.reset(); - // execute callback that a message was sent - if (_cb) - _cb(this, event_t::msgSent); + // no use case for this for now + //_sendEvent(event_t::msgSent); // if there are free in-flight credits try to pull next msg from Q if (_in_flight_credit && _evictOutQueue()){ @@ -320,10 +334,6 @@ bool WSocketClient::_evictOutQueue(){ // this function assumes it's callee will take care of actually sending the header and message body further } -void WSocketClient::_onError(int8_t) { - // Serial.println("onErr"); -} - void WSocketClient::_onTimeout(uint32_t time) { if (!_client) { return; @@ -334,19 +344,9 @@ void WSocketClient::_onTimeout(uint32_t time) { } void WSocketClient::_onDisconnect(AsyncClient *c) { - Serial.println("TCP client disconencted"); - delete c; - _client = c = nullptr; _connection = conn_state_t::disconnected; - - #ifdef ESP32 - std::lock_guard lock(_outQlock); - #endif - // clear out Q, we won't be able to send it anyway from now on - _messageQueueOut.clear(); - // execute callback - if (_cb) - _cb(this, event_t::disconnect); + log_d("TCP client disconnected"); + _sendEvent(event_t::disconnect); } void WSocketClient::_onData(void *pbuf, size_t plen) { @@ -428,9 +428,7 @@ void WSocketClient::_onData(void *pbuf, size_t plen) { _messageQueueOut.push_front(_inFrame.msg); } _inFrame.msg.reset(); - if (_cb) - _cb(this, event_t::msgRecv); - + _sendEvent(event_t::msgRecv); break; } @@ -445,53 +443,20 @@ void WSocketClient::_onData(void *pbuf, size_t plen) { } default: { - log_e("other msg"); // any other messages { #ifdef ESP32 - std::unique_lock lock(_inQlock); + std::lock_guard lock(_inQlock); #endif - // check in-queue for overflow - if (_messageQueueIn.size() >= _max_qcap){ - log_e("q overflow"); - switch (_overflow_policy){ - case overflow_t::discard : - // discard incoming message - break; - case overflow_t::drophead : - _messageQueueIn.pop_front(); - _messageQueueIn.push_back(_inFrame.msg); - break; - case overflow_t::droptail : - _messageQueueIn.pop_back(); - _messageQueueIn.push_back(_inFrame.msg); - break; - case overflow_t::disconnect : { - #ifdef ESP32 - std::unique_lock lock(_inQlock); - #endif - _messageQueueOut.push_front( std::make_shared(1011, "Server Q overflow") ); - break; - } - default:; - } - - } else { - log_e("push new to Q"); - _messageQueueIn.push_back(_inFrame.msg); - } + _messageQueueIn.push_back(_inFrame.msg); } _inFrame.msg.reset(); - log_e("evt on new msg"); - if (_cb) - _cb(this, event_t::msgRecv); - + _sendEvent(event_t::msgRecv); break; } } } } - } std::pair WSocketClient::_mkNewFrame(char* data, size_t len, WSMessageFrame& frame){ @@ -503,7 +468,7 @@ std::pair WSocketClient::_mkNewFrame(char* data, size_t len, W // read frame size frame.len = data[1] & 0x7F; size_t offset = 2; // first 2 bytes - log_d("ws hdr: "); + Serial.print("ws hdr: "); //Serial.println(frame.mask, HEX); char buffer[10] = {}; // Buffer for hex conversion char* ptr = data; @@ -553,7 +518,43 @@ std::pair WSocketClient::_mkNewFrame(char* data, size_t len, W _inFrame.index = bodylen; return {offset, 1009}; // code 'message too big' } - + + // check in-queue for overflow + if (_messageQueueIn.size() >= _max_qcap){ + log_w("q overflow"); + switch (_overflow_policy){ + case overflow_t::discard : + // silently discard incoming message + _inFrame.msg = std::make_shared(static_cast(opcode, 1007)); // code 'Invalid frame payload data' + offset += bodylen; + _inFrame.index = bodylen; + return {offset, 0}; + + case overflow_t::disconnect : { + // discard incoming message and send close message + _inFrame.msg = std::make_shared(static_cast(opcode, 1007)); // code 'Invalid frame payload data' + #ifdef ESP32 + std::lock_guard lock(_inQlock); + #endif + _messageQueueOut.push_front( std::make_shared(1011, "Server Q overflow") ); + offset += bodylen; + _inFrame.index = bodylen; + return {offset, 0}; + } + + case overflow_t::drophead : + _messageQueueIn.pop_front(); + break; + case overflow_t::droptail : + _messageQueueIn.pop_back(); + break; + + default:; + } + + _sendEvent(event_t::msgDropped); + } + // let's unmask payload right in sock buff, later it could be consumed as raw data if (masked){ wsMaskPayload(_inFrame.mask, 0, data + offset, bodylen); @@ -590,12 +591,12 @@ std::pair WSocketClient::_mkNewFrame(char* data, size_t len, W _inFrame.index = bodylen; } - log_e("new msg offset:%u, body:%u", offset, bodylen); + log_e("new msg frame size:%u, bodylen:%u", offset, bodylen); // return the number of consumed data from input buffer return {offset, 0}; } -WSocketClient::err_t WSocketClient::enqueueMessage(std::shared_ptr mptr){ +WSocketClient::err_t WSocketClient::enqueueMessage(WSMessagePtr mptr){ if (_connection != conn_state_t::connected) return err_t::disconnected; @@ -611,11 +612,11 @@ WSocketClient::err_t WSocketClient::enqueueMessage(std::shared_ptr WSocketClient::dequeueMessage(){ +WSMessagePtr WSocketClient::dequeueMessage(){ #ifdef ESP32 std::unique_lock lock(_inQlock); #endif - std::shared_ptr msg; + WSMessagePtr msg; if (_messageQueueIn.size()){ msg.swap(_messageQueueIn.front()); _messageQueueIn.pop_front(); @@ -644,6 +645,13 @@ WSocketClient::err_t WSocketClient::close(uint16_t code, const char *message){ return err_t::ok; } +void WSocketClient::_sendEvent(event_t e){ + if (_eventGroup) + xEventGroupSetBits(_eventGroup, enum2uint32(e)); + if (_cb) + _cb(this, e); +} + // ***** WSocketServer implementation ***** @@ -652,20 +660,20 @@ bool WSocketServer::newClient(AsyncWebServerRequest *request){ _purgeClients(); { #ifdef ESP32 - std::lock_guard lock (_lock); + std::lock_guard lock(clientslock); #endif - _clients.emplace_back(getNextId(), request, [this](WSocketClient *c, WSocketClient::event_t e){ _clientEvents(c, e); }); + _clients.emplace_back(getNextId(), request, [this](WSocketClient *c, WSocketClient::event_t e){ + if (eventHandler) + eventHandler(c, e); + else + c->dequeueMessage(); } // silently discard incoming messages when there is no callback set + ); } - _clientEvents(&_clients.back(), WSocketClient::event_t::connect); + _clients.back().setOverflowPolicy(_overflow_policy); + if (eventHandler) eventHandler(&_clients.back(), WSocketClient::event_t::connect); return true; } -void WSocketServer::_clientEvents(WSocketClient *client, WSocketClient::event_t event){ - log_d("_clientEvents: %u", event); - if (_eventHandler) - _eventHandler(client, event); -} - void WSocketServer::handleRequest(AsyncWebServerRequest *request) { if (!request->hasHeader(WS_STR_VERSION) || !request->hasHeader(WS_STR_KEY)) { request->send(400); @@ -734,7 +742,7 @@ WSocketServer::msgall_err_t WSocketServer::pingAll(const char *data, size_t len) return cnt == _clients.size() ? msgall_err_t::ok : msgall_err_t::partial; } -WSocketServer::msgall_err_t WSocketServer::messageAll(std::shared_ptr m){ +WSocketServer::msgall_err_t WSocketServer::messageAll(WSMessagePtr m){ size_t cnt{0}; for (auto &c : _clients) { if ( c.enqueueMessage(m) == WSocketClient::err_t::ok) @@ -747,7 +755,7 @@ WSocketServer::msgall_err_t WSocketServer::messageAll(std::shared_ptr lock (clientslock); + #endif + _clients.emplace_back(getNextId(), request, [this](WSocketClient *c, WSocketClient::event_t e){ if (_task_hndlr) xTaskNotifyGive(_task_hndlr); }); + } + + // create events group where we'll pick events + _clients.back().createEventGroupHandle(); + _clients.back().setOverflowPolicy(getOverflowPolicy()); + xEventGroupSetBits(_clients.back().getEventGroupHandle(), enum2uint32(WSocketClient::event_t::connect)); + if (_task_hndlr) + xTaskNotifyGive(_task_hndlr); + return true; +} + +void WSocketServerWorker::start(uint32_t stack, UBaseType_t uxPriority, BaseType_t xCoreID){ + if (_task_hndlr) return; // we are already running + + xTaskCreatePinnedToCore( + [](void* pvParams){ static_cast(pvParams)->_taskRunner(); }, + WS_SRV_TASK, + stack, + (void *)this, + uxPriority, + &_task_hndlr, + xCoreID ); // == pdPASS; +} + +void WSocketServerWorker::stop(){ + if (_task_hndlr){ + vTaskDelete(_task_hndlr); + _task_hndlr = nullptr; + } +} + +void WSocketServerWorker::_taskRunner(){ + for (;;){ + + // go through our client's list looking for pending events, do not care to lock the list here, + // 'cause nobody would be able to remove anything from it but this loop and adding new client won't invalidate current iterator + auto it = _clients.begin(); + while (it != _clients.end()){ + EventBits_t uxBits; + + // check if this a new client + uxBits = xEventGroupClearBits(it->getEventGroupHandle(), enum2uint32(WSocketClient::event_t::connect) ); + log_d("uxBits:%u", uxBits); + if ( uxBits & enum2uint32(WSocketClient::event_t::connect) ){ + _ecb(WSocketClient::event_t::connect, it->id); + } + + // check if 'inbound Q full' flag set + uxBits = xEventGroupClearBits(it->getEventGroupHandle(), enum2uint32(WSocketClient::event_t::inQfull) ); + if ( uxBits & enum2uint32(WSocketClient::event_t::inQfull) ){ + _ecb(WSocketClient::event_t::inQfull, it->id); + } + + // check for dropped messages flag + uxBits = xEventGroupClearBits(it->getEventGroupHandle(), enum2uint32(WSocketClient::event_t::msgDropped) ); + if ( uxBits & enum2uint32(WSocketClient::event_t::msgDropped) ){ + _ecb(WSocketClient::event_t::msgDropped, it->id); + } + + // process all the messages from inbound Q + xEventGroupClearBits(it->getEventGroupHandle(), enum2uint32(WSocketClient::event_t::msgRecv) ); + while (auto m{it->dequeueMessage()}){ + _mcb(m, it->id); + } + + // check for disconnected client - do not care for group bits, cause if it's deleted, we will destruct the client object + if (it->canSend() == WSocketClient::err_t::disconnected){ + auto id = it->id; + { + #ifdef ESP32 + std::lock_guard lock (clientslock); + #endif + it = _clients.erase(it); + } + // run a callback + _ecb(WSocketClient::event_t::disconnect, id); + } else { + // advance iterator + ++it; + } + } + + // wait for next event here, using counted notification we could do some dry-runs but won't miss any events + ulTaskNotifyTake(pdFALSE, portMAX_DELAY); + + // end of a task loop + } + vTaskDelete(NULL); +} + diff --git a/src/AsyncWSocket.h b/src/AsyncWSocket.h index a750714b2..7e9265abd 100644 --- a/src/AsyncWSocket.h +++ b/src/AsyncWSocket.h @@ -7,7 +7,13 @@ #pragma once #include "AsyncWebSocket.h" +#include "freertos/FreeRTOS.h" +#ifndef WS_IN_FLIGHT_CREDITS +#define WS_IN_FLIGHT_CREDITS 4 +#endif + +// forward declaration for WSocketServer class WSocketServer; /** @@ -117,6 +123,8 @@ class WSMessageGeneric { }; +using WSMessagePtr = std::shared_ptr; + template class WSMessageContainer : public WSMessageGeneric { protected: @@ -240,7 +248,7 @@ struct WSMessageFrame { /** offset in the current chunk of data, used to when sending message in chunks (not WS fragments!), for single chunged messages chunk_offset and index are same */ size_t chunk_offset; // message object - std::shared_ptr msg; + WSMessagePtr msg; }; /** @@ -261,10 +269,12 @@ class WSocketClient { * */ enum class event_t { - connect, - disconnect, - msgRecv, - msgSent + connect = 0x01, + disconnect = 0x02, + msgRecv = 0x04, + msgSent = 0x08, + msgDropped = 0x10, + inQfull = 0x20 }; // error codes @@ -282,14 +292,15 @@ class WSocketClient { droptail }; - using event_callback_t = std::function; + // event callback alias + using event_cb_t = std::function; // Client connection ID (increments for each new connection for the given server) const uint32_t id; private: AsyncClient *_client; - event_callback_t _cb; + event_cb_t _cb; // incoming message size limit size_t _max_msgsize; // cummulative maximum of the data messages held in message queues, both in and out @@ -306,7 +317,7 @@ class WSocketClient { * @param msgcap - incoming message size limit, if incoming msg advertizes larger size the connection would be dropped * @param qcap - in/out queues sizes (in number of messages) */ - WSocketClient(uint32_t id, AsyncWebServerRequest *request, WSocketClient::event_callback_t call_back, size_t msgsize = 8 * 1024, size_t qcap = 4); + WSocketClient(uint32_t id, AsyncWebServerRequest *request, WSocketClient::event_cb_t call_back, size_t msgsize = 8 * 1024, size_t qcap = 4); ~WSocketClient(); /** @@ -320,10 +331,10 @@ class WSocketClient { /** * @brief retrieve message from inbout Q * - * @return std::shared_ptr + * @return WSMessagePtr * @note if Q is empty, then empty pointer is returned, so it should be validated */ - std::shared_ptr dequeueMessage(); + WSMessagePtr dequeueMessage(); conn_state_t status() const { return _connection; @@ -338,7 +349,7 @@ class WSocketClient { } /** - * @brief Set inbound queueu overflow Policy + * @brief Set inbound queue overflow Policy * * @param policy inbound queue overflow policy * @@ -347,7 +358,7 @@ class WSocketClient { * The big issue with this behavior is that is can cause the UI to automatically re-create a new WS connection, which can be filled again, * and so on, causing a resource exhaustion. * - * @note overflow_t::discard - silently discard new messages if the queue is full. + * @note overflow_t::discard - silently discard new messages if the queue is full (only a discard event would be generated to notify about drop) * This is the default behavior in the original ESPAsyncWebServer library from me-no-dev. This behavior allows the best performance at the expense of unreliable message delivery in case the queue is full. * * @note overflow_t::drophead - drop the oldest message from inbound queue to fit new message @@ -368,13 +379,33 @@ class WSocketClient { // check if client can enqueue and send new messages err_t canSend() const; + /** + * @brief access Event Group Handle for the client + * + * @return EventGroupHandle_t + */ + EventGroupHandle_t getEventGroupHandle(){ return _eventGroup; }; + + /** + * @brief Create a Event Group for the client + * if Event Group is created then client will set event bits in the group + * when various events are generate, i.e. message received, connect/disconnect, etc... + * + * @return EventGroupHandle_t + */ + EventGroupHandle_t createEventGroupHandle(){ + if (!_eventGroup) _eventGroup = xEventGroupCreate(); + return _eventGroup; + } + private: conn_state_t _connection{conn_state_t::connected}; // frames in transit WSMessageFrame _inFrame{}, _outFrame{}; // message queues - std::deque< std::shared_ptr > _messageQueueIn; - std::deque< std::shared_ptr > _messageQueueOut; + std::deque< WSMessagePtr > _messageQueueIn; + std::deque< WSMessagePtr > _messageQueueOut; + EventGroupHandle_t _eventGroup{nullptr}; #ifdef ESP32 // access mutex'es @@ -389,7 +420,7 @@ class WSocketClient { // amount of sent data in-flight, i.e. copied to socket buffer, but not acked yet from lwip side size_t _in_flight{0}; // in-flight data credits - size_t _in_flight_credit{2}; + size_t _in_flight_credit{WS_IN_FLIGHT_CREDITS}; /** * @brief go through out Q and send message data () @@ -413,38 +444,42 @@ class WSocketClient { */ std::pair _mkNewFrame(char* data, size_t len, WSMessageFrame& frame); + /** + * @brief run a callback for event / set event group bits + * + * @param e + */ + void _sendEvent(event_t e); + // AsyncTCP callbacks - void _onError(int8_t); void _onTimeout(uint32_t time); void _onDisconnect(AsyncClient *c); void _onData(void *pbuf, size_t plen); }; - /** - * @brief WebServer Handler implementation that plays the role of a socket server + * @brief WebServer Handler implementation that plays the role of a WebSocket server + * it inherits behavior of original WebSocket server and uses AsyncTCP callback + * to handle incoming messages * */ class WSocketServer : public AsyncWebHandler { public: - // error enque to all + // error enqueue to all enum class msgall_err_t { ok = 0, // message was enqueued for delivering to all clients partial, // some of clients queueus are full, message was not enqueued there none // no clients or all outbound queues are full, message discarded }; - using WSocketServerEvent_t = std::function; - - - explicit WSocketServer(const char* url, WSocketServerEvent_t handler = {}) : _url(url), _eventHandler(handler) {} + explicit WSocketServer(const char* url, WSocketClient::event_cb_t handler = {}) : _url(url), eventHandler(handler) {} ~WSocketServer() = default; /** * @brief check if client with specified id can accept new message for sending * * @param id - * @return WSocketClient::err_t + * @return WSocketClient::err_t - ready to send only if returned value is err_t::ok, otherwise err reason is returned */ WSocketClient::err_t canSend(uint32_t id) const { return _getClient(id) ? _getClient(id)->canSend() : WSocketClient::err_t::disconnected; }; @@ -463,6 +498,12 @@ class WSocketServer : public AsyncWebHandler { return _getClient(id) != nullptr; } + /** + * @copydoc WSClient::setOverflowPolicy(overflow_t policy) + */ + void setOverflowPolicy(WSocketClient::overflow_t policy){ _overflow_policy = policy; } + WSocketClient::overflow_t getOverflowPolicy() const { return _overflow_policy; } + /** * @brief disconnect client * @@ -470,7 +511,9 @@ class WSocketServer : public AsyncWebHandler { * @param code * @param message */ - void close(uint32_t id, uint16_t code = 0, const char *message = NULL){ if (WSocketClient *c = _getClient(id)) { c->close(code, message); } } + void close(uint32_t id, uint16_t code = 0, const char *message = NULL){ + if (WSocketClient *c = _getClient(id)) c->close(code, message); + } /** * @brief disconnect all clients @@ -489,7 +532,12 @@ class WSocketServer : public AsyncWebHandler { * @return true * @return false */ - WSocketClient::err_t ping(uint32_t id, const char *data = NULL, size_t len = 0){ if (WSocketClient *c = _getClient(id)) { return c->ping(data, len); } } + WSocketClient::err_t ping(uint32_t id, const char *data = NULL, size_t len = 0){ + if (WSocketClient *c = _getClient(id)) + return c->ping(data, len); + else + return WSocketClient::err_t::disconnected; + } /** * @brief send ping to all clients @@ -507,7 +555,12 @@ class WSocketServer : public AsyncWebHandler { * @param m * @return WSocketClient::err_t */ - WSocketClient::err_t message(uint32_t id, std::shared_ptr m){ if (WSocketClient *c = _getClient(id)) { return c->enqueueMessage(std::move(m)); } } + WSocketClient::err_t message(uint32_t id, WSMessagePtr m){ + if (WSocketClient *c = _getClient(id)) + return c->enqueueMessage(std::move(m)); + else + return WSocketClient::err_t::disconnected; + } /** * @brief send message to all available clients @@ -515,7 +568,7 @@ class WSocketServer : public AsyncWebHandler { * @param m * @return msgall_err_t */ - msgall_err_t messageAll(std::shared_ptr m); + msgall_err_t messageAll(WSMessagePtr m); /* @@ -555,46 +608,44 @@ class WSocketServer : public AsyncWebHandler { _handshakeHandler = handler; } - /** - * @brief access clients list - * @note manipulating the list of clients without the locking could be dangerous! - * - * @return std::list& - */ - std::list &getClients() { - return _clients; - } - - // return next available client's ID - uint32_t getNextId() { - return ++_cNextId; - } - // return bound URL const char *url() const { return _url.c_str(); } /** - * @brief callback for AsyncServe - onboard new ws client + * @brief callback for AsyncServer - onboard new ws client * * @param request * @return true * @return false */ - bool newClient(AsyncWebServerRequest *request); + virtual bool newClient(AsyncWebServerRequest *request); -private: - String _url; - WSocketServerEvent_t _eventHandler; - AwsHandshakeHandler _handshakeHandler; +protected: std::list _clients; - uint32_t _cNextId{0}; - bool _enabled{true}; #ifdef ESP32 - std::mutex _lock; + std::mutex clientslock; #endif + // WSocketClient events handler + WSocketClient::event_cb_t eventHandler; + // return next available client's ID + uint32_t getNextId() { + return ++_cNextId; + } + + /** + * @brief go through clients list and remove those ones that are disconnected and have no messages pending + * + */ + void _purgeClients(); + +private: + std::string _url; + AwsHandshakeHandler _handshakeHandler; + uint32_t _cNextId{0}; + WSocketClient::overflow_t _overflow_policy{WSocketClient::overflow_t::disconnect}; /** * @brief Get ptr to client with specified id @@ -605,17 +656,73 @@ class WSocketServer : public AsyncWebHandler { WSocketClient* _getClient(uint32_t id); WSocketClient const* _getClient(uint32_t id) const; + + // WebServer methods + bool canHandle(AsyncWebServerRequest *request) const override final { return request->isWebSocketUpgrade() && request->url().equals(_url.c_str()); }; + void handleRequest(AsyncWebServerRequest *request) override final; +}; + +/** + * @brief WebServer Handler implementation that plays the role of a WebSocket server + * + */ +class WSocketServerWorker : public WSocketServer { +public: + + // event callback alias + using event_cb_t = std::function; + // message callback alias + using msg_cb_t = std::function; + + explicit WSocketServerWorker(const char* url, msg_cb_t msg_handler, event_cb_t event_handler) + : WSocketServer(url), _mcb(msg_handler), _ecb(event_handler) {} + + ~WSocketServerWorker(){ stop(); }; + /** - * @brief go through clients list and remove those ones that are disconnected and have no messages pending + * @brief start worker task to process WS Messages * + * @param stack + * @param uxPriority + * @param xCoreID */ - void _purgeClients(); + void start(uint32_t stack = 4096, UBaseType_t uxPriority = 4, BaseType_t xCoreID = tskNO_AFFINITY); - // WSocketClient events handler - void _clientEvents(WSocketClient *client, WSocketClient::event_t event); + /** + * @brief stop worker task + * + */ + void stop(); - // WebServer methods - bool canHandle(AsyncWebServerRequest *request) const override final { return _enabled && request->isWebSocketUpgrade() && request->url().equals(_url); }; - void handleRequest(AsyncWebServerRequest *request) override final; + /** + * @brief Set Message Callback function + * + * @param handler + */ + void setMessageHandler(msg_cb_t handler){ _mcb = handler; }; + + /** + * @brief Set Event Callback function + * + * @param handler + */ + void setEventHandler(event_cb_t handler){ _ecb = handler; }; + + /** + * @brief callback for AsyncServer - onboard new ws client + * + * @param request + * @return true + * @return false + */ + bool newClient(AsyncWebServerRequest *request) override; + + +private: + msg_cb_t _mcb; + event_cb_t _ecb; + // worker task that handles messages + TaskHandle_t _task_hndlr{nullptr}; + void _taskRunner(); }; From e2ec190e8f38d345cc9035d67cdacdab7918c32b Mon Sep 17 00:00:00 2001 From: Emil Muratov Date: Thu, 9 Oct 2025 01:10:02 +0900 Subject: [PATCH 05/15] AsyncWSocket keepalive pongs - keepalive pongs implementation for Client class - message size/q cap for WSocketServer class --- src/AsyncWSocket.cpp | 33 ++++++++++++----- src/AsyncWSocket.h | 86 ++++++++++++++++++++++++++++++++++---------- 2 files changed, 93 insertions(+), 26 deletions(-) diff --git a/src/AsyncWSocket.cpp b/src/AsyncWSocket.cpp index 586f7cc99..31af65547 100644 --- a/src/AsyncWSocket.cpp +++ b/src/AsyncWSocket.cpp @@ -152,17 +152,18 @@ WSocketClient::WSocketClient(uint32_t id, AsyncWebServerRequest *request, WSocke _max_msgsize(msgsize), _max_qcap(qcapsize) { + _lastPong = millis(); // disable connection timeout _client->setRxTimeout(0); _client->setNoDelay(true); // set AsyncTCP callbacks _client->onAck( [](void *r, AsyncClient *c, size_t len, uint32_t rtt) { (void)c; reinterpret_cast(r)->_clientSend(len); }, this ); //_client->onAck( [](void *r, AsyncClient *c, size_t len, uint32_t rtt) { (void)c; reinterpret_cast(r)->_onAck(len, rtt); }, this ); - _client->onDisconnect( [](void *r, AsyncClient *c) { reinterpret_cast(r)->_onDisconnect(c); }, this ); - _client->onTimeout( [](void *r, AsyncClient *c, uint32_t time) { (void)c; reinterpret_cast(r)->_onTimeout(time); }, this ); - _client->onData( [](void *r, AsyncClient *c, void *buf, size_t len) { (void)c; reinterpret_cast(r)->_onData(buf, len); }, this ); - _client->onPoll( [](void *r, AsyncClient *c) { (void)c; reinterpret_cast(r)->_clientSend(); }, this ); - _client->onError( [](void *r, AsyncClient *c, int8_t error) { (void)c; log_e("err:%d", error); }, this ); + _client->onDisconnect( [](void *r, AsyncClient *c) { reinterpret_cast(r)->_onDisconnect(c); }, this ); + _client->onTimeout( [](void *r, AsyncClient *c, uint32_t time) { (void)c; reinterpret_cast(r)->_onTimeout(time); }, this ); + _client->onData( [](void *r, AsyncClient *c, void *buf, size_t len) { (void)c; reinterpret_cast(r)->_onData(buf, len); }, this ); + _client->onPoll( [](void *r, AsyncClient *c) { (void)c; reinterpret_cast(r)->_keepalive(); reinterpret_cast(r)->_clientSend(); }, this ); + _client->onError( [](void *r, AsyncClient *c, int8_t error) { (void)c; log_e("err:%d", error); }, this ); delete request; } @@ -652,6 +653,13 @@ void WSocketClient::_sendEvent(event_t e){ _cb(this, e); } +void WSocketClient::_keepalive(){ + if (millis() - _lastPong > _keepAlivePeriod){ + enqueueMessage(std::make_shared< WSMessageContainer >(WSFrameType_t::pong, true, "WSocketClient Pong" )); + _lastPong = millis(); + } +} + // ***** WSocketServer implementation ***** @@ -666,10 +674,11 @@ bool WSocketServer::newClient(AsyncWebServerRequest *request){ if (eventHandler) eventHandler(c, e); else - c->dequeueMessage(); } // silently discard incoming messages when there is no callback set - ); + c->dequeueMessage(); }, // silently discard incoming messages when there is no callback set + msgsize, qcap); } _clients.back().setOverflowPolicy(_overflow_policy); + _clients.back().setKeepALive(_keepAlivePeriod); if (eventHandler) eventHandler(&_clients.back(), WSocketClient::event_t::connect); return true; } @@ -742,6 +751,13 @@ WSocketServer::msgall_err_t WSocketServer::pingAll(const char *data, size_t len) return cnt == _clients.size() ? msgall_err_t::ok : msgall_err_t::partial; } +WSocketClient::err_t WSocketServer::message(uint32_t id, WSMessagePtr m){ +if (WSocketClient *c = _getClient(id)) + return c->enqueueMessage(std::move(m)); +else + return WSocketClient::err_t::disconnected; +} + WSocketServer::msgall_err_t WSocketServer::messageAll(WSMessagePtr m){ size_t cnt{0}; for (auto &c : _clients) { @@ -781,12 +797,13 @@ bool WSocketServerWorker::newClient(AsyncWebServerRequest *request){ #ifdef ESP32 std::lock_guard lock (clientslock); #endif - _clients.emplace_back(getNextId(), request, [this](WSocketClient *c, WSocketClient::event_t e){ if (_task_hndlr) xTaskNotifyGive(_task_hndlr); }); + _clients.emplace_back(getNextId(), request, [this](WSocketClient *c, WSocketClient::event_t e){ if (_task_hndlr) xTaskNotifyGive(_task_hndlr); }, msgsize, qcap); } // create events group where we'll pick events _clients.back().createEventGroupHandle(); _clients.back().setOverflowPolicy(getOverflowPolicy()); + _clients.back().setKeepALive(_keepAlivePeriod); xEventGroupSetBits(_clients.back().getEventGroupHandle(), enum2uint32(WSocketClient::event_t::connect)); if (_task_hndlr) xTaskNotifyGive(_task_hndlr); diff --git a/src/AsyncWSocket.h b/src/AsyncWSocket.h index 7e9265abd..8e3a59b24 100644 --- a/src/AsyncWSocket.h +++ b/src/AsyncWSocket.h @@ -211,7 +211,7 @@ class WSMessageClose : public WSMessageContainer { WSMessageClose (uint16_t status, Args&&... args) : WSMessageContainer(WSFrameType_t::close, true, std::forward(args)...), _status_code(status) { // convert code to message body uint16_t buff = htons (status); - container.append((char*)(&buff), 2); + container.insert(0, (char*)(&buff), 2); }; uint16_t getStatusCode() const override { return _status_code; } @@ -379,6 +379,18 @@ class WSocketClient { // check if client can enqueue and send new messages err_t canSend() const; + /** + * @brief Set the WebSOcket ping Keep A Live + * if set, client will send pong packet it's peer periodically to keep the connection alive + * ping does not require a reply from peer + * + * @param seconds + */ + void setKeepALive(size_t seconds){ _keepAlivePeriod = seconds * 1000; }; + + // get keepalive value + size_t getKeepALive() const { return _keepAlivePeriod / 1000; }; + /** * @brief access Event Group Handle for the client * @@ -422,6 +434,9 @@ class WSocketClient { // in-flight data credits size_t _in_flight_credit{WS_IN_FLIGHT_CREDITS}; + // keepalive + unsigned long _keepAlivePeriod{0}, _lastPong; + /** * @brief go through out Q and send message data () * @note this method will grab a mutex lock on outQ internally @@ -451,6 +466,8 @@ class WSocketClient { */ void _sendEvent(event_t e); + void _keepalive(); + // AsyncTCP callbacks void _onTimeout(uint32_t time); void _onDisconnect(AsyncClient *c); @@ -459,8 +476,8 @@ class WSocketClient { /** * @brief WebServer Handler implementation that plays the role of a WebSocket server - * it inherits behavior of original WebSocket server and uses AsyncTCP callback - * to handle incoming messages + * it inherits behavior of original WebSocket server and uses AsyncTCP's thread to + * run callbacks on incoming messages * */ class WSocketServer : public AsyncWebHandler { @@ -472,9 +489,34 @@ class WSocketServer : public AsyncWebHandler { none // no clients or all outbound queues are full, message discarded }; - explicit WSocketServer(const char* url, WSocketClient::event_cb_t handler = {}) : _url(url), eventHandler(handler) {} + explicit WSocketServer(const char* url, WSocketClient::event_cb_t handler = {}, size_t msgsize = 8 * 1024, size_t qcap = 4) : _url(url), eventHandler(handler) {} ~WSocketServer() = default; + /** + * @copydoc WSClient::setOverflowPolicy(overflow_t policy) + */ + void setOverflowPolicy(WSocketClient::overflow_t policy){ _overflow_policy = policy; } + WSocketClient::overflow_t getOverflowPolicy() const { return _overflow_policy; } + + /** + * @brief Set Message Size limit for the incoming messages + * if peer tries to send us message larger then defined limit size, + * the message will be discarded and peer's connection would be closed with respective error code + * @note only new connections would be affected with changed value + * + * @param size + */ + void setMaxMessageSize(size_t size){ msgsize = size; } + size_t getMaxMessageSize(size_t size) const { return msgsize; } + + /** + * @brief Set in/out Message Queue Size + * + * @param size + */ + void setMessageQueueSize(size_t size){ qcap = size; } + size_t getMessageQueueSize(size_t size){ return qcap; } + /** * @brief check if client with specified id can accept new message for sending * @@ -498,12 +540,6 @@ class WSocketServer : public AsyncWebHandler { return _getClient(id) != nullptr; } - /** - * @copydoc WSClient::setOverflowPolicy(overflow_t policy) - */ - void setOverflowPolicy(WSocketClient::overflow_t policy){ _overflow_policy = policy; } - WSocketClient::overflow_t getOverflowPolicy() const { return _overflow_policy; } - /** * @brief disconnect client * @@ -549,21 +585,30 @@ class WSocketServer : public AsyncWebHandler { msgall_err_t pingAll(const char *data = NULL, size_t len = 0); /** - * @brief send message to specific client + * @brief Set the WebSocket client Keep A Live + * if set, server will pong it's peers periodically to keep connections alive + * @note it does not check for replies and it's validity, it only sends messages to + * help keep TCP connection alive through firewalls/routers + * + * @param seconds + */ + void setKeepALive(size_t seconds){ _keepAlivePeriod = seconds; }; + + // get keepalive value + size_t getKeepALive() const { return _keepAlivePeriod; }; + + + /** + * @brief send generic message to specific client * * @param id * @param m * @return WSocketClient::err_t */ - WSocketClient::err_t message(uint32_t id, WSMessagePtr m){ - if (WSocketClient *c = _getClient(id)) - return c->enqueueMessage(std::move(m)); - else - return WSocketClient::err_t::disconnected; - } + WSocketClient::err_t message(uint32_t id, WSMessagePtr m); /** - * @brief send message to all available clients + * @brief send genric message to all available clients * * @param m * @return msgall_err_t @@ -629,6 +674,11 @@ class WSocketServer : public AsyncWebHandler { #endif // WSocketClient events handler WSocketClient::event_cb_t eventHandler; + unsigned long _keepAlivePeriod{0}; + // max message size + size_t msgsize; + // client's queue capacity + size_t qcap; // return next available client's ID uint32_t getNextId() { From cde857bca5c0c0e9113fc50de12b3d53b0f2b7ca Mon Sep 17 00:00:00 2001 From: Emil Muratov Date: Thu, 9 Oct 2025 20:21:42 +0900 Subject: [PATCH 06/15] WServer keepalive and server-side echo - keepalive would send periodical unsolicited pong messages to peer as per https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.3 - server-side echo. When activated server will echo incoming messages from any client to all other connected clients. This could be usefull for applications that share messages between all connected clients, i.e. WebUIs to reflect controls across all connected clients --- src/AsyncWSocket.cpp | 92 ++++++++++++++++++++--------- src/AsyncWSocket.h | 137 +++++++++++++++++++++++++++---------------- 2 files changed, 153 insertions(+), 76 deletions(-) diff --git a/src/AsyncWSocket.cpp b/src/AsyncWSocket.cpp index 31af65547..b8869ddde 100644 --- a/src/AsyncWSocket.cpp +++ b/src/AsyncWSocket.cpp @@ -88,7 +88,7 @@ void wsMaskPayload(uint32_t mask, size_t mask_offset, char *data, size_t length) } size_t webSocketSendHeader(AsyncClient *client, WSMessageFrame& frame) { - if (!client || !client->canSend()) { + if (!client) { return 0; } @@ -373,7 +373,7 @@ void WSocketClient::_onData(void *pbuf, size_t plen) { } // receiving a new frame from here data += framelen; - plen -= framelen; + plen -= std::min(framelen, plen); // safety measure from bad parsing, we can't deduct more than sockbuff size } else { // continuation of existing frame size_t payload_len = std::min(static_cast(_inFrame.len - _inFrame.index), plen); @@ -416,20 +416,23 @@ void WSocketClient::_onData(void *pbuf, size_t plen) { } // otherwise it's a close request from a peer - echo back close message as per https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.1 + log_d("recv client's ws-close req"); { - log_d("recv client's ws-close req"); #ifdef ESP32 std::unique_lock lockin(_inQlock); std::unique_lock lockout(_outQlock); #endif - // push message to recv Q, client might use it to understand disconnection reason - _messageQueueIn.push_back(_inFrame.msg); + // push message to recv Q if it has body, client might use it to understand disconnection reason + if (_inFrame.len > 2) + _messageQueueIn.push_back(_inFrame.msg); // purge the out Q and echo recieved frame back to client, once it's tcp-acked from the other side we can close tcp connection _messageQueueOut.clear(); _messageQueueOut.push_front(_inFrame.msg); } _inFrame.msg.reset(); - _sendEvent(event_t::msgRecv); + // send event only when message has body + if (_inFrame.len > 2) + _sendEvent(event_t::msgRecv); break; } @@ -522,7 +525,7 @@ std::pair WSocketClient::_mkNewFrame(char* data, size_t len, W // check in-queue for overflow if (_messageQueueIn.size() >= _max_qcap){ - log_w("q overflow"); + log_w("q overflow, client id:%u, qsize:%u", id, _messageQueueIn.size()); switch (_overflow_policy){ case overflow_t::discard : // silently discard incoming message @@ -610,7 +613,7 @@ WSocketClient::err_t WSocketClient::enqueueMessage(WSMessagePtr mptr){ return err_t::ok; } - return err_t::nospace; + return err_t::outQfull; } WSMessagePtr WSocketClient::dequeueMessage(){ @@ -625,9 +628,15 @@ WSMessagePtr WSocketClient::dequeueMessage(){ return msg; } -WSocketClient::err_t WSocketClient::canSend() const { +WSMessagePtr WSocketClient::peekMessage(){ + return _messageQueueIn.size() ? _messageQueueIn.front() : WSMessagePtr(); +} + +WSocketClient::err_t WSocketClient::state() const { if (_connection != conn_state_t::connected) return err_t::disconnected; - if (_messageQueueOut.size() >= _max_qcap ) return err_t::nospace; + if (_messageQueueOut.size() >= _max_qcap && _messageQueueIn.size() >= _max_qcap ) return err_t::Qsfull; + if (_messageQueueIn.size() >= _max_qcap) return err_t::inQfull; + if (_messageQueueOut.size() >= _max_qcap) return err_t::outQfull; return err_t::ok; } @@ -670,15 +679,18 @@ bool WSocketServer::newClient(AsyncWebServerRequest *request){ #ifdef ESP32 std::lock_guard lock(clientslock); #endif - _clients.emplace_back(getNextId(), request, [this](WSocketClient *c, WSocketClient::event_t e){ - if (eventHandler) - eventHandler(c, e); - else - c->dequeueMessage(); }, // silently discard incoming messages when there is no callback set + _clients.emplace_back(getNextId(), request, + [this](WSocketClient *c, WSocketClient::event_t e){ + // server echo call + if (e == WSocketClient::event_t::msgRecv) serverEcho(c); + if (eventHandler) + eventHandler(c, e); + else + c->dequeueMessage(); }, // silently discard incoming messages when there is no callback set msgsize, qcap); } _clients.back().setOverflowPolicy(_overflow_policy); - _clients.back().setKeepALive(_keepAlivePeriod); + _clients.back().setKeepAlive(_keepAlivePeriod); if (eventHandler) eventHandler(&_clients.back(), WSocketClient::event_t::connect); return true; } @@ -718,7 +730,7 @@ void WSocketServer::handleRequest(AsyncWebServerRequest *request) { request->send(response); } -WSocketClient* WSocketServer::_getClient(uint32_t id) { +WSocketClient* WSocketServer::getClient(uint32_t id) { auto iter = std::find_if(_clients.begin(), _clients.end(), [id](const WSocketClient &c) { return c.id == id; }); if (iter != std::end(_clients)) return &(*iter); @@ -726,7 +738,7 @@ WSocketClient* WSocketServer::_getClient(uint32_t id) { return nullptr; } -WSocketClient const* WSocketServer::_getClient(uint32_t id) const { +WSocketClient const* WSocketServer::getClient(uint32_t id) const { const auto iter = std::find_if(_clients.cbegin(), _clients.cend(), [id](const WSocketClient &c) { return c.id == id; }); if (iter != std::cend(_clients)) return &(*iter); @@ -734,8 +746,15 @@ WSocketClient const* WSocketServer::_getClient(uint32_t id) const { return nullptr; } -WSocketServer::msgall_err_t WSocketServer::canSend() const { - size_t cnt = std::count_if(std::begin(_clients), std::end(_clients), [](const WSocketClient &c) { return c.canSend() == WSocketClient::err_t::ok; }); +WSocketClient::err_t WSocketServer::clientState(uint32_t id) const { + if (auto c = getClient(id)) + return c->state(); + else + return WSocketClient::err_t::disconnected; +}; + +WSocketServer::msgall_err_t WSocketServer::clientsState() const { + size_t cnt = std::count_if(std::cbegin(_clients), std::cend(_clients), [](const WSocketClient &c) { return c.state() == WSocketClient::err_t::ok; }); if (!cnt) return msgall_err_t::none; return cnt == _clients.size() ? msgall_err_t::ok : msgall_err_t::partial; } @@ -752,7 +771,7 @@ WSocketServer::msgall_err_t WSocketServer::pingAll(const char *data, size_t len) } WSocketClient::err_t WSocketServer::message(uint32_t id, WSMessagePtr m){ -if (WSocketClient *c = _getClient(id)) +if (WSocketClient *c = getClient(id)) return c->enqueueMessage(std::move(m)); else return WSocketClient::err_t::disconnected; @@ -773,13 +792,26 @@ void WSocketServer::_purgeClients(){ log_d("purging clients"); std::lock_guard lock(clientslock); // purge clients that are disconnected and with all messages consumed - std::erase_if(_clients, [](const WSocketClient& c){ return (c.status() == WSocketClient::conn_state_t::disconnected && !c.inQueueSize() ); }); + std::erase_if(_clients, [](const WSocketClient& c){ return (c.connection() == WSocketClient::conn_state_t::disconnected && !c.inQueueSize() ); }); } size_t WSocketServer::activeClientsCount() const { - return std::count_if(std::begin(_clients), std::end(_clients), [](const WSocketClient &c) { return c.status() == WSocketClient::conn_state_t::connected; }); + return std::count_if(std::begin(_clients), std::end(_clients), [](const WSocketClient &c) { return c.connection() == WSocketClient::conn_state_t::connected; }); }; +void WSocketServer::serverEcho(WSocketClient *c){ + if (!_serverEcho) return; + auto m = c->peekMessage(); + if (m && (m->type == WSFrameType_t::text || m->type == WSFrameType_t::binary) ){ + // echo only text or bin messages + for (auto &i: _clients){ + if (!_serverEchoSplitHorizon || i.id != c->id){ + i.enqueueMessage(m); + } + } + } +} + // ***** WSMessageClose implementation ***** @@ -797,13 +829,20 @@ bool WSocketServerWorker::newClient(AsyncWebServerRequest *request){ #ifdef ESP32 std::lock_guard lock (clientslock); #endif - _clients.emplace_back(getNextId(), request, [this](WSocketClient *c, WSocketClient::event_t e){ if (_task_hndlr) xTaskNotifyGive(_task_hndlr); }, msgsize, qcap); + _clients.emplace_back(getNextId(), request, + [this](WSocketClient *c, WSocketClient::event_t e){ + log_d("client event id:%u state:%u", c->id, c->state()); + // server echo call + if (e == WSocketClient::event_t::msgRecv) serverEcho(c); + if (_task_hndlr) xTaskNotifyGive(_task_hndlr); + }, + msgsize, qcap); } // create events group where we'll pick events _clients.back().createEventGroupHandle(); _clients.back().setOverflowPolicy(getOverflowPolicy()); - _clients.back().setKeepALive(_keepAlivePeriod); + _clients.back().setKeepAlive(_keepAlivePeriod); xEventGroupSetBits(_clients.back().getEventGroupHandle(), enum2uint32(WSocketClient::event_t::connect)); if (_task_hndlr) xTaskNotifyGive(_task_hndlr); @@ -841,7 +880,6 @@ void WSocketServerWorker::_taskRunner(){ // check if this a new client uxBits = xEventGroupClearBits(it->getEventGroupHandle(), enum2uint32(WSocketClient::event_t::connect) ); - log_d("uxBits:%u", uxBits); if ( uxBits & enum2uint32(WSocketClient::event_t::connect) ){ _ecb(WSocketClient::event_t::connect, it->id); } @@ -865,7 +903,7 @@ void WSocketServerWorker::_taskRunner(){ } // check for disconnected client - do not care for group bits, cause if it's deleted, we will destruct the client object - if (it->canSend() == WSocketClient::err_t::disconnected){ + if (it->connection() == WSocketClient::conn_state_t::disconnected){ auto id = it->id; { #ifdef ESP32 diff --git a/src/AsyncWSocket.h b/src/AsyncWSocket.h index 8e3a59b24..6a58fcad5 100644 --- a/src/AsyncWSocket.h +++ b/src/AsyncWSocket.h @@ -261,7 +261,7 @@ class WSocketClient { enum class conn_state_t { connected, // connected and exchangin messages disconnecting, // awaiting close ack - disconnected // ws client is disconnected + disconnected // ws peer is disconnected }; /** @@ -279,9 +279,10 @@ class WSocketClient { // error codes enum class err_t { - ok, // no problem :) - nospace, // message queues are overflowed, can't accept more data - messageTooBig, // can't accept such a large message + ok, // all correct + inQfull, // inbound Q is full, won't receive new messages + outQfull, // outboud Q is full, won't send new messages + Qsfull, // both Qs are full disconnected // peer connection is broken }; @@ -336,10 +337,31 @@ class WSocketClient { */ WSMessagePtr dequeueMessage(); - conn_state_t status() const { + /** + * @brief access first avaiable message from inbound queue + * @note this call will NOT remove message from queue but access message in-place! + * + * @return WSMessagePtr if Q is empty, then empty pointer is returned + */ + WSMessagePtr peekMessage(); + + /** + * @brief return peer connection state + * + * @return conn_state_t + */ + conn_state_t connection() const { return _connection; } + /** + * @brief get client's state + * returns error status if client is available to send/receive data + * + * @return err_t + */ + err_t state() const; + AsyncClient *client() { return _client; } @@ -376,9 +398,6 @@ class WSocketClient { size_t inQueueSize() const { return _messageQueueIn.size(); }; size_t outQueueSize() const { return _messageQueueOut.size(); }; - // check if client can enqueue and send new messages - err_t canSend() const; - /** * @brief Set the WebSOcket ping Keep A Live * if set, client will send pong packet it's peer periodically to keep the connection alive @@ -386,10 +405,10 @@ class WSocketClient { * * @param seconds */ - void setKeepALive(size_t seconds){ _keepAlivePeriod = seconds * 1000; }; + void setKeepAlive(size_t seconds){ _keepAlivePeriod = seconds * 1000; }; - // get keepalive value - size_t getKeepALive() const { return _keepAlivePeriod / 1000; }; + // get keepalive value, seconds + size_t getKeepAlive() const { return _keepAlivePeriod / 1000; }; /** * @brief access Event Group Handle for the client @@ -489,7 +508,7 @@ class WSocketServer : public AsyncWebHandler { none // no clients or all outbound queues are full, message discarded }; - explicit WSocketServer(const char* url, WSocketClient::event_cb_t handler = {}, size_t msgsize = 8 * 1024, size_t qcap = 4) : _url(url), eventHandler(handler) {} + explicit WSocketServer(const char* url, WSocketClient::event_cb_t handler = {}, size_t msgsize = 8 * 1024, size_t qcap = 4) : _url(url), eventHandler(handler), msgsize(msgsize), qcap(qcap) {} ~WSocketServer() = default; /** @@ -517,27 +536,67 @@ class WSocketServer : public AsyncWebHandler { void setMessageQueueSize(size_t size){ qcap = size; } size_t getMessageQueueSize(size_t size){ return qcap; } + /** + * @brief Set the WebSocket client Keep A Live + * if set, server will pong it's peers periodically to keep connections alive + * @note it does not check for replies and it's validity, it only sends messages to + * help keep TCP connection alive through firewalls/routers + * + * @param seconds + */ + void setKeepAlive(size_t seconds){ _keepAlivePeriod = seconds; }; + + // get keepalive value + size_t getKeepAlive() const { return _keepAlivePeriod; }; + + /** + * @brief activate server-side message echo + * when activated server will echo incoming messages from any client to all other connected clients. + * This could be usefull for applications that share messages between all connected clients, i.e. WebUIs + * to reflect controls across all connected clients + * @note only messages with text and binary types are echoed, control messages are not echoed + * + * @param enabled + * @param splitHorizon - when true echo message to all clients but the one from where message was received, + * when false, echo back message to all clients include the one who sent it + */ + void setServerEcho(bool enabled = true, bool splitHorizon = true){ _serverEcho = enabled, _serverEchoSplitHorizon = splitHorizon; }; + + // get server echo mode + bool getServerEcho(){ return _serverEcho; }; + + /** * @brief check if client with specified id can accept new message for sending * * @param id * @return WSocketClient::err_t - ready to send only if returned value is err_t::ok, otherwise err reason is returned */ - WSocketClient::err_t canSend(uint32_t id) const { return _getClient(id) ? _getClient(id)->canSend() : WSocketClient::err_t::disconnected; }; + WSocketClient::err_t clientState(uint32_t id) const; /** - * @brief check how many clients are available for sending data + * @brief check if all of the connected clients are available for sending data, + * i.e. connection state available and outbound Q is not full * * @return msgall_err_t */ - msgall_err_t canSend() const; + msgall_err_t clientsState() const; - // return number of active clients + // return number of active (connected) clients size_t activeClientsCount() const; + /** + * @brief Get ptr to client with specified id + * + * @param id + * @return WSocketClient* - nullptr if client not found + */ + WSocketClient* getClient(uint32_t id); + WSocketClient const* getClient(uint32_t id) const; + // find if there is a client with specified id bool hasClient(uint32_t id) const { - return _getClient(id) != nullptr; + return getClient(id) != nullptr; } /** @@ -547,8 +606,8 @@ class WSocketServer : public AsyncWebHandler { * @param code * @param message */ - void close(uint32_t id, uint16_t code = 0, const char *message = NULL){ - if (WSocketClient *c = _getClient(id)) c->close(code, message); + void close(uint32_t id, uint16_t code = 1000, const char* message = NULL){ + if (WSocketClient* c = getClient(id)) c->close(code, message); } /** @@ -557,7 +616,7 @@ class WSocketServer : public AsyncWebHandler { * @param code * @param message */ - void closeAll(uint16_t code = 0, const char *message = NULL){ for (auto &c : _clients) { c.close(code, message); } } + void closeAll(uint16_t code = 1000, const char* message = NULL){ for (auto &c : _clients) { c.close(code, message); } } /** * @brief sned ping to client @@ -568,8 +627,8 @@ class WSocketServer : public AsyncWebHandler { * @return true * @return false */ - WSocketClient::err_t ping(uint32_t id, const char *data = NULL, size_t len = 0){ - if (WSocketClient *c = _getClient(id)) + WSocketClient::err_t ping(uint32_t id, const char* data = NULL, size_t len = 0){ + if (WSocketClient *c = getClient(id)) return c->ping(data, len); else return WSocketClient::err_t::disconnected; @@ -582,21 +641,7 @@ class WSocketServer : public AsyncWebHandler { * @param len * @return msgall_err_t */ - msgall_err_t pingAll(const char *data = NULL, size_t len = 0); - - /** - * @brief Set the WebSocket client Keep A Live - * if set, server will pong it's peers periodically to keep connections alive - * @note it does not check for replies and it's validity, it only sends messages to - * help keep TCP connection alive through firewalls/routers - * - * @param seconds - */ - void setKeepALive(size_t seconds){ _keepAlivePeriod = seconds; }; - - // get keepalive value - size_t getKeepALive() const { return _keepAlivePeriod; }; - + msgall_err_t pingAll(const char* data = NULL, size_t len = 0); /** * @brief send generic message to specific client @@ -665,7 +710,7 @@ class WSocketServer : public AsyncWebHandler { * @return true * @return false */ - virtual bool newClient(AsyncWebServerRequest *request); + virtual bool newClient(AsyncWebServerRequest* request); protected: std::list _clients; @@ -685,6 +730,8 @@ class WSocketServer : public AsyncWebHandler { return ++_cNextId; } + void serverEcho(WSocketClient *c); + /** * @brief go through clients list and remove those ones that are disconnected and have no messages pending * @@ -696,15 +743,7 @@ class WSocketServer : public AsyncWebHandler { AwsHandshakeHandler _handshakeHandler; uint32_t _cNextId{0}; WSocketClient::overflow_t _overflow_policy{WSocketClient::overflow_t::disconnect}; - - /** - * @brief Get ptr to client with specified id - * - * @param id - * @return WSocketClient* - nullptr if client not founc - */ - WSocketClient* _getClient(uint32_t id); - WSocketClient const* _getClient(uint32_t id) const; + bool _serverEcho{false}, _serverEchoSplitHorizon; // WebServer methods @@ -724,8 +763,8 @@ class WSocketServerWorker : public WSocketServer { // message callback alias using msg_cb_t = std::function; - explicit WSocketServerWorker(const char* url, msg_cb_t msg_handler, event_cb_t event_handler) - : WSocketServer(url), _mcb(msg_handler), _ecb(event_handler) {} + explicit WSocketServerWorker(const char* url, msg_cb_t msg_handler, event_cb_t event_handler, size_t msgsize = 8 * 1024, size_t qcap = 4) + : WSocketServer(url, nullptr, msgsize, qcap), _mcb(msg_handler), _ecb(event_handler) {} ~WSocketServerWorker(){ stop(); }; From b07333c90abfbd3497d944227545fe55da807086 Mon Sep 17 00:00:00 2001 From: Emil Muratov Date: Fri, 10 Oct 2025 01:40:00 +0900 Subject: [PATCH 07/15] WSockerServer add generic templated methods to send text/string/binary variadic tpls would allow easy sending for text/strings contructables --- src/AsyncWSocket.h | 134 ++++++++++++++++++++++++++++++++------------- 1 file changed, 96 insertions(+), 38 deletions(-) diff --git a/src/AsyncWSocket.h b/src/AsyncWSocket.h index 6a58fcad5..43a798c6d 100644 --- a/src/AsyncWSocket.h +++ b/src/AsyncWSocket.h @@ -283,7 +283,8 @@ class WSocketClient { inQfull, // inbound Q is full, won't receive new messages outQfull, // outboud Q is full, won't send new messages Qsfull, // both Qs are full - disconnected // peer connection is broken + disconnected, // peer connection is broken + na // client is not available }; enum class overflow_t { @@ -606,8 +607,10 @@ class WSocketServer : public AsyncWebHandler { * @param code * @param message */ - void close(uint32_t id, uint16_t code = 1000, const char* message = NULL){ - if (WSocketClient* c = getClient(id)) c->close(code, message); + WSocketClient::err_t close(uint32_t id, uint16_t code = 1000, const char* message = NULL){ + if (WSocketClient* c = getClient(id)) + return c->close(code, message); + else return WSocketClient::err_t::na; } /** @@ -630,8 +633,7 @@ class WSocketServer : public AsyncWebHandler { WSocketClient::err_t ping(uint32_t id, const char* data = NULL, size_t len = 0){ if (WSocketClient *c = getClient(id)) return c->ping(data, len); - else - return WSocketClient::err_t::disconnected; + else return WSocketClient::err_t::na; } /** @@ -653,47 +655,103 @@ class WSocketServer : public AsyncWebHandler { WSocketClient::err_t message(uint32_t id, WSMessagePtr m); /** - * @brief send genric message to all available clients + * @brief send generic message to all available clients * * @param m * @return msgall_err_t */ msgall_err_t messageAll(WSMessagePtr m); + /** + * @brief Send text message to client + * this template can accept anything that std::string can be made of + * + * @tparam Args + * @param id + * @param args + * @return WSocketClient::err_t + */ + template + WSocketClient::err_t text(uint32_t id, Args&&... args){ + if (hasClient(id)) + return message(id, std::make_shared>(WSFrameType_t::text, true, std::forward(args)...)); + else return WSocketClient::err_t::na; + } + + /** + * @brief Send text message to all avalable clients + * this template can accept anything that std::string can be made of + * + * @tparam Args + * @param args + * @return WSocketClient::err_t + */ + template + msgall_err_t textAll(uint32_t id, Args&&... args){ + return messageAll(std::make_shared>(WSFrameType_t::text, true, std::forward(args)...)); + } + + /** + * @brief Send String text message to client + * this template can accept anything that Arduino String can be made of + * + * @tparam Args Arduino String constructor arguments + * @param id clien id to send message to + * @param args + * @return WSocketClient::err_t + */ + template + WSocketClient::err_t string(uint32_t id, Args&&... args){ + if (hasClient(id)) + return message(std::make_shared>(WSFrameType_t::text, true, std::forward(args)...)); + else return WSocketClient::err_t::na; + } - /* - bool text(uint32_t id, const uint8_t *message, size_t len); - bool text(uint32_t id, const char *message, size_t len); - bool text(uint32_t id, const char *message); - bool text(uint32_t id, const String &message); - bool text(uint32_t id, AsyncWebSocketMessageBuffer *buffer); - bool text(uint32_t id, AsyncWebSocketSharedBuffer buffer); - - enqueue_err_t textAll(const uint8_t *message, size_t len); - enqueue_err_t textAll(const char *message, size_t len); - enqueue_err_t textAll(const char *message); - enqueue_err_t textAll(const String &message); - enqueue_err_t textAll(AsyncWebSocketMessageBuffer *buffer); - enqueue_err_t textAll(AsyncWebSocketSharedBuffer buffer); - - bool binary(uint32_t id, const uint8_t *message, size_t len); - bool binary(uint32_t id, const char *message, size_t len); - bool binary(uint32_t id, const char *message); - bool binary(uint32_t id, const String &message); - bool binary(uint32_t id, AsyncWebSocketMessageBuffer *buffer); - bool binary(uint32_t id, AsyncWebSocketSharedBuffer buffer); - - enqueue_err_t binaryAll(const uint8_t *message, size_t len); - enqueue_err_t binaryAll(const char *message, size_t len); - enqueue_err_t binaryAll(const char *message); - enqueue_err_t binaryAll(const String &message); - enqueue_err_t binaryAll(AsyncWebSocketMessageBuffer *buffer); - enqueue_err_t binaryAll(AsyncWebSocketSharedBuffer buffer); - - size_t printf(uint32_t id, const char *format, ...) __attribute__((format(printf, 3, 4))); - size_t printfAll(const char *format, ...) __attribute__((format(printf, 2, 3))); -*/ + /** + * @brief Send String text message all avalable clients + * this template can accept anything that Arduino String can be made of + * + * @tparam Args + * @param id client id to send message to + * @param args Arduino String constructor arguments + * @return WSocketClient::err_t + */ + template + msgall_err_t stringAll(uint32_t id, Args&&... args){ + return messageAll(std::make_shared>(WSFrameType_t::text, true, std::forward(args)...)); + } + + /** + * @brief Send binary message to client + * this template can accept anything that std::vector can be made of + * + * @tparam Args + * @param id client id to send message to + * @param args std::vector constructor arguments + * @return WSocketClient::err_t + */ + template + WSocketClient::err_t binary(uint32_t id, Args&&... args){ + if (hasClient(id)) + return message(id, std::make_shared< WSMessageContainer> >(WSFrameType_t::binary, true, std::forward(args)...)); + else return WSocketClient::err_t::na; + } + + /** + * @brief Send binary message all avalable clients + * this template can accept anything that std::vector can be made of + * + * @tparam Args + * @param id client id to send message to + * @param args std::vector constructor arguments + * @return WSocketClient::err_t + */ + template + msgall_err_t binaryAll(uint32_t id, Args&&... args){ + return messageAll(std::make_shared< WSMessageContainer> >(WSFrameType_t::binary, true, std::forward(args)...)); + } + // set webhanshake handler void handleHandshake(AwsHandshakeHandler handler) { _handshakeHandler = handler; } From abc9aa943e5e492865d402d87320325a712b855b Mon Sep 17 00:00:00 2001 From: Emil Muratov Date: Fri, 10 Oct 2025 02:02:40 +0900 Subject: [PATCH 08/15] WSocket Server example - WebChat This example would show how WSocketServer could register connections/disconnections for every new user, deliver messages to all connected user and replicate incoming data amoung all members of chat room. Change you WiFi creds below, build and flash the code. Connect to console to monitor debug messages. Figure out IP of your board and access the board via web browser using two or more different devices, Chat with yourself, have fun :) --- wsocket_examples/WebChat/WebChat.ino | 110 +++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 wsocket_examples/WebChat/WebChat.ino diff --git a/wsocket_examples/WebChat/WebChat.ino b/wsocket_examples/WebChat/WebChat.ino new file mode 100644 index 000000000..81d03592e --- /dev/null +++ b/wsocket_examples/WebChat/WebChat.ino @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +// +// A simple WebChat room working over WebSockets +// + +/* + This example would show how WSocketServer could register connections/disconnections for every new user, + deliver messages to all connected user and replicate incoming data amoung all members of chat room. + Change you WiFi creds below, build and flash the code. + Connect to console to monitor debug messages. + Figure out IP of your board and access the board via web browser using two or more different devices, + Chat with yourself, have fun :) +*/ + +#include +#include +#include "html.h" + +#define WIFI_SSID "YourSSID" +#define WIFI_PASSWD "YourPasswd" + +// WS Event server callback declaration +void wsEvent(WSocketClient *client, WSocketClient::event_t event); + +// Our WebServer +AsyncWebServer server(80); +// WSocket Server URL and callback function +WSocketServer ws("/chat", wsEvent); + +void wsEvent(WSocketClient *client, WSocketClient::event_t event){ + switch (event){ + // new client connected + case WSocketClient::event_t::connect : { + Serial.printf("Client id:%u connected\n", client->id); + char buff[100]; + snprintf(buff, 100, "WServer: Hello user, your id is:%u, there are %u members on-line, pls be polite here!", client->id, ws.activeClientsCount()); + // greet new user personally + ws.text(client->id, buff); + snprintf(buff, 100, "WServer: New client with id:%u joined the room", client->id); + // Announce new user entered the room + ws.textAll(client->id, buff); + break; + } + + // client diconnected + case WSocketClient::event_t::disconnect : { + Serial.printf("Client id:%u disconnected\n", client->id); + char buff[100]; + snprintf(buff, 100, "WServer: Client with id:%u left the room, %u members on-line", client->id, ws.activeClientsCount()); + ws.textAll(client->id, buff); + break; + } + + // new messages from clients + case WSocketClient::event_t::msgRecv : + // any incoming messages we must deQ and discard, + // there is no need to resend it back to, it's handled on the server side. + // If not discaded messages will overflow incoming Q + client->dequeueMessage(); + break; + + default:; + } +} + + +void setup() { + Serial.begin(115200); + + WiFi.mode(WIFI_STA); + WiFi.begin(WIFI_SSID, WIFI_PASSWD); + + // Wait for connection + while (WiFi.status() != WL_CONNECTED) { + delay(250); + Serial.print("."); + } + Serial.println(); + Serial.println("Connected to WIFI_SSID"); + Serial.print("IP address: "); + Serial.println(WiFi.localIP()); + Serial.println(); + Serial.printf("to access WebChat pls open http://%s/\n", WiFi.localIP().toString().c_str()); + + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { + // need to cast to uint8_t* + // if you do not, the const char* will be copied in a temporary String buffer + request->send(200, "text/html", (uint8_t *)htmlChatPage, strlen(htmlChatPage)); + }); + + // keep TCP/WS connection open + ws.setKeepAlive(20); + + /* + Enable server echo to reflect incoming messages to all participans of the current chat room + */ + ws.setServerEcho(true, true); // 2nd 'true' here is 'SplitHorizon' - server will reflect all message to every peer except the one where it came from + + server.addHandler(&ws); + + server.begin(); +} + + +void loop() { + // nothing to do here + delay(100); +} From 8c56d01ced6e8cb58448d7afd9d7ba44a47ac0c1 Mon Sep 17 00:00:00 2001 From: Emil Muratov Date: Sun, 12 Oct 2025 03:55:48 +0900 Subject: [PATCH 09/15] WSMessageStaticBlob to zero-copy send large data A message that carries a pointer to arbitrary blob of data it could be used to send large blocks of memory in zero-copy mode, i.e. avoid intermediary buffering copies. + bunch of bugfixes for dataflow control, frame assembly, message containers methods --- src/AsyncWSocket.cpp | 100 ++++++++++++++++++++---------------- src/AsyncWSocket.h | 120 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 152 insertions(+), 68 deletions(-) diff --git a/src/AsyncWSocket.cpp b/src/AsyncWSocket.cpp index b8869ddde..cf11bd81d 100644 --- a/src/AsyncWSocket.cpp +++ b/src/AsyncWSocket.cpp @@ -6,12 +6,14 @@ #include "AsyncWSocket.h" #include "literals.h" +#define WS_MAX_HEADER_SIZE 16 + constexpr const char WS_STR_CONNECTION[] = "Connection"; constexpr const char WS_STR_VERSION[] = "Sec-WebSocket-Version"; constexpr const char WS_STR_KEY[] = "Sec-WebSocket-Key"; constexpr const char WS_STR_PROTOCOL[] = "Sec-WebSocket-Protocol"; -// WSockServer worke task +// WSockServer worker task constexpr const char WS_SRV_TASK[] = "WSSrvtask"; // cast enum class to uint (for bit set) @@ -138,6 +140,7 @@ size_t webSocketSendHeader(AsyncClient *client, WSMessageFrame& frame) { } size_t sent = client->add((const char*)buf, headLen); + //log_d("send ws header, hdr size:%u, body len:%u", headLen, frame.len); // return size of a header added or 0 if any error return sent == headLen ? sent : 0; } @@ -155,6 +158,7 @@ WSocketClient::WSocketClient(uint32_t id, AsyncWebServerRequest *request, WSocke _lastPong = millis(); // disable connection timeout _client->setRxTimeout(0); + // disable Nagle's algo _client->setNoDelay(true); // set AsyncTCP callbacks _client->onAck( [](void *r, AsyncClient *c, size_t len, uint32_t rtt) { (void)c; reinterpret_cast(r)->_clientSend(len); }, this ); @@ -184,7 +188,7 @@ WSocketClient::~WSocketClient() { //#ifdef NOTHING // callback acknowledges sending pieces of data for outgoing frame void WSocketClient::_clientSend(size_t acked_bytes){ - if (!_client || _connection == conn_state_t::disconnected || !_client->space()) + if (!_client || _connection == conn_state_t::disconnected) return; /* @@ -201,14 +205,13 @@ void WSocketClient::_clientSend(size_t acked_bytes){ // 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 and minimizing events in asynctcp's Q if (acked_bytes){ - // if it's the ack call from AsyncTCP - wait for lock! - lock.lock(); - log_d("_clientSend, ack:%u/%u, space:%u", acked_bytes, _in_flight, _client ? _client->space() : 0); - + auto sock_space = _client->space(); + //log_d("ack:%u/%u, sock space:%u", acked_bytes, _in_flight, sock_space); _in_flight -= std::min(acked_bytes, _in_flight); - // return buffer credit on acked data - ++_in_flight_credit; - log_d("infl:%u, credits:%u, conn state:%u", _in_flight, _in_flight_credit, _connection); + if (!sock_space){ + return; + } + //log_d("infl:%u, credits:%u", _in_flight, _in_flight_credit); // check if we were waiting to ack our disconnection frame if (!_in_flight && (_connection == conn_state_t::disconnecting)){ log_d("closing tcp-conn"); @@ -217,25 +220,36 @@ void WSocketClient::_clientSend(size_t acked_bytes){ _client->close(); return; } + + // if it's the ack call from AsyncTCP - wait for lock! + lock.lock(); } else { // if there is no acked data - just quit if won't be able to grab a lock, we are already sending something if (!lock.try_lock()) return; + + auto sock_space = _client->space(); + log_d("no ack infl:%u, space:%u, data pending:%u", _in_flight, sock_space, (uint32_t)(_outFrame.len - _outFrame.index)); + // + if (!sock_space) + return; } - if (_in_flight > _client->space() || !_in_flight_credit) { - log_d("defer ws send call, in-flight:%u/%u, credit:%u", _in_flight, _client->space(), _in_flight_credit); + // ignore the call if available sock space is smaller then acked data and we won't be able to fit message's ramainder there + // this will reduce AsyncTCP's event Q pressure under heavy load + if ((_outFrame.msg && (_outFrame.len - _outFrame.index > _client->space())) && (_client->space() < acked_bytes) ){ + log_d("defer ws send call, in-flight:%u/%u", _in_flight, _client->space()); return; } - // no message in transit, try to evict one from a Q - if (!_outFrame.msg){ + // no message in transit and we have enough space in sockbuff - try to evict new msg from a Q + if (!_outFrame.msg && _client->space() > WS_MAX_HEADER_SIZE){ if (_evictOutQueue()){ - // generate header and add to the socket buffer + // generate header and add to the socket buffer. todo: check returned size? _in_flight += webSocketSendHeader(_client, _outFrame); } else - return; // nothing to send now + return; // nothing to send now } // if there is a pending _outFrame - send the data from there @@ -250,6 +264,8 @@ void WSocketClient::_clientSend(size_t acked_bytes){ _outFrame.index += payload_pend; _outFrame.chunk_offset += payload_pend; _in_flight += payload_pend; + //size_t l = _outFrame.len; + //log_d("add to sock:%u, fidx:%u/%u, infl:%u", payload_pend, (uint32_t)_outFrame.index, (uint32_t)_outFrame.len, _in_flight); } if (_outFrame.index == _outFrame.len){ @@ -257,7 +273,6 @@ void WSocketClient::_clientSend(size_t acked_bytes){ // increment in-flight counter and take the credit if (!_client->send()) _client->abort(); - --_in_flight_credit; if (_outFrame.msg->type == WSFrameType_t::close){ // if we just sent close frame, then change client state and purge out queue, we won't transmit anything from now on @@ -274,8 +289,8 @@ void WSocketClient::_clientSend(size_t acked_bytes){ // no use case for this for now //_sendEvent(event_t::msgSent); - // if there are free in-flight credits try to pull next msg from Q - if (_in_flight_credit && _evictOutQueue()){ + // if there are free in-flight credits and buffer space available try to pull next msg from Q + if (_client->space() > WS_MAX_HEADER_SIZE && _evictOutQueue()){ // generate header and add to the socket buffer _in_flight += webSocketSendHeader(_client, _outFrame); continue; @@ -284,12 +299,10 @@ void WSocketClient::_clientSend(size_t acked_bytes){ } } - if (!_client->space()){ + if (_client->space() <= WS_MAX_HEADER_SIZE){ // we have exhausted socket buffer, send it and quit if (!_client->send()) _client->abort(); - // take in-flight credit - --_in_flight_credit; return; } @@ -300,8 +313,6 @@ void WSocketClient::_clientSend(size_t acked_bytes){ if (next_chunk_size == 0){ // chunk is not ready yet, need to async wait and return for data later, we quit here and reevaluate on next ack or poll event from AsyncTCP if (!_client->send()) _client->abort(); - // take in-flight credit - --_in_flight_credit; return; } else if (next_chunk_size == -1){ // something is wrong! there would be no more chunked data but the message has not reached it's full size yet, can do nothing but close the coonections @@ -318,7 +329,7 @@ void WSocketClient::_clientSend(size_t acked_bytes){ bool WSocketClient::_evictOutQueue(){ // check if we have something in the Q and enough sock space to send a header at least - if (_messageQueueOut.size() && _client->space() > 16 ){ + if (_messageQueueOut.size() && _client->space() > WS_MAX_HEADER_SIZE ){ { #ifdef ESP32 std::unique_lock lockout(_outQlock); @@ -372,8 +383,8 @@ void WSocketClient::_onData(void *pbuf, size_t plen) { return; } // receiving a new frame from here - data += framelen; - plen -= std::min(framelen, plen); // safety measure from bad parsing, we can't deduct more than sockbuff size + data += std::min(framelen, plen); // safety measure from bad parsing, we can't deduct more than sockbuff size + plen -= std::min(framelen, plen); } else { // continuation of existing frame size_t payload_len = std::min(static_cast(_inFrame.len - _inFrame.index), plen); @@ -389,7 +400,7 @@ void WSocketClient::_onData(void *pbuf, size_t plen) { // if we got whole frame now if (_inFrame.index == _inFrame.len){ - Serial.printf("_onData, cmplt msg len:%lu\n", _inFrame.len); + log_d("_onData, cmplt msg len:%u", (uint32_t)_inFrame.len); if (_inFrame.msg->getStatusCode() == 1007){ // this is a dummy/corrupted message, we discard it @@ -403,7 +414,7 @@ void WSocketClient::_onData(void *pbuf, size_t plen) { if (_connection == conn_state_t::disconnecting){ log_d("recv close ack"); // if it was ws-close ack - we can close TCP connection - _connection == conn_state_t::disconnected; + _connection = conn_state_t::disconnected; // normally we should call close() here and wait for other side also close tcp connection with TCP-FIN, but // for various reasons ws clients could linger connection when received TCP-FIN not closing it from the app side (even after // two side ws-close exchange, i.e. websocat, websocket-client) @@ -497,7 +508,7 @@ std::pair WSocketClient::_mkNewFrame(char* data, size_t len, W offset += 8; } - log_d("new hdr, sock data:%u, msg body size:%u", len, frame.len); + log_d("recv hdr, sock data:%u, msg body size:%u", len, frame.len); // if ws.close() is called, Safari sends a close frame with plen 2 and masked bit set. We must not try to read mask key from beyond packet size if (masked && len >= offset + 4) { @@ -512,12 +523,12 @@ std::pair WSocketClient::_mkNewFrame(char* data, size_t len, W size_t bodylen = std::min(static_cast(frame.len), len - offset); if (!bodylen){ - // if there is no body in message, then it must a specific control message with no payload + // if there is no body in message, then it must be a specific control message with no payload _inFrame.msg = std::make_shared(static_cast(opcode)); } else { if (frame.len > _max_msgsize){ // message is bigger than we are allowed to accept, create a dummy container for it, it will just discard all incoming data - _inFrame.msg = std::make_shared(static_cast(opcode, 1007)); // code 'Invalid frame payload data' + _inFrame.msg = std::make_shared(static_cast(opcode), 1007); // code 'Invalid frame payload data' offset += bodylen; _inFrame.index = bodylen; return {offset, 1009}; // code 'message too big' @@ -529,14 +540,14 @@ std::pair WSocketClient::_mkNewFrame(char* data, size_t len, W switch (_overflow_policy){ case overflow_t::discard : // silently discard incoming message - _inFrame.msg = std::make_shared(static_cast(opcode, 1007)); // code 'Invalid frame payload data' + _inFrame.msg = std::make_shared(static_cast(opcode), 1007); // code 'Invalid frame payload data' offset += bodylen; _inFrame.index = bodylen; return {offset, 0}; case overflow_t::disconnect : { // discard incoming message and send close message - _inFrame.msg = std::make_shared(static_cast(opcode, 1007)); // code 'Invalid frame payload data' + _inFrame.msg = std::make_shared(static_cast(opcode), 1007); // code 'Invalid frame payload data' #ifdef ESP32 std::lock_guard lock(_inQlock); #endif @@ -573,11 +584,9 @@ std::pair WSocketClient::_mkNewFrame(char* data, size_t len, W case WSFrameType_t::close : { uint16_t status_code = ntohs(*(uint16_t*)(data + offset)); - offset += 2; if (bodylen > 2){ - bodylen -= 2; // create a text message container consuming as much data as possible from current payload - _inFrame.msg = std::make_shared(status_code, data + offset, bodylen); + _inFrame.msg = std::make_shared(status_code, data + offset + 2, bodylen -2); // deduce 2 bytes of message code } else { // must be close message w/o body _inFrame.msg = std::make_shared(status_code); @@ -587,8 +596,9 @@ std::pair WSocketClient::_mkNewFrame(char* data, size_t len, W default: _inFrame.msg = std::make_shared>>(static_cast(opcode), bodylen); - // copy data - memcpy(_inFrame.msg->getData(), data + offset, bodylen); + // copy as much data as it is available in current sock buff + // todo: for now assume object will consume all the payload provided + _inFrame.msg->addChunk(data + offset, bodylen, 0); } offset += bodylen; @@ -605,10 +615,12 @@ WSocketClient::err_t WSocketClient::enqueueMessage(WSMessagePtr mptr){ return err_t::disconnected; if (_messageQueueOut.size() < _max_qcap){ - #ifdef ESP32 - std::lock_guard lock(_outQlock); - #endif - _messageQueueOut.emplace_back( std::move(mptr) ); + { + #ifdef ESP32 + std::lock_guard lock(_outQlock); + #endif + _messageQueueOut.emplace_back( std::move(mptr) ); + } _clientSend(); return err_t::ok; } @@ -663,7 +675,7 @@ void WSocketClient::_sendEvent(event_t e){ } void WSocketClient::_keepalive(){ - if (millis() - _lastPong > _keepAlivePeriod){ + if (_keepAlivePeriod && (millis() - _lastPong > _keepAlivePeriod)){ enqueueMessage(std::make_shared< WSMessageContainer >(WSFrameType_t::pong, true, "WSocketClient Pong" )); _lastPong = millis(); } @@ -792,7 +804,7 @@ void WSocketServer::_purgeClients(){ log_d("purging clients"); std::lock_guard lock(clientslock); // purge clients that are disconnected and with all messages consumed - std::erase_if(_clients, [](const WSocketClient& c){ return (c.connection() == WSocketClient::conn_state_t::disconnected && !c.inQueueSize() ); }); + _clients.remove_if([](const WSocketClient& c){ return (c.connection() == WSocketClient::conn_state_t::disconnected && !c.inQueueSize() ); }); } size_t WSocketServer::activeClientsCount() const { diff --git a/src/AsyncWSocket.h b/src/AsyncWSocket.h index 43a798c6d..4862bf367 100644 --- a/src/AsyncWSocket.h +++ b/src/AsyncWSocket.h @@ -9,9 +9,6 @@ #include "AsyncWebSocket.h" #include "freertos/FreeRTOS.h" -#ifndef WS_IN_FLIGHT_CREDITS -#define WS_IN_FLIGHT_CREDITS 4 -#endif // forward declaration for WSocketServer class WSocketServer; @@ -51,8 +48,6 @@ enum class WSMessageStatus_t { */ class WSMessageGeneric { friend class WSocketClient; - /** Is this the last chunk in a fragmented message ?*/ - bool _final; public: const WSFrameType_t type; @@ -75,12 +70,20 @@ class WSMessageGeneric { * @note we cast it to char* 'cause of two reasons: * - the LWIP's _tcp_write() function accepts const char* * - websocket is mostly text anyway unless compressed - * @note for messages with type 'text' this lib will always include NULL terminator at the end of data, NULL byte is NOT included in total size of the message returned by getSize() + * @note for messages with type 'text' this lib will ALWAYS include NULL terminator at the end of data, NULL byte is NOT included in total size of the message returned by getSize() * a care should be taken when accessing the data for messages with type 'binary', it won't have NUL terminator at the end and would be valid up to getSize() in length! * @note buffer should be writtable so that client class could apply masking on data if needed * @return char* const */ - virtual char* getData() = 0; + virtual const char* getData() = 0; + + /** + * @brief Get mutable access to underlying Buffer memory + * container might not allow this and return nullptr in this case + * + * @return char* + */ + virtual char* getBuffer(){ return nullptr; } /** * @brief WebSocket message status code as defined in https://datatracker.ietf.org/doc/html/rfc6455#section-7.4 @@ -91,15 +94,17 @@ class WSMessageGeneric { virtual uint16_t getStatusCode() const { return 0; } protected: + /** Is this the last chunk in a fragmented message ?*/ + bool _final; /** * @brief access message buffer in chunks - * this method is used internally by WSocketClient class when sending message. In simple case it just wraps around getDtata() call + * this method is used internally by WSocketClient class when sending message. In simple case it just wraps around getDtataConst() call * but could also be implemented in derived classes for chunked transfers * * @return std::pair */ - virtual std::pair getCurrentChunk(){ return std::pair(getData(), getSize()); } ; + virtual std::pair getCurrentChunk(){ return std::pair(getData(), getSize()); } /** * @brief move to the next chunk of message data assuming current chunk has been consumed already @@ -125,6 +130,15 @@ class WSMessageGeneric { using WSMessagePtr = std::shared_ptr; +/** + * @brief templated Message container + * it can use any implementation of container classes to hold message data + * two main types to use are std::string for text messages and std::vector for binary + * @note Arduino's String class has limited functionality on accessing underlying buffer + * and resizing and should be avoided (but still possible) + * + * @tparam T + */ template class WSMessageContainer : public WSMessageGeneric { protected: @@ -145,16 +159,16 @@ class WSMessageContainer : public WSMessageGeneric { // specialisation for Arduino String if constexpr(std::is_same_v>) return container.length(); - // otherwise we assume either STL container is used, (i.e. st::vector or std::string) or derived class should implement same methods + // otherwise we assume either STL container is used, (i.e. std::vector or std::string) or derived class should implement same methods if constexpr(!std::is_same_v>) return container.size(); }; /** * @copydoc WSMessageGeneric::getData() - * @details though casted to const char* the data there is NOT NULL-terminated string! + * @details though casted to const char* the data there MIGHT not be NULL-terminated string (depending on underlying container) */ - char* getData() override { + const char* getData() override { // specialization for Arduino String if constexpr(std::is_same_v>) return container.c_str(); @@ -163,6 +177,19 @@ class WSMessageContainer : public WSMessageGeneric { return reinterpret_cast(container.data()); } + /** + * @copydoc WSMessageGeneric::getBuffer() + * @details though casted to const char* the data there MIGHT not be NULL-terminated string (depending on underlying container) + */ + char* getBuffer() override { + // specialization for Arduino String - it does not allow accessing underlying buffer, so return nullptr here + if constexpr(std::is_same_v>) + return nullptr; + // otherwise we assume either STL container is used, (i.e. st::vector or std::string) or derived class should implement same methods + if constexpr(!std::is_same_v>) + return reinterpret_cast(container.data()); + } + // access message container object T& getContainer(){ return container; } @@ -220,20 +247,66 @@ class WSMessageClose : public WSMessageContainer { /** * @brief Dummy message that does not carry any data * could be used as a container for bodyless control messages or - * specific cased (to gracefully handle oversised incoming messages) + * specific cases (to gracefully handle oversised incoming messages) */ class WSMessageDummy : public WSMessageGeneric { const uint16_t _code; public: explicit WSMessageDummy(WSFrameType_t type, uint16_t status_code = 0) : WSMessageGeneric(type, true), _code(status_code) {}; size_t getSize() const override { return 0; }; - char* getData() override { return nullptr; }; + const char* getData() override { return nullptr; }; uint16_t getStatusCode() const override { return _code; } protected: void addChunk(char* data, size_t len, size_t offset) override {}; }; +/** + * @brief A message that carries a pointer to arbitrary blob of data + * it could be used to send large blocks of memory in zero-copy mode, + * i.e. avoid intermediary buffering copies. + * The concern here is that pointer MUST persist for the duration of message + * transfer period. Due to async nature it' it unknown how much time it would + * take to complete the transfer. For this a callback function is provided + * that triggers on object's destruction. It allows to get the event on + * transfer completetion and (possibly) release the ponter or do something else + * + */ +class WSMessageStaticBlob : public WSMessageGeneric { +public: + + // callback prototype that is triggered on message destruction + using event_cb_t = std::function; + + /** + * @brief Construct a new WSMessageStaticBlob object + * + * @param type WebSocket message type + * @param final WS final bit + * @param blob a pointer to blob object (must be casted to const char*) + * @param size a size of the object + * @param cb a callback function to be call when message complete sending/errored + * @param token a unique token to identify packet instance in callback + */ + explicit WSMessageStaticBlob(WSFrameType_t type, bool final, const char* blob, size_t size, event_cb_t cb = {}, uint32_t token = 0) + : WSMessageGeneric(type, final), _blob(blob), _size(size), _callback(cb), _token(token) {}; + + // d-tor, we call the callback here + ~WSMessageStaticBlob(){ if (_callback) _callback(WSMessageStatus_t::complete, _token); } + + size_t getSize() const override { return _size; }; + const char* getData() override { return _blob; }; + +private: + const char* _blob; + size_t _size; + event_cb_t _callback; + // unique token identifying the packet + uint32_t _token; + // it is not allowed to store anything here + void addChunk(char* data, size_t len, size_t offset) override final {}; + +}; /** * @brief structure that owns the message (or fragment) while sending/receiving by WSocketClient @@ -451,8 +524,6 @@ class WSocketClient { // amount of sent data in-flight, i.e. copied to socket buffer, but not acked yet from lwip side size_t _in_flight{0}; - // in-flight data credits - size_t _in_flight_credit{WS_IN_FLIGHT_CREDITS}; // keepalive unsigned long _keepAlivePeriod{0}, _lastPong; @@ -506,7 +577,7 @@ class WSocketServer : public AsyncWebHandler { enum class msgall_err_t { ok = 0, // message was enqueued for delivering to all clients partial, // some of clients queueus are full, message was not enqueued there - none // no clients or all outbound queues are full, message discarded + none // no clients available, or all outbound queues are full, message discarded }; explicit WSocketServer(const char* url, WSocketClient::event_cb_t handler = {}, size_t msgsize = 8 * 1024, size_t qcap = 4) : _url(url), eventHandler(handler), msgsize(msgsize), qcap(qcap) {} @@ -535,7 +606,7 @@ class WSocketServer : public AsyncWebHandler { * @param size */ void setMessageQueueSize(size_t size){ qcap = size; } - size_t getMessageQueueSize(size_t size){ return qcap; } + size_t getMessageQueueSize(size_t size) const { return qcap; } /** * @brief Set the WebSocket client Keep A Live @@ -564,7 +635,7 @@ class WSocketServer : public AsyncWebHandler { void setServerEcho(bool enabled = true, bool splitHorizon = true){ _serverEcho = enabled, _serverEchoSplitHorizon = splitHorizon; }; // get server echo mode - bool getServerEcho(){ return _serverEcho; }; + bool getServerEcho() const { return _serverEcho; }; /** @@ -687,7 +758,7 @@ class WSocketServer : public AsyncWebHandler { * @return WSocketClient::err_t */ template - msgall_err_t textAll(uint32_t id, Args&&... args){ + msgall_err_t textAll(Args&&... args){ return messageAll(std::make_shared>(WSFrameType_t::text, true, std::forward(args)...)); } @@ -717,7 +788,7 @@ class WSocketServer : public AsyncWebHandler { * @return WSocketClient::err_t */ template - msgall_err_t stringAll(uint32_t id, Args&&... args){ + msgall_err_t stringAll(Args&&... args){ return messageAll(std::make_shared>(WSFrameType_t::text, true, std::forward(args)...)); } @@ -771,12 +842,14 @@ class WSocketServer : public AsyncWebHandler { virtual bool newClient(AsyncWebServerRequest* request); protected: + std::string _url; + // WSocketClient events handler + WSocketClient::event_cb_t eventHandler; + std::list _clients; #ifdef ESP32 std::mutex clientslock; #endif - // WSocketClient events handler - WSocketClient::event_cb_t eventHandler; unsigned long _keepAlivePeriod{0}; // max message size size_t msgsize; @@ -797,7 +870,6 @@ class WSocketServer : public AsyncWebHandler { void _purgeClients(); private: - std::string _url; AwsHandshakeHandler _handshakeHandler; uint32_t _cNextId{0}; WSocketClient::overflow_t _overflow_policy{WSocketClient::overflow_t::disconnect}; From ba4ab0707979156d0f18c7296d8fe706dfa23b77 Mon Sep 17 00:00:00 2001 From: Emil Muratov Date: Sun, 12 Oct 2025 04:01:55 +0900 Subject: [PATCH 10/15] MJPEG Cam Video streaming over WebSockets using AI-Thinker ESP32-CAM module This example implements a WebCam streaming in browser using cheap ESP32-cam module. The feature here is that frames are trasfered to the browser via WebSockets, it has several advantages over traditional streamed multipart content via HTTP - websockets delivers each frame in a separate message - webserver is not blocked when stream is flowing, you can still send/receive other data via HTTP - websockets can multiplex vide data and control messages, in this example you can also get memory stats / frame sizing from controller along with video stream - WSocketServer can easily replicate stream to multiple connected clients here in this example you can connect 2-3 clients simultaneously and get smooth stream (limited by wifi bandwidth) --- .../VideoStreaming/VideoStreaming.ino | 213 ++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 wsocket_examples/VideoStreaming/VideoStreaming.ino diff --git a/wsocket_examples/VideoStreaming/VideoStreaming.ino b/wsocket_examples/VideoStreaming/VideoStreaming.ino new file mode 100644 index 000000000..fde992e59 --- /dev/null +++ b/wsocket_examples/VideoStreaming/VideoStreaming.ino @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +// +// MJPEG Cam Video streaming over WebSockets using AI-Thinker ESP32-CAM module +// you would need a module with camera to run this example https://www.espboards.dev/esp32/esp32cam/ + +/* + This example implements a WebCam streaming in browser using cheap ESP32-cam module. + The feature here is that frames are trasfered to the browser via WebSockets, + it has several advantages over traditional streamed multipart content via HTTP + - websockets delivers each frame in a separate message + - webserver is not blocked when stream is flowing, you can still send/receive other data via HTTP + - websockets can multiplex vide data and control messages, in this example you can also get memory + stats / frame sizing from controller along with video stream + - WSocketServer can easily replicate stream to multiple connected clients + here in this example you can connect 2-3 clients simultaneously and get smooth stream (limited by wifi bandwidth) + + Change you WiFi creds below, build and flash the code. + Connect to console to monitor for debug messages + Figure out IP of your board and access the board via web browser, watch for video stream +*/ + +#include +#include +#include "esp_camera.h" +#include "mjpeg.h" + + +#define WIFI_SSID "your_ssid" +#define WIFI_PASSWD "your_pass" + +// AI-Thinker ESP32-CAM config - for more details see https://github.com/rzeldent/esp32cam-rtsp/ project +camera_config_t cfg { + .pin_pwdn = 32, + .pin_reset = -1, + .pin_xclk = 0, + + .pin_sscb_sda = 26, + .pin_sscb_scl = 27, + + // Note: LED GPIO is apparently 4 not sure where that goes + // per https://github.com/donny681/ESP32_CAMERA_QR/blob/e4ef44549876457cd841f33a0892c82a71f35358/main/led.c + .pin_d7 = 35, + .pin_d6 = 34, + .pin_d5 = 39, + .pin_d4 = 36, + .pin_d3 = 21, + .pin_d2 = 19, + .pin_d1 = 18, + .pin_d0 = 5, + .pin_vsync = 25, + .pin_href = 23, + .pin_pclk = 22, + .xclk_freq_hz = 20000000, + .ledc_timer = LEDC_TIMER_1, + .ledc_channel = LEDC_CHANNEL_1, + .pixel_format = PIXFORMAT_JPEG, + // .frame_size = FRAMESIZE_UXGA, // needs 234K of framebuffer space + // .frame_size = FRAMESIZE_SXGA, // needs 160K for framebuffer + // .frame_size = FRAMESIZE_XGA, // needs 96K or even smaller FRAMESIZE_SVGA - can work if using only 1 fb + .frame_size = FRAMESIZE_SVGA, + .jpeg_quality = 15, //0-63 lower numbers are higher quality + .fb_count = 2, // if more than one i2s runs in continous mode. Use only with jpeg + .fb_location = CAMERA_FB_IN_PSRAM, + .grab_mode = CAMERA_GRAB_LATEST, + .sccb_i2c_port = 0 +}; + +// camera frame buffer pointer +camera_fb_t *fb{nullptr}; + +// WS Event server callback declaration +void wsEvent(WSocketClient *client, WSocketClient::event_t event); + +// Our WebServer +AsyncWebServer server(80); +// WSocket Server URL and callback function +WSocketServer ws("/wsstream", wsEvent); +// a unique steam id - it is used to avoid sending dublicate frames when multiple clients connected to stream +uint32_t client_id{0}; + +void sendFrame(uint32_t token){ + if (client_id == 0){ + // first client connected grabs the stream token + client_id = token; + } else if (token != client_id) { + // we will send next frame only upon delivery the previous one to client owning the token, others we ignore + // this is to avoid stacking frames clones in the Q when multiple clients are connected to the server + return; + } + + //return the frame buffer back to the driver for reuse + esp_camera_fb_return(fb); + // get 2nd buffer from camera + fb = esp_camera_fb_get(); + + // generate metadata info frame + // this text frame contains memory stat and will be displayed along video stream + char buff[100]; + snprintf(buff, 100, "FrameSize:%u, Mem:%lu, PSRAM:%lu", fb->len, ESP.getFreeHeap(), ESP.getFreePsram()); + ws.textAll(buff); + + // here we MUST ensure that client owning the stream is able to send data, otherwise recursion would crash controller + if (ws.clientState(client_id) == WSocketClient::err_t::ok){ + /* + for video frame sending we will use WSMessageStaticBlob object. + It can send large memory buffers directly to websocket peers without intermediate buffering and data copies + and it is the most efficient way to send static data + */ + auto m = std::make_shared( + WSFrameType_t::binary, // binary message + true, // final message + reinterpret_cast(fb->buf), fb->len, // buffer to transfer + // the key here to understand when frame buffer completes delivery - for this we set + // the callback back to ourself, so that when when frame delivery would be completed, + // this function is called again to obtain a new frame buffer from camera + [](WSMessageStatus_t s, uint32_t t){ sendFrame(t); }, // a callback executed on message delivery + client_id // stream token + ); + // replicate frame to ALL peers + ws.messageAll(m); + } else { + // current client can't receive stream (maybe he disconnected), we reset token here so that other client + // can reconnect and take the ownership of the stream + client_id = 0; + } + + /* + Note! Though this example is able to send video stream to multiple clients simultaneously, it has one gap - + when same buffer is streamed to multiple peers and the 'owner' of stream completes transfer, others might + still be in-progress. The buffer pointer is switched to next one from camera only upon full delivery on + message object destruction. It does not allow pipelining and slower clients could affect the others. + The question of synchronization multiple clients is out of scope of this simple example. It's just a + demonstarion of working with WebSockets. + */ +} + +void wsEvent(WSocketClient *client, WSocketClient::event_t event){ + switch (event){ + // new client connected + case WSocketClient::event_t::connect : { + Serial.printf("Client id:%lu connected\n", client->id); + if (fb) + sendFrame(client->id); + else + ws.text(client->id, "Cam init failed!"); + break; + } + + // client diconnected + case WSocketClient::event_t::disconnect : { + Serial.printf("Client id:%lu disconnected\n", client->id); + if (client_id == client->id) + // reset stream token + client_id = 0; + break; + } + + // any other events + default:; + // incoming messages could be used for controls or any other functionality + // not implemented in this example but should be considered + // If not discaded messages will overflow incoming Q + client->dequeueMessage(); + break; + } +} + +void setup() { + Serial.begin(115200); + + WiFi.mode(WIFI_STA); + WiFi.begin(WIFI_SSID, WIFI_PASSWD); + + // Wait for connection + while (WiFi.status() != WL_CONNECTED) { + delay(250); + Serial.print("."); + } + Serial.println(); + Serial.print("Connected to "); + Serial.println(WIFI_SSID); + Serial.print("IP address: "); + Serial.println(WiFi.localIP()); + Serial.println(); + Serial.printf("to access VideoStream pls open http://%s/\n", WiFi.localIP().toString().c_str()); + + // init camera + esp_err_t err = esp_camera_init(&cfg); + if (err != ESP_OK) + { + Serial.printf("Camera probe failed with error 0x%x\n", err); + } else + fb = esp_camera_fb_get(); + + // server serves index page + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { + // need to cast to uint8_t* + // if you do not, the const char* will be copied in a temporary String buffer + request->send(200, "text/html", (uint8_t *)htmlPage, std::string_view(htmlPage).length()); + }); + + // attach our WSocketServer + server.addHandler(&ws); + server.begin(); +} + + +void loop() { + // nothing to do here at all + vTaskDelete(NULL); +} From d8d95b681b547cec8ba0f57520fdf001c91745b1 Mon Sep 17 00:00:00 2001 From: Emil Muratov Date: Tue, 14 Oct 2025 21:06:58 +0900 Subject: [PATCH 11/15] limit build to C++17 capable toolchains only --- src/AsyncWSocket.cpp | 3 +++ src/AsyncWSocket.h | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/AsyncWSocket.cpp b/src/AsyncWSocket.cpp index cf11bd81d..30a712686 100644 --- a/src/AsyncWSocket.cpp +++ b/src/AsyncWSocket.cpp @@ -3,6 +3,8 @@ // A new experimental implementation of Async WebSockets client/server +// We target C++17 capable toolchain +#if __cplusplus >= 201703L #include "AsyncWSocket.h" #include "literals.h" @@ -939,3 +941,4 @@ void WSocketServerWorker::_taskRunner(){ vTaskDelete(NULL); } +#endif // __cplusplus >= 201703L diff --git a/src/AsyncWSocket.h b/src/AsyncWSocket.h index 4862bf367..ae7b56afd 100644 --- a/src/AsyncWSocket.h +++ b/src/AsyncWSocket.h @@ -3,7 +3,7 @@ // A new experimental implementation of Async WebSockets client/server - +#if __cplusplus >= 201703L #pragma once #include "AsyncWebSocket.h" @@ -945,3 +945,7 @@ class WSocketServerWorker : public WSocketServer { void _taskRunner(); }; + +#else // __cplusplus >= 201703L +#warning "WSocket requires C++17, won't build" +#endif // __cplusplus >= 201703L From 01954eada81128ca94e5ba012c899865c1c261f5 Mon Sep 17 00:00:00 2001 From: Emil Muratov Date: Wed, 15 Oct 2025 12:42:07 +0900 Subject: [PATCH 12/15] Async WSocketServer can bind to multiple URLs a single instance of WSocketServer can serve multiple websocket URLs connection URL is hashed to 32 bit and kept as a member of respective WSocketClient struct a set of methods are provided to get/set/check server and client's URL --- src/AsyncWSocket.cpp | 83 +++++++++++++++----- src/AsyncWSocket.h | 183 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 228 insertions(+), 38 deletions(-) diff --git a/src/AsyncWSocket.cpp b/src/AsyncWSocket.cpp index 30a712686..da88b18be 100644 --- a/src/AsyncWSocket.cpp +++ b/src/AsyncWSocket.cpp @@ -6,6 +6,7 @@ // We target C++17 capable toolchain #if __cplusplus >= 201703L #include "AsyncWSocket.h" +#if defined(ESP32) && (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)) #include "literals.h" #define WS_MAX_HEADER_SIZE 16 @@ -170,6 +171,9 @@ WSocketClient::WSocketClient(uint32_t id, AsyncWebServerRequest *request, WSocke _client->onData( [](void *r, AsyncClient *c, void *buf, size_t len) { (void)c; reinterpret_cast(r)->_onData(buf, len); }, this ); _client->onPoll( [](void *r, AsyncClient *c) { (void)c; reinterpret_cast(r)->_keepalive(); reinterpret_cast(r)->_clientSend(); }, this ); _client->onError( [](void *r, AsyncClient *c, int8_t error) { (void)c; log_e("err:%d", error); }, this ); + // bind URL hash + setURLHash(request->url().c_str()); + delete request; } @@ -232,7 +236,7 @@ void WSocketClient::_clientSend(size_t acked_bytes){ return; auto sock_space = _client->space(); - log_d("no ack infl:%u, space:%u, data pending:%u", _in_flight, sock_space, (uint32_t)(_outFrame.len - _outFrame.index)); + //log_d("no ack infl:%u, space:%u, data pending:%u", _in_flight, sock_space, (uint32_t)(_outFrame.len - _outFrame.index)); // if (!sock_space) return; @@ -702,10 +706,11 @@ bool WSocketServer::newClient(AsyncWebServerRequest *request){ else c->dequeueMessage(); }, // silently discard incoming messages when there is no callback set msgsize, qcap); + _clients.back().setOverflowPolicy(_overflow_policy); + _clients.back().setKeepAlive(_keepAlivePeriod); } - _clients.back().setOverflowPolicy(_overflow_policy); - _clients.back().setKeepAlive(_keepAlivePeriod); - if (eventHandler) eventHandler(&_clients.back(), WSocketClient::event_t::connect); + if (eventHandler) + eventHandler(&_clients.back(), WSocketClient::event_t::connect); return true; } @@ -714,6 +719,7 @@ void WSocketServer::handleRequest(AsyncWebServerRequest *request) { request->send(400); return; } + if (_handshakeHandler != nullptr) { if (!_handshakeHandler(request)) { request->send(401); @@ -741,9 +747,19 @@ void WSocketServer::handleRequest(AsyncWebServerRequest *request) { // ToDo: check protocol response->addHeader(WS_STR_PROTOCOL, protocol->value()); } + request->send(response); } +bool WSocketServer::canHandle(AsyncWebServerRequest *request) const { + if (request->isWebSocketUpgrade()){ + auto url = request->url().c_str(); + auto i = std::find_if(_urlhashes.cbegin(), _urlhashes.cend(), [url](auto const &h){ return h == asyncsrv::hash_djb2a(url); }); + return (i != _urlhashes.cend()); + } + return false; +}; + WSocketClient* WSocketServer::getClient(uint32_t id) { auto iter = std::find_if(_clients.begin(), _clients.end(), [id](const WSocketClient &c) { return c.id == id; }); if (iter != std::end(_clients)) @@ -784,8 +800,8 @@ WSocketServer::msgall_err_t WSocketServer::pingAll(const char *data, size_t len) return cnt == _clients.size() ? msgall_err_t::ok : msgall_err_t::partial; } -WSocketClient::err_t WSocketServer::message(uint32_t id, WSMessagePtr m){ -if (WSocketClient *c = getClient(id)) +WSocketClient::err_t WSocketServer::message(uint32_t clientid, WSMessagePtr m){ +if (WSocketClient *c = getClient(clientid)) return c->enqueueMessage(std::move(m)); else return WSocketClient::err_t::disconnected; @@ -802,6 +818,20 @@ WSocketServer::msgall_err_t WSocketServer::messageAll(WSMessagePtr m){ return cnt == _clients.size() ? msgall_err_t::ok : msgall_err_t::partial; } +WSocketServer::msgall_err_t WSocketServer::messageToEndpoint(uint32_t hash, WSMessagePtr m){ + size_t cnt{0}, cntt{0}; + for (auto &c : _clients){ + if (c.getURLHash() == hash){ + ++cntt; + if ( c.enqueueMessage(m) == WSocketClient::err_t::ok) + ++cnt; + } + } + if (!cnt) + return msgall_err_t::none; + return cnt == cntt ? msgall_err_t::ok : msgall_err_t::partial; +} + void WSocketServer::_purgeClients(){ log_d("purging clients"); std::lock_guard lock(clientslock); @@ -810,8 +840,16 @@ void WSocketServer::_purgeClients(){ } size_t WSocketServer::activeClientsCount() const { - return std::count_if(std::begin(_clients), std::end(_clients), [](const WSocketClient &c) { return c.connection() == WSocketClient::conn_state_t::connected; }); -}; + return std::count_if(std::begin(_clients), std::end(_clients), + [](const WSocketClient &c) { return c.connection() == WSocketClient::conn_state_t::connected; } + ); +} + +size_t WSocketServer::activeEndpointClientsCount(uint32_t hash) const { + return std::count_if(std::begin(_clients), std::end(_clients), + [hash](const WSocketClient &c) { return c.connection() == WSocketClient::conn_state_t::connected && c.getURLHash() == hash; } + ); +} void WSocketServer::serverEcho(WSocketClient *c){ if (!_serverEcho) return; @@ -826,6 +864,10 @@ void WSocketServer::serverEcho(WSocketClient *c){ } } +void WSocketServer::removeURLendpoint(std::string_view url){ + _urlhashes.erase(remove_if(_urlhashes.begin(), _urlhashes.end(), [url](auto const &v){ return v == asyncsrv::hash_djb2a(url); }), _urlhashes.end()); +} + // ***** WSMessageClose implementation ***** @@ -851,13 +893,14 @@ bool WSocketServerWorker::newClient(AsyncWebServerRequest *request){ if (_task_hndlr) xTaskNotifyGive(_task_hndlr); }, msgsize, qcap); - } - // create events group where we'll pick events - _clients.back().createEventGroupHandle(); - _clients.back().setOverflowPolicy(getOverflowPolicy()); - _clients.back().setKeepAlive(_keepAlivePeriod); - xEventGroupSetBits(_clients.back().getEventGroupHandle(), enum2uint32(WSocketClient::event_t::connect)); + // create events group where we'll pick events + _clients.back().createEventGroupHandle(); + _clients.back().setOverflowPolicy(getOverflowPolicy()); + _clients.back().setKeepAlive(_keepAlivePeriod); + _clients.back().setURLHash(request->url().c_str()); + xEventGroupSetBits(_clients.back().getEventGroupHandle(), enum2uint32(WSocketClient::event_t::connect)); + } if (_task_hndlr) xTaskNotifyGive(_task_hndlr); return true; @@ -895,19 +938,19 @@ void WSocketServerWorker::_taskRunner(){ // check if this a new client uxBits = xEventGroupClearBits(it->getEventGroupHandle(), enum2uint32(WSocketClient::event_t::connect) ); if ( uxBits & enum2uint32(WSocketClient::event_t::connect) ){ - _ecb(WSocketClient::event_t::connect, it->id); + _ecb(&(*it), WSocketClient::event_t::connect); } // check if 'inbound Q full' flag set uxBits = xEventGroupClearBits(it->getEventGroupHandle(), enum2uint32(WSocketClient::event_t::inQfull) ); if ( uxBits & enum2uint32(WSocketClient::event_t::inQfull) ){ - _ecb(WSocketClient::event_t::inQfull, it->id); + _ecb(&(*it), WSocketClient::event_t::inQfull); } // check for dropped messages flag uxBits = xEventGroupClearBits(it->getEventGroupHandle(), enum2uint32(WSocketClient::event_t::msgDropped) ); if ( uxBits & enum2uint32(WSocketClient::event_t::msgDropped) ){ - _ecb(WSocketClient::event_t::msgDropped, it->id); + _ecb(&(*it), WSocketClient::event_t::msgDropped); } // process all the messages from inbound Q @@ -918,15 +961,14 @@ void WSocketServerWorker::_taskRunner(){ // check for disconnected client - do not care for group bits, cause if it's deleted, we will destruct the client object if (it->connection() == WSocketClient::conn_state_t::disconnected){ - auto id = it->id; + // run a callback + _ecb(&(*it), WSocketClient::event_t::disconnect); { #ifdef ESP32 std::lock_guard lock (clientslock); #endif it = _clients.erase(it); } - // run a callback - _ecb(WSocketClient::event_t::disconnect, id); } else { // advance iterator ++it; @@ -941,4 +983,5 @@ void WSocketServerWorker::_taskRunner(){ vTaskDelete(NULL); } +#endif // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) #endif // __cplusplus >= 201703L diff --git a/src/AsyncWSocket.h b/src/AsyncWSocket.h index ae7b56afd..7a240a41c 100644 --- a/src/AsyncWSocket.h +++ b/src/AsyncWSocket.h @@ -3,12 +3,29 @@ // A new experimental implementation of Async WebSockets client/server -#if __cplusplus >= 201703L #pragma once +#if __cplusplus >= 201703L +#ifndef ESP32 +#warning "WSocket is now supported on ESP32 only" +#else + #include "AsyncWebSocket.h" #include "freertos/FreeRTOS.h" +namespace asyncsrv { +// literals hashing +// https://learnmoderncpp.com/2020/06/01/strings-as-switch-case-labels/ + +inline constexpr auto hash_djb2a(const std::string_view sv) { + uint32_t hash{ 5381 }; + for (unsigned char c : sv) { + hash = ((hash << 5) + hash) ^ c; + } + return hash; +} + +} // forward declaration for WSocketServer class WSocketServer; @@ -380,6 +397,8 @@ class WSocketClient { size_t _max_msgsize; // cummulative maximum of the data messages held in message queues, both in and out size_t _max_qcap; + // hashed url bound to client's request + uint32_t _urlhash; public: @@ -444,6 +463,30 @@ class WSocketClient { return _client; } + /** + * @brief bind URL hash to client instance + * a hash could be used to differentiate clients attached via various URLs + * + * @param url + */ + void setURLHash(std::string_view url) { _urlhash = asyncsrv::hash_djb2a(url); } + + /** + * @brief get URL hash bound to client + * + * @return uint32_t + */ + uint32_t getURLHash() const { return _urlhash; } + + /** + * @brief check if client's bound URL matches string + * + * @param url + * @return true + * @return false + */ + bool matchURL(std::string_view url) { return _urlhash == asyncsrv::hash_djb2a(url); } + /** * @brief Set inbound queue overflow Policy * @@ -580,8 +623,38 @@ class WSocketServer : public AsyncWebHandler { none // no clients available, or all outbound queues are full, message discarded }; - explicit WSocketServer(const char* url, WSocketClient::event_cb_t handler = {}, size_t msgsize = 8 * 1024, size_t qcap = 4) : _url(url), eventHandler(handler), msgsize(msgsize), qcap(qcap) {} - ~WSocketServer() = default; + /** + * @brief Construct a new WSocketServer object + * + * @param url - URL endpoint + * @param handler - event callback handler + * @param msgsize - max inbound message size (8k by default) + * @param qcap - queues size limit + */ + explicit WSocketServer(const char* url, WSocketClient::event_cb_t handler = {}, size_t msgsize = 8 * 1024, size_t qcap = 4) : eventHandler(handler), msgsize(msgsize), qcap(qcap) { _urlhashes.push_back(asyncsrv::hash_djb2a(url)); } + virtual ~WSocketServer(){}; + + /** + * @brief add additional URL to a list of websocket handlers + * + * @param url + */ + void addURLendpoint(std::string_view url){ _urlhashes.push_back(asyncsrv::hash_djb2a(url)); } + + /** + * @brief remove URL from a list of websocket handlers + * + * @param url + */ + void removeURLendpoint(std::string_view url); + + /** + * @brief clear list of handled URLs + * @note new client's won't be able to connect to server unless at least + * one new endpoint added + * + */ + void clearURLendpoints(){ _urlhashes.clear(); } /** * @copydoc WSClient::setOverflowPolicy(overflow_t policy) @@ -657,6 +730,10 @@ class WSocketServer : public AsyncWebHandler { // return number of active (connected) clients size_t activeClientsCount() const; + // return number of active (connected) clients to specific endpoint + size_t activeEndpointClientsCount(std::string_view endpoint) const { return activeEndpointClientsCount(asyncsrv::hash_djb2a(endpoint)); } + size_t activeEndpointClientsCount(uint32_t hash) const; + /** * @brief Get ptr to client with specified id * @@ -723,7 +800,25 @@ class WSocketServer : public AsyncWebHandler { * @param m * @return WSocketClient::err_t */ - WSocketClient::err_t message(uint32_t id, WSMessagePtr m); + WSocketClient::err_t message(uint32_t clientid, WSMessagePtr m); + + /** + * @brief Send message to all clients bound to specified endpoint + * + * @param hash endpoint hash + * @param m message + * @return WSocketClient::err_t + */ + msgall_err_t messageToEndpoint(uint32_t hash, WSMessagePtr m); + + /** + * @brief Send message to all clients bound to specified endpoint + * + * @param urlpath endpoint path + * @param m message + * @return WSocketClient::err_t + */ + msgall_err_t messageToEndpoint(std::string_view urlpath, WSMessagePtr m){ return messageToEndpoint(asyncsrv::hash_djb2a(urlpath) , m); }; /** * @brief send generic message to all available clients @@ -749,6 +844,25 @@ class WSocketServer : public AsyncWebHandler { else return WSocketClient::err_t::na; } + /** + * @brief Send text message to all clients in specified endpoint + * this template can accept anything that std::string can be made of + * + * @tparam Args + * @param hash urlpath hash to send to + * @param args + * @return WSocketClient::err_t + */ + template + msgall_err_t textToEndpoint(uint32_t hash, Args&&... args){ + return messageToEndpoint(hash, std::make_shared>(WSFrameType_t::text, true, std::forward(args)...)); + } + + template + msgall_err_t textToEndpoint(std::string_view urlpath, Args&&... args){ + return textToEndpoint(asyncsrv::hash_djb2a(urlpath), std::forward(args)...); + } + /** * @brief Send text message to all avalable clients * this template can accept anything that std::string can be made of @@ -778,6 +892,25 @@ class WSocketServer : public AsyncWebHandler { else return WSocketClient::err_t::na; } + /** + * @brief Send String text message to all clients in specified endpoint + * this template can accept anything that Arduino String can be made of + * + * @tparam Args + * @param hash urlpath hash to send to + * @param args Arduino String constructor arguments + * @return WSocketClient::err_t + */ + template + msgall_err_t stringToEndpoint(uint32_t hash, Args&&... args){ + return messageToEndpoint(hash, std::make_shared>(WSFrameType_t::text, true, std::forward(args)...)); + } + + template + msgall_err_t stringToEndpoint(std::string_view urlpath, Args&&... args){ + return stringToEndpoint(asyncsrv::hash_djb2a(urlpath), std::forward(args)...); + } + /** * @brief Send String text message all avalable clients * this template can accept anything that Arduino String can be made of @@ -808,6 +941,25 @@ class WSocketServer : public AsyncWebHandler { else return WSocketClient::err_t::na; } + /** + * @brief Send binary message to all clients in specified endpoint + * this template can accept anything that std::vector can be made of + * + * @tparam Args + * @param hash urlpath hash to send to + * @param args std::vector constructor arguments + * @return WSocketClient::err_t + */ + template + msgall_err_t binaryToEndpoint(uint32_t hash, Args&&... args){ + return messageToEndpoint(std::make_shared< WSMessageContainer> >(hash, WSFrameType_t::binary, true, std::forward(args)...)); + } + + template + msgall_err_t binaryToEndpoint(std::string_view urlpath, Args&&... args){ + return binaryToEndpoint(std::make_shared< WSMessageContainer> >(asyncsrv::hash_djb2a(urlpath), WSFrameType_t::binary, true, std::forward(args)...)); + } + /** * @brief Send binary message all avalable clients * this template can accept anything that std::vector can be made of @@ -818,7 +970,7 @@ class WSocketServer : public AsyncWebHandler { * @return WSocketClient::err_t */ template - msgall_err_t binaryAll(uint32_t id, Args&&... args){ + msgall_err_t binaryAll(Args&&... args){ return messageAll(std::make_shared< WSMessageContainer> >(WSFrameType_t::binary, true, std::forward(args)...)); } @@ -827,11 +979,6 @@ class WSocketServer : public AsyncWebHandler { _handshakeHandler = handler; } - // return bound URL - const char *url() const { - return _url.c_str(); - } - /** * @brief callback for AsyncServer - onboard new ws client * @@ -842,7 +989,8 @@ class WSocketServer : public AsyncWebHandler { virtual bool newClient(AsyncWebServerRequest* request); protected: - std::string _url; + // a list of url hashes this server is bound to + std::vector _urlhashes; // WSocketClient events handler WSocketClient::event_cb_t eventHandler; @@ -863,7 +1011,7 @@ class WSocketServer : public AsyncWebHandler { void serverEcho(WSocketClient *c); - /** + /** * @brief go through clients list and remove those ones that are disconnected and have no messages pending * */ @@ -877,7 +1025,7 @@ class WSocketServer : public AsyncWebHandler { // WebServer methods - bool canHandle(AsyncWebServerRequest *request) const override final { return request->isWebSocketUpgrade() && request->url().equals(_url.c_str()); }; + bool canHandle(AsyncWebServerRequest *request) const override final; void handleRequest(AsyncWebServerRequest *request) override final; }; @@ -888,12 +1036,10 @@ class WSocketServer : public AsyncWebHandler { class WSocketServerWorker : public WSocketServer { public: - // event callback alias - using event_cb_t = std::function; // message callback alias using msg_cb_t = std::function; - explicit WSocketServerWorker(const char* url, msg_cb_t msg_handler, event_cb_t event_handler, size_t msgsize = 8 * 1024, size_t qcap = 4) + explicit WSocketServerWorker(const char* url, msg_cb_t msg_handler, WSocketClient::event_cb_t event_handler, size_t msgsize = 8 * 1024, size_t qcap = 4) : WSocketServer(url, nullptr, msgsize, qcap), _mcb(msg_handler), _ecb(event_handler) {} ~WSocketServerWorker(){ stop(); }; @@ -925,7 +1071,7 @@ class WSocketServerWorker : public WSocketServer { * * @param handler */ - void setEventHandler(event_cb_t handler){ _ecb = handler; }; + void setEventHandler(WSocketClient::event_cb_t handler){ _ecb = handler; }; /** * @brief callback for AsyncServer - onboard new ws client @@ -939,13 +1085,14 @@ class WSocketServerWorker : public WSocketServer { private: msg_cb_t _mcb; - event_cb_t _ecb; + WSocketClient::event_cb_t _ecb; // worker task that handles messages TaskHandle_t _task_hndlr{nullptr}; void _taskRunner(); }; +#endif // ESP32 #else // __cplusplus >= 201703L #warning "WSocket requires C++17, won't build" #endif // __cplusplus >= 201703L From beb79e4ec2dd1ce106e835ba38745dfb7dd35f82 Mon Sep 17 00:00:00 2001 From: Emil Muratov Date: Wed, 15 Oct 2025 22:11:23 +0900 Subject: [PATCH 13/15] WSocket MultiEndpoint example --- .../MultiEndpoint/MultiEndpoint.ino | 241 +++++ wsocket_examples/MultiEndpoint/endpoints.h | 837 ++++++++++++++++++ wsocket_examples/VideoStreaming/mjpeg.h | 570 ++++++++++++ wsocket_examples/WebChat/html.h | 249 ++++++ 4 files changed, 1897 insertions(+) create mode 100644 wsocket_examples/MultiEndpoint/MultiEndpoint.ino create mode 100644 wsocket_examples/MultiEndpoint/endpoints.h create mode 100644 wsocket_examples/VideoStreaming/mjpeg.h create mode 100644 wsocket_examples/WebChat/html.h diff --git a/wsocket_examples/MultiEndpoint/MultiEndpoint.ino b/wsocket_examples/MultiEndpoint/MultiEndpoint.ino new file mode 100644 index 000000000..9aa71d806 --- /dev/null +++ b/wsocket_examples/MultiEndpoint/MultiEndpoint.ino @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +/* + this example shows how to handle multiple websocket endpoints within same server instance + +*/ + +#include +#include +#include "endpoints.h" + +#define WIFI_SSID "your_ssid" +#define WIFI_PASSWD "your_pass" + +// WebSocket endpoints to serve +constexpr const char WSENDPOINT_DEFAULT[] = "/ws"; // default endpoint - used for logging messages +constexpr const char WSENDPOINT_ECHO[] = "/wsecho"; // echo server - it replies back messages it receives +constexpr const char WSENDPOINT_SPEED[] = "/wsspeed"; // upstream speed test - sending large data chunks from server to browser + +// *** Event handlers *** +// WS Events dispatcher +void wsEventDispatcher(WSocketClient *client, WSocketClient::event_t event); +// speed tester +void wsSpeedService(WSocketClient *client, WSocketClient::event_t event); +// echo service +void wsEchoService(WSocketClient *client, WSocketClient::event_t event); +// default service +void wsDefaultService(WSocketClient *client, WSocketClient::event_t event); + +// Web Sever +AsyncWebServer server(80); +// WebSocket server instance +WSocketServer ws(WSENDPOINT_DEFAULT, wsEventDispatcher); + +// this function is attached as main callback function for websocket events +void wsEventDispatcher(WSocketClient *client, WSocketClient::event_t event){ + if (event == WSocketClient::event_t::connect || event == WSocketClient::event_t::disconnect){ + // report all new connections to default endpoint + char buff[100]; + snprintf(buff, 100, "Client %s, id:%lu, IP:%s:%u\n", + event == WSocketClient::event_t::connect ? "connected" : "disconnected" , + client->id, + IPAddress (client->client()->getRemoteAddress4().addr).toString().c_str(), + client->client()->getRemotePort() + ); + // send message to clients connected to default /ws endpoint + ws.textToEndpoint(WSENDPOINT_DEFAULT, buff); + Serial.print(buff); + } + + // here we identify on which endpoint we received and event and dispatch to the corresponding handler + switch (client->getURLHash()) + { + case asyncsrv::hash_djb2a(WSENDPOINT_ECHO) : + wsEchoService(client, event); + break; + + case asyncsrv::hash_djb2a(WSENDPOINT_SPEED) : + wsSpeedService(client, event); + break; + + default: + wsDefaultService(client, event); + break; + } + +} + + +// default service - we will use it for event logging and information reports +void wsDefaultService(WSocketClient *client, WSocketClient::event_t event){ + switch (event){ + case WSocketClient::event_t::msgRecv : { + // we do nothing but discard messages here (if any), no use for now + client->dequeueMessage(); + break; + } + + default:; + } +} + +// default service - we will use it for event logging and information reports +void wsEchoService(WSocketClient *client, WSocketClient::event_t event){ + switch (event){ + case WSocketClient::event_t::connect : { + ws.text(client->id, "Hello Client, this is an echo endpoint, message me something and I will reply it back"); + break; + } + + // incoming message + case WSocketClient::event_t::msgRecv : { + auto m = client->dequeueMessage(); + if (m->type == WSFrameType_t::text){ + // got a text message, reformat it and reply + std::string msg("Your message was: "); + msg.append(m->getData()); + // avoid copy and move string to message queue + ws.text(client->id, std::move(msg)); + } + + break; + } + default:; + } +} + + +// for speed test load +uint8_t *buff = nullptr; +size_t buff_size = 32 * 1024; // will send 32k message buffer +//size_t cnt{0}; +// a unique stream id - it is used to avoid sending dublicate frames when multiple clients connected to speedtest endpoint +uint32_t client_id{0}; + +void bulkSend(uint32_t token){ + if (!buff) return; + if (client_id == 0){ + // first client connected grabs the stream + client_id = token; + } else if (token != client_id) { + // we will send next frame only upon delivery the previous one to client owning the stream, for others we ignore + // this is to avoid stacking new frames in the Q when multiple clients are connected to the server + return; + } + + // generate metadata info frame + // this text frame will carry our resources stat + char msg[120]; + snprintf(msg, 120, "FrameSize:%u, Mem:%lu, psram:%lu, token:%lu", buff_size, ESP.getFreeHeap(), ESP.getFreePsram(), client_id); + + ws.textToEndpoint(WSENDPOINT_SPEED, msg); + + // here we MUST ensure that client owning the stream is able to send data, otherwise recursion would crash controller + if (ws.clientState(client_id) == WSocketClient::err_t::ok){ + // for bulk load sending we will use WSMessageStaticBlob object, it will directly send + // payload to websocket peers without intermediate buffer copies and + // it is the most efficient way to send large objects from memory/ROM + auto m = std::make_shared( + WSFrameType_t::binary, // bynary message + true, // final message + reinterpret_cast(buff), buff_size, // buffer to transfer + // the key here to understand when frame buffer completes delivery - for this we set + // the callback back to ourself, so that when when + // this frame would complete delivery, this function is called again to obtain a new frame buffer from camera + [](WSMessageStatus_t s, uint32_t t){ bulkSend(t); }, + client_id // message token + ); + // send message to all peers of this endpoint + ws.messageToEndpoint(WSENDPOINT_SPEED, m); + //++cnt; + } else { + client_id = 0; + } +} + +// speed tester - this endpoint will send bulk dummy payload to anyone who connects here +void wsSpeedService(WSocketClient *client, WSocketClient::event_t event){ + switch (event){ + case WSocketClient::event_t::connect : { + // prepare a buffer with some junk data + if (!buff) + buff = (uint8_t*)malloc(buff_size); + // start an endless bulk transfer + bulkSend(client->id); + break; + } + + // incoming message + case WSocketClient::event_t::msgRecv : { + // silently discard here everything comes in + client->dequeueMessage(); + } + break; + + case WSocketClient::event_t::disconnect : + // if no more clients are connected, release memory + if ( ws.activeEndpointClientsCount(WSENDPOINT_SPEED) == 0){ + delete buff; + buff = nullptr; + } + break; + + default:; + } + +} + + + +// setup our server +void setup() { + Serial.begin(115200); + +#ifndef CONFIG_IDF_TARGET_ESP32H2 + WiFi.mode(WIFI_STA); + WiFi.begin(WIFI_SSID, WIFI_PASSWD); + //WiFi.softAP("esp-captive"); +#endif + // Wait for connection + while (WiFi.status() != WL_CONNECTED) { + delay(250); + Serial.print("."); + } + Serial.println(""); + Serial.print("Connected to "); + //Serial.println(ssid); + Serial.print("IP address: "); + Serial.println(WiFi.localIP()); + Serial.printf("Open the browser and connect to http://%s/\n", WiFi.localIP()); + + // HTTP endpoint + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { + // need to cast to uint8_t* + // if you do not, the const char* will be copied in a temporary String buffer + request->send(200, "text/html", (uint8_t *)htmlPage, std::string_view(htmlPage).length()); + }); + + // add endpoint for bulk speed testing + ws.addURLendpoint("/wsspeed"); + + // add endpoint for message echo testing + ws.addURLendpoint("/wsecho"); + + // attach WebSocket server to web server + server.addHandler(&ws); + + // start server + server.begin(); + + //log_e("e setup end"); + //log_w("w server started"); + //log_d("d server debug"); +} + + +void loop() { + // nothing to do here + vTaskDelete(NULL); +} diff --git a/wsocket_examples/MultiEndpoint/endpoints.h b/wsocket_examples/MultiEndpoint/endpoints.h new file mode 100644 index 000000000..331d1d111 --- /dev/null +++ b/wsocket_examples/MultiEndpoint/endpoints.h @@ -0,0 +1,837 @@ +// this page provides minimalistic WebSocket testing dashboard +// disclaimer: this page and code was generated with the help of DeepSeek AI tool + +static const char *htmlPage = R"( + + + + + + WebSocket Dashboard + + + + +
+
+

WebSocket Dashboard

+
+ Server: ws://localhost:8080 +
+
+ + +
+
+

Log Messages /ws

+
+ Status: Disconnected +
+
+
+
+
+
+ + +
+
+

Echo Chat /wsecho

+
+ + +
+ Status: Disconnected +
+
+
+
+
+
+ + +
+
+
+ + +
+
+

Speed Test /wsspeed

+
+ + +
+ Status: Disconnected +
+
+
+
+
+
+ + 0 +
+
+ + 0 +
+
+ + 0 B/s +
+
+ + 0 B +
+
+ + 0 B +
+
+ + 0s +
+
+
+
+
+ + + + +)"; diff --git a/wsocket_examples/VideoStreaming/mjpeg.h b/wsocket_examples/VideoStreaming/mjpeg.h new file mode 100644 index 000000000..2b1b65911 --- /dev/null +++ b/wsocket_examples/VideoStreaming/mjpeg.h @@ -0,0 +1,570 @@ +// this page provides minimalistic WebSocket MJPEG CAM streaming app +// disclaimer: this page and code was generated with the help of DeepSeek AI tool + +static const char *htmlPage = R"( + + + + + + MJPEG WebSocket Stream + + + + +
+

MJPEG WebSocket Video Stream

+ +
+ + +
+ Status: Disconnected +
+
+ +
+ +
+ +
+
+

FPS: 0

+

Frame Count: 0

+
+
+

Frame Size: 0 bytes

+

Memory: 0 bytes free

+

PSRAM: 0 bytes free

+
+
+
+ + + + +)"; diff --git a/wsocket_examples/WebChat/html.h b/wsocket_examples/WebChat/html.h new file mode 100644 index 000000000..e1ff64690 --- /dev/null +++ b/wsocket_examples/WebChat/html.h @@ -0,0 +1,249 @@ +// this page provides minimalistic WebChat room +// disclaimer: this page and code was generated with the help of AI tools + +static const char *htmlChatPage = R"( + + + + + + AsyncWebSocket Chat example + + + + + +
+
+

Enter your username

+ +
+ +
+
+ + +
+ + +
+ + +
+ + + + +)"; From 3c1fc5982072853d6b29c4850fefa343d1776cae Mon Sep 17 00:00:00 2001 From: Emil Muratov Date: Wed, 15 Oct 2025 22:17:25 +0900 Subject: [PATCH 14/15] WSocket comment out log messages --- src/AsyncWSocket.cpp | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/AsyncWSocket.cpp b/src/AsyncWSocket.cpp index da88b18be..586afaa33 100644 --- a/src/AsyncWSocket.cpp +++ b/src/AsyncWSocket.cpp @@ -6,7 +6,8 @@ // We target C++17 capable toolchain #if __cplusplus >= 201703L #include "AsyncWSocket.h" -#if defined(ESP32) && (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)) +#if defined(ESP32) +#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)) #include "literals.h" #define WS_MAX_HEADER_SIZE 16 @@ -220,7 +221,7 @@ void WSocketClient::_clientSend(size_t acked_bytes){ //log_d("infl:%u, credits:%u", _in_flight, _in_flight_credit); // check if we were waiting to ack our disconnection frame if (!_in_flight && (_connection == conn_state_t::disconnecting)){ - log_d("closing tcp-conn"); + //log_d("closing tcp-conn"); // we are server, should close connection first as per https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.1 // here we close from the app side, send TCP-FIN to the party and move to FIN_WAIT_1/2 states _client->close(); @@ -245,7 +246,7 @@ void WSocketClient::_clientSend(size_t acked_bytes){ // ignore the call if available sock space is smaller then acked data and we won't be able to fit message's ramainder there // this will reduce AsyncTCP's event Q pressure under heavy load if ((_outFrame.msg && (_outFrame.len - _outFrame.index > _client->space())) && (_client->space() < acked_bytes) ){ - log_d("defer ws send call, in-flight:%u/%u", _in_flight, _client->space()); + //log_d("defer ws send call, in-flight:%u/%u", _in_flight, _client->space()); return; } @@ -363,12 +364,12 @@ void WSocketClient::_onTimeout(uint32_t time) { void WSocketClient::_onDisconnect(AsyncClient *c) { _connection = conn_state_t::disconnected; - log_d("TCP client disconnected"); + //log_d("TCP client disconnected"); _sendEvent(event_t::disconnect); } void WSocketClient::_onData(void *pbuf, size_t plen) { - Serial.printf("_onData, len:%u\n", plen); + //log_d("_onData, len:%u\n", plen); if (!pbuf || !plen || _connection == conn_state_t::disconnected) return; char *data = (char *)pbuf; @@ -406,7 +407,7 @@ void WSocketClient::_onData(void *pbuf, size_t plen) { // if we got whole frame now if (_inFrame.index == _inFrame.len){ - log_d("_onData, cmplt msg len:%u", (uint32_t)_inFrame.len); + //log_d("cmplt msg len:%u", (uint32_t)_inFrame.len); if (_inFrame.msg->getStatusCode() == 1007){ // this is a dummy/corrupted message, we discard it @@ -418,7 +419,7 @@ void WSocketClient::_onData(void *pbuf, size_t plen) { // received close message case WSFrameType_t::close : { if (_connection == conn_state_t::disconnecting){ - log_d("recv close ack"); + //log_d("recv close ack"); // if it was ws-close ack - we can close TCP connection _connection = conn_state_t::disconnected; // normally we should call close() here and wait for other side also close tcp connection with TCP-FIN, but @@ -433,7 +434,7 @@ void WSocketClient::_onData(void *pbuf, size_t plen) { } // otherwise it's a close request from a peer - echo back close message as per https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.1 - log_d("recv client's ws-close req"); + //log_d("recv client's ws-close req"); { #ifdef ESP32 std::unique_lock lockin(_inQlock); @@ -489,6 +490,7 @@ std::pair WSocketClient::_mkNewFrame(char* data, size_t len, W // read frame size frame.len = data[1] & 0x7F; size_t offset = 2; // first 2 bytes +/* Serial.print("ws hdr: "); //Serial.println(frame.mask, HEX); char buffer[10] = {}; // Buffer for hex conversion @@ -500,7 +502,7 @@ std::pair WSocketClient::_mkNewFrame(char* data, size_t len, W ++ptr; } Serial.println(); - +*/ // find message size from header if (frame.len == 126 && len >= 4) { // two byte @@ -514,7 +516,7 @@ std::pair WSocketClient::_mkNewFrame(char* data, size_t len, W offset += 8; } - log_d("recv hdr, sock data:%u, msg body size:%u", len, frame.len); + //log_d("recv hdr, sock data:%u, msg body size:%u", len, frame.len); // if ws.close() is called, Safari sends a close frame with plen 2 and masked bit set. We must not try to read mask key from beyond packet size if (masked && len >= offset + 4) { @@ -611,7 +613,7 @@ std::pair WSocketClient::_mkNewFrame(char* data, size_t len, W _inFrame.index = bodylen; } - log_e("new msg frame size:%u, bodylen:%u", offset, bodylen); + //log_e("new msg frame size:%u, bodylen:%u", offset, bodylen); // return the number of consumed data from input buffer return {offset, 0}; } @@ -736,9 +738,6 @@ void WSocketServer::handleRequest(AsyncWebServerRequest *request) { const AsyncWebHeader *key = request->getHeader(WS_STR_KEY); AsyncWebServerResponse *response = new AsyncWebSocketResponse(key->value(), [this](AsyncWebServerRequest *r){ return newClient(r); }); if (response == NULL) { -#ifdef ESP32 - log_e("Failed to allocate"); -#endif request->abort(); return; } @@ -833,7 +832,6 @@ WSocketServer::msgall_err_t WSocketServer::messageToEndpoint(uint32_t hash, WSMe } void WSocketServer::_purgeClients(){ - log_d("purging clients"); std::lock_guard lock(clientslock); // purge clients that are disconnected and with all messages consumed _clients.remove_if([](const WSocketClient& c){ return (c.connection() == WSocketClient::conn_state_t::disconnected && !c.inQueueSize() ); }); @@ -887,7 +885,7 @@ bool WSocketServerWorker::newClient(AsyncWebServerRequest *request){ #endif _clients.emplace_back(getNextId(), request, [this](WSocketClient *c, WSocketClient::event_t e){ - log_d("client event id:%u state:%u", c->id, c->state()); + //log_d("client event id:%u state:%u", c->id, c->state()); // server echo call if (e == WSocketClient::event_t::msgRecv) serverEcho(c); if (_task_hndlr) xTaskNotifyGive(_task_hndlr); @@ -983,5 +981,6 @@ void WSocketServerWorker::_taskRunner(){ vTaskDelete(NULL); } -#endif // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) +#endif // (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)) +#endif // defined(ESP32) #endif // __cplusplus >= 201703L From 06f565963532b6ef77dc37ba9ce1726219d5d0e1 Mon Sep 17 00:00:00 2001 From: Emil Muratov Date: Sat, 18 Oct 2025 15:53:18 +0900 Subject: [PATCH 15/15] WSocket - TCP window control In case if there are pending messages in the Q we clamp window size gradually to push sending party back. The larger the Q grows then more window is closed. This works pretty well for messages sized about or more than TCP windows size (5,7k default for Arduino). It could prevent Q overflow and sieze incoming data flow without blocking the entie network stack. Mostly usefull with websocket worker where AsyncTCP thread is not blocked by user callbacks. --- src/AsyncWSocket.cpp | 85 ++++++++++++++++++++++++++++++++++++-------- src/AsyncWSocket.h | 4 +++ 2 files changed, 74 insertions(+), 15 deletions(-) diff --git a/src/AsyncWSocket.cpp b/src/AsyncWSocket.cpp index 586afaa33..b4aa9674e 100644 --- a/src/AsyncWSocket.cpp +++ b/src/AsyncWSocket.cpp @@ -166,11 +166,10 @@ WSocketClient::WSocketClient(uint32_t id, AsyncWebServerRequest *request, WSocke _client->setNoDelay(true); // set AsyncTCP callbacks _client->onAck( [](void *r, AsyncClient *c, size_t len, uint32_t rtt) { (void)c; reinterpret_cast(r)->_clientSend(len); }, this ); - //_client->onAck( [](void *r, AsyncClient *c, size_t len, uint32_t rtt) { (void)c; reinterpret_cast(r)->_onAck(len, rtt); }, this ); _client->onDisconnect( [](void *r, AsyncClient *c) { reinterpret_cast(r)->_onDisconnect(c); }, this ); _client->onTimeout( [](void *r, AsyncClient *c, uint32_t time) { (void)c; reinterpret_cast(r)->_onTimeout(time); }, this ); _client->onData( [](void *r, AsyncClient *c, void *buf, size_t len) { (void)c; reinterpret_cast(r)->_onData(buf, len); }, this ); - _client->onPoll( [](void *r, AsyncClient *c) { (void)c; reinterpret_cast(r)->_keepalive(); reinterpret_cast(r)->_clientSend(); }, this ); + _client->onPoll( [](void *r, AsyncClient *c) { reinterpret_cast(r)->_onPoll(c); }, this ); _client->onError( [](void *r, AsyncClient *c, int8_t error) { (void)c; log_e("err:%d", error); }, this ); // bind URL hash setURLHash(request->url().c_str()); @@ -212,9 +211,9 @@ void WSocketClient::_clientSend(size_t acked_bytes){ // 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 and minimizing events in asynctcp's Q if (acked_bytes){ - auto sock_space = _client->space(); //log_d("ack:%u/%u, sock space:%u", acked_bytes, _in_flight, sock_space); _in_flight -= std::min(acked_bytes, _in_flight); + auto sock_space = _client->space(); if (!sock_space){ return; } @@ -236,10 +235,10 @@ void WSocketClient::_clientSend(size_t acked_bytes){ if (!lock.try_lock()) return; - auto sock_space = _client->space(); + //auto sock_space = _client->space(); //log_d("no ack infl:%u, space:%u, data pending:%u", _in_flight, sock_space, (uint32_t)(_outFrame.len - _outFrame.index)); // - if (!sock_space) + if (!_client->space()) return; } @@ -277,7 +276,6 @@ void WSocketClient::_clientSend(size_t acked_bytes){ if (_outFrame.index == _outFrame.len){ // if we complete writing entire message, send the frame right away - // increment in-flight counter and take the credit if (!_client->send()) _client->abort(); @@ -296,7 +294,7 @@ void WSocketClient::_clientSend(size_t acked_bytes){ // no use case for this for now //_sendEvent(event_t::msgSent); - // if there are free in-flight credits and buffer space available try to pull next msg from Q + // if there is still buffer space available try to pull next msg from Q if (_client->space() > WS_MAX_HEADER_SIZE && _evictOutQueue()){ // generate header and add to the socket buffer _in_flight += webSocketSendHeader(_client, _outFrame); @@ -369,10 +367,12 @@ void WSocketClient::_onDisconnect(AsyncClient *c) { } void WSocketClient::_onData(void *pbuf, size_t plen) { - //log_d("_onData, len:%u\n", plen); + //log_d("_onData, 0x%08" PRIx32 " len:%u", (uint32_t)pbuf, plen); if (!pbuf || !plen || _connection == conn_state_t::disconnected) return; char *data = (char *)pbuf; + size_t pb_len = plen; + while (plen){ if (!_inFrame.msg){ // it's a new frame, need to parse header data @@ -401,6 +401,7 @@ void WSocketClient::_onData(void *pbuf, size_t plen) { // todo: for now assume object will consume all the payload provided _inFrame.msg->addChunk(data, payload_len, _inFrame.index); + _inFrame.index += payload_len; data += payload_len; plen -= payload_len; } @@ -461,6 +462,9 @@ void WSocketClient::_onData(void *pbuf, size_t plen) { // just reply to ping, does user needs this ping message? _messageQueueOut.emplace_front( std::make_shared>(WSFrameType_t::pong, true, _inFrame.msg->getData()) ); _inFrame.msg.reset(); + // send frame is no other message is in progress + if (!_outFrame.msg) + _clientSend(); break; } @@ -479,6 +483,40 @@ void WSocketClient::_onData(void *pbuf, size_t plen) { } } } + + /* + Applying TCP window control here. In case if there are pending messages in the Q + we clamp window size gradually to push sending party back. The larger the Q grows + then more window is closed. This works pretty well for messages sized about or more + than TCP windows size (5,7k default for Arduino). It could prevent Q overflow and + sieze incoming data flow without blocking the entie network stack. Mostly usefull + with websocket worker where AsyncTCP thread is not blocked by user callbacks. + */ + if (_messageQueueIn.size()){ + _client->ackLater(); + size_t reduce_size = pb_len * _messageQueueIn.size() / _max_qcap; + _client->ack(pb_len - reduce_size); + _pending_ack += reduce_size; + //log_d("delay ack:%u, total pending:%u", reduce_size, _pending_ack); + } +} + +void WSocketClient::_onPoll(AsyncClient *c){ + /* + Window control - we open window deproportionally to Q size letting data flow a bit + */ + if (_pending_ack){ + size_t to_keep = _pending_ack * (_messageQueueIn.size() + 1) / _max_qcap; + _client->ack(_pending_ack - to_keep); + size_t bak = _pending_ack; + _pending_ack = to_keep; + //log_d("poll ack:%u, left:%u\n", bak - _pending_ack, _pending_ack); + } + _keepalive(); + // call send if no other message is in progress and Q is not empty somehow, + // otherwise rely on ack events + if (!_outFrame.msg && _messageQueueOut.size()) + _clientSend(); } std::pair WSocketClient::_mkNewFrame(char* data, size_t len, WSMessageFrame& frame){ @@ -522,8 +560,8 @@ std::pair WSocketClient::_mkNewFrame(char* data, size_t len, W if (masked && len >= offset + 4) { // mask bytes order are LSB, so we can copy it as-is frame.mask = *reinterpret_cast(data + offset); - Serial.printf("mask key at %u, :0x", offset); - Serial.println(frame.mask, HEX); + //Serial.printf("mask key at %u, :0x", offset); + //Serial.println(frame.mask, HEX); offset += 4; } @@ -629,7 +667,9 @@ WSocketClient::err_t WSocketClient::enqueueMessage(WSMessagePtr mptr){ #endif _messageQueueOut.emplace_back( std::move(mptr) ); } - _clientSend(); + // send frame if no other message is in progress + if (!_outFrame.msg) + _clientSend(); return err_t::ok; } @@ -637,14 +677,27 @@ WSocketClient::err_t WSocketClient::enqueueMessage(WSMessagePtr mptr){ } WSMessagePtr WSocketClient::dequeueMessage(){ - #ifdef ESP32 - std::unique_lock lock(_inQlock); - #endif WSMessagePtr msg; if (_messageQueueIn.size()){ + #ifdef ESP32 + std::unique_lock lock(_inQlock); + #endif msg.swap(_messageQueueIn.front()); _messageQueueIn.pop_front(); } + /* + Window control - we open window deproportionally to Q size letting data flow once a message is deQ'd + */ + if (_pending_ack){ + if (!_messageQueueIn.size()){ + _client->ack(0xffff); // on empty Q we ack whatever is left (max TCP win size) + } else { + size_t ackpart =_pending_ack * (_max_qcap - _messageQueueIn.size()) / _max_qcap; + //log_d("ackdq:%u/%u", ackpart, _pending_ack); + _client->ack(ackpart); + _pending_ack -= ackpart; + } + } return msg; } @@ -671,7 +724,9 @@ WSocketClient::err_t WSocketClient::close(uint16_t code, const char *message){ _messageQueueOut.emplace_front( std::make_shared(code, message) ); else _messageQueueOut.emplace_front( std::make_shared(code) ); - _clientSend(); + // send frame if no other message is in progress + if (!_outFrame.msg) + _clientSend(); return err_t::ok; } diff --git a/src/AsyncWSocket.h b/src/AsyncWSocket.h index 7a240a41c..d45fe30a1 100644 --- a/src/AsyncWSocket.h +++ b/src/AsyncWSocket.h @@ -567,6 +567,8 @@ class WSocketClient { // amount of sent data in-flight, i.e. copied to socket buffer, but not acked yet from lwip side size_t _in_flight{0}; + // counter for consumed data from tcp_pcbs, but delayd ack to hold the window + size_t _pending_ack{0}; // keepalive unsigned long _keepAlivePeriod{0}, _lastPong; @@ -606,6 +608,7 @@ class WSocketClient { void _onTimeout(uint32_t time); void _onDisconnect(AsyncClient *c); void _onData(void *pbuf, size_t plen); + void _onPoll(AsyncClient *c); }; /** @@ -658,6 +661,7 @@ class WSocketServer : public AsyncWebHandler { /** * @copydoc WSClient::setOverflowPolicy(overflow_t policy) + * @note default is 'disconnect' */ void setOverflowPolicy(WSocketClient::overflow_t policy){ _overflow_policy = policy; } WSocketClient::overflow_t getOverflowPolicy() const { return _overflow_policy; }