Определение
WebSocket – это протокол компьютерной связи, предоставляющий полнодуплексные каналы связи по одному TCP-соединению.
В отличие от HTTP, WebSocket обеспечивает полнодуплексную связь. Более того, WebSocket позволяет передавать потоки сообщений поверх TCP. Только TCP обрабатывает потоки байтов без присущей концепции сообщения.
Таким образом, с помощью WebSocket вы можете осуществлять полнодуплексную связь, а подписавшись на канал, вы можете избежать опроса. Этот протокол используется во многих приложениях, и они станут будущим современных приложений.
Архитектура
По сути, у вас есть сервер, который совместно использует определенную службу, и набор клиентов, которые могут подключаться к серверу.
Соединение создает канал от устройств, но для получения информации вы должны подписаться на тему (иногда подписка неявно связана с соединением).
Большое отличие от сервера REST заключается в том, что в HTTP-запросе вы отправляете запрос, и вы должны дождаться ответа, чтобы получить данные и запустить новый запрос в том же соединении, с помощью WS вы можете передавать потоковые запросы и потоковые ответы, а затем работать, когда захотите.
Но как мы можем использовать WebSocket в наших проектах?
Классический пример — таблица с большим количеством строк, сначала мы делаем REST-вызов и получаем все данные таблицы, а для обновления одной ячейки используем Подписка WebSocket, а с помощью JavaScript можно обновить одну ячейку, а также получить приятный графический эффект.
WebSocket-клиент
Теперь мы увидим простой клиент WebSocket, который будет подключаться к эхо-серверу WebSocket. Его поведение довольно простое: вы отправляете сообщение, а сервер отвечает тем же сообщением, поэтому вы можете проверить отправку и получение вашего клиента.
JavaScript WebSocket-клиент
Вот упрощенная версия тестового клиента WebSocket
<! —
js простой клиент WebSocket
Я использую эхо-сервер ws://echo.websocket.org/
Когда вы отправляете сообщение на этот сервер, вы получаете
ответ с тем же сообщением
→
‹!DOCTYPE html›
‹html›
‹метакодировка="utf-8/›
‹title›Тест WebSocket‹/название›
#выход {
граница: сплошная 1px #999999;
цвет верхней границы: #CCCCCC;
цвет левой границы: #CCCCCC;
отступ: 5px;
ширина: 300 пикселей;
высота: 172 пикселя;
переполнение-у: прокрутка;
семейство шрифтов: «Open Sans»;
размер шрифта: 13px;
}
‹язык сценария=”javascript” тип=”текст/javascript”›
var wsUri = «wss://echo.websocket.org/»;
вывод переменной;
инициализация функции () {
вывод = документ.getElementById («выход»);
тестВеб-сокет();
}
функция testWebSocket() {
веб-сокет = новый веб-сокет (wsUri);
websocket.onopen = функция (evt) {
onOpen(evt)
};
websocket.onclose = функция (evt) {
при закрытии (evt)
};
websocket.onmessage = функция (evt) {
onMessage (evt)
};
websocket.onerror = функция (evt) {
при ошибке (evt)
};
}
функция onOpen(evt) {
writeToScreen("ПОДКЛЮЧЕНО");
// doSend("Привет, я простой клиент Ayush JS!!");
}
функция onClose (evt) {
writeToScreen("ОТКЛЮЧЕНО");
}
функция onMessage(evt) {
writeToScreen(‘‹span style="color: blue;"›ОТВЕТ: ‘ + evt.data + ‘‹/span›’);
// веб-сокет.close();
}
функция при ошибке (evt) {
writeToScreen(‘‹span style=”color: red;”›ERROR:‹/span› ‘ + evt.data);
}
функция doSend(сообщение) {
writeToScreen("ОТПРАВЛЕНО: " + сообщение);
websocket.send (сообщение);
}
функция writeToScreen(сообщение) {
var pre = document.createElement("p");
pre.style.wordWrap = «разрывное слово»;
pre.innerHTML = сообщение;
вывод.appendChild (предварительно);
output.scrollTop = output.scrollHeight;
}
window.addEventListener («загрузка», инициализация, ложь);
‹/скрипт›
‹h2›Тест WebSocket‹/h2›
‹div id="вывод"›‹/div›
<br/>
‹тип ввода=”кнопка” значение=”Отправить сообщение!” onclick="doSend('Простое сообщение клиента Ayush js!!')"/›‹/div›
‹input type="button" value="Закрыть соединение!" onclick="websocket.close()"/›‹/div›
‹/html›
Важной частью кода является
функция testWebSocket() {
веб-сокет = новый веб-сокет (wsUri);
websocket.onopen = функция (evt) {
onOpen(evt)
};
websocket.onclose = функция (evt) {
при закрытии (evt)
};
websocket.onmessage = функция (evt) {
onMessage (evt)
};
websocket.onerror = функция (evt) {
при ошибке (evt)
};
}
Это простой код для установления соединения и присоединения ряда функций обратного вызова к указанным событиям:
§ onopen: вызывается при установлении соединения;
§ onclose: вызывается при отключении клиента;
§ onmessage: здесь вызывается, когда приходит сообщение;
§ onerror: вызывается при возникновении ошибки.
Как вы можете видеть, чтобы отправить сообщение, вы должны просто вызвать websocket.send
после соединения.
Библиотека
Вы можете найти библиотеку непосредственно в репозитории библиотек Arduino (Tools --> Manage libraries..
).
Обратите внимание, что для esp8266 и esp32 необходимо использовать версию 2.x.x.
esp8266 WebSocket-клиент
Чтобы подключиться к вашему esp8266, код довольно прост.
/*
* esp8266 простой клиент WebSocket
*
* Я использую эхо-сервер ws://echo.websocket.org/
* Когда вы отправляете сообщение на этот сервер, вы получаете
* ответ с тем же сообщением
*
*/
#include ‹Arduino.h›
#include ‹ESP8266WiFi.h›
#include ‹WebSocketsClient.h›
WebSocketsClient webSocket;
const char *ssid = «‹ВАШ-SSID›»;
const char *password = «‹ВАШ-ПАРОЛЬ›»;
длинное сообщение без знака Interval = 5000;
логическое соединение = ложь;
#define Серийный номер DEBUG_SERIAL
void webSocketEvent (тип WStype_t, uint8_t * полезная нагрузка, длина size_t) {
переключатель (тип) {
случай WStype_DISCONNECTED:
DEBUG_SERIAL.printf("[WSc] Отключено!\n");
подключен = ложь;
перерыв;
случай WStype_CONNECTED: {
DEBUG_SERIAL.printf("[WSc] Подключен к URL: %s\n", полезная нагрузка);
подключен = истина;
// отправляем сообщение на сервер при подключении
DEBUG_SERIAL.println("[WSc] SENT: Connected");
webSocket.sendTXT («Подключено»);
}
перерыв;
случай WStype_TEXT:
DEBUG_SERIAL.printf("[WSc] RESPONSE: %s\n", полезная нагрузка);
перерыв;
случай WStype_BIN:
DEBUG_SERIAL.printf("[WSc] получить двоичную длину: %u\n", длина);
hexdump(полезная нагрузка, длина);
перерыв;
случай WStype_PING:
// понг будет отправлен автоматически
DEBUG_SERIAL.printf("[WSc] получить ping\n");
перерыв;
случай WStype_PONG:
// ответ на пинг, который мы отправляем
DEBUG_SERIAL.printf("[WSc] get pong\n");
перерыв;
}
}
недействительная установка () {
DEBUG_SERIAL.begin(115200);
// DEBUG_SERIAL.setDebugOutput(true);
DEBUG_SERIAL.println();
DEBUG_SERIAL.println();
DEBUG_SERIAL.println();
for(uint8_t t = 4; t › 0; t — ) {
DEBUG_SERIAL.printf("[НАСТРОЙКА] ОЖИДАНИЕ ЗАГРУЗКИ %d…\n", t);
DEBUG_SERIAL.flush();
задержка(1000);
}
WiFi.begin(ssid, пароль);
в то время как ( WiFi.status() != WL_CONNECTED ) {
задержка ( 500 );
Serial.print (".");
}
DEBUG_SERIAL.print («Локальный IP:»); DEBUG_SERIAL.println(WiFi.localIP());
// адрес сервера, порт и URL
webSocket.begin("echo.websocket.org", 80, "/");
// обработчик события
webSocket.onEvent(webSocketEvent);
}
unsigned long lastUpdate = millis();
недействительный цикл () {
веб-сокет.цикл();
если (подключено && lastUpdate+messageInterval‹millis()){
DEBUG_SERIAL.println("[WSc] SENT: Простое сообщение клиента js!!");
webSocket.sendTXT("Простое клиентское сообщение Ayush js!!");
последнее обновление = миллис();
}
}
Основная часть:
переключатель (тип) {
случай WStype_DISCONNECTED:
DEBUG_SERIAL.printf("[WSc] Отключено!\n");
подключен = ложь;
перерыв;
случай WStype_CONNECTED: {
DEBUG_SERIAL.printf("[WSc] Подключен к URL: %s\n", полезная нагрузка);
подключен = истина;
// отправляем сообщение на сервер при подключении
DEBUG_SERIAL.println("[WSc] SENT: Connected");
webSocket.sendTXT («Подключено»);
}
перерыв;
случай WStype_TEXT:
DEBUG_SERIAL.printf("[WSc] RESPONSE: %s\n", полезная нагрузка);
перерыв;
случай WStype_BIN:
DEBUG_SERIAL.printf("[WSc] получить двоичную длину: %u\n", длина);
hexdump(полезная нагрузка, длина);
перерыв;
случай WStype_PING:
// понг будет отправлен автоматически
DEBUG_SERIAL.printf("[WSc] получить ping\n");
перерыв;
случай WStype_PONG:
// ответ на пинг, который мы отправляем
DEBUG_SERIAL.printf("[WSc] get pong\n");
перерыв;
}
§ WStype_CONNECTED: когда соединение установлено;
§ WStype_DISCONNECTED: когда клиент отключен;
§ WStype_TEXT: при получении текстового сообщения;
§ WStype_BIN: при поступлении двоичного сообщения;
§ WStype_PING и WStype_PONG: здесь используется как сообщение «подтверждения активности».
Для подключения необходимо использовать эту команду:
// адрес сервера, порт и URL
webSocket.begin("echo.websocket.org", 80, "/");
и с помощью webSocket.onEvent
мы присоединяем обратный вызов к событиям.
С клиентом для отправки простого сообщения мы используем webSocket.sendTXT
.
Как мы видим, управление WS достаточно простое и одинаковое на всех языках.
WebSocket-клиент esp32
Версия esp32 очень похожа на версию esp8266.
/*
* esp32 простой клиент WebSocket
*
* Я использовал эхо-сервер ws://echo.websocket.org/
* Когда вы отправляете сообщение на этот сервер, вы получаете
* ответ с тем же сообщением
*
*/
#include ‹Arduino.h›
#include ‹WiFi.h›
#include ‹WebSocketsClient.h›
WebSocketsClient webSocket;
const char *ssid = «‹ВАШ-SSID›»;
const char *password = «‹ВАШ-ПАРОЛЬ›»;
длинное сообщение без знака Interval = 5000;
логическое соединение = ложь;
#define Серийный номер DEBUG_SERIAL
void hexdump(const void *mem, uint32_t len, uint8_t cols = 16) {
const uint8_t* src = (const uint8_t*) mem;
DEBUG_SERIAL.printf("\n[HEXDUMP] Адрес: 0x%08X len: 0x%X (%d)", (ptrdiff_t)src, len, len);
for(uint32_t i = 0; i ‹ len; i++) {
если (i% столбцов == 0) {
DEBUG_SERIAL.printf("\n[0x%08X] 0x%08X: ", (ptrdiff_t)src, i);
}
DEBUG_SERIAL.printf("%02X", *src);
источник++;
}
DEBUG_SERIAL.printf("\n");
}
void webSocketEvent (тип WStype_t, uint8_t * полезная нагрузка, длина size_t) {
переключатель (тип) {
случай WStype_DISCONNECTED:
DEBUG_SERIAL.printf("[WSc] Отключено!\n");
подключен = ложь;
перерыв;
случай WStype_CONNECTED: {
DEBUG_SERIAL.printf("[WSc] Подключен к URL: %s\n", полезная нагрузка);
подключен = истина;
// отправляем сообщение на сервер при подключении
DEBUG_SERIAL.println("[WSc] SENT: Connected");
webSocket.sendTXT («Подключено»);
}
перерыв;
случай WStype_TEXT:
DEBUG_SERIAL.printf("[WSc] RESPONSE: %s\n", полезная нагрузка);
перерыв;
случай WStype_BIN:
DEBUG_SERIAL.printf("[WSc] получить двоичную длину: %u\n", длина);
hexdump(полезная нагрузка, длина);
перерыв;
случай WStype_PING:
// понг будет отправлен автоматически
DEBUG_SERIAL.printf("[WSc] получить ping\n");
перерыв;
случай WStype_PONG:
// ответ на пинг, который мы отправляем
DEBUG_SERIAL.printf("[WSc] get pong\n");
перерыв;
случай WStype_ERROR:
случай WStype_FRAGMENT_TEXT_START:
случай WStype_FRAGMENT_BIN_START:
случай WStype_FRAGMENT:
случай WStype_FRAGMENT_FIN:
перерыв;
}
}
недействительная установка () {
DEBUG_SERIAL.begin(115200);
// DEBUG_SERIAL.setDebugOutput(true);
DEBUG_SERIAL.println();
DEBUG_SERIAL.println();
DEBUG_SERIAL.println();
for(uint8_t t = 4; t › 0; t — ) {
DEBUG_SERIAL.printf("[НАСТРОЙКА] ОЖИДАНИЕ ЗАГРУЗКИ %d…\n", t);
DEBUG_SERIAL.flush();
задержка(1000);
}
WiFi.begin(ssid, пароль);
в то время как ( WiFi.status() != WL_CONNECTED ) {
задержка ( 500 );
DEBUG_SERIAL.print («.»);
}
DEBUG_SERIAL.print («Локальный IP:»); DEBUG_SERIAL.println(WiFi.localIP());
// адрес сервера, порт и URL
webSocket.begin("echo.websocket.org", 80, "/");
// обработчик события
webSocket.onEvent(webSocketEvent);
}
unsigned long lastUpdate = millis();
недействительный цикл () {
веб-сокет.цикл();
если (подключено && lastUpdate+messageInterval‹millis()){
DEBUG_SERIAL.println("[WSc] SENT: Простое клиентское сообщение Ayush js!");
webSocket.sendTXT("Простое клиентское сообщение Ayush js!");
последнее обновление = миллис();
}
}
Но мы можем сразу выделить некоторые отличия, в webSocketEvent надо обратить внимание на метод hexdump для декодирования бинарного сообщения и есть набор переключателей case, которые надо реализовать (их я добавил в конце).
esp8266 сервер веб-сокетов
Сервер WebSocket Это очень похоже на клиент, у нас есть серия событий для управления сообщениями и статусом соединения, вот пример
/*
* esp8266 простой сервер WebSocket
*
* Ответ сервера с
* эхо сообщения, которое вы отправляете
* и отправлять рассылку каждые 5 секунд
*
*/
#include ‹Arduino.h›
#include ‹ESP8266WiFi.h›
#include ‹WebSocketsServer.h›
const char *ssid = «‹ВАШ-SSID›»;
const char *password = «‹ВАШ-ПАРОЛЬ›»;
const uint8_t wsPort = 81;
длинное сообщение без знака Interval = 5000;
логическое соединение = ложь;
#define Серийный номер DEBUG_SERIAL
WebSocketsServer webSocket = WebSocketsServer(wsPort);
void webSocketEvent (число uint8_t, тип WStype_t, uint8_t * полезная нагрузка, длина size_t) {
переключатель (тип) {
случай WStype_DISCONNECTED:
DEBUG_SERIAL.printf("[%u] Отключено!\n", число);
перерыв;
случай WStype_CONNECTED:
{
IP-адрес ip = webSocket.remoteIP(число);
DEBUG_SERIAL.printf("[%u] Подключено с URL-адреса %d.%d.%d.%d: %s\n", num, ip[0], ip[1], ip[2], ip[3 ], полезная нагрузка);
// отправляем сообщение клиенту
webSocket.sendTXT (число, «Подключено»);
}
перерыв;
случай WStype_TEXT:
DEBUG_SERIAL.printf("[%u] ПОЛУЧИТЬ TXT: %s\n", число, полезная нагрузка);
// отправляем сообщение клиенту
webSocket.sendTXT(num, "(ECHO MESSAGE) "+String((char *)payload));
// отправляем данные всем подключенным клиентам
// webSocket.broadcastTXT("сообщение здесь");
перерыв;
случай WStype_BIN:
DEBUG_SERIAL.printf("[%u] получить двоичную длину: %u\n", число, длина);
hexdump(полезная нагрузка, длина);
// отправляем сообщение клиенту
// webSocket.sendBIN(число, полезная нагрузка, длина);
перерыв;
}
}
недействительная установка () {
DEBUG_SERIAL.begin(115200);
// DEBUG_SERIAL.setDebugOutput(true);
DEBUG_SERIAL.println();
DEBUG_SERIAL.println();
DEBUG_SERIAL.println();
for(uint8_t t = 4; t › 0; t — ) {
DEBUG_SERIAL.printf("[НАСТРОЙКА] ОЖИДАНИЕ ЗАГРУЗКИ %d…\n", t);
DEBUG_SERIAL.flush();
задержка(1000);
}
WiFi.begin(ssid, пароль);
в то время как ( WiFi.status() != WL_CONNECTED ) {
задержка ( 500 );
Serial.print (".");
}
DEBUG_SERIAL.println («Полный URI WebSocket: «);
DEBUG_SERIAL.print("ws://");
DEBUG_SERIAL.print(WiFi.localIP());
DEBUG_SERIAL.print («:»);
DEBUG_SERIAL.print(wsPort);
DEBUG_SERIAL.println («/»);
веб-сокет.начать();
webSocket.onEvent(webSocketEvent);
}
unsigned long lastUpdate = millis();
недействительный цикл () {
веб-сокет.цикл();
если (lastUpdate+messageInterval‹millis()){
DEBUG_SERIAL.println("[WSc] SENT: Простое широковещательное клиентское сообщение!!");
webSocket.broadcastTXT("Простое широковещательное клиентское сообщение!!");
последнее обновление = миллис();
}
}
Вы можете получить ws uri для установки клиента из последовательного вывода:
[НАСТРОЙКА] ОЖИДАНИЕ ЗАГРУЗКИ 4…
[НАСТРОЙКА] ОЖИДАНИЕ ЗАГРУЗКИ 3…
[НАСТРОЙКА] ОЖИДАНИЕ ЗАГРУЗКИ 2…
[НАСТРОЙКА] ОЖИДАНИЕ ЗАГРУЗКИ 1…
…..
Полный uri WebSocket:
ws://192.168.1.127:81/
Структура довольно похожа на клиент, но вы должны обратить внимание на:
Когда клиент подключен, он связан с идентификатором, и его можно использовать для отправки сообщения конкретному клиенту, например, эхо-сообщение.
// отправляем сообщение клиенту
webSocket.sendTXT(num, "(ECHO MESSAGE) "+String((char *)payload));
Чтобы отправить сообщение всем клиентам, вы можете использовать broadcastTXT
:
webSocket.broadcastTXT("Простое широковещательное клиентское сообщение!!");
esp32 WebSocket-сервер
/*
* esp32 простой сервер WebSocket
*
* Ответ сервера с
* эхо сообщения, которое вы отправляете
* и отправлять рассылку каждые 5 секунд
*
*/
#include ‹Arduino.h›
#include ‹WiFi.h›
#include ‹WebSocketsServer.h›
const char *ssid = «‹ВАШ-SSID›»;
const char *password = «‹ВАШ-ПАРОЛЬ›»;
const uint8_t wsPort = 81;
длинное сообщение без знака Interval = 5000;
логическое соединение = ложь;
#define Серийный номер DEBUG_SERIAL
WebSocketsServer webSocket = WebSocketsServer(wsPort);
void hexdump(const void *mem, uint32_t len, uint8_t cols = 16) {
const uint8_t* src = (const uint8_t*) mem;
DEBUG_SERIAL.printf("\n[HEXDUMP] Адрес: 0x%08X len: 0x%X (%d)", (ptrdiff_t)src, len, len);
for(uint32_t i = 0; i ‹ len; i++) {
если (i% столбцов == 0) {
DEBUG_SERIAL.printf("\n[0x%08X] 0x%08X: ", (ptrdiff_t)src, i);
}
DEBUG_SERIAL.printf("%02X", *src);
источник++;
}
DEBUG_SERIAL.printf("\n");
}
void webSocketEvent (число uint8_t, тип WStype_t, uint8_t * полезная нагрузка, длина size_t) {
переключатель (тип) {
случай WStype_DISCONNECTED:
DEBUG_SERIAL.printf("[%u] Отключено!\n", число);
перерыв;
случай WStype_CONNECTED:
{
IP-адрес ip = webSocket.remoteIP(число);
DEBUG_SERIAL.printf("[%u] Подключено с URL-адреса %d.%d.%d.%d: %s\n", num, ip[0], ip[1], ip[2], ip[3 ], полезная нагрузка);
// отправляем сообщение клиенту
webSocket.sendTXT (число, «Подключено»);
}
перерыв;
случай WStype_TEXT:
DEBUG_SERIAL.printf("[%u] ПОЛУЧИТЬ TXT: %s\n", число, полезная нагрузка);
// отправляем сообщение клиенту
webSocket.sendTXT(num, "(ECHO MESSAGE) "+String((char *)payload));
// отправляем данные всем подключенным клиентам
// webSocket.broadcastTXT("сообщение здесь");
перерыв;
случай WStype_BIN:
DEBUG_SERIAL.printf("[%u] получить двоичную длину: %u\n", число, длина);
hexdump(полезная нагрузка, длина);
// отправляем сообщение клиенту
// webSocket.sendBIN(число, полезная нагрузка, длина);
перерыв;
случай WStype_ERROR:
случай WStype_FRAGMENT_TEXT_START:
случай WStype_FRAGMENT_BIN_START:
случай WStype_FRAGMENT:
случай WStype_FRAGMENT_FIN:
случай WStype_PING:
случай WStype_PONG:
перерыв;
}
}
недействительная установка () {
DEBUG_SERIAL.begin(115200);
// DEBUG_SERIAL.setDebugOutput(true);
DEBUG_SERIAL.println();
DEBUG_SERIAL.println();
DEBUG_SERIAL.println();
for(uint8_t t = 4; t › 0; t — ) {
DEBUG_SERIAL.printf("[НАСТРОЙКА] ОЖИДАНИЕ ЗАГРУЗКИ %d…\n", t);
DEBUG_SERIAL.flush();
задержка(1000);
}
WiFi.begin(ssid, пароль);
в то время как ( WiFi.status() != WL_CONNECTED ) {
задержка ( 500 );
DEBUG_SERIAL.print («.»);
}
DEBUG_SERIAL.println();DEBUG_SERIAL.println("Полный URI WebSocket: ");
DEBUG_SERIAL.print("ws://");
DEBUG_SERIAL.print(WiFi.localIP());
DEBUG_SERIAL.print («:»);
DEBUG_SERIAL.print(wsPort);
DEBUG_SERIAL.println («/»);
веб-сокет.начать();
webSocket.onEvent(webSocketEvent);
}
unsigned long lastUpdate = millis()+messageInterval;
недействительный цикл () {
веб-сокет.цикл();
если (lastUpdate+messageInterval‹millis()){
DEBUG_SERIAL.println("[WSc] SENT: Простое широковещательное клиентское сообщение!!");
webSocket.broadcastTXT("Простое широковещательное клиентское сообщение!!");
последнее обновление = миллис();
}
}
Есть несколько отличий: импорт WiFi, шестнадцатеричный дамп, не реализованный изначально, и некоторые параметры, которые являются обязательными в случае переключателя управления событиями.