WebSocket: смотрим как работает за кулисами
source icon
WebSocket: смотрим как работает за кулисами — Разработка на vc.ru
Первая ступень развития: HTTP
Для начала стоит начать с того что такое HTTP?
HTTP - Протокол для передачи гипертесктовых данных (Hyper Text Transfer Protocol), который используется повсеместно. HTTP используется в клиент-серверной архитектуре, там всю работу можно показать с помощью одной диаграммы:
Процесс отправки и получения HTTP-запроса
Особенности HTTP
  • HTTP не поддерживает соединение, после того, как отдает ответ на запрос.
  • HTTP обязует клиентов заранее оговаривать действие, которое клиент хочет сделать в заголовке (HTTP Headers) - GET, POST, PUT, DELETE
  • Мы отправляем заголовок что хотим сделать каждый раз, как обраемся к серверу
Существует большое количество сайтов, с помощью которых вы можете посмотреть как работает HTTP, давайте возьмем забавный сайт с REST API по мультивселенной Рик и Морти — https://rickandmortyapi.com/documentation.
Вы можете отправить запрос с помощью Postman или обычного cURL, я буду использовать второй. Давайте возьмем информацию о персонаже (Рике) и посмотрим что нам пришлёт сервер, для этого используем данную ссылку: https://rickandmortyapi.com/api/character/1
# Отправляем запрос с помощью cURL и парсим пришедший ответ с помощью JQ
curl https://rickandmortyapi.com/api/character/1 | jq
После данной команды мы получим следующий ответ. Как мы видим мы просто отправили запрос на получение информации с сервера (GET), сервер отдал нам информацию и после этого мы разрываем соединение. После того как мы получим ответ мы ничего не знаем о сервере 👀
Нам пришли данные в формате JSON как ответ от сервера.
Вторая ступень: AJAX
AJAX - асинхронные запросы с помощью JavaScript (Asynchonous JavaScript and XML). AJAX преследует все те же цели, что и HTTP, только делает это уже асинхронно. Если ранее нужно было для каждого запроса прописывать свой URL и перезагружать страницу, то теперь можно просто использовать AJAX и отслеживать асинхронные данные с помощью промисов или коллбэков.
Особенности AJAX
  • Все ещё обычный запрос, который не поддерживает соединение, после того, как отдает ответ на запрос.
  • Все ещё заранее оговариваем действие, которое клиент хочет сделать в заголовке (HTTP Headers) - GET, POST, PUT, DELETE
  • Мы отправляем заголовок что хотим сделать каждый раз, как обраемся к серверу
  • Теперь мы делаем это асинхронно благодаря JavaScript
Самым простым примером AJAX является следующая реализация:
ajax-node.js
// Реализация на NodeJS ^17.5
// Будет работать в браузерах ~если вы не на Internet Explorer 8~

/**
 * Метод для того чтобы показать вывод AJAX-запроса
 * @param {string} url - ссылка, которую будем подтягивать
 * @returns {void}
 */
function showAJAXResponse(url) {

    // Выполняем запрос на сервер
    fetch(url)
        .then(res => res.json())          // Формируем JSON
        .then(console.log);               // Выводим
}

// Отправляем AJAX запрос 🏀
showAJAXResponse('https://rickandmortyapi.com/api/character/1');
Вот что мы получим в итоге:
Как мы можем увидеть, мы все ещё не держим связь с сервером. Мы отправили запрос, получили ответ и все. Что дальше происходит с сервером нам неизвестно.
Третья ступень: WS или WebSocket
WebSocket - протокол для общения между клиентом и сервером, предоставляющий двухсторонне общение сверх протокола TCP.
Мы подключаем WS один раз, а затем сервер может отдавать нам ответы тогда, когда посчитает нужным:
Как это работает?
Первое что мы делаем — отправляем обычный TCP-запрос на сервер. Мы говорим, что хотим подключиться к серверу и ждём от него ответа. Такой процесс называется “рукопожатие” (Handshake), он используется повсеместно, например когда вы подключаетесь к роутеру ваш телефон отправляем запрос роутеру с ключами, роутер отвечает ОК и вы успешно подключаетесь.
Затем происходит обмен данными: допустим один из множества клиентов отправил HTTP-запрос серверу и нужно отдать ответ не только одному клиенту, а целой сети! Сервер в таком случае отдаст обычный ответ отправителю запроса, а всем другим пришлёт пакеты по WebSocket-соединению с полезными данными.
Разберем более подробно на примере. Вы — клиент, отправляете запрос серверу на подключение. Запрос и ответ будут выглядеть примерно так:
# Отправляем запрос серверу по ссылке example.com/connect-to-ws
# Вот что примерно мы пришлём:
GET /connect-to-ws HTTP/1.1
Host: example.com:8000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

# А вот что нам на такой запрос ответит сервер при успешном рукопожатии:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Как мы видим сервер ответил не кодом 200 (успешное завершение запроса), а 101 — переключение протоколов. Это происходит потому, что мы отправили HTTP запрос, а хотим получить не только HTTP-ответ, а ещё и другие ответы по WS, сервер как бы предупреждает клиент, что будет присылать ответы множество раз🔙
А как сервер узнает, что мы до сих пор подключены?😅
Ответ на данный вопрос достаточно легкий — сервер и клиент играют в пинг-понг😳🏓😳
Сервер периодически присылает ответ по WS с просьбой о действии - послать запрос на сервер. Если клиент отвечает до истечения тайм-аута — он подключен, если нет, то происходит разрыв соединения до следующего рукопожатия👋
Почему соединение называется двухсторонним (дуплексным), а ответы мы получаем только от сервера?
На самом деле мы не только получаем ответы от сервера, а ещё и можем в двухстороннем порядке отправлять через WS запросы!
Чтобы лучше понять, давайте рассмотрим легкий и полностью задокументированный код на JavaScript:
send-ws.js
// Создаем WS-объект, с помощьюю него будем рулить потоками
// (отправка, принятие запросов)
const myWS = new WebSocket(url, protocols);

// До того как сервер и клиент совершат рукопожатие
// статус у WS-клиента будет CONNECTING
console.log(myWS.readyState); // CONNECTING

// После того как рукопожатие (Handshake) пройдет успешно
// readyState будет OPEN
console.log(myWS.readyState); // OPEN
После того как мы открыли соединение по WS мы сразу же можем отправить сообщение серверу:
sendWs.js
/*
Не забываем, что мы можем отправить сообщение серверу
только если соединение открыто
Поместим все общение с сервером внутрь ивента onopen,
именно он срабатывает, когда соединение открыто
*/
myWS.onopen = function (event) {

    // Отправляем сообщение по WS
    myWS.send('Привет, сервер!');
}
Сервер получит данный запрос и возможно захочет ответить, но мы не сможем прочитать ответ! Почему? Да просто потому что у нас нет слушателя на событие получения сообщения от сервера. Сделаем же его:
receiveWs.js
// Вешаем слушатель на принятие сообщений
myWS.onmessage = (event) => {

    // Делаем с данными все что захотим, я их просто выведу
    console.log(event.data);
}
Закрытие соединения
Для закрытия соединения мы должны отправить запрос серверу, а он по истечению таймаута тоже должен отправить ответ на подтверждение закрытия. В JavaScript это делается одним методом:
closeWs.js
// Закрываем соединение
myWS.close();

// Ну и естественно слушаем событие onclose, чтобы выполнить какие-то действия
myWS.onclose = (event) => {
    // ...
};
Особенности WS
  • Поддерживает двухсторонее соединение в реальном времени
  • Отправляет заголовок только один раз
Дебаггинг WS
Отлаживать WS-соединение совсем несложно. Рассмотрим пример отладки WS на Google Chrome🌎, перейдем на данный сайт: https://websocketstest.com/
Откроем DevTools, выберем вкладку Networks и перейдем в таб WS:
Как мы видим ответ от сервера действительно 101 Switching Protocols, однако как нам увидеть данные, которые приходят по WS, вкладки Response же нет🤔
Вкладки Response нет, зато появилась новая - Messages. Открываем её и видим там примерно следующее:
Красной стрелкой вниз показаны пакеты, которые пришли нам (пусть вас не вводит в заблуждение красная стрелка, это не упавшие, а пришедшие пакеты), отправленные пакеты в свою очередь будут показаны зелёной стрелкой, которая стремится вверх⬆