Что такое веб-сокеты и как они вообще работают

Читать в полной версии →

Рассказываем, как работает протокол связи WebSocket, и почему современный веб не сможет существовать без веб-сокетов в таком виде, в каком мы привыкли его видеть.

Эта статья — адаптированный перевод материала WebSockets for fun and profit, опубликованного в блоге сервиса StackOverflow.

Коммуникация в режиме реального времени и практически мгновенная передача данных являются обязательными стандартами современного интернета. Чтобы удовлетворить эти стандарты, в 2011 году появился протокол связи WebSocket, который позволяет сайтам отправлять и получать данные без задержки. С помощью веб-сокетов можно создавать многопользовательские игры, мессенджеры, а также сервисы для совместной работы.

Что такое веб-сокеты

WebSocket — это технология, которая позволяет клиенту установить двухстороннюю («дуплексную») связь с сервером. Сразу поясним: клиент — это приложение на компьютере или смартфоне пользователя, а сервер — это удаленный компьютер, на котором хранится веб-сайт и связанные с ним данные.

Ключевое слово в этом определении — двусторонний: с помощью веб-сокетов клиент и сервер могут инициировать связь друг с другом, а также могут отправлять сообщения одновременно. Почему это так важно? Чтобы в полной мере оценить возможности WebSocket, сделаем шаг назад и рассмотрим несколько самых распространенных способов, с помощью которых компьютеры могут получать данные с сервера.

Как технически устроен обмен данными в интернете

Ответ на запрос

В традиционном HTTP, который сегодня использует большинство сайтов, веб-сервер предназначен для приема запросов от клиентов, а также для ответа на них. При этом коммуникация может быть инициирована только в одном направлении: от клиента к серверу. Код сервера определяет, какой тип запросов он должен ожидать и как реагировать на каждый из них.

Этот вид коммуникации можно сравнить с работой кухни в ресторане:

Здесь важно отметить, что кухня понятия не имеет, от кого исходит заказ. Технический способ сказать это так: «HTTP не имеет состояния», — он рассматривает каждый новый запрос как полностью независимый.

Читайте также: Фильтр Блума: зачем нужен и как работает

У нас есть способы обойти это правило — например, можно отправлять файлы cookie, которые помогают серверу идентифицировать клиента. При этом сами HTTP-сообщения все равно будут читаться и выполняться независимо друг от друга.

В этом подходе кроется достаточно серьезная проблема: кухня не может сама прислать официанта к клиенту. Она может только выдать официанту блюдо или сказать, что еды больше нет — и только в том случае, когда клиент отправил официанта на кухню. То есть кухня ничего не знает о клиенте: она получает информацию только о поступающих заказах. Говоря языком сервера, единственный способ для клиентов получать обновленную информацию с сервера — отправлять запросы.

Представьте себе чат-приложение, в котором вы разговариваете с другом. Вы отправляете сообщение на сервер в виде запроса с текстом в качестве полезной нагрузки. Сервер получает ваш запрос и сохраняет сообщение. Но у него нет возможности связаться с компьютером вашего друга. Компьютер вашего друга также должен отправить запрос на проверку наличия новых сообщений — только тогда сервер может отправить ваше сообщение другому пользователю.

В таком виде оба клиента должны постоянно проверять сервер на наличие обновлений, внося неловкие задержки между каждым сообщением.

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

Короткий опрос

Одним из самых простых решений этой проблемы является метод «Короткий опрос». Просто заставьте клиента повторно пинговать сервер, скажем, каждые 500 мс. Таким образом, пользователь получит новые данные каждые 500 мс. Однако у этого подхода есть несколько очевидных недостатков:

Долгий опрос

Еще одним обходным путем задержки получения данных является метод «Долгий опрос». При его использовании сервер получает запрос от клиента, но не отвечает на него, пока не получит новые данные из другого запроса. Долгий опрос более эффективен, чем многократная проверка связи с сервером, поскольку он избавляет от хлопот, связанных с анализом заголовков запросов, запросом новых данных и отправкой часто пустых ответов.

Однако теперь сервер должен отслеживать несколько запросов и порядок их получения. Кроме того, время ожидания запросов может истекать, поэтому пользователю необходимо периодически отправлять новые запросы.

Читайте также: HTTP/3: прошлое, настоящее и будущее

События, отправленные сервером: (SSE)/EventSource

Другой метод отправки сообщений — Server-Sent Events API , который позволяет серверу отправлять обновления клиенту, используя интерфейс JavaScript EventSource. Этот интерфейс создает постоянное однонаправленное соединение с сервером через HTTP и использует специальный заголовок текста/потока событий. В итоге все запросы обрабатываются кодом как события JavaScript, поэтому практически нет задержки между запросом и ответом.

Это почти то, что мы ищем — теперь мы можем получать обновления с сервера. Поскольку события Server-Sent (SSE) — однонаправленные, они отлично подходят для приложений, в которых вам не нужно отправлять на сервер какие-либо данные. К таким сервисам можно приравнять ленту новостей в Twitter или информационную панель с котировками акций в режиме реального времени.

Однако SSE не поддерживается старыми браузерами, а большинство существующих браузеров ограничивают количество одновременных подключений SSE. Но и этого еще недостаточно для создания современного мессенджера. Получать обновления в реальном времени — хорошо, но мы хотели бы иметь возможность их отправлять — и тоже в режиме реального времени.

Подробнее про веб-сокеты

Итак, нам нужен способ для моментальной отправки информации на сервер и такого же быстрого получения обновлений с сервера. Это возвращает нас к двусторонней («дуплексной») связи, с которой хорошо справляются именно веб-сокеты.

Поддерживаемый почти всеми современными браузерами API WebSocket позволяет открывать именно такое двустороннее соединение с сервером. Кроме того, сервер может отслеживать каждого клиента и отправлять сообщения подмножеству клиентов. То есть с помощью веб-сокетов мы можем пригласить всех друзей в наш чат и отправлять сообщения всем или некоторым из них, а не только одному человеку, как при использовании других протоколов связи.

Веб-сокеты под капотом

Итак, как именно работает это волшебство? Не пугайтесь настройки — современные библиотеки WebSocket, такие как socket.io, настраиваются почти автоматически. Более глубоко разобраться в настройке можно здесь, а мы приведем краткую выжимку по устройству веб-сокетов.

В тексте мы уже несколько раз упоминали HTTP — протокол и набор правил взаимодействия компьютеров в сети. Он состоит из запросов и ответов, каждый из которых содержит строку запроса («GET /assets/icon.png»), заголовки и необязательное тело сообщения, используемое, например, в POST-запросах для отправки некоторых данных на сервер.

WebSocket — это еще один протокол для отправки и получения сообщений. Как и HTTP, веб-сокеты отправляют сообщения через соединение TCP (протокол управления передачей данных). Это стандарт, который обеспечивает надежную и предсказуемую доставку данных, отправляемых в пакетах.

Читайте также: DevTools: как открыть инструменты разработчика в браузере и почему они делают работу эффективнее

Итак, HTTP и WebSocket используют один и тот же механизм доставки на уровне пакетов, но протоколы структурирования сообщений у них различаются:

После обновления соединения протокол переключается с HTTP на WebSocket. И хотя все пакеты по-прежнему отправляются через TCP, связь теперь соответствует формату сообщений WebSocket. Это происходит, потому что TCP является дуплексным протоколом, где клиент и сервер могут отправлять сообщения одновременно. Все данные могут быть фрагментированы, поэтому через этот формат можно отправить даже очень большое сообщение — например, изображение. В этом случае веб-сокеты разбивают его на фреймы. Каждый фрейм содержит небольшой заголовок, который указывает длину и тип полезной нагрузки, а также информацию, является ли этот кадр последним.

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

Наконец, когда соединение можно закрыть, либо клиент, либо сервер могут отправить сообщение «закрыть».

Как все это работает

В нашем примере для внешнего интерфейса мы будем использовать JavaScript, с помощью которого установим соединение с сервером с поддержкой WebSockets. После этого интерфейс будет воспринимать сообщения как события JavaScript — так же, как он обрабатывает все генерируемые пользователем события, такие как клики и нажатия клавиш.

В ванильном JavaScript мы делаем это, создавая новый объект WebSocket и добавляя обработчик событий для действий open, message и close. Мы также используем метод send для отправки данных на сервер:

const socket = new WebSocket('ws://localhost:8080'); 
socket.addEventListener('open', (event) => { 
  socket.send('Hello Server!'); 
}); 

socket.addEventListener('message', (event) => { 
  console.log('Message from server ', event.data); 
});

socket.addEventListener('close', (event) => { 
  console.log('The connection has been closed'); 
});

На сервере нам нужно аналогично обработать запросы веб-сокетов. Например, в Node.js мы можем использовать популярный пакет ws для открытия соединения и обработки сообщений:

const WebSocket = require('ws');
const ws = new WebSocket.Server({ port: 8080 });

ws.on('connection', (wsConnection) => {
  wsConnection.on('message', (message) => {
    console.log(`server received: ${message}`);
  });

  wsConnection.send('got your message!');
});

Хотя в этом примере мы отправляем просто строки, самым распространенным вариантом использования WebSocket является отправка строковых данных в формате JSON, либо в двоичном формате. Это позволяет структурировать сообщения в удобном для пользователя виде.

В качестве еще одного примера можно привести Socket.io, популярный интерфейсный фреймворк для создания и управления соединениями веб-сокетов. В нем есть фантастическое пошаговое руководство по созданию приложения для чата Node/JavaScript. Эта библиотека автоматически переключается между WebSocket и методом «Долгий опрос», а также упрощает рассылку сообщений группам подключенных пользователей.

Как устанавливать соединение веб-сокетов

Процесс начинается с рукопожатия WebSocket — он включает в себя использование ws или wss, о котором мы писали чуть выше. Чтобы быстрее понять их, можно считать ws или wss эквивалентными HTTP и безопасному HTTP (HTTPS), соответственно.

Ожидается, что при использовании этой схемы серверы и клиенты будут следовать стандартному протоколу подключения WebSocket. Установка соединения веб-сокетов начинается с обновления HTTP-запроса, который содержит пару заголовков, таких как Connection: Upgrade, Upgrade: WebSocket, Sec-WebSocket-Key.

Запрос

Заголовок Upgrade в коде запроса ниже означает рукопожатие WebSocket, в то время как Sec-WebSocket-Key содержит случайное значение с использованием кодировки Base64. Это значение произвольно генерируется во время каждого рукопожатия WebSocket. Кроме того, заголовок ключа также является частью этого запроса.

Перечисленные выше заголовки образуют GET-запрос:

GET ws://websocketexample.com:8181/ HTTP/1.1
Host: localhost:8181
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: b6gjhT32u488lpuRwKaOWs==

Ответ

При получении ответа заголовок Sec-WebSocket-Accept содержит специальным образом вычисленный хэш из переданного значения в заголовке Sec-WebSocket-Key. Это связано со спецификацией протокола — такой подход используется для подтверждения, что сервер поддерживает веб-сокет и позволяет обойти ошибки, в случае, когда веб-сокеты не поддерживаются:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: rG8wsswmHTJ85lJgAE3M5RTmcCE=

Где используют веб-сокеты

Хотя вы можете вручную написать серверный код с использованием веб-сокетов, они уже очень удобно встроены во многие популярные платформы. Помимо Socket.io, существует множество других реализаций веб-сокетов в разных языках программирования:

Итак, какие типы проектов чаще всего используют веб-сокеты?

Итоги

Протокол WebSocket — прекрасный инструмент для создания сервисов, которые могут обновляться в реальном времени. Но, как и все инструменты, это не панацея.

Использование WebSocket может быть излишним для простых приложений. Для обычной ленты новостей, ленты метрик или любого приложения, в котором вам нужно обновить контент, но не получать информацию взамен, веб-сокеты будут просто не нужны. Особенно учитывая, что настраивать запросы и HTTP-вызовы намного проще, чем веб-сокеты.