ESP32 WebSocket Server
В этом уроке вы узнаете, как создать веб-сервер с ESP32, используя протокол связи WebSocket. В качестве примера мы покажем вам, как создать веб-страницу для удаленного управления выводами ESP32. Состояние вывода отображается на веб-странице и автоматически обновляется на всех клиентах.
ESP32 будет запрограммирован с использованием Arduino IDE и библиотеки ESPAsyncWebServer. У нас также есть похожее руководство по WebSocket для ESP8266.
Если вы следили за некоторыми из наших предыдущих проектов веб-серверов, таких как этот, вы могли заметить, что если у вас открыто несколько вкладок (на том же или на разных устройствах) одновременно, состояние не обновляется во всех вкладках автоматически, если не обновить веб-страницу. Чтобы решить эту проблему, мы можем использовать протокол WebSocket – все клиенты могут быть уведомлены, когда происходит изменение, и обновить веб-страницу соответствующим образом.
Содержание
Введение в WebSocket
WebSocket – это постоянное соединение между клиентом и сервером, которое позволяет двустороннюю связь между обеими сторонами с использованием TCP-соединения. Это означает, что вы можете отправлять данные от клиента на сервер и от сервера на клиента в любое время.

Клиент устанавливает соединение WebSocket с сервером через процесс, известный как WebSocket handshake (рукопожатие WebSocket). Рукопожатие начинается с HTTP-запроса/ответа, позволяя серверам обрабатывать HTTP-соединения, а также соединения WebSocket на одном и том же порту. Как только соединение установлено, клиент и сервер могут отправлять данные WebSocket в режиме полного дуплекса.
Используя протокол WebSocket, сервер (плата ESP32) может отправлять информацию клиенту или всем клиентам без запроса. Это также позволяет нам отправлять информацию в веб-браузер, когда происходит изменение.
Это изменение может быть вызвано чем-то, что произошло на веб-странице (вы нажали кнопку) или чем-то, что произошло на стороне ESP32, например, нажатие физической кнопки на схеме.
Обзор проекта
Вот веб-страница, которую мы создадим в этом проекте:

Веб-сервер ESP32 отображает веб-страницу с кнопкой для переключения состояния GPIO 2.
Для простоты мы управляем GPIO 2 — встроенным светодиодом. Вы можете использовать этот пример для управления любым другим GPIO.
Интерфейс показывает текущее состояние GPIO. Когда состояние GPIO изменяется, интерфейс мгновенно обновляется.
Состояние GPIO автоматически обновляется на всех клиентах. Это означает, что если у вас открыто несколько вкладок браузера на одном или разных устройствах, они все обновляются одновременно.
Как это работает?
На изображении ниже показано, что происходит, когда вы нажимаете кнопку «Toggle» (Переключить):

Нажмите кнопку «Toggle».
Клиент (ваш браузер) отправляет данные по протоколу WebSocket с сообщением «toggle».
ESP32 (сервер) получает это сообщение и знает, что нужно переключить состояние светодиода. Если светодиод был выключен, включите его.
Затем он отправляет данные с новым состоянием светодиода всем клиентам также по протоколу WebSocket.
Клиенты получают сообщение и обновляют состояние светодиода на веб-странице соответственно. Это позволяет мгновенно обновлять всех клиентов при изменении состояния.
Подготовка Arduino IDE
Мы будем программировать плату ESP32 с использованием Arduino IDE, поэтому убедитесь, что у вас установлена последняя версия Arduino IDE.
Установка библиотек – Async Web Server
Для создания веб-сервера мы будем использовать библиотеку ESPAsyncWebServer. Эта библиотека требует библиотеку AsyncTCP для корректной работы. Скачайте библиотеки по следующим ссылкам:
Эти библиотеки недоступны для установки через Arduino Library Manager, поэтому вам нужно скопировать файлы библиотек в папку Libraries установки Arduino. В качестве альтернативы, в Arduino IDE можно перейти в Sketch > Include Library > Add .zip Library и выбрать библиотеки, которые вы только что скачали.
Код для ESP32 WebSocket-сервера
Скопируйте следующий код в вашу Arduino IDE.
// Импорт необходимых библиотек
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
// Замените на ваши сетевые данные
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
bool ledState = 0;
const int ledPin = 2;
// Создайте объект AsyncWebServer на порту 80
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");
const char index_html[] PROGMEM = R"rawliteral(<!DOCTYPE HTML><html>
<head>
<title>ESP Web Server</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
<style>
html { font-family: Arial, Helvetica, sans-serif; text-align: center; }
h1 { font-size: 1.8rem; color: white; }
h2 { font-size: 1.5rem; font-weight: bold; color: #143642; }
.topnav { overflow: hidden; background-color: #143642; }
body { margin: 0; }
.content { padding: 30px; max-width: 600px; margin: 0 auto; }
.card {
background-color: #F8F7F9;
box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);
padding-top:10px; padding-bottom:20px;
}
.button {
padding: 15px 50px; font-size: 24px; text-align: center;
outline: none; color: #fff; background-color: #0f8b8d;
border: none; border-radius: 5px; user-select: none;
}
.button:active {
background-color: #0f8b8d;
box-shadow: 2 2px #CDCDCD;
transform: translateY(2px);
}
.state { font-size: 1.5rem; color:#8c8c8c; font-weight: bold; }
</style>
</head>
<body>
<div class="topnav">
<h1>ESP WebSocket Server</h1>
</div>
<div class="content">
<div class="card">
<h2>Output - GPIO 2</h2>
<p class="state">state: <span id="state">%STATE%</span></p>
<p><button id="button" class="button">Toggle</button></p>
</div>
</div>
<script>
var gateway = `ws://${window.location.hostname}/ws`;
var websocket;
window.addEventListener('load', onLoad);
function initWebSocket() {
console.log('Trying to open a WebSocket connection...');
websocket = new WebSocket(gateway);
websocket.onopen = onOpen;
websocket.onclose = onClose;
websocket.onmessage = onMessage;
}
function onOpen(event) {
console.log('Connection opened');
websocket.send('hi');
}
function onClose(event) {
console.log('Connection closed');
setTimeout(initWebSocket, 2000);
}
function onMessage(event) {
var state;
if (event.data == "1"){
state = "ON";
}
else{
state = "OFF";
}
document.getElementById('state').innerHTML = state;
}
function onLoad(event) {
initWebSocket();
initButton();
}
function initButton() {
document.getElementById('button').addEventListener('click', toggle);
}
function toggle(){
websocket.send('toggle');
}
</script>
</body>
</html>)rawliteral";
void notifyClients() {
ws.textAll(String(ledState));
}
void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) {
AwsFrameInfo *info = (AwsFrameInfo*)arg;
if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
data[len] = 0;
if (strcmp((char*)data, "toggle") == 0) {
ledState = !ledState;
notifyClients();
}
}
}
void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type,
void *arg, uint8_t *data, size_t len) {
switch (type) {
case WS_EVT_CONNECT:
Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
break;
case WS_EVT_DISCONNECT:
Serial.printf("WebSocket client #%u disconnected\n", client->id());
break;
case WS_EVT_DATA:
handleWebSocketMessage(arg, data, len);
break;
case WS_EVT_PONG:
case WS_EVT_ERROR:
break;
}
}
void initWebSocket() {
ws.onEvent(onEvent);
server.addHandler(&ws);
}
String processor(const String& var){
Serial.println(var);
if(var == "STATE"){
return ledState ? "ON" : "OFF";
}
return String();
}
void setup(){
Serial.begin(115200);
pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, LOW);
WiFi::begin(ssid, password);
while (WiFi::status() != WL_CONNECTED) {
delay(1000);
Serial.println("Подключение к Wi-Fi..");
}
Serial.println(WiFi::localIP());
initWebSocket();
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, "text/html", index_html, processor);
});
server.begin();
}
void loop() {
ws.cleanupClients();
digitalWrite(ledPin, ledState);
}
Вставьте ваши сетевые данные в следующие переменные, и код начнет работать:
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
Объяснение кода
Импорт библиотек
Импортируйте необходимые библиотеки для создания веб-сервера:
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
Сетевые данные
Вставьте ваши сетевые данные в следующие переменные:
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
GPIO Output
Создайте переменную ledState
для хранения состояния GPIO и переменную ledPin
, которая относится к GPIO, который вы хотите контролировать.
В данном случае, мы будем управлять встроенным светодиодом (подключенным к GPIO 2):
bool ledState = 0;
const int ledPin = 2;
AsyncWebServer и AsyncWebSocket
Создайте объект AsyncWebServer
на порту 80:
AsyncWebServer server(80);
Библиотека ESPAsyncWebServer
включает плагин WebSocket, который упрощает обработку соединений WebSocket.
Создайте объект AsyncWebSocket
, называемый ws
, для обработки соединений по пути /ws
:
AsyncWebSocket ws("/ws");
Создание веб-страницы
Переменная index_html
содержит HTML, CSS и JavaScript, необходимые для создания и стилизации веб-страницы,
а также для обработки взаимодействий клиент-сервер с использованием протокола WebSocket.
Примечание
Мы размещаем всё содержимое веб-страницы прямо в переменной index_html
, чтобы загрузить её напрямую из памяти.
Однако более гибким и масштабируемым подходом является сохранение отдельных файлов .html, .css, .js во
внутреннюю файловую систему ESP32 (например, SPIFFS или LittleFS) и обращение к ним через SPIFFS.open().
Вот содержимое переменной index_html
:
<!DOCTYPE HTML><html>
<head>
<title>ESP Web Server</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
<style>
html {
font-family: Arial, Helvetica, sans-serif;
text-align: center;
}
h1 {
font-size: 1.8rem;
color: white;
}
h2{
font-size: 1.5rem;
font-weight: bold;
color: #143642;
}
.topnav {
overflow: hidden;
background-color: #143642;
}
body {
margin: 0;
}
.content {
padding: 30px;
max-width: 600px;
margin: 0 auto;
}
.card {
background-color: #F8F7F9;
box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);
padding-top:10px;
padding-bottom:20px;
}
.button {
padding: 15px 50px;
font-size: 24px;
text-align: center;
outline: none;
color: #fff;
background-color: #0f8b8d;
border: none;
border-radius: 5px;
user-select: none;
}
.button:active {
background-color: #0f8b8d;
box-shadow: 2 2px #CDCDCD;
transform: translateY(2px);
}
.state {
font-size: 1.5rem;
color:#8c8c8c;
font-weight: bold;
}
</style>
</head>
<body>
<div class="topnav">
<h1>ESP WebSocket Server</h1>
</div>
<div class="content">
<div class="card">
<h2>Output - GPIO 2</h2>
<p class="state">state: <span id="state">%STATE%</span></p>
<p><button id="button" class="button">Toggle</button></p>
</div>
</div>
<script>
var gateway = `ws://${window.location.hostname}/ws`;
var websocket;
window.addEventListener('load', onLoad);
function initWebSocket() {
console.log('Trying to open a WebSocket connection...');
websocket = new WebSocket(gateway);
websocket.onopen = onOpen;
websocket.onclose = onClose;
websocket.onmessage = onMessage;
}
function onOpen(event) {
console.log('Connection opened');
websocket.send('hi');
}
function onClose(event) {
console.log('Connection closed');
setTimeout(initWebSocket, 2000);
}
function onMessage(event) {
var state;
if (event.data == "1"){
state = "ON";
}
else{
state = "OFF";
}
document.getElementById('state').innerHTML = state;
}
function onLoad(event) {
initWebSocket();
initButton();
}
function initButton() {
document.getElementById('button').addEventListener('click', toggle);
}
function toggle(){
websocket.send('toggle');
}
</script>
</body>
</html>
CSS
Между тегами <style>...</style>
мы включаем стили для оформления веб-страницы с использованием CSS.
Вы можете изменить их, чтобы веб-страница выглядела так, как вам нужно.
Примечание
Мы не будем подробно объяснять работу CSS в этом уроке, так как он посвящён WebSocket-соединению, а не вёрстке и стилям.
Вот пример встроенного CSS, использованного в веб-странице:
html {
font-family: Arial, Helvetica, sans-serif;
text-align: center;
}
h1 {
font-size: 1.8rem;
color: white;
}
h2 {
font-size: 1.5rem;
font-weight: bold;
color: #143642;
}
.topnav {
overflow: hidden;
background-color: #143642;
}
body {
margin: 0;
}
.content {
padding: 30px;
max-width: 600px;
margin: 0 auto;
}
.card {
background-color: #F8F7F9;
box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);
padding-top: 10px;
padding-bottom: 20px;
}
.button {
padding: 15px 50px;
font-size: 24px;
text-align: center;
outline: none;
color: #fff;
background-color: #0f8b8d;
border: none;
border-radius: 5px;
user-select: none;
}
.button:active {
background-color: #0f8b8d;
box-shadow: 2 2px #CDCDCD;
transform: translateY(2px);
}
.state {
font-size: 1.5rem;
color: #8c8c8c;
font-weight: bold;
HTML
Между тегами <body>...</body>
размещается основное содержимое веб-страницы, которое видно пользователю.
Пример HTML-разметки:
<div class="topnav">
<h1>ESP WebSocket Server</h1>
</div>
<div class="content">
<div class="card">
<h2>Output - GPIO 2</h2>
<p class="state">state: <span id="state">%STATE%</span></p>
<p><button id="button" class="button">Toggle</button></p>
</div>
</div>
Объяснение содержимого:
Заголовок 1 с текстом «ESP WebSocket Server»:
<h1>ESP WebSocket Server</h1>
Вы можете заменить этот текст на любой другой по вашему усмотрению.
Заголовок 2 отображает, с каким GPIO работает система:
<h2>Output - GPIO 2</h2>
Абзац с текущим состоянием GPIO:
<p class="state">state: <span id="state">%STATE%</span></p>
Здесь %STATE% — это заполнитель, который будет заменён на актуальное значение при отправке страницы с ESP32. Все такие переменные-заполнители заключаются в % и заменяются с помощью функции processor() на ESP32.
Чтобы JavaScript мог обратиться к этому элементу на странице и обновить его содержимое, мы задаём ему id=»state».
Кнопка переключения состояния GPIO:
<p><button id="button" class="button">Toggle</button></p>
Здесь кнопке присвоен id=»button», что позволяет JavaScript «поймать» событие нажатия и отправить соответствующую команду через WebSocket.
JavaScript – Обработка WebSocket
JavaScript размещается между тегами <script>...</script>
и отвечает за:
инициализацию соединения WebSocket с сервером;
обработку входящих и исходящих сообщений;
управление состоянием интерфейса и элементов HTML.
Ниже представлен полный скрипт, используемый в веб-интерфейсе:
<script>
var gateway = `ws://${window.location.hostname}/ws`;
var websocket;
window.addEventListener('load', onLoad);
function initWebSocket() {
console.log('Trying to open a WebSocket connection...');
websocket = new WebSocket(gateway);
websocket.onopen = onOpen;
websocket.onclose = onClose;
websocket.onmessage = onMessage;
}
function onOpen(event) {
console.log('Connection opened');
websocket.send('hi');
}
function onClose(event) {
console.log('Connection closed');
setTimeout(initWebSocket, 2000);
}
function onMessage(event) {
var state;
if (event.data == "1"){
state = "ON";
}
else{
state = "OFF";
}
document.getElementById('state').innerHTML = state;
}
function onLoad(event) {
initWebSocket();
initButton();
}
function initButton() {
document.getElementById('button').addEventListener('click', toggle);
}
function toggle(){
websocket.send('toggle');
}
</script>
Объяснение ключевых частей
1. Указание точки входа WebSocket:
var gateway = `ws://${window.location.hostname}/ws`;
window.location.hostname — возвращает IP-адрес ESP32, на котором открыта веб-страница.
2. Объявление переменной WebSocket:
var websocket;
3. Инициализация WebSocket после загрузки страницы:
window.addEventListener('load', onLoad);
4. Функция `onLoad()` запускает подключение и настраивает кнопку:
function onLoad(event) {
initWebSocket();
initButton();
}
5. Установка WebSocket-соединения:
function initWebSocket() {
console.log('Trying to open a WebSocket connection...');
websocket = new WebSocket(gateway);
websocket.onopen = onOpen;
websocket.onclose = onClose;
websocket.onmessage = onMessage;
}
6. Когда соединение установлено — отправляем тестовое сообщение:
function onOpen(event) {
console.log('Connection opened');
websocket.send('hi');
}
7. Если соединение закрыто — пытаемся переподключиться:
function onClose(event) {
console.log('Connection closed');
setTimeout(initWebSocket, 2000);
}
8. При получении сообщения от ESP32 обновляем состояние в интерфейсе:
function onMessage(event) {
var state;
if (event.data == "1"){
state = "ON";
}
else{
state = "OFF";
}
document.getElementById('state').innerHTML = state;
}
9. Подключаем обработчик клика на кнопку:
function initButton() {
document.getElementById('button').addEventListener('click', toggle);
}
10. Функция toggle отправляет команду на ESP32:
function toggle(){
websocket.send('toggle');
}
ESP32 в ответ на сообщение "toggle"
должен переключить состояние GPIO и отправить обновлённое состояние обратно клиенту.
Обработка WebSocket – Сервер
В этом разделе показано, как обрабатывать соединения WebSocket на стороне ESP32-сервера с помощью библиотеки ESPAsyncWebServer
.
Уведомление всех клиентов
Функция notifyClients()
отправляет текущее состояние светодиода всем подключённым клиентам:
void notifyClients() {
ws.textAll(String(ledState));
}
Метод textAll()
класса AsyncWebSocket
рассылает сообщение сразу всем клиентам.
Обработка сообщений WebSocket
Функция handleWebSocketMessage()
вызывается при получении данных от клиента:
void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) {
AwsFrameInfo *info = (AwsFrameInfo*)arg;
if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
data[len] = 0;
if (strcmp((char*)data, "toggle") == 0) {
ledState = !ledState;
notifyClients();
}
}
}
Если клиент отправляет "toggle"
, состояние светодиода переключается и рассылается обратно всем клиентам.
Настройка WebSocket-сервера
Функция onEvent()
обрабатывает события WebSocket:
void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type,
void *arg, uint8_t *data, size_t len) {
switch (type) {
case WS_EVT_CONNECT:
Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
break;
case WS_EVT_DISCONNECT:
Serial.printf("WebSocket client #%u disconnected\n", client->id());
break;
case WS_EVT_DATA:
handleWebSocketMessage(arg, data, len);
break;
case WS_EVT_PONG:
case WS_EVT_ERROR:
break;
}
}
Значения типа AwsEventType
:
WS_EVT_CONNECT
— клиент подключёнWS_EVT_DISCONNECT
— клиент отключёнWS_EVT_DATA
— пришли данныеWS_EVT_PONG
— ответ на пингWS_EVT_ERROR
— ошибка соединения
Инициализация WebSocket
void initWebSocket() {
ws.onEvent(onEvent);
server.addHandler(&ws);
}
processor()
Функция processor()
заменяет заполнитель %STATE%
в HTML-коде на текущее состояние GPIO:
String processor(const String& var){
Serial.println(var);
if(var == "STATE"){
return ledState ? "ON" : "OFF";
}
return String();
}
setup()
Основная инициализация:
void setup(){
Serial.begin(115200);
pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, LOW);
WiFi::begin(ssid, password);
while (WiFi::status() != WL_CONNECTED) {
delay(1000);
Serial.println("Подключение к Wi-Fi..");
}
Serial.println(WiFi::localIP());
initWebSocket();
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, "text/html", index_html, processor);
});
server.begin();
}
loop()
Физическое управление пином и очистка соединений:
void loop() {
ws.cleanupClients();
digitalWrite(ledPin, ledState);
}
Примечание
Вызов ws.cleanupClients()
предотвращает утечку памяти при неправильно закрытых соединениях WebSocket со стороны браузеров.
Рекомендуется вызывать эту функцию хотя бы раз в секунду для стабильности сервера.
Демонстрация
После вставки ваших сетевых данных в переменные ssid и password, вы можете загрузить код на свою плату. Не забудьте проверить, что у вас выбрана правильная плата и COM-порт.
После загрузки кода откройте Серийный монитор на скорости 115200 и нажмите кнопку EN/RST на плате. Должен быть напечатан IP-адрес ESP.
Откройте браузер в вашей локальной сети и введите IP-адрес ESP32. Вы должны получить доступ к веб-странице для управления выводом.

Нажмите на кнопку, чтобы переключить светодиод. Вы можете открыть несколько вкладок веб-браузера одновременно или получить доступ к веб-серверу с разных устройств, и состояние светодиода будет автоматически обновляться на всех клиентах, когда происходит изменение.
Заключение
В этом уроке вы узнали, как настроить WebSocket-сервер с помощью ESP32. Протокол WebSocket позволяет двустороннюю связь между клиентом и сервером. После инициализации сервер и клиент могут обмениваться данными в любое время.