Chapter 97: WebSocket Server Implementation

Chapter Objectives

After completing this chapter, you will be able to:

  • Understand the role and responsibilities of a WebSocket server.
  • Explain how the ESP-IDF HTTP server component facilitates WebSocket upgrades.
  • Implement a basic WebSocket server on an ESP32 device.
  • Manage connected WebSocket clients and their life cycles.
  • Send and receive WebSocket frames (text and binary) with connected clients.
  • Broadcast messages to multiple connected clients.
  • Implement a secure WebSocket Server (WSS) using the ESP-IDF.
  • Identify common issues and troubleshoot WebSocket server applications on ESP32.

Introduction

In the previous chapter, we explored how an ESP32 can act as a WebSocket client, initiating connections and exchanging real-time messages with a server. Now, we shift our focus to the other side of the communication link: enabling the ESP32 itself to function as a WebSocket server. This capability allows the ESP32 to accept incoming WebSocket connections from various clients (such as web browsers, mobile applications, or other microcontrollers) and engage in bidirectional, real-time communication with them.

Running a WebSocket server directly on an ESP32 is powerful for applications like:

  • Providing a direct real-time control interface for the ESP32 from a web browser.
  • Serving live sensor data directly to multiple connected clients.
  • Creating local network-based interactive applications without needing an intermediary cloud server.
  • Custom device-to-device communication protocols over Wi-Fi.

This chapter will guide you through using the ESP-IDF’s esp_http_server component to handle the initial HTTP upgrade request and then manage WebSocket connections for persistent, full-duplex communication. We will cover both unsecure (ws://) and secure (wss://) WebSocket server implementations.

Theory

Role of a WebSocket Server

A WebSocket server’s primary role is to listen for incoming HTTP requests that ask to “upgrade” to the WebSocket protocol. Once the handshake is successfully completed (as described in Chapter 96), the server maintains the underlying TCP connection for persistent, bidirectional communication with the connected client.

Key responsibilities of a WebSocket server include:

  1. Listening for Connections: The server listens on a specific network port for incoming TCP connections.
  2. Handling HTTP Upgrade Requests: It parses initial HTTP GET requests, checking for the Upgrade: websocket and other relevant headers.
  3. Performing the Handshake: If the server agrees to upgrade, it sends back an HTTP 101 Switching Protocols response, including the Sec-WebSocket-Accept header.
  4. Managing Client Connections: The server must keep track of all active WebSocket clients. This typically involves storing information about each client, such as their socket descriptor.
  5. Receiving and Processing Data Frames: The server receives WebSocket frames from clients, unmasks the payload (if masked, which is mandatory for client-to-server frames), and processes the data according to the frame’s opcode (text, binary, control).
  6. Sending Data Frames: The server can send WebSocket frames (text or binary) to specific clients or broadcast to multiple clients. Server-to-client frames are not masked.
  7. Handling Control Frames: The server should respond to Ping frames with Pong frames to keep connections alive and handle Close frames to terminate connections gracefully.
  8. Closing Connections: The server must properly close connections when requested by the client, when an error occurs, or when the server itself decides to terminate a session.
Responsibility Description ESP-IDF Context (esp_http_server)
Listening for Connections Monitors a specific network port for incoming TCP connections that might initiate a WebSocket handshake. Managed by esp_http_server when started via httpd_start() or httpd_ssl_start().
Handling HTTP Upgrade Requests Parses initial HTTP GET requests, checking for Upgrade: websocket, Connection: Upgrade, and other WebSocket-specific headers. Partially automated by esp_http_server when a URI handler has .is_websocket = true.
Performing Handshake If upgrade is accepted, sends an HTTP 101 Switching Protocols response with a calculated Sec-WebSocket-Accept header. esp_http_server can automatically generate the response.
Managing Client Connections Tracks active WebSocket clients, typically by their socket file descriptors and any associated session data. Application’s responsibility. Requires storing sockfd obtained via httpd_req_to_sockfd(). List/array management needed.
Receiving & Processing Data Frames Reads incoming WebSocket frames, unmasks client-to-server payloads, and interprets data based on opcode (text, binary, control). Use httpd_ws_recv_frame(). Unmasking is handled by the function.
Sending Data Frames Constructs and sends WebSocket frames (text, binary) to specific clients or broadcasts to multiple. Server-to-client frames are NOT masked. Use httpd_ws_send_frame() or httpd_ws_send_frame_async().
Handling Control Frames Responds to Ping frames with Pong frames. Processes Close frames to terminate connections. PING/PONG can be auto-handled if .handle_ws_control_frames = true. CLOSE frames trigger events/closure.
Closing Connections Properly shuts down WebSocket connections upon client request, error, or server decision. Releases associated resources. Managed via close_fn in httpd_uri_t, or by detecting errors and closing sockets. esp_http_server handles underlying socket closure.
%%{init: {"flowchart": {"htmlLabels": true}} }%%
graph TD
    subgraph ESP32_WebSocket_Server ["ESP32 WebSocket Server"]
        direction LR
        ServerCore["HTTP Server Core<br>(Listens on Port, e.g., 80/443)"]
        WSManager["WebSocket Session Manager<br>(Tracks active clients by sockfd)"]
        ServerCore -- "HTTP Upgrade Req for /ws" --> WSManager
        WSManager -- "Handshake OK" --> ServerCore
        ServerCore -- "101 Switching Protocols" --> Client1Id
        ServerCore -- "101 Switching Protocols" --> Client2Id
        ServerCore -- "101 Switching Protocols" --> ClientNId
        
        WSManager <--> Client1Sock["Client 1<br>(sockfd_A)"]
        WSManager <--> Client2Sock["Client 2<br>(sockfd_B)"]
        WSManager <--> ClientNSock["Client N<br>(sockfd_X)"]

        Client1Sock -- "WS Frame (Text/Binary)" --> WSManager
        WSManager -- "WS Frame (Text/Binary)" --> Client1Sock
        
        Client2Sock -- "WS Frame (Ping)" --> WSManager
        WSManager -- "WS Frame (Pong)" --> Client2Sock

        WSManager -- "Broadcast Frame" --> Client1Sock
        WSManager -- "Broadcast Frame" --> Client2Sock
        WSManager -- "Broadcast Frame" --> ClientNSock
    end

    Client1Id["Web Browser (Client 1)"]
    Client2Id["Mobile App (Client 2)"]
    ClientNId["Another ESP32 (Client N)"]

    Client1Id -- "1- HTTP GET /ws (Upgrade)" --> ServerCore
    Client2Id -- "1- HTTP GET /ws (Upgrade)" --> ServerCore
    ClientNId -- "1* HTTP GET /ws (Upgrade)" --> ServerCore
    
    Client1Id <--> Client1Sock
    Client2Id <--> Client2Sock
    ClientNId <--> ClientNSock

    classDef default fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef serverNode fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef clientNode fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46;
    classDef processNode fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;

    class ServerCore serverNode;
    class WSManager serverNode;
    class Client1Sock processNode;
    class Client2Sock processNode;
    class ClientNSock processNode;
    class Client1Id clientNode;
    class Client2Id clientNode;
    class ClientNId clientNode;

ESP-IDF esp_http_server for WebSocket Upgrades

The ESP-IDF provides the esp_http_server component, which is primarily an HTTP server but includes functionality to facilitate WebSocket communication. It does not manage the entire WebSocket session state out-of-the-box (like a dedicated WebSocket library might), but it handles the crucial HTTP upgrade handshake and provides tools to send and receive WebSocket frames over the established connection.

How it Works:

  1. HTTP Server Setup: You configure and start an esp_http_server instance.
  2. URI Handler for WebSocket: You register a URI handler for the path where WebSocket connections are expected (e.g., /ws).
  3. Upgrade Detection: Within this URI handler, when an HTTP GET request arrives, the esp_http_server component can identify it as a WebSocket upgrade request. The httpd_req_t structure passed to the handler contains information about this.
  4. Handshake: The esp_http_server can automatically handle the generation of the Sec-WebSocket-Accept header and send the 101 Switching Protocols response if you configure the URI handler appropriately for WebSockets.
  5. Connection Persists: After the handshake, the underlying TCP socket connection remains open. The esp_http_server provides the socket file descriptor to your application.
  6. WebSocket Frame Handling: Your application is then responsible for using this socket descriptor to send and receive WebSocket frames. The esp_http_server component provides helper functions for this:
    • httpd_ws_recv_frame(): To receive a WebSocket frame from a client.
    • httpd_ws_send_frame(): To send a WebSocket frame to a client.
%%{init: {"flowchart": {"htmlLabels": true}} }%%
graph TD
    A[Client sends HTTP GET Request<br>with Upgrade headers to /ws] --> B{"ESP-IDF HTTP Server<br>(esp_http_server)"};
    B --> C{URI Handler for /ws registered?};
    C -- No --> D[HTTP 404 Not Found];
    C -- Yes --> E{Is <br><tt>uri_config.is_websocket == true</tt>?};
    E -- No --> F["Treat as normal HTTP GET<br>(or 403 if method not allowed)"];
    E -- Yes --> G{"Validate WebSocket Headers?<br>(e.g., Sec-WebSocket-Key, Version)"};
    G -- Invalid --> H[HTTP 400 Bad Request];
    G -- Valid --> I[Server generates<br>Sec-WebSocket-Accept response header];
    I --> J[Server sends HTTP 101<br>Switching Protocols response];
    J --> K{Underlying TCP Socket<br>Connection Persists};
    K --> L{"If <tt>uri_config.open_fn</tt> defined,<br>call <tt>open_fn(hd, sockfd)</tt>"};
    L --> M[Connection Upgraded to WebSocket Protocol];
    M --> N["Application's <tt>uri_config.handler</tt><br>now receives/sends WebSocket frames<br>using <tt>httpd_ws_recv_frame()</tt> / <tt>httpd_ws_send_frame()</tt>"];
    N --> O{"Handle PING/PONG?<br>(if <tt>handle_ws_control_frames == false</tt>)"};
    O -- Yes (App handles) --> N;
    O -- No (Server handles) --> N;
    N -- Connection Closes/Error --> P{"If <tt>uri_config.close_fn</tt> defined,<br>call <tt>close_fn(hd, sockfd)</tt>"};
    P --> Q[Connection Terminated];

    classDef startEnd fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef decision fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
    classDef success fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46;
    classDef error fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B;

    class A,Q startEnd;
    class B,I,J,K,M,N process;
    class C,E,G,L,O,P decision;
    class D,F,H error;
    class J,M success;

Key esp_http_server Structures and Functions for WebSockets:

  • httpd_uri_t structure: When registering a URI handler, you can specify:
    • .is_websocket = true: Indicates that this URI can be upgraded to a WebSocket.
    • .ws_handler = your_websocket_handler_function: A pointer to a custom handler function that will be called after the WebSocket handshake is complete. This handler receives the httpd_req_t object, and its sess_ctx can be used to get the socket descriptor.
    • .handle_ws_control_frames = true/false: If true, the HTTP server will attempt to handle Ping frames automatically by sending Pongs. If false, your application must handle them.
Field Name in httpd_uri_t Type Relevance to WebSockets Description
uri const char* Primary The URI path for WebSocket connections (e.g., “/ws”, “/comms”).
method httpd_method_t Primary Must be HTTP_GET for WebSocket handshake requests.
handler esp_err_t (*)(httpd_req_t *r) Primary Pointer to the function that handles incoming WebSocket data frames after the handshake. Also handles the initial GET request for handshake if open_fn is not used for that.
user_ctx void* Optional User context pointer passed to the handler function.
is_websocket bool Crucial Set to true to indicate that this URI endpoint supports WebSocket upgrades. This enables the HTTP server to handle the handshake.
handle_ws_control_frames bool Important If true, the HTTP server will automatically respond to PING frames with PONG frames. If false, the application’s handler must process PING/PONG/CLOSE frames. Default is false.
supported_subprotocol const char* Optional A string specifying a subprotocol the server supports (e.g., “chat”, “json”). If the client requests this subprotocol, it will be acknowledged in the handshake.
open_fn esp_err_t (*)(httpd_handle_t hd, int sockfd) Recommended Callback function invoked when a new WebSocket connection is successfully opened (after handshake). Ideal for adding the client’s sockfd to a management list.
(Note: Signature might vary slightly by IDF version, check docs. The example uses esp_err_t (*on_open)(httpd_req_t *req) which is also common.) The provided text example has `on_ws_open(httpd_req_t *req)`.
close_fn void (*)(httpd_handle_t hd, int sockfd) Recommended Callback function invoked when a WebSocket connection is closed (either by client, server, or error). Ideal for removing the client’s sockfd from a management list and cleaning up resources.
(Note: Signature might vary. The provided text example has `on_ws_close(httpd_req_t *req)`.)
  • httpd_req_t structure (request object):
    • httpd_req_to_sockfd(req): This function retrieves the socket file descriptor associated with the request, which is crucial for sending/receiving WebSocket data outside the initial URI handler context (e.g., in a separate task managing the WebSocket session).
  • httpd_ws_frame_t structure:
    • type: The WebSocket frame type/opcode (e.g., HTTPD_WS_TYPE_TEXT, HTTPD_WS_TYPE_BINARY, HTTPD_WS_TYPE_PING, HTTPD_WS_TYPE_PONG, HTTPD_WS_TYPE_CLOSE).
    • payload: Pointer to the payload data.
    • len: Length of the payload.
    • final: Indicates if this is the final frame of a message (for fragmented messages).
  • httpd_ws_send_frame(httpd_req_t *req, httpd_ws_frame_t *ws_pkt): Sends a WebSocket frame to the client associated with the request req.
  • httpd_ws_send_frame_async(httpd_req_t *req, httpd_ws_frame_t *ws_pkt): Asynchronously sends a WebSocket frame.
  • httpd_ws_recv_frame(httpd_req_t *req, httpd_ws_frame_t *ws_pkt, TickType_t timeout): Receives a WebSocket frame from the client.
Item Type/Signature Description
httpd_ws_frame_t Structure Fields
final bool Indicates if this is the final frame of a message (for fragmented messages). true if final, false otherwise.
fragmented bool (Less commonly used directly by app, final and opcode imply fragmentation state) Indicates if the frame is part of a fragmented message.
type httpd_ws_type_t WebSocket frame type/opcode (e.g., HTTPD_WS_TYPE_TEXT, HTTPD_WS_TYPE_BINARY, HTTPD_WS_TYPE_PING, HTTPD_WS_TYPE_PONG, HTTPD_WS_TYPE_CLOSE, HTTPD_WS_TYPE_CONTINUE).
payload uint8_t* Pointer to the payload data. For sending, this buffer must be valid. For receiving, the library allocates this (or user provides buffer).
len size_t Length of the payload in bytes.
Key WebSocket Helper Functions
httpd_req_to_sockfd(req) int Retrieves the socket file descriptor (sockfd) associated with the HTTP request req. Essential for managing the connection outside the initial handler.
httpd_ws_recv_frame(req, ws_pkt, timeout) esp_err_t Receives a WebSocket frame from the client associated with req. Populates the ws_pkt structure. timeout is in FreeRTOS ticks. Can be called in two steps: first with ws_pkt.len=0 to get frame length, then with allocated buffer and actual length.
httpd_ws_send_frame(req, ws_pkt) esp_err_t Sends a WebSocket frame (ws_pkt) to the client associated with req. This is a synchronous send.
httpd_ws_send_frame_async(hd, sockfd, ws_pkt) esp_err_t Asynchronously sends a WebSocket frame (ws_pkt) to a client identified by sockfd using the server instance hd. Useful for sending from tasks other than the HTTP server’s task.
httpd_sess_trigger_send(server_handle, sockfd, data, len) esp_err_t Triggers an asynchronous send of raw data on a given session/socket. If sending WebSocket frames, the frame must be pre-constructed.
httpd_queue_work(hd, work_fn, arg) esp_err_t Schedules a function (work_fn) to be executed by the HTTP server’s internal task. Useful for performing operations that need server context, like sending frames using httpd_ws_send_frame with a stored req (if its context is still valid) or its sockfd.

Managing Client Connections

A server often needs to communicate with multiple WebSocket clients simultaneously. The esp_http_server itself can handle multiple concurrent HTTP connections, and thus multiple WebSocket handshakes. After the handshake, each WebSocket connection is represented by a unique socket file descriptor.

Strategy Description Pros Cons ESP32 Suitability
List/Array of Sockets Maintain a global or static array/list of active client socket file descriptors (sockfd). Add on connect, remove on disconnect. Access protected by a mutex. Simple to implement for basic scenarios. Low overhead per client if only sockfd is stored. Scalability limited by array size. Iterating for broadcasts can be slow with many clients. Requires careful synchronization (mutex). Good for small to moderate number of clients. Common in examples.
Dedicated Task per Client Spawn a new FreeRTOS task for each connected WebSocket client. Each task manages its own client’s lifecycle and communication. Isolation of client logic. Can simplify handling of individual client states. Potentially more responsive to individual client events if tasks are well-designed. Resource-intensive (RAM for task stack, TCB). Can quickly exhaust ESP32 resources with many clients. Task management overhead. Suitable for very few, long-lived, complex client interactions. Generally not recommended for many clients on ESP32.
Single Task with select() One dedicated FreeRTOS task monitors all active client sockets for incoming data using select() or a similar polling mechanism (e.g., poll(), epoll() – though select() is common). Processes events for ready sockets. Highly scalable for many connections. Efficient use of resources (single task). Centralized client management. More complex to implement. Logic for select() and managing file descriptor sets can be intricate. Can become a bottleneck if processing per client is heavy. Good for applications expecting a larger number of concurrent clients, offering better resource efficiency than task-per-client.
HTTP Server Task + Work Queuing Leverage the HTTP server’s internal task(s). Client state (sockfd) is stored. For actions like broadcasting or handling non-HTTP events, work is queued to the HTTP server’s task using httpd_queue_work(). Utilizes existing server infrastructure. httpd_ws_send_frame_async() simplifies sending from other tasks. Can overload the HTTP server task if too much custom work is queued. Requires careful design to avoid blocking server’s primary functions. A practical approach, especially when combined with a list of sockets for broadcasting via httpd_ws_send_frame_async().

Your application needs a strategy to manage these file descriptors and any associated client state. Common approaches include:

  • A list or array of active client sockets: When a new WebSocket connection is established (e.g., in the ws_handler or the initial URI handler after confirming upgrade), add its socket descriptor to a list.
  • A dedicated task per client: While possible, this can be resource-intensive on an ESP32 if many clients are expected.
  • A single task managing all clients: This task can use select() or a similar mechanism to monitor all active client sockets for incoming data. This is generally more scalable.

When a client disconnects or an error occurs, its socket descriptor must be closed and removed from the list of active clients.

Broadcasting Messages

A common requirement is to broadcast a message to all connected WebSocket clients. This involves iterating through your list of active client socket descriptors and sending the message to each one using httpd_ws_send_frame() or a similar socket write operation if you are manually framing.

%%{init: {"flowchart": {"htmlLabels": true}} }%%
graph TD
    subgraph Server_Side ["ESP32 Server Application"]
        A["Event triggers broadcast<br>(e.g., timer, sensor update, command)"] --> B{Initiate Broadcast};
        B --> C["Access List of Active Client<br>Socket Descriptors (<b>client_sockets[]</b>)"];
        C --> D{Loop through each <b>sockfd</b> in list};
        D -- Has Next Client --> E["Prepare WebSocket Frame<br>(<b>httpd_ws_frame_t pkt</b>)<br>pkt.payload = message_data<br>pkt.len = message_len<br>pkt.type = HTTPD_WS_TYPE_TEXT/BINARY"];
        E --> F["Send Frame to current <b>sockfd</b><br><b>httpd_ws_send_frame_async(server_hd, sockfd, &pkt)</b>"];
        F --> G{Send OK?};
        G -- Yes --> D;
        G -- No --> H["Error Handling for <b>sockfd</b><br>(e.g., log error, mark client for removal)"];
        H --> D;
        D -- No More Clients --> I[Broadcast Complete];
    end

    subgraph Connected_Clients ["Connected WebSocket Clients"]
        Client1["Client 1 (sockfd_A)"]
        Client2["Client 2 (sockfd_B)"]
        ClientN["Client N (sockfd_X)"]
    end

    F -.-> Client1;
    F -.-> Client2;
    F -.-> ClientN;
    
    style Server_Side fill:#FFF9EB,stroke:#D97706
    style Connected_Clients fill:#E0F2FE,stroke:#0284C7

    classDef startEnd fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef decision fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
    classDef io fill:#D1FAE5,stroke:#059669,stroke-width:1px,color:#065F46;
    classDef error fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B;

    class A,I startEnd;
    class B,C,E,F process;
    class D,G decision;
    class Client1,Client2,ClientN io;
    class H error;

Important: When using httpd_ws_send_frame() or httpd_ws_recv_frame() outside the original HTTP request handler function (e.g., from a different task), you cannot directly use the httpd_req_t *req pointer from the initial handshake. Instead, you need to:

  1. Obtain the socket file descriptor using httpd_req_to_sockfd(req) during the handshake/connection setup.
  2. Store this file descriptor.
  3. Use httpd_ws_send_frame_async_direct(httpd_handle_t hd, int sockfd, httpd_ws_frame_t *ws_pkt) or httpd_sess_send_frame_async_direct (check API for exact function in v5.x for sending directly via sockfd if httpd_ws_send_frame_async_direct is not present, or use httpd_sess_trigger_send) to send data using the server handle and the stored socket descriptor. Alternatively, for receiving, you might need to use standard socket recv() calls and then parse the WebSocket frames manually if httpd_ws_recv_frame cannot be used with just a sockfd.

The esp_http_server documentation for v5.x clarifies that for sending asynchronous messages (e.g., from another task), you should use httpd_queue_work() to schedule sending within the HTTP server’s context, or manage the socket directly.

For sending to a client whose httpd_req_t is no longer in scope, httpd_ws_send_frame_async is designed for this. You pass the original httpd_handle_t (server instance) and the client’s sockfd.

Let’s re-verify the API for sending to a specific client via sockfd:

The esp_http_server example for WebSockets (protocols/http_server/ws_echo_server) often shows how to get the sockfd and then uses httpd_ws_send_frame within the context of a handler where req is valid. For sending from another task, the approach is to get the sockfd from req using httpd_req_to_sockfd(req) and then use httpd_queue_work to schedule a function that calls httpd_ws_send_frame on the target sockfd.

A more direct way for asynchronous send is httpd_sess_trigger_send(httpd_handle_t server, int sockfd, const void *buf, size_t buf_len). This sends raw data. For WebSocket frames, you’d need to construct the frame yourself or use httpd_ws_send_frame within a work-queued function.

The ws_echo_server example uses a session context (httpd_sess_ctx_t) to store application-specific data, including the httpd_ws_frame_t for sending. It then uses httpd_sess_trigger_send or similar mechanisms.

The esp_http_server component has httpd_ws_get_fd_info(httpd_handle_t server, int sockfd) which can give you the type of socket.

The ws_handler is a good place to manage the lifecycle of the WebSocket connection after the HTTP server has handled the upgrade.

Secure WebSocket Server (WSS)

To implement a WSS server, you first need to set up an HTTPS server. The esp_http_server component supports this through the httpd_ssl_config_t structure.

%%{init: {"flowchart": {"htmlLabels": true}} }%%
graph TD
    A[Start: Configure WSS Server] --> B["Define Server Certificate & Private Key<br>(e.g., server_cert.pem, server_key.pem)"];
    B --> C["Populate <b>httpd_ssl_config_t</b><br>- <tt>servercert</tt>, <tt>servercert_len</tt><br>- <tt>prvtkey_pem</tt>, <tt>prvtkey_len</tt><br>- Base HTTPD config (ports, max sockets) via <tt>httpd</tt> member"];
    C --> D["Start Secure HTTP Server:<br><b>httpd_ssl_start(&server_handle, &ssl_config)</b>"];
    D --> E{Server Started Successfully?};
    E -- No --> F["Error: Secure Server Start Failed<br>(Check certs, port, memory)"];
    E -- Yes --> G["Register URI Handler for WebSocket Endpoint (e.g., /wss)<br>with <tt>is_websocket = true</tt>"];
    G --> H["WSS Server Ready & Listening on HTTPS Port (e.g., 443)"];

    H --> I["Client Initiates WSS Connection<br>(e.g., wss://esp32-ip/wss)"];
    I --> J["1- TCP Connection Established"];
    J --> K["2- TLS Handshake Occurs<br>(Client verifies server cert, secure channel established)"];
    K --> L["3- HTTP GET Request (for WS upgrade)<br>Sent Over Encrypted TLS Channel"];
    L --> M["4- ESP32 HTTP Server Processes Upgrade Request<br>(as per normal WebSocket upgrade flow, but encrypted)"];
    M --> N["5- HTTP 101 Switching Protocols Sent (Encrypted)"];
    N --> O["Secure WebSocket (WSS) Channel Active<br>All subsequent frames are encrypted/decrypted by TLS layer"];

    classDef startEnd fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef config fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
    classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef decision fill:#FFFBEB,stroke:#F59E0B,stroke-width:1px,color:#B45309; 
    classDef success fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46;
    classDef error fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B;
    classDef network fill:#E0E7FF,stroke:#4338CA,stroke-width:1px,color:#3730A3; 

    class A,O startEnd;
    class B,C config;
    class D,G,J,K,L,M,N process;
    class E decision;
    class F error;
    class H,O success;
    class I,J,K,L,M,N network;
  1. Configure HTTPS: Provide server certificate and private key (see Chapter 95).
  2. Start HTTPS Server: Start the esp_http_server with SSL configuration.
  3. Handle WSS Upgrade: Register a URI handler with .is_websocket = true as before. When a wss:// connection request arrives:
    • The TLS handshake occurs first, establishing a secure channel.
    • The HTTP GET request for WebSocket upgrade is then sent over this secure channel.
    • The esp_http_server handles the WebSocket handshake.
    • Subsequent WebSocket frames are automatically encrypted/decrypted by the underlying TLS layer.

The application logic for sending/receiving WebSocket frames remains largely the same as for an unsecure server, as TLS encryption is handled at a lower layer.

Practical Examples

Before running these examples, ensure your ESP32 is configured in Wi-Fi SoftAP mode or Station mode on a network where clients can reach it. For simplicity, these examples will often use SoftAP mode.

Example 1: Basic WebSocket Echo Server

This server will accept WebSocket connections on /ws and echo back any text message it receives.

1. Project Setup and CMakeLists.txt:

In your main component’s CMakeLists.txt:

Plaintext
idf_component_register(SRCS "main.c"
                    INCLUDE_DIRS "."
                    REQUIRES esp_wifi esp_event esp_log nvs_flash netif_stack esp_netif esp_http_server)

2. main.c Implementation:

C
#include <stdio.h>
#include <string.h>
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include "esp_http_server.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"

#define WIFI_SSID      "ESP32-WS-Server"
#define WIFI_PASS      "password" // At least 8 characters
#define MAX_STA_CONN       4

static const char *TAG = "WS_SERVER_EXAMPLE";

// Structure to manage client sessions (socket file descriptors)
#define MAX_WS_CLIENTS 5
static int client_sockets[MAX_WS_CLIENTS];
static uint8_t client_count = 0;
static SemaphoreHandle_t client_list_mutex;


// Function to add a client socket to the list
static void add_client_socket(int sockfd) {
    xSemaphoreTake(client_list_mutex, portMAX_DELAY);
    if (client_count < MAX_WS_CLIENTS) {
        client_sockets[client_count++] = sockfd;
        ESP_LOGI(TAG, "Client socket %d added. Total clients: %d", sockfd, client_count);
    } else {
        ESP_LOGW(TAG, "Max clients reached. Cannot add socket %d", sockfd);
        // Optionally close the new socket if list is full
        close(sockfd);
    }
    xSemaphoreGive(client_list_mutex);
}

// Function to remove a client socket from the list
static void remove_client_socket(int sockfd) {
    xSemaphoreTake(client_list_mutex, portMAX_DELAY);
    int i, j;
    for (i = 0; i < client_count; i++) {
        if (client_sockets[i] == sockfd) {
            // Shift elements to fill the gap
            for (j = i; j < client_count - 1; j++) {
                client_sockets[j] = client_sockets[j + 1];
            }
            client_count--;
            ESP_LOGI(TAG, "Client socket %d removed. Total clients: %d", sockfd, client_count);
            break; // Exit once found and removed
        }
    }
    xSemaphoreGive(client_list_mutex);
}


// WebSocket open handler (called after handshake)
// This is where you'd typically add the client to a list
static esp_err_t on_ws_open(httpd_req_t *req) {
    int sockfd = httpd_req_to_sockfd(req);
    if (sockfd < 0) {
        ESP_LOGE(TAG, "Failed to get socket descriptor for new WebSocket connection");
        return ESP_FAIL;
    }
    ESP_LOGI(TAG, "New WebSocket connection opened, sockfd: %d", sockfd);
    add_client_socket(sockfd);
    // You can set a user context for this session if needed
    // httpd_sess_set_ctx(req->handle, sockfd, your_custom_context, free_context_function);
    return ESP_OK;
}


// WebSocket close handler
// This is called when the HTTP server detects the socket is closed by client or error
// Or when httpd_sess_close is called by the server
static void on_ws_close(httpd_req_t *req) {
    int sockfd = httpd_req_to_sockfd(req);
    ESP_LOGI(TAG, "WebSocket connection closed, sockfd: %d", sockfd);
    remove_client_socket(sockfd);
    // No need to close(sockfd) here, http_server component handles it
    // after this handler returns, or if this handler is not registered.
}


/*
 * Structure holding server handle
 * and task handle of a task sending server side events
 */
struct async_resp_arg {
    httpd_handle_t hd;
    int fd;
};


// Handler for WebSocket messages
static esp_err_t ws_handler(httpd_req_t *req) {
    // First, check if this is an open or close event if using ws_handler directly
    // For this example, we use dedicated open/close handlers via httpd_uri_t config

    if (req->method == HTTP_GET) {
        // This part is for the HTTP GET request that initiates the WebSocket handshake
        ESP_LOGI(TAG, "Handshake done, new connection was opened");
        // The on_ws_open handler (if registered via .open_fn) would have been called
        // If not using .open_fn, you'd add client sockfd here.
        // For this example, .open_fn handles adding the client.
        return ESP_OK;
    }

    // If it's not a GET, it's a data frame (or the server is cleaning up a closed session)
    httpd_ws_frame_t ws_pkt;
    uint8_t *buf = NULL;
    memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t));
    ws_pkt.type = HTTPD_WS_TYPE_TEXT; // Expecting text

    // Set max_len = 0 to get the frame len
    esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 0);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "httpd_ws_recv_frame failed to get frame len with %d", ret);
        return ret;
    }
    ESP_LOGI(TAG, "frame len is %d", ws_pkt.len);

    if (ws_pkt.len) {
        buf = calloc(1, ws_pkt.len + 1); // +1 for null terminator
        if (buf == NULL) {
            ESP_LOGE(TAG, "Failed to calloc memory for WebSocket frame");
            return ESP_ERR_NO_MEM;
        }
        ws_pkt.payload = buf;
        // Set max_len = ws_pkt.len to get the frame payload
        ret = httpd_ws_recv_frame(req, &ws_pkt, ws_pkt.len);
        if (ret != ESP_OK) {
            ESP_LOGE(TAG, "httpd_ws_recv_frame failed with %d", ret);
            free(buf);
            return ret;
        }
        // Ensure null termination for text messages
        buf[ws_pkt.len] = 0;

        if (ws_pkt.type == HTTPD_WS_TYPE_TEXT) {
            ESP_LOGI(TAG, "Received Pkt from sockfd %d: type=%d, len=%d, data=%s",
                     httpd_req_to_sockfd(req), ws_pkt.type, ws_pkt.len, (char*)ws_pkt.payload);
            
            // Echo back the received message
            ret = httpd_ws_send_frame(req, &ws_pkt);
            if (ret != ESP_OK) {
                ESP_LOGE(TAG, "httpd_ws_send_frame failed with %d", ret);
            }
        } else if (ws_pkt.type == HTTPD_WS_TYPE_BINARY) {
             ESP_LOGI(TAG, "Received Binary Pkt from sockfd %d: len=%d", httpd_req_to_sockfd(req), ws_pkt.len);
             // Echo back binary
            ret = httpd_ws_send_frame(req, &ws_pkt);
            if (ret != ESP_OK) {
                ESP_LOGE(TAG, "httpd_ws_send_frame failed with %d", ret);
            }
        } else if (ws_pkt.type == HTTPD_WS_TYPE_CLOSE) {
            // Client sent a close frame
            ESP_LOGI(TAG, "Received Close Pkt from sockfd %d", httpd_req_to_sockfd(req));
            // The server will automatically send a close response and then
            // the on_ws_close handler will be called.
        } else {
            ESP_LOGI(TAG, "Received Pkt from sockfd %d: type=%d", httpd_req_to_sockfd(req), ws_pkt.type);
        }
        free(buf);
    }
    return ret;
}


// URI handler structure for WebSocket
static const httpd_uri_t uri_ws = {
    .uri        = "/ws",
    .method     = HTTP_GET, // WebSocket handshake is a GET request
    .handler    = ws_handler, // Handles data frames after handshake
    .user_ctx   = NULL,
    .is_websocket = true,
    .handle_ws_control_frames = true, // HTTP server handles PING/PONG
    .supported_subprotocol = "chat", // Optional: specify a subprotocol
    .open_fn    = on_ws_open,   // Called when new WS connection is opened
    .close_fn   = on_ws_close   // Called when WS connection is closed
};

// Function to start the WebSocket server
static httpd_handle_t start_webserver(void) {
    httpd_handle_t server = NULL;
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    config.max_open_sockets = MAX_WS_CLIENTS + 3; // Max WS clients + some buffer for HTTP
    config.lru_purge_enable = true; // Enable LRU purge of sockets

    ESP_LOGI(TAG, "Starting server on port: '%d'", config.server_port);
    if (httpd_start(&server, &config) == ESP_OK) {
        ESP_LOGI(TAG, "Registering URI handlers");
        httpd_register_uri_handler(server, &uri_ws);
        return server;
    }

    ESP_LOGI(TAG, "Error starting server!");
    return NULL;
}

// Function to stop the server
static void stop_webserver(httpd_handle_t server) {
    if (server) {
        httpd_stop(server);
    }
}

// Wi-Fi event handler
static void wifi_event_handler(void* arg, esp_event_base_t event_base,
                               int32_t event_id, void* event_data) {
    if (event_id == WIFI_EVENT_AP_STACONNECTED) {
        wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data;
        ESP_LOGI(TAG, "Station "MACSTR" joined, AID=%d", MAC2STR(event->mac), event->aid);
    } else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) {
        wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*) event_data;
        ESP_LOGI(TAG, "Station "MACSTR" left, AID=%d", MAC2STR(event->mac), event->aid);
    }
}

// Wi-Fi AP Initialization
void wifi_init_softap(void) {
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_t *p_netif = esp_netif_create_default_wifi_ap();
    assert(p_netif);

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
                                                        ESP_EVENT_ANY_ID,
                                                        &wifi_event_handler,
                                                        NULL,
                                                        NULL));

    wifi_config_t wifi_config = {
        .ap = {
            .ssid = WIFI_SSID,
            .ssid_len = strlen(WIFI_SSID),
            .password = WIFI_PASS,
            .max_connection = MAX_STA_CONN,
            .authmode = WIFI_AUTH_WPA_WPA2_PSK,
            .pmf_cfg = {
                .required = false,
            },
        },
    };
    if (strlen(WIFI_PASS) == 0) {
        wifi_config.ap.authmode = WIFI_AUTH_OPEN;
    }

    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
    ESP_ERROR_CHECK(esp_wifi_start());

    ESP_LOGI(TAG, "wifi_init_softap finished. SSID:%s password:%s", WIFI_SSID, WIFI_PASS);

    esp_netif_ip_info_t ip_info;
    esp_netif_get_ip_info(p_netif, &ip_info);
    ESP_LOGI(TAG, "AP IP Address: " IPSTR, IP2STR(&ip_info.ip));
}


void app_main(void) {
    // Initialize NVS
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
      ESP_ERROR_CHECK(nvs_flash_erase());
      ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    client_list_mutex = xSemaphoreCreateMutex();
    if (client_list_mutex == NULL) {
        ESP_LOGE(TAG, "Failed to create client list mutex");
        return;
    }

    ESP_LOGI(TAG, "ESP_WEBSOCKET_SERVER_EXAMPLE");

    wifi_init_softap(); // Initialize Wi-Fi in SoftAP mode
    start_webserver();  // Start the HTTP/WebSocket server
}

3. Build, Flash, and Observe:

  1. Build and flash the project to your ESP32.
  2. Connect your computer or mobile device to the Wi-Fi network “ESP32-WS-Server” with the password “password”.
  3. Use a WebSocket client tool (e.g., a browser extension like “Simple WebSocket Client”, or an online tool like https://www.piesocket.com/websocket-tester) to connect to ws://192.168.4.1/ws (192.168.4.1 is the default IP of ESP32 in AP mode).
  4. Send a text message. You should see it echoed back by the ESP32.
  5. Observe the ESP32’s serial monitor logs for connection and message details.

Tip: The open_fn and close_fn in httpd_uri_t are convenient for managing client lists right when connections open or close at the HTTP server level. The main handler (ws_handler in this case) is then primarily for data exchange on established connections.

Example 2: Server Broadcasting Messages

This example extends the previous one. The server will broadcast a message to all connected clients periodically.

1. Add a Broadcasting Task and Modify main.c:

C
// ... (Keep previous includes, defines, TAG, client_sockets, client_count, client_list_mutex, 
//      add_client_socket, remove_client_socket, on_ws_open, on_ws_close, ws_handler, uri_ws, 
//      stop_webserver, wifi_event_handler, wifi_init_softap)

// Global server handle for broadcasting task
static httpd_handle_t server_handle = NULL;

// Task to broadcast messages
static void broadcast_task(void *pvParameters) {
    char broadcast_msg[50];
    int counter = 0;

    while (1) {
        vTaskDelay(pdMS_TO_TICKS(5000)); // Broadcast every 5 seconds

        if (server_handle == NULL || client_count == 0) {
            //ESP_LOGI(TAG, "No server or no clients, skipping broadcast");
            continue;
        }

        sprintf(broadcast_msg, "Server broadcast: Hello #%d", counter++);
        ESP_LOGI(TAG, "Broadcasting to %d clients: %s", client_count, broadcast_msg);

        httpd_ws_frame_t ws_pkt;
        memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t));
        ws_pkt.payload = (uint8_t*)broadcast_msg;
        ws_pkt.len = strlen(broadcast_msg);
        ws_pkt.type = HTTPD_WS_TYPE_TEXT;
        // ws_pkt.final = true; // This is implicitly true for non-fragmented frames

        xSemaphoreTake(client_list_mutex, portMAX_DELAY);
        for (int i = 0; i < client_count; i++) {
            int client_sockfd = client_sockets[i];
            
            // httpd_ws_send_frame_async requires the server handle and client sockfd
            esp_err_t ret = httpd_ws_send_frame_async(server_handle, client_sockfd, &ws_pkt);
            if (ret != ESP_OK) {
                ESP_LOGE(TAG, "httpd_ws_send_frame_async failed for sockfd %d with error: %s", client_sockfd, esp_err_to_name(ret));
                // Consider removing client if send fails persistently
            } else {
                ESP_LOGI(TAG, "Sent to sockfd %d", client_sockfd);
            }
        }
        xSemaphoreGive(client_list_mutex);
    }
}

// Modify start_webserver to store the handle
static httpd_handle_t start_webserver(void) {
    //httpd_handle_t server = NULL; // server_handle is now global
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    config.max_open_sockets = MAX_WS_CLIENTS + 3;
    config.lru_purge_enable = true;

    ESP_LOGI(TAG, "Starting server on port: '%d'", config.server_port);
    if (httpd_start(&server_handle, &config) == ESP_OK) { // Use global server_handle
        ESP_LOGI(TAG, "Registering URI handlers");
        httpd_register_uri_handler(server_handle, &uri_ws);
        return server_handle;
    }

    ESP_LOGI(TAG, "Error starting server!");
    server_handle = NULL;
    return NULL;
}


void app_main(void) {
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
      ESP_ERROR_CHECK(nvs_flash_erase());
      ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    client_list_mutex = xSemaphoreCreateMutex();
    if (client_list_mutex == NULL) {
        ESP_LOGE(TAG, "Failed to create client list mutex");
        return;
    }
    memset(client_sockets, 0, sizeof(client_sockets)); // Initialize client sockets array

    ESP_LOGI(TAG, "ESP_WEBSOCKET_SERVER_BROADCAST_EXAMPLE");

    wifi_init_softap();
    if (start_webserver() != NULL) {
        xTaskCreate(broadcast_task, "broadcast_task", 4096, NULL, 5, NULL);
    }
}

2. Build, Flash, and Observe:

  • Connect one or more WebSocket clients to ws://192.168.4.1/ws.
  • Each client will receive the echo for messages they send.
  • Additionally, every 5 seconds, all connected clients should receive the “Server broadcast: Hello #X” message.
  • Check the ESP32’s serial monitor for logs.

Important for Broadcasting: The function httpd_ws_send_frame_async(httpd_handle_t hd, int sockfd, httpd_ws_frame_t *ws_pkt) is crucial here. It allows sending a WebSocket frame to a specific client (identified by sockfd) using the main server instance handle (hd). This is suitable for use outside the immediate request handler context, like in our broadcast_task.

Example 3: Secure WebSocket Server (WSS)

To create a WSS server, we need to configure the esp_http_server for HTTPS.

1. Generate Server Certificate and Private Key:

Refer to Chapter 95 or use OpenSSL tools to generate server_cert.pem and server_key.pem. For example:

Bash
openssl req -newkey rsa:2048 -nodes -keyout prvtkey.pem -x509 -days 365 -out servercert.pem -subj "/CN=esp32-wss-server"

Convert these PEM files into C string arrays using a utility or by manually formatting them, similar to how client certificates were handled in Chapter 96. Place these in your main component, e.g., server_cert.h.

server_cert.h (Example Structure):

C
#ifndef MAIN_SERVER_CERT_H_
#define MAIN_SERVER_CERT_H_

// -----BEGIN CERTIFICATE-----
// ... (Your server certificate PEM data) ...
// -----END CERTIFICATE-----
const unsigned char server_cert_pem_start[] = "-----BEGIN CERTIFICATE-----\n"
    // ... content of your servercert.pem ...
    "-----END CERTIFICATE-----\n";
const unsigned int server_cert_pem_len = sizeof(server_cert_pem_start);

// -----BEGIN PRIVATE KEY-----
// ... (Your private key PEM data) ...
// -----END PRIVATE KEY-----
const unsigned char server_key_pem_start[] = "-----BEGIN PRIVATE KEY-----\n"
    // ... content of your prvtkey.pem ...
    "-----END PRIVATE KEY-----\n";
const unsigned int server_key_pem_len = sizeof(server_key_pem_start);

#endif /* MAIN_SERVER_CERT_H_ */

Include this header in your main.c.

2. Modify start_webserver() for HTTPS:

C
// ... (include server_cert.h)
#include "server_cert.h" // Assuming you created this file

// Modify start_webserver for WSS
static httpd_handle_t start_secure_webserver(void) {
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    // For WSS, the default port is 443. HTTPD_DEFAULT_CONFIG() sets port 80.
    // If you want to use a different port for HTTPS/WSS, set config.server_port
    // For WSS, the URI scheme will be wss://<ip>:<port>/ws
    // If using standard port 443, then wss://<ip>/ws
    config.server_port = 443; // Standard HTTPS/WSS port
    config.ctrl_port = 32769; // Different from server_port

    config.max_open_sockets = MAX_WS_CLIENTS + 3;
    config.lru_purge_enable = true;

    // SSL configuration
    httpd_ssl_config_t ssl_config = HTTPD_SSL_CONFIG_DEFAULT();
    ssl_config.servercert = server_cert_pem_start;
    ssl_config.servercert_len = server_cert_pem_len;
    ssl_config.prvtkey_pem = server_key_pem_start;
    ssl_config.prvtkey_len = server_key_pem_len;
    // For production, you might want to set httpd_ssl_config.transport_mode and .port_secure

    ESP_LOGI(TAG, "Starting WSS server on port: '%d'", config.server_port);
    // The httpd_ssl_start function is deprecated. Use httpd_start with ssl_config.
    // esp_err_t ret = httpd_ssl_start(&server_handle, &config, &ssl_config);
    // The modern way is to pass ssl_config to httpd_start via config.global_transport_ctx
    // and config.global_transport_ctx_free_fn.
    // However, for basic SSL, httpd_config_t has direct members for this in later IDF versions.
    // Let's check ESP-IDF v5.x httpd_config_t:
    // It has .server_port and .ctrl_port.
    // For SSL, we pass the ssl_config to httpd_start.
    // No, httpd_ssl_config is separate. httpd_start takes httpd_config_t.
    // The function to start an SSL server is httpd_ssl_start in older IDFs.
    // In ESP-IDF v5.x, you use httpd_start and provide SSL config through a different mechanism
    // if httpd_ssl_start is fully removed.
    //
    // Re-checking ESP-IDF v5.x docs for http_server with SSL:
    // You create an httpd_config_t.
    // Then, to start an HTTPS server, you call httpd_ssl_start(&server_handle, &config).
    // This function is indeed available in v5.0, v5.1, v5.2.
    // So, the old way is still the way for esp_http_server with SSL.
    
    // Correction: The `httpd_ssl_config_t` is now part of `httpd_config_t` as `global_user_ctx`
    // and you set a specific session_creation_cb or use `HTTPD_SSL_CONFIG_DEFAULT()` which populates these.
    // The `httpd_ssl_start` and `httpd_ssl_stop` are indeed the functions to use.

    // Let's use the recommended way from ESP-IDF examples for v5.x if they've changed.
    // The `esp_http_server` component's `CMakeLists.txt` might conditionally compile SSL support.
    // Ensure `CONFIG_ESP_TLS_SERVER` is enabled in menuconfig if not by default.

    // The httpd_ssl_config_t structure is used with httpd_ssl_start()
    // This is the correct approach for ESP-IDF v5.x based on current API docs.
    
    esp_err_t ret = httpd_ssl_start(&server_handle, &ssl_config); // Pass the httpd_config_t via ssl_config.base_cfg
    // No, the httpd_ssl_config_t has a base_path member, not base_cfg.
    // The httpd_ssl_config_t IS the config for httpd_ssl_start.
    // Let's assume httpd_config_t is implicitly used for port etc.
    // The `httpd_ssl_config_t` contains the `httpd_config_t` as `httpd_config_t base;`
    // So we need to populate that.

    httpd_ssl_config_t https_conf = HTTPD_SSL_CONFIG_DEFAULT(); // This macro populates base config too.
    https_conf.httpd.max_open_sockets = MAX_WS_CLIENTS + 3;
    https_conf.httpd.lru_purge_enable = true;
    https_conf.httpd.server_port = 443; // Default for HTTPD_SSL_CONFIG_DEFAULT is 443
    https_conf.httpd.ctrl_port = 32769; // Ensure this is different

    https_conf.servercert = server_cert_pem_start;
    https_conf.servercert_len = server_cert_pem_len;
    https_conf.prvtkey_pem = server_key_pem_start;
    https_conf.prvtkey_len = server_key_pem_len;

    ESP_LOGI(TAG, "Starting WSS server on port: '%d'", https_conf.httpd.server_port);
    if (httpd_ssl_start(&server_handle, &https_conf) == ESP_OK) {
        ESP_LOGI(TAG, "Registering URI handlers for WSS");
        httpd_register_uri_handler(server_handle, &uri_ws); // Same URI handler works
        return server_handle;
    }

    ESP_LOGE(TAG, "Error starting secure server!");
    server_handle = NULL;
    return NULL;
}

// In app_main, call start_secure_webserver()
void app_main(void) {
    // ... (NVS init, mutex init, logging) ...
    wifi_init_softap();
    // server_handle = start_webserver(); // For WS
    server_handle = start_secure_webserver(); // For WSS

    if (server_handle != NULL) {
        xTaskCreate(broadcast_task, "broadcast_task", 4096, NULL, 5, NULL);
    }
}

3. Build, Flash, and Observe:

  • Ensure server_cert.h is correctly populated and included.
  • Build and flash.
  • Connect clients to wss://192.168.4.1/ws (or wss://192.168.4.1:443/ws).
  • Your browser/client will likely warn about a self-signed certificate. You’ll need to accept the security exception to proceed.
  • Communication should now be encrypted.

Warning: For production WSS, use certificates signed by a trusted Certificate Authority (CA) to avoid security warnings on the client-side. Self-signed certificates are for testing and development.

Variant Notes

  • ESP32, ESP32-S2, ESP32-S3: These variants have sufficient resources and hardware crypto acceleration (for WSS) to run WebSocket servers effectively. Dual-core variants (ESP32, ESP32-S3) can dedicate a core to networking tasks if needed.
  • ESP32-C3, ESP32-C6: These single-core RISC-V MCUs can also run WebSocket servers. However, resource management (RAM, CPU for handling multiple clients, especially with WSS) is more critical. Hardware crypto acceleration helps with WSS performance. The number of concurrent clients might be more limited compared to dual-core variants if the application is also CPU-intensive.
  • ESP32-H2: As this variant lacks built-in Wi-Fi or Ethernet, running a standard IP-based WebSocket server directly is not its primary use case. Similar to the client scenario, it would typically require a gateway device that exposes a WebSocket server and bridges communication to the ESP32-H2 over 802.15.4 protocols (Thread, Zigbee) or Bluetooth LE.

General Considerations:

  • Maximum Clients: The max_open_sockets in httpd_config_t and available RAM will limit the number of concurrent WebSocket clients. Each connection consumes resources.
  • Memory for WSS: WSS requires significantly more RAM than WS due to TLS contexts for each connection. Monitor heap usage closely.
  • Task Priorities: Ensure the HTTP server task and any custom WebSocket handling tasks have appropriate FreeRTOS priorities.

Common Mistakes & Troubleshooting Tips

Mistake / Issue Symptom(s) Troubleshooting / Solution
Incorrect URI Registration or Client URL Client gets HTTP 404 Not Found when trying to connect. WebSocket handshake fails. Mistake: URI path in client’s WebSocket URL (e.g., ws://esp32-ip/socket) doesn’t match the URI registered with httpd_register_uri_handler() (e.g., handler registered for /ws).
Fix: Ensure client URL path and server URI registration path are identical. Verify .is_websocket = true and .method = HTTP_GET in httpd_uri_t.
Firewall / Network Configuration (ESP32 in STA mode) Clients cannot connect to ESP32’s IP address and port. Connection timeout. Mistake: If ESP32 is a station on a Wi-Fi network, router firewall or other network policies might block incoming connections to the ESP32.
Fix: Configure port forwarding on the router if necessary. Ensure no local firewalls are blocking. For testing, SoftAP mode on ESP32 is often simpler as clients connect directly.
WSS Certificate/Key Errors For WSS server: httpd_ssl_start() fails. Clients fail TLS handshake (security warnings, connection refused). Mistake: Server certificate/private key PEM data is incorrect, malformed, doesn’t match, or is expired. Incorrect paths or lengths provided to httpd_ssl_config_t.
Fix:
  • Verify PEM data integrity (including BEGIN/END markers and newlines).
  • Ensure certificate and private key match. Use OpenSSL to verify.
  • Check certificate validity period.
  • Use correct lengths for servercert_len and prvtkey_len.
  • Enable ESP-TLS logs for detailed error messages.
Exceeding Max Open Sockets / Max Clients New clients fail to connect when server is under load. httpd_accept_conn errors. Mistake: httpd_config_t.max_open_sockets is too low. Custom client list (e.g., MAX_WS_CLIENTS) is full.
Fix: Increase max_open_sockets (consider RAM). Ensure your client management logic handles limits gracefully (e.g., refuse new connections or close oldest).
Blocking Operations in Handlers Server becomes unresponsive. Other clients experience high latency or disconnects. Watchdog timeouts. Mistake: Performing long delays, heavy computations, or blocking I/O directly within httpd_uri_t handlers (.handler, .open_fn, .close_fn).
Fix: Offload lengthy operations to separate FreeRTOS tasks. Handlers should be brief. Use queues or event groups for inter-task communication.
Client List Concurrency Issues Crashes (Guru Meditation). Corrupted client list leading to failed broadcasts or messages to wrong clients. Mistake: Accessing shared client list/data from multiple contexts (HTTP server task, broadcast task, etc.) without proper mutex protection.
Fix: Use a FreeRTOS mutex (xSemaphoreCreateMutex()) to guard all reads and writes to the shared client management structures.
Improper Client Disconnection Handling “Ghost” clients in list. Resource leaks (memory, sockets). Attempts to send to closed sockets. Mistake: Not removing client’s sockfd from list when httpd_ws_recv_frame or httpd_ws_send_frame returns error, or when close_fn is called.
Fix: Robustly check return codes of send/receive functions. Always clean up client entry (remove from list, free associated memory) in close_fn or upon detecting an unrecoverable error with a client socket. Close the sockfd if not handled by httpd.
Forgetting .is_websocket = true Server responds with HTTP 200 OK to WebSocket handshake instead of 101 Switching Protocols. Client connection fails. Mistake: The .is_websocket field in the httpd_uri_t for the WebSocket endpoint is false or not set.
Fix: Ensure .is_websocket = true; for the URI handler intended for WebSocket connections.

Exercises

  1. Client Counter Display:
    • Modify the WebSocket server to broadcast the current number of connected clients to all clients every time a new client connects or an existing client disconnects.
    • Hint: Trigger a broadcast from on_ws_open and on_ws_close (or the task that manages these events).
  2. Targeted Messaging:
    • Assign a unique ID to each client upon connection (e.g., their socket descriptor or a simple counter).
    • Implement a mechanism where a client can send a message like {"to": <target_id>, "message": "Hello specific client!"}.
    • The server should parse this JSON, find the target client by its ID, and forward the “message” part only to that specific client. If the target ID is not found, send an error back to the sender.
  3. Resource Monitoring via WebSocket:
    • Create a WebSocket server that, upon client request (e.g., client sends “stats”), replies with the ESP32’s current free heap size (esp_get_free_heap_size()) and CPU usage (more advanced, might involve vTaskGetRunTimeStats).
    • The client can then display these real-time stats.
  4. Simple Web Page Controller:
    • Create a simple HTML page (served by the ESP32’s HTTP server on a different URI, or hosted externally) with a button.
    • When the button is clicked, the JavaScript on the page connects to your ESP32 WebSocket server and sends a specific command (e.g., “TOGGLE_LED”).
    • The ESP32 WebSocket server receives this command and toggles an onboard LED. It can also send back a status message (e.g., “LED is ON”).

Summary

  • An ESP32 can act as a WebSocket server using the esp_http_server component, which handles the initial HTTP upgrade handshake.
  • Key server responsibilities include listening, handshake, client management, and data frame exchange.
  • The httpd_uri_t structure is configured with .is_websocket = true and handlers like .handler, .open_fn, and .close_fn to manage WebSocket interactions.
  • httpd_ws_recv_frame() and httpd_ws_send_frame() (or httpd_ws_send_frame_async) are used for data communication over established WebSocket connections.
  • Managing a list of active client socket descriptors is crucial for interacting with multiple clients and for broadcasting messages.
  • A Secure WebSocket Server (WSS) is implemented by starting an HTTPS server (using httpd_ssl_start with httpd_ssl_config_t) and then handling WebSocket upgrades over the secure channel.
  • Proper resource management (sockets, memory, task synchronization) is essential, especially with multiple clients or WSS.
  • Most ESP32 variants can host WebSocket servers, with performance and client capacity depending on resources and whether WSS is used. ESP32-H2 requires a gateway.

Further Reading

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top