WebSockets

Что это такое

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

Почему это важно

Для чата, совместного редактирования, присутствия и живых дашбордов, где любая сторона может породить сообщение в любой момент, модель «запрос — ответ» у HTTP подходит плохо. Опрос приближает её и тратит запросы впустую, тогда как долгоживущее соединение отправляет только изменившееся. А плата — соединение с состоянием на каждого пользователя, и это полностью меняет эксплуатационную картину: сервер должен держать это состояние, прокси нужно настроить так, чтобы его пропускать, а при развёртывании думать, как соединения закрывают.

Ключевые идеи

Обновление соединения согласуют обычным запросом

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

GET /chat  Upgrade: websocket  ->  101 Switching Protocols

Любая сторона может отправить сообщение в любой момент, поэтому ответ не с чем сопоставить

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

Соединение — это состояние, а состояние не переживает перезапуск

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

Масштабирование — это либо липкая маршрутизация, либо общее состояние

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

Переподключение, проверки живости и задержка — часть протокола, а не дополнение

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

Токен предъявляют во время рукопожатия, и соединение может пережить его: то, которое никогда не проходит повторную аутентификацию, остаётся действительным после отзыва учётных данных. Это окно ограничивают периодической повторной аутентификацией или предельным временем жизни соединения.

Часто это не тот инструмент, который нужен

Для ленты, где говорит только сервер, server-sent events поверх обычного ответа HTTP делают ту же работу меньшим числом механизмов и с автоматическим переподключением. Клиенту, который опрашивает раз в несколько секунд и терпит эту задержку, проще в эксплуатации и кэшировании обычный HTTP. Однако своя цена у WebSockets оправдана лишь тогда, когда взаимодействие действительно двустороннее и частое, — а для ленты, идущей в основном от сервера к клиенту, они добавляют состояние, настройку прокси и логику переподключения без всякой выгоды.

Практика

Сравните три транспорта для одного требования

Возьмите живое табло, которое обновляется каждые несколько секунд и только читается, и опишите его через опрос, через поток server-sent events и через WebSocket.

Готово, когда: вы можете назвать эксплуатационную цену, которую добавляет каждый вариант, и обосновать тот, который выпустили бы.

Спроектируйте путь переподключения

Опишите поведение клиента, у которого соединение рвётся посреди сессии: как он обнаруживает разрыв, сколько ждёт перед повтором и как узнаёт о пропущенном.

Готово, когда: замысел называет стратегию задержки, источник восстановления и поведение на случай, если сервер недоступен час.

Частые ошибки

  • Использование WebSockets для ленты, идущей только от сервера к клиенту — состояние, настройка прокси и логика переподключения добавляются впустую; исправить: используйте односторонний поток или опрос.
  • Забытый пропуск обновления на прокси — в продакшене соединение не устанавливается, хотя локально работает; исправить: настройте прокси осознанно.
  • Переподключение в плотном цикле — клиенты превращают сбой в поток запросов; исправить: экспоненциальная задержка с разбросом.
  • Допущение, что сообщения переживают отключение — всё, отправленное пока клиент был офлайн, теряется; исправить: сохраняйте и сверяйте при переподключении.
  • Отсутствие повторной аутентификации долгоживущего соединения — отозванные учётные данные остаются действительными; исправить: ограничьте время жизни соединения или периодически аутентифицируйтесь заново.

Что читать дальше

  • RESTful API (05-backend-data/06-restful-api) — модель «запрос — ответ», от которой уходит эта запись; читайте в первую очередь.
  • Nginx (06-cloud-devops/19-nginx) — обратный прокси, которому нужно разрешить обновление.
  • Очереди сообщений (Kafka / RabbitMQ) (05-backend-data/17-message-queues-kafka-rabbitmq) — надёжная доставка, которой сброшенное соединение WebSocket дать не может.