Los WebSockets permiten comunicación bidireccional entre una página web y un ESP32 con una conexión persistente.
El ejemplo está orientado a ESP32. En muchos casos también puede adaptarse a ESP8266 cambiando librerías y algunos detalles de pines.
Cuando servimos una página web desde el ESP32, necesitamos decidir cómo se comunicará el frontend con el backend. Los formularios web son una forma sencilla, pero resultan limitados porque obligan al cliente a refrescar la página web al enviar información.
Las peticiones Ajax permiten tramitar una solicitud al servidor sin recargar la página. Pero tienen la desventaja de que el servidor no tiene un mecanismo directo para llamar al cliente y, además, son relativamente lentas porque tienen que generar una conexión en cada petición.
Aquí vamos a ver los WebSockets, otro mecanismo de comunicación entre el cliente y el servidor, que permite una comunicación bidireccional y con bajo lag.
Los Websockets son una “versión moderna” de los sockets tradicionales que funcionan sobre HTTP, y están diseñados para funcionar en aplicaciones web. En un websocket se crea una conexión entre el cliente y el servidor que se mantiene abierta. Por este motivo tienen un lag mucho menos, y la comunicación puede realizarse en ambas direcciones.
Sin embargo, hay que descartar que los Websockets no son la solución definitiva o que sustituya a Ajax, sino que son tecnologías compatibles. Incluso es posible aplicaciones híbridas, que emplean ambas soluciones simultáneamente.
Los Websockets son adecuados para aplicaciones que requieran una alta velocidad de refresco (captura de datos en tiempo real, control de sistemas de luces, controlar un motor, etc) y donde sea necesaria una comunicación rápida desde el servidor al cliente.
Sin embargo, mantener la conexión abierta supone un consumo de recursos, lo que se traduce en una menor cantidad de clientes que podemos atender frente a una solución basada en un API servido a través de Ajax.
Ejemplos de código
Vamos a hacer un ejemplo mínimo con WebSockets en ESP32, simplemente actualizando un número en la página web con el valor de millis() obtenido desde el servidor. Es suficiente para ilustrar la comunicación sin despistar con elementos adicionales.
De hecho, vamos a ver dos ejemplos sobre el mismo código (el primero de ellos está comentado). En ambos casos el cliente va a realizar una conexión de websocket, y la diferencia es:
- Ejemplo 1: El cliente enviará datos periódicamente, y recibe ‘millis()’ como respuesta
- Ejemplo 2: El servidor usa un broadcast para informar a los clientes del valor de ‘millis()’
El ejemplo 1 está comentado en el código. Tal cuál está, el código ejecuta el ejemplo 2, que emplea broadcast.
Vamos a ver cómo queda nuestro código.
En nuestro programa principal, hemos añadido las dependencias adicionales a la librería ‘WebSocketsServer.h’ y referencias a ‘WebSockets.hpp’ referencia a ‘ESP32_Utils_WS.hpp’ que va a contener código repetitivo relativo al uso de Websockets.
Por otro lado, vemos que en el loop llamamos a ‘websocket.loop()’, que gestiona el proceso de Websockets. Y también tenemos parte del código del ejemplo 2, donde el servidor usa la función ‘broadcastTXT(…);’ para informar a todos los clientes del valor de ‘millis()’.
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <FS.h>
#include <WebSocketsServer.h>
#include "config.h" // Sustituir con datos de vuestra red
#include "WebSockets.hpp"
#include "Server.hpp"
#include "ESP32_Utils.hpp"
#include "ESP32_Utils_WS.hpp"
void setup(void)
{
Serial.begin(115200);
SPIFFS.begin();
ConnectWiFi_STA();
InitWebSockets();
InitServer();
}
void loop(void)
{
webSocket.loop();
// Ejemplo 2, llamada desde servidor
String message = GetMillis();
webSocket.broadcastTXT(message);
}
Nuestro fichero ‘server.hpp’ queda muy simplificado. No tenemos endpoints, y simplemente servimos el contenido desde el SPIFFS.
AsyncWebServer server(80);
void InitServer()
{
server.serveStatic("/", SPIFFS, "/").setDefaultFile("index.html");
server.onNotFound([](AsyncWebServerRequest *request) {
request->send(400, "text/plain", "Not found");
});
server.begin();
Serial.println("HTTP server started");
}
Respecto al fichero ‘ESP32_Utils_WS.hpp’, contiene código que podemos reutilizar para su uso con Websockets.
En este fichero, en primer lugar definimos un nuevo Websocket asociado al puerto 81. Al final, tenemos la función ‘InitWebSockets()’, que inicia el Websocket y asocia la función de callback ‘webSocketEvent(…)’ a los eventos de Websockets.
En la función de callback, discriminamos el tipo de evento recibido. En caso de que sea un texto recibido, generamos una respuesta con ‘ProcessRequest()’ y la enviamos al cliente.
WebSocketsServer webSocket = WebSocketsServer(81);
void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t lenght)
{
switch(type) {
case WStype_DISCONNECTED:
break;
case WStype_CONNECTED:
//IPAddress ip = webSocket.remoteIP(num);
//webSocket.sendTXT(num, "Connected");
break;
case WStype_TEXT:
String response = ProcessRequest();
webSocket.sendTXT(num, response);
break;
}
}
void InitWebSockets() {
webSocket.begin();
webSocket.onEvent(webSocketEvent);
Serial.println("WebSocket server started");
}
Finalmente, tenemos el archivo ‘WebSockets.hpp’, que contiene la definición de nuestro ‘API’ websocket. Aquí definimos la función ‘ProcessRequest()’, que hemos empleado en el fichero anterior. En este ejemplo, simplemente devolvemos el valor de ‘millis()’ codificado como string.
String GetMillis()
{
return String(millis(), DEC);
}
String ProcessRequest()
{
return GetMillis();
}
Respecto al frontend tenemos
El fichero ‘index.html’ es exactamente igual al que vimos en el ejemplo Ajax.
<!DOCTYPE html>
<html class="no-js" lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>ESP8266 WebSockets</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>Millis</h1>
<div id="counterDiv">---</div>
</body>
<script type="text/javascript" src="./js/main.js"></script>
</html>
Lo que cambia es el fichero ‘main.js’. En este código en Javascript, vemos que tenemos la función ‘updateCounterUI(…)’ que ya usamos en el ejemplo anterior, y que simplemente actualiza el div counter con el valor oportuno.
Por otro lado, creamos un Websocket a la dirección del servidor en el puerto 81. A continuación asociamos los distintos eventos a sus funciones de callback. En particular, el evento ‘onmessage()’ lanza la función ‘updateCounterUI(…)’ con el valor recibido.
var counterDiv = document.getElementById('counterDiv');
function updateCounterUI(counter)
{
counterDiv.innerHTML = counter;
}
var connection = new WebSocket('ws://' + location.hostname + ':81/', ['arduino']);
connection.onopen = function () {
console.log('Connected: ');
// Ejemplo 1, peticion desde cliente
//(function scheduleRequest() {
// connection.send("");
// setTimeout(scheduleRequest, 100);
//})();
};
connection.onerror = function (error) {
console.log('WebSocket Error ', error);
};
connection.onmessage = function (e) {
updateCounterUI(e.data);
console.log('Server: ', e.data);
};
connection.onclose = function () {
console.log('WebSocket connection closed');
};
En el ejemplo 2, en el que el servidor hace un broadcast a todos los clientes, no es necesario más código que este.
En el ejemplo 1, en el que el cliente realiza periódicamente peticiones al servidor, tenemos el código comentado en el evento ‘onopen’. Aquí, definimos un temporizador cada 100ms, para enviar un texto (en este caso, un texto vacío), al servidor y “provocar” que nos envíe la respuesta.
Resultado
Subimos todo al ESP32 y accedemos a la página web, para ver que el valor de millis() se actualiza correctamente. Igual que en el caso de Ajax pero muchísimo más rápido.

Con esto ya tenemos otra forma de comunicar el frontend con el ESP32 actuando como backend. Frente a Ajax, los WebSockets son especialmente útiles cuando necesitamos refresco rápido o mensajes iniciados desde el servidor.
Descarga el código
Todo el código de esta entrada está disponible para su descarga en Github.
Versión para el ESP8266: https://github.com/luisllamasbinaburo/ESP8266-Examples
Versión para el ESP32: https://github.com/luisllamasbinaburo/ESP32-Examples

