Send binary WebSocket frames in one write

This commit is contained in:
2026-09-08 23:52:00 +02:00
parent 042499e4d6
commit 60d9c54bb4
10 changed files with 334 additions and 8 deletions
+49 -1
View File
@@ -11,7 +11,7 @@
#include "secure_random.h"
#if ESP_IDF_VERSION != ESP_IDF_VERSION_VAL(5, 5, 0)
#error "Reaudit HTTPD headers, upgrade, request completion and idle cleanup for this IDF"
#error "Reaudit HTTPD headers, upgrade, idle cleanup and WS/TLS send contracts for this IDF"
#endif
/* IDF 5.5.0 httpd_sess_process increments lru_counter only AFTER successful
@@ -60,6 +60,54 @@ void web_httpd_idle_sweep(httpd_handle_t server,
}
}
static int web_httpd_aborted_send(httpd_handle_t server, int fd,
const char *buffer, size_t length, int flags)
{
(void)server; (void)fd; (void)buffer; (void)length; (void)flags;
return HTTPD_SOCK_ERR_FAIL;
}
esp_err_t web_httpd_ws_send_binary(httpd_handle_t server, int fd,
const void *expected_context,
const uint8_t *payload, size_t length)
{
struct httpd_data *hd = server;
if (!hd || fd < 0 || !expected_context || (!payload && length) ||
length > WEB_HTTPD_WS_BINARY_MAX_PAYLOAD) return ESP_ERR_INVALID_ARG;
if (httpd_os_thread_handle() != hd->hd_td.handle || hd->hd_req_aux.sd)
return ESP_ERR_INVALID_STATE;
struct sock_db *sd = httpd_sess_get(hd, fd);
if (!sd || sd->ctx != expected_context || !sd->ws_handshake_done ||
sd->ws_close || sd->for_async_req || !sd->send_fn)
return ESP_ERR_INVALID_STATE;
/* IDF 5.5.0 httpd_ws_send_frame_async emits header/payload separately.
* Keep the existing send override (HTTPS -> esp_tls_conn_write), not raw
* socket IO. Owner-local scratch lives through the synchronous call only. */
uint8_t wire[WEB_HTTPD_WS_BINARY_MAX_PAYLOAD + 4U];
size_t header = length <= 125U ? 2U : 4U;
wire[0] = 0x82; /* FIN, binary; server frames are never masked. */
wire[1] = header == 2U ? (uint8_t)length : 126U;
if (header == 4U) {
wire[2] = (uint8_t)(length >> 8U);
wire[3] = (uint8_t)length;
}
if (length) memcpy(wire + header, payload, length);
size_t total = header + length;
int sent = sd->send_fn(hd, fd, (const char *)wire, total, 0);
if (sent == (int)total) return ESP_OK;
/* Owner retains this validated session across the synchronous send. TLS
* may hold pending output after short/zero/WANT/error: never retry it with
* different arguments. Deferred close alone permits SDK automatic PONG or
* CLOSE first. Shutdown alone cannot block buffered-input TLS calls either.
* Reject all sends before shutdown, even if shutdown fails. ws_close also
* skips SDK request processing; normal HTTPD still owns TLS destruction. */
sd->send_fn = web_httpd_aborted_send;
sd->ws_close = true;
(void)shutdown(fd, SHUT_RDWR);
return ESP_FAIL;
}
bool web_httpd_headers_valid(httpd_req_t *request)
{
if (!request || !request->aux) return false;
+9
View File
@@ -26,6 +26,15 @@ void web_httpd_wipe_request(httpd_req_t *request, bool closing);
esp_err_t web_httpd_upgrade(httpd_req_t *request,
esp_err_t (*handler)(httpd_req_t *));
#define WEB_HTTPD_WS_BINARY_MAX_PAYLOAD 512U
/* HTTPD-owner work only; caller retains generation/lifecycle and one-work-slot
* ownership. Synchronous FIN/binary/unmasked send through the session override.
* Non-full writes synchronously block further session sends and abort the socket;
* caller must retain normal failure/cleanup accounting, never replay the frame. */
esp_err_t web_httpd_ws_send_binary(httpd_handle_t server, int fd,
const void *expected_context,
const uint8_t *payload, size_t length);
/* Serialized server startup only, exact-match ordinary GET, URI <= 127 bytes.
* Stage both allocations before publication; HTTPD owns/frees them on success. */
esp_err_t web_httpd_register_optional_get(httpd_handle_t server, const httpd_uri_t *uri);
+6 -1
View File
@@ -65,6 +65,9 @@ typedef struct {
uint32_t generation;
} web_serial_work_t;
_Static_assert(WEB_SERIAL_TRANSPORT_TX_PAYLOAD_SIZE == WEB_HTTPD_WS_BINARY_MAX_PAYLOAD,
"Reaudit bounded binary WS send when changing serial payload size");
typedef struct web_serial_slot {
web_serial_slot_state_t state;
httpd_handle_t server;
@@ -1057,7 +1060,9 @@ static void web_serial_send_work(void *argument)
};
send_called = true;
if (sample_epoch && performance_gate() == sample_epoch) send_start = esp_timer_get_time();
result = httpd_ws_send_frame_async(server, socket_fd, &frame);
result = type == HTTPD_WS_TYPE_BINARY
? web_httpd_ws_send_binary(server, socket_fd, slot, slot->tx_data, length)
: httpd_ws_send_frame_async(server, socket_fd, &frame);
if (sample_epoch && performance_gate() == sample_epoch) send_end = esp_timer_get_time();
if (result == ESP_OK) {
(void)httpd_sess_update_lru_counter(server, socket_fd);