Справочник по WebSocket

Узнайте о технологии, лежащей в основе Интернета в реальном времени, и создайте свое первое веб-приложение на базе WebSockets

Алекс Диакону

2022

перевод В.Айсин

Особые благодарности

В произвольном порядке: Джо Франкетти (за участие в главе 4 и создание демонстрационного приложения), Рамиру Нуньес Досио (за то, что побудил меня написать книгу, дал ценные советы и удалил блокировщики), Джонатану Мерсье-Ганади (за технический обзор), Джо Стичбери (за редакционную рецензию), Леони Уортону, Крису Хипсону, Джейми Уотсону (за всю работу по дизайну), Бену Гэмблу (за помощь в определении и написании главы 5).

Об авторе

Алекс Диакону — энтузиаст WebSocket, который провел большую часть своей профессиональной карьеры, работая бок о бок с командами инженеров, менеджерами по техническим продуктам и системными архитекторами, одновременно создавая статьи о веб-технологиях. В настоящее время Алекс работает автором технического контента в Ably, где он исследует мир технологий реального времени и пишет о многих проблемах, связанных с архитектурами, управляемыми событиями, и распределенными системами. В свободное время Алекс любит ходить в походы, смотреть любимую футбольную команду, играть в баскетбол и читать научную фантастику и историю.

Предисловие

Наш повседневный цифровой опыт находится в разгаре революции в реальном времени. Говорим ли мы о виртуальных мероприятиях, образовательных технологиях, новостях и финансовой информации, устройствах Интернета вещей, отслеживании активов и логистике, обновлении результатов в реальном времени или играх, потребители все чаще ожидают, что цифровой опыт в реальном времени станет стандартом. А что может быть лучше для обеспечения такого взаимодействия в реальном времени, чем WebSockets?

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

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

Для кого эта книга

Эта книга предназначена для разработчиков (и любой другой технической аудитории), которые хотят:

Для получения максимальной отдачи от этой книги необходимы знание/знакомство с HTML, JavaScript (и Node.js), HTTP, веб-API и веб-разработкой.

О чем эта книга

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

Глава 2: Протокол WebSocket охватывает ключевые аспекты, связанные с протоколом WebSocket. Вы узнаете, как установить соединение WebSocket и обмениваться сообщениями, какие данные можно отправлять через WebSockets, какие типы расширений и подпротоколов можно использовать для расширения WebSocket.

Глава 3: API WebSocket содержит подробную информацию о составляющих компонентах API WebSocket — его событиях, методах и свойствах, а также примеры использования каждого из них.

Глава 4: Создание веб-приложения с помощью WebSockets содержит подробные пошаговые инструкции по созданию веб-приложения реального времени с помощью WebSockets и Node.js: интерактивную демонстрацию совместного использования позиции курсора.

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

Ресурсы — коллекция статей, видеороликов и решений WebSocket, которые вы, возможно, захотите изучить.

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

Глава 1
Дорога к веб-сокетам

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

Первые веб-приложения реального времени начали появляться в 2000-х годах, пытаясь обеспечить гибкий, динамичный и интерактивный опыт для конечных пользователей. Однако в то время доступ к сети в реальном времени был более трудным и медленным, чем мы привыкли сейчас; он был создан путем взлома существующих технологий на основе HTTP, которые не были разработаны и оптимизированы для приложений реального времени. Быстро стало очевидно, что необходима лучшая альтернатива.

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

Рождение Всемирной паутины

В 1989 году, работая инженером-программистом в Европейской организации ядерных исследований (ЦЕРН), Тим Бернерс-Ли разочаровался в том, насколько сложно получить доступ к информации, хранящейся на разных компьютерах (и, вдобавок ко всему, нужно запускать разные типы программного обеспечения). Это побудило Бернерса-Ли разработать проект под названием «WorldWideWeb».

Проект предлагал «сеть» гипертекстовых документов, которую можно было просматривать браузерами через Интернет с использованием клиент-серверной архитектуры. Сеть имела потенциал объединить мир так, как это было ранее невозможно, и значительно облегчила людям во всем мире получение информации, обмен информацией и общение. Первоначально использовавшийся в ЦЕРНе, Интернет вскоре стал доступен всему миру: первые веб-сайты для повседневного использования начали появляться в 1993–1994 годах.

Бернерсу-Ли удалось создать сеть, объединив две существующие технологии: гипертекст и Интернет. В процессе он разработал три основных строительных блока:

Эта первоначальная версия HTTP[1] (широко известная как HTTP/0.9), разработанная Бернерсом-Ли, была невероятно простой. Запросы состояли из одной строки и начинались с единственного поддерживаемого метода GET, за которым следовал путь к ресурсу:

GET /mypage.html

Ответ, содержащий только гипертекст, также был чрезвычайно простым:

<HTML>
My HTML page
</HTML>

Не было заголовков HTTP, кодов состояния, URL-адресов или управления версиями, и соединение было разорвано сразу после получения ответа.

Поскольку интерес к Интернету стремительно рос, а HTTP/0.9 был сильно ограничен, и браузеры, и серверы быстро сделали протокол более универсальным, добавив новые возможности. Некоторые ключевые изменения:

Эти модификации не были сделаны упорядоченным или согласованным образом, что привело к появлению различных разновидностей HTTP/0.9 в дикой природе, что, в свою очередь, вызвало проблемы совместимости. Для решения этих проблем была создана HTTP Working Group[2], которая в 1996 году опубликовала HTTP/1.0[3] (определенный в RFC 1945). Это был информационный RFC, просто документирующий все случаи использования на тот момент. Таким образом, HTTP/1.0 не считается формальной спецификацией или интернет-стандартом.

Параллельно с работой над HTTP/1.0 велась работа по надлежащей стандартизации HTTP. Первая стандартизированная версия протокола HTTP/1.1 была первоначально определена в RFC 2068[4] и выпущена в январе 1997 года. С тех пор было выпущено несколько последующих RFC[5] HTTP/1.1, последний раз в 2014 году.

HTTP/1.1 содержит множество улучшений функций и оптимизации производительности, в том числе:

  1. Исходный HTTP, определенный в 1991 году.
  2. The IETF HTTP Working Group
  3. RFC 1945: Hypertext Transfer Protocol - HTTP/1.0
  4. RFC 2068: Hypertext Transfer Protocol - HTTP/1.1
  5. IETF HTTP Working Group, HTTP Documentation, Core Specifications

JavaScript присоединяется к нам

В то время как HTTP развивался и стандартизировался, интерес и распространение Интернета быстро росли. Быстро началась конкуренция (так называемые «войны браузеров») за доминирование в использовании веб-браузеров, в ходе которой первоначально Microsoft Internet Explorer противопоставлялся Netscape Navigator. Обе компании хотели иметь лучший браузер, поэтому в их браузеры неизбежно регулярно добавлялись функции и возможности. Эта конкуренция за превосходство стала катализатором быстрых технологических прорывов.

В 1995 году компания Netscape наняла Брендана Эйха с целью встроить возможности создания сценариев в свой браузер Netscape Navigator. Так родился JavaScript. Первая версия языка была простой, и ее можно было использовать только для некоторых целей, например для базовой проверки полей ввода перед отправкой HTML-формы на сервер. Каким бы ограниченным он ни был в то время, JavaScript привнес динамическое взаимодействие в сеть, которая до этого момента была полностью статичной. Постепенно JavaScript был усовершенствован, стандартизирован и принят всеми браузерами, став одной из основных технологий Интернета, каким мы его знаем сегодня.

Создание сети в реальном времени

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

Теперь мы рассмотрим основные HTTP-ориентированные модели проектирования, возникшие для разработки приложений реального времени: AJAX и Comet.

AJAX

AJAX (сокращение от Asynchronous JavaScript и XML) — это метод асинхронного обмена данными с сервером в фоновом режиме и обновления частей веб-страницы — без необходимости обновления всей страницы (обратной передачи).

Термин AJAX, впервые публично использованный в 2005[6] году, включает в себя несколько технологий:

Стоит подчеркнуть важность XMLHttpRequest, встроенного объекта браузера, который позволяет выполнять HTTP-запросы в JavaScript. Концепция XHR была первоначально создана в Microsoft и включена в Internet Explorer 5 в 1999 году. Всего через несколько лет XMLHttpRequest получит широкое распространение и будет реализован в Mozilla Firefox, Safari, Opera и других браузерах.

Давайте теперь посмотрим, как работает AJAX, сравнив его с классической моделью создания веб-приложения.

Рисунок 1.1. Классическая модель веб-приложения в сравнении с моделью AJAX.

В классической модели большинство действий пользователя в пользовательском интерфейсе запускают HTTP-запрос, отправляемый на сервер. Сервер обрабатывает запрос и возвращает клиенту всю HTML-страницу.

Для сравнения, AJAX представляет посредника (движок AJAX) между пользователем и сервером. Хотя это может показаться нелогичным, посредник значительно повышает оперативность. Вместо загрузки веб-страницы в начале сеанса клиент загружает движок AJAX, который отвечает за:

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

  1. Джесси Джеймс Гарретт, Ajax: новый подход к веб-приложениям
  2. XMLHttpRequest Living Standard

Comet

Comet[8], созданный в 2006 году, представляет собой модель дизайна веб-приложений, которая позволяет веб-серверу передавать данные в браузер. Подобно AJAX, Comet обеспечивает асинхронную связь. В отличие от классического AJAX (когда клиент периодически опрашивает сервер на наличие обновлений), Comet использует долгоживущие HTTP-соединения, позволяющие серверу отправлять обновления всякий раз, когда они доступны, без явного запроса клиента.

Модель Comet прославилась благодаря таким организациям, как Google и Meebo. Первая первоначально использовала Comet для добавления веб-чата в Gmail, а Meebo использовала его для своего веб-приложения для чата, которое позволяло пользователям подключаться к чат-платформам AOL, Yahoo и Microsoft через браузер. За короткое время Comet стал стандартом по умолчанию для создания адаптивных интерактивных веб-приложений.

Для реализации модели Comet можно использовать несколько различных методов, наиболее известными из которых являются длинный опрос[9] и потоковая передача HTTP. Давайте теперь быстро рассмотрим, как эти два метода работают.

  1. Алекс Рассел, Comet: данные с низкой задержкой для браузера
  2. Длинный опрос – концепции и соображения
Длинный опрос (Long polling)

По сути, это более эффективная форма опроса, длинный опрос — это метод, при котором сервер решает удерживать соединение клиента открытым как можно дольше, доставляя ответ только после того, как данные станут доступны или будет достигнуто пороговое значение тайм-аута. После получения ответа сервера клиент обычно немедленно отправляет еще один запрос. Длинный опрос часто реализуется на основе XMLHttpRequest — того же объекта, который играет ключевую роль в модели AJAX.

Рисунок 1.2: Общий обзор длительного опроса

Потоковая передача HTTP

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

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

Вот пример фрагментированного ответа:

HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked

7\r\n
Chunked\r\n
8\r\n
Response\r\n
7\r\n
Example\r\n
0\r\n
\r\n

Server-Sent Events[10] (SSE), — это еще один вариант, который можно использовать для реализации потоковой передачи HTTP. SSE — это серверная технология push, обычно используемая для отправки обновлений сообщений или непрерывных потоков данных клиенту браузера. SSE стремится улучшить нативную межбраузерную потоковую передачу между сервером и клиентом с помощью API JavaScript под названием EventSource, стандартизированного[11] как часть HTML5 Консорциумом World Wide Web (W3C).

Вот краткий пример открытия потока через SSE:

var source = new EventSource('URL_TO_EVENT_STREAM');
source.onopen = function () {
  console.log('connection to stream has been opened');
};
source.onerror = function (error) {
  console.log('An error has occurred while receiving stream', error);
};
source.onmessage = function (stream) {
  console.log('received stream', stream);
};
  1. Server-Sent Events (SSE): A Conceptual Deep Dive
  2. Server-sent events, HTML Living Standard

Ограничения HTTP

AJAX и Comet проложили путь к созданию динамических веб-приложений реального времени. Однако, даже несмотря на то, что они продолжают использоваться в настоящее время, в меньшей степени, и AJAX, и Comet имеют свои недостатки.

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

Взлом технологий на основе HTTP для эмуляции Интернета в реальном времени неизбежно должен был привести к разного рода недостаткам. Сейчас мы рассмотрим основные из них (не будучи исчерпывающим).

Ограниченная масштабируемость

Например, HTTP-опрос включает отправку запросов на сервер через фиксированные интервалы времени, чтобы узнать, есть ли какие-либо новые обновления для их получения. Высокая частота опросов приводит к увеличению сетевого трафика и требований к серверу; это плохо масштабируется, особенно по мере роста числа одновременных пользователей. Низкая частота опроса будет менее обременительной для сервера, но может привести к доставке устаревшей информации, потерявшей (частично) свою ценность.

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

Ненадежный порядок заказа и гарантии доставки сообщений

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

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

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

Задержка

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

Хотя методы потоковой передачи HTTP лучше подходят для более низких задержек, чем (длительный) опрос, они ограничены (как и любой другой механизм на основе HTTP) заголовками HTTP, которые увеличивают размер сообщения и вызывают ненужные задержки. Часто заголовки HTTP в ответе перевешивают доставляемые основные данные[12].

Нет двунаправленной потоковой передачи

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

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

  1. Мэтью О'Риордан, Google — опросы, как будто сейчас 90-е

Введение веб-сокетов

В 2008 году трудности и ограничения использования Comet при реализации чего-либо, напоминающего реальное время, особенно остро ощущались разработчиками Майклом Картером и Яном Хиксоном. Благодаря сотрудничеству над списками рассылки IRC[13] и W3C[14] они разработали план по внедрению нового стандарта для современного общения в сети в реальном времени. Таким образом, было придумано название «WebSocket».

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

Рисунок 1.3: WebSockets в сравнении с традиционной моделью HTTP-запрос/ответ

Технология WebSocket включает в себя два основных строительных блока:

  1. IRC logs, 18.06.2008
  2. W3C mailing lists, TCPConnection feedback

Сравнение WebSockets и HTTP

В то время как HTTP управляется запросами, WebSockets управляются событиями. Таблица ниже иллюстрирует фундаментальные различия между двумя технологиями.

Таблица 1.1: Сравнение характеристик WebSockets и HTTP/1.1
WEBSOCKETS HTTP/1.1
Коммуникация
Полнодуплексная Полудуплексная
Схема обмена сообщениями
Двунаправленная Запрос-ответ
Server push
Основная функция Не поддерживается изначально
Оверхэд (Накладные расходы)
Умеренные затраты на установление соединения и минимальные затраты на каждое сообщение. Умеренные накладные расходы на запрос/соединение.
Состояние
С сохранением состояния Без сохранения состояния

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

Варианты использования и преимущества

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

Мы можем разделить случаи использования WebSocket на две отдельные категории:

Вот некоторые из основных преимуществ использования WebSockets:

Принятие

Интерфейс WebSocket, первоначально называвшийся TCPConnection, вошел в спецификацию HTML5[15], которая впервые была выпущена в виде черновика в январе 2008 года. Протокол WebSocket был стандартизирован в 2011 году посредством RFC 6455; подробнее об этом в Главе 2: Протокол WebSocket.

В декабре 2009 года Google Chrome 4 стал первым браузером с полной поддержкой WebSockets. В следующие несколько лет другие производители браузеров начали следовать этому примеру; сегодня все основные браузеры имеют полную поддержку WebSockets. Выходя за рамки веб-браузеров, WebSockets можно использовать для обеспечения связи в реальном времени между различными типами пользовательских агентов, например мобильными приложениями.

В настоящее время WebSockets являются ключевой технологией для создания масштабируемых веб-приложений реального времени. API и протокол WebSocket имеют процветающее сообщество, о чем свидетельствуют разнообразные варианты клиентов и серверов (как с открытым исходным кодом, так и коммерческие), экосистемы разработчиков и множество реальных реализаций.

  1. Web sockets, HTML Living Standard

Глава 2
Протокол WebSocket

В декабре 2011 года Инженерная группа Интернета (IETF) стандартизировала протокол WebSocket посредством RFC 6455[16]. В координации с IETF Управление по присвоению номеров Интернета (IANA) поддерживает реестры протоколов WebSocket[17], которые определяют многие коды и идентификаторы параметров, используемые протоколом.

В этой главе рассматриваются ключевые моменты, связанные с протоколом WebSocket, как описано в RFC 6455. Вы узнаете, как установить соединение WebSocket и обмениваться сообщениями, какие данные можно отправлять через WebSocket, какие типы расширений и подпротоколов вы можете использовать для расширения WebSockets.

  1. RFC 6455: The WebSocket Protocol
  2. IANA WebSocket Protocol Registries

Обзор протокола

Протокол WebSocket обеспечивает непрерывную полнодуплексную двустороннюю связь между веб-серверами и веб-клиентами через базовое TCP-соединение.

В двух словах, базовый протокол WebSocket состоит из открытия рукопожатия (обновление соединения с HTTP на WebSockets), за которым следует передача данных. После того, как клиент и сервер успешно согласовывают начальное рукопожатие, соединение WebSocket действует как постоянный полнодуплексный канал связи, где каждая сторона может независимо отправлять данные по своему желанию. Клиенты и серверы передают данные туда и обратно в концептуальных единицах, называемых сообщениями, которые, как мы вскоре опишем, могут состоять из одного или нескольких кадров. Как только соединение WebSocket выполнит свою задачу, его можно прервать с помощью закрывающего рукопожатия.

Рисунок 2.1: Общий обзор соединения WebSocket.

Схемы и синтаксис URI

Протокол WebSocket определяет две схемы URI для трафика между сервером и клиентом:

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

Рисунок 2.2: Компоненты WebSocket URI

Стоит отметить, что:

Открытие рукопожатия

Процесс установления соединения WebSocket известен как открывающее рукопожатие и состоит из обмена запросами/ответами HTTP/1.1 между клиентом и сервером. Клиент всегда инициирует рукопожатие; он отправляет запрос GET на сервер, указывая, что он хочет обновить соединение с HTTP на WebSockets. Сервер должен вернуть код ответа протокола коммутации HTTP 101 для установки соединения WebSocket. Как только это произойдет, соединение WebSocket можно будет использовать для постоянной двунаправленной полнодуплексной связи между сервером и клиентом.

  1. RFC 8441: Bootstrapping WebSockets with HTTP/2

Запрос клиента

Вот базовый пример запроса GET, сделанного клиентом для инициирования открывающего рукопожатия:

GET wss://example.com:8181/ HTTP/1.1
Host: localhost: 8181
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: zy6Dy9mSAIM7GJZNf9rI1A==

Запрос должен содержать следующие заголовки:

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

Ответ сервера

Сервер должен вернуть код ответа протокола коммутации HTTP 101 для успешного установления соединения WebSocket:

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

Ответ должен содержать несколько заголовков: Connection, Upgrade и Sec-WebSocket-Accept. Могут быть включены и другие необязательные заголовки, такие как Sec-WebSocket-Extensions или Sec-WebSocket-Protocol (при условии, что они были переданы в клиентском запросе). Дополнительные сведения см. в разделе «Открытие заголовков подтверждения» в этой главе.

Открытие заголовков рукопожатия

В таблице ниже описаны заголовки, используемые клиентом и сервером во время открытия рукопожатия.

Таблица 2.1: Открытие заголовков рукопожатия
ЗАГОЛОВОК ОБЯЗАТЕЛЬНОСТЬ ОПИСАНИЕ
Host Да Имя хоста и, при необходимости, номер порта сервера, на который отправляется запрос. Если номер порта не указан, подразумевается значение по умолчанию (80 для ws или 433 для wss).
Connection Да Указывает, что клиент хочет согласовать изменение способа использования соединения. Значение должно быть Upgrade.
Также возвращается сервером.
Upgrade Да Указывает, что клиент хочет обновить соединение до альтернативных средств связи.
Значение должно быть websocket.
Также возвращается сервером.
Sec-WebSocket-Version Да Единственное допустимое значение — 13. Любая другая версия, переданная в этом заголовке, недействительна.
Sec-WebSocket-Key Да Одноразовое случайное значение (nonce) в кодировке Base64, отправленное клиентом. Автоматически обрабатывается большинством библиотек WebSocket или с помощью класса WebSocket, предоставляемого в браузерах.
Дополнительные сведения см. в разделах Sec-WebSocket-Key и Sec-WebSocket-Accept в этой главе.
Sec-WebSocket-Accept Да Хеш-значение SHA-1 в кодировке Base64, возвращаемое сервером как прямой ответ на Sec-WebSocket-Key.
Указывает, что сервер готов инициировать соединение WebSocket.
Дополнительные сведения см. в разделах Sec-WebSocket-Key и Sec-WebSocket-Accept в этой главе.
Sec-WebSocket-Protocol Нет Необязательное поле заголовка, содержащее список значений, указывающих, какие подпротоколы хочет использовать клиент, упорядоченные по предпочтениям.
Серверу необходимо включить это поле вместе с одним из выбранных значений подпротокола (первым из списка, который он поддерживает) в ответ.
Дополнительные сведения см. в разделе «Подпротоколы» далее в этой главе.
Sec-WebSocket-Extensions Нет Необязательное поле заголовка, первоначально отправляемое от клиента на сервер, а затем впоследствии отправляемое с сервера клиенту.
Это помогает клиенту и серверу согласовать набор расширений уровня протокола, которые будут использоваться на протяжении всего соединения.
Дополнительные сведения см. в разделе «Расширения» далее в этой главе.
Origin Нет Поле заголовка, отправляемое всеми браузерными клиентами (необязательно для небраузерных клиентов).
Используется для защиты от несанкционированного использования сервера WebSocket из разных источников сценариями, использующими API WebSocket в веб-браузере.
Соединение будет отклонено, если указанный Origin неприемлем для сервера.

Некоторые общие дополнительные заголовки, такие как User-Agent, Referer или Cookie, также могут использоваться при начальном рукопожатии. Однако мы исключили их из таблицы выше, поскольку они не имеют прямого отношения к WebSockets.

Sec-WebSocket-Key и Sec-WebSocket-Accept

Давайте теперь быстро рассмотрим два обязательных заголовка, используемых во время открывающего рукопожатия: Sec-WebSocket-Key и Sec-WebSocket-Accept. Вместе эти заголовки необходимы для обеспечения возможности взаимодействия как сервера, так и клиента через WebSockets.

Во-первых, у нас есть Sec-WebSocket-Key, который передается клиентом на сервер и содержит 16-байтовое одноразовое случайное значение (nonce) в кодировке Base64. Его цель — гарантировать, что сервер не принимает соединения от клиентов, отличных от WebSocket (например, клиентов HTTP), которые используются (или неправильно настраиваются) для отправки данных на ничего не подозревающие серверы WebSocket. Вот пример Sec-WebSocket-Key:

Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

Что касается Sec-WebSocket-Key, ответ сервера включает заголовок Sec-WebSocket-Accept. Этот заголовок содержит хеш-значение SHA-1 в кодировке Base64, созданное путем объединения одноразового номера Sec-WebSocket-Key, отправленного клиентом, и статического значения (UUID) 258EAFA5-E914-47DA-95CA-C5AB0DC85B11.

На основе приведенного выше примера Sec-WebSocket-Key приведен заголовок Sec-WebSocket-Accept, возвращаемый сервером:

Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Фреймы сообщений

После успешного открытия соединения клиент и сервер могут использовать соединение WebSocket для обмена сообщениями в полнодуплексном режиме. Сообщение WebSocket состоит из одного или нескольких кадров (более подробную информацию о многокадровых сообщениях см. в разделе «Биты и фрагментация FIN» далее в этой главе).

Фрейм WebSocket имеет двоичный синтаксис и содержит несколько фрагментов информации, как показано на следующем рисунке:

Рисунок 2.3: Анатомия фрейма WebSocket.

Давайте кратко опишем их:

Теперь мы более подробно рассмотрим все эти составные части фрейма WebSocket.

Бит FIN и фрагментация

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

Согласно RFC 6455[19], другой вариант использования фрагментации представлен мультиплексированием, где «[...] нежелательно, чтобы большое сообщение в одном логическом канале монополизировало выходной канал, поэтому мультиплексирование должно быть свободным, чтобы разделить сообщение на более мелкие фрагменты, чтобы лучше разделить выходной канал».

Давайте теперь рассмотрим несколько быстрых примеров, иллюстрирующих фрагментацию. Вот как может выглядеть однокадровое сообщение:

0x81 0x05 0x48 0x65 0x6c 0x6c 0x6f (contains "Hello")

Для сравнения, при фрагментации то же сообщение будет выглядеть так:

0x01 0x03 0x48 0x65 0x6c (contains "Hel")
0x80 0x02 0x6c 0x6f (contains "lo")

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

  1. RFC 6455: The WebSocket Protocol

RSV 1-3

RSV1, RSV2 и RSV3 — зарезервированные биты. Они должны быть равны 0, если во время открытия соединения не было согласовано расширение, определяющее ненулевые значения. Дополнительную информацию см. в разделе «Расширения» в этой главе.

Коды операций

Каждый кадр имеет код операции, который определяет, как интерпретировать полезные данные этого кадра. Используемые в настоящее время стандартные коды операций определены в RFC 6455 и поддерживаются IANA[20].

Таблица 2.2: Коды операций кадра
OPCODE ОПИСАНИЕ
0 Кадр продолжения; продолжает полезную нагрузку из предыдущего кадра.
1 Указывает текстовый фрейм (текстовые данные UTF-8).
2 Указывает двоичный кадр.
3-7 Зарезервировано для пользовательских кадров данных.
8 Соединение закрытого фрейма; приводит к разрыву соединения.
9 Кадр проверки связи. Служит механизмом контрольного сигнала, гарантируя, что соединение все еще живо. Получатель должен ответить фреймом «понг».
10 Фрейм для понга. Служит механизмом контрольного сигнала, гарантируя, что соединение все еще живо. Отправляется в качестве ответа после получения кадра проверки связи.
11-15 Зарезервировано для пользовательских кадров управления.
  1. IANA WebSocket Opcode Registry

Маскировка

Каждый кадр WebSocket, отправляемый клиентом на сервер, должен быть замаскирован с помощью случайного masking-key (32-битное значение). Этот ключ содержится внутри кадра и используется для сокрытия данных полезной нагрузки. Однако когда данные передаются в обратном порядке, сервер не должен маскировать никакие кадры, которые он отправляет клиенту.

На стороне сервера кадры, полученные от клиента, должны быть демаскированы перед дальнейшей обработкой. Вот пример того, как вы можете это сделать:

var unmask = function(mask, buffer) {
  var payload = new Buffer(buffer.length);
  for (var i=0; i<buffer.length; i++) {
    payload[i] = mask[i % 4] ^ buffer[i];
  }
  return payload;
}

Длина полезной нагрузки

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

Данные полезной нагрузки

Протокол WebSocket поддерживает два типа полезных данных: text (текст в формате Юникода UTF-8) и binary. В JavaScript текстовые данные называются строками, а двоичные данные представлены классами ArrayBuffer и Blob. Подробные сведения об отправке и получении данных через WebSockets, а также примеры использования см. в главе 3: API WebSocket.

Тип полезной нагрузки каждого кадра указывается с помощью 4-битного opcode (кода операции) (1 для текста или 2 для двоичного кода).

Заключительное рукопожатие

По сравнению с вступительным рукопожатием, закрывающее рукопожатие представляет собой гораздо более простой процесс. Вы инициируете его, отправляя кадр close с кодом операции 8. Помимо кода операции, кадр закрытия может содержать тело, указывающее причину закрытия. Это тело состоит из кода состояния (целого числа) и строки в кодировке UTF-8 (причины).

Стандартные коды состояния, которые можно использовать во время закрывающего подтверждения, определены в RFC 6455; дополнительные пользовательские коды закрытия можно зарегистрировать в IANA[21].

Таблица 2.3: Коды состояния закрытия соединения
КОД СОСТОЯНИЯ НАЗВАНИЕ ОПИСАНИЕ
0-999 N/A Коды ниже 1000 недействительны и не могут использоваться.
1000 Normal closure Указывает на нормальное закрытие, означающее, что цель, ради которой было установлено соединение WebSocket, достигнута.
1001 Going away Следует использовать при закрытии соединения, и нет ожиданий, что будет предпринята попытка последующего соединения (например, выключение сервера или уход браузера со страницы).
1002 Protocol error Конечная точка разрывает соединение из-за ошибки протокола.
1003 Unsupported data Соединение разрывается, поскольку конечная точка получила данные типа, который она не может обработать (например, конечная точка, работающая только с текстом, получает двоичные данные).
1004 Reserved Значение может быть определено в будущем.
1005 No status received Используется приложениями и API WebSocket для указания того, что код состояния не был получен, хотя он и ожидался.
1006 Abnormal closure Используется приложениями и API WebSocket для указания того, что соединение было закрыто ненормально (например, без отправки или получения кадра close).
1007 Invalid payload data Конечная точка разрывает соединение, поскольку она получила сообщение, содержащее противоречивые данные (например, данные, отличные от UTF-8, в текстовом сообщении).
1008 Policy violation Конечная точка разрывает соединение, поскольку получила сообщение, нарушающее ее политику. Это общий код состояния; его следует использовать, когда другие коды состояния не подходят или если необходимо скрыть определенные сведения о политике.
1009 Message too big Конечная точка разрывает соединение из-за получения кадра данных, который слишком велик для обработки.
1010 Mandatory extension Клиент разрывает соединение, поскольку серверу не удалось согласовать расширение во время открытия соединения.
1011 Internal error Сервер разрывает соединение, поскольку обнаружил непредвиденное условие, которое не позволило ему выполнить запрос.
1012 Service restart Сервер разрывает соединение, поскольку он перезапускается.
1013 Try again later Сервер разрывает соединение из-за временной ситуации, например, из-за перегрузки.
1014 Bad gateway Сервер действовал как шлюз или прокси-сервер и получил недопустимый ответ от вышестоящего сервера. Аналогично коду состояния HTTP 502 Bad Gateway.
1015 TLS handshake Зарезервировано. Указывает, что соединение было закрыто из-за невозможности выполнить подтверждение TLS (например, сертификат сервера не может быть проверен).
1016-1999 N/A Зарезервировано для будущего использования стандартом WebSocket.
2000-2999 N/A Зарезервировано для будущего использования расширениями WebSocket.
3000-3999 N/A Зарезервировано для использования библиотеками, платформами и приложениями. Доступно для регистрации в IANA в порядке очереди.
4000-4999 N/A Диапазон зарезервирован для частного использования в приложениях.

И клиент, и сервер могут инициировать заключительное рукопожатие. При получении кадра закрытия конечная точка (клиент или сервер) должна отправить кадр закрытия в качестве ответа (отражая полученный код состояния).

После того как конечная точка отправила и получила кадр close, закрывающее рукопожатие завершается, и соединение WebSocket считается закрытым.

  1. IANA WebSocket Close Code Number Registry

Подпротоколы

Подпротоколы (это терминология, используемая в RFC 6455) — это протоколы уровня приложения, расположенные поверх необработанного протокола WebSocket. Они помогают определить конкретные форматы и семантику более высокого уровня для обмена данными между клиентом и сервером. Подпротоколы могут гарантировать согласие не только о том, как структурированы данные, но и о том, как связь должна начинаться, продолжаться и в конечном итоге прекращаться.

Субпротоколы можно разделить на три основные категории:

Подпротоколы согласовываются во время вступительного рукопожатия. Клиент использует заголовок Sec-WebSocket-Protocol для передачи одного или нескольких подпротоколов, разделенных запятыми, как показано в этом примере:

Sec-WebSocket-Protocol: amqp, v12.stomp

При условии, что сервер понимает подпротоколы, переданные в клиентском запросе, он должен выбрать один (и только один) и вернуть его вместе с заголовком Sec-WebSocket-Protocol. С этого момента клиент и сервер могут взаимодействовать по согласованному подпротоколу.

  1. IANA WebSocket Subprotocol Name Registry
  2. Kayla Matthews, MQTT: A Conceptual Deep-Dive
  3. The Simple Text Oriented Messaging Protocol (STOMP)

Расширения

Расширения названы так, потому что они расширяют протокол WebSocket. Их можно использовать для добавления новых кодов операций, полей данных и дополнительных возможностей (например, мультиплексирования) к стандартному протоколу WebSocket.

На момент написания в IANA[25] зарегистрировано всего несколько расширений, таких как permessage-deflate, которое сжимает часть полезных данных в кадрах WebSocket. Если вы заинтересованы в разработке собственного расширения, вы можете использовать платформу с открытым исходным кодом, например websocket-extensions[26].

Расширения согласовываются во время вступительного рукопожатия. Клиент использует заголовок Sec-Websocket-Extensions для передачи расширений, которые он хочет использовать, как показано в этом примере:

Sec-WebSocket-Extensions: permessage-deflate, my-custom-extension

При условии, что он поддерживает расширения, отправленные в запросе клиента, сервер должен включить их в ответ вместе с заголовком Sec-WebSocket-Extensions. С этого момента клиент и сервер могут взаимодействовать через WebSockets, используя согласованные ими расширения.

  1. IANA WebSocket Extension Name Registry
  2. The websocket-extensions framework

Безопасность

В этом разделе мы рассмотрим некоторые механизмы, которые вы можете использовать для защиты соединений WebSocket и связи, осуществляемой по протоколу WebSocket. Это ни в коем случае не исчерпывающий раздел; его цель – лишь предоставить общий обзор некоторых соображений, связанных с безопасностью. Более подробная информация о сложной теме безопасности WebSockets будет рассмотрена в будущих версиях этой книги.

Начнем с заголовка Origin, который отправляется всеми браузерными клиентами (необязательно для не-браузеров) на сервер во время открывающего рукопожатия. Заголовок Origin необходим для обеспечения безопасности междоменной связи. В частности, если указанный Origin неприемлем, сервер может не выполнить рукопожатие (обычно возвращая код состояния HTTP 403 Forbidden). Эта возможность может быть чрезвычайно полезна для смягчения атак типа «отказ в обслуживании» (DoS).

Говоря о заголовках, используемых при начальном рукопожатии, следует также упомянуть Sec-WebSocket-Key и Sec-WebSocket-Accept. В двух словах, цель этих заголовков — защитить ничего не подозревающие сервера WebSocket от межпротокольных атак, инициированных клиентами, не являющимися WebSocket. Вместе Sec-WebSocket-Key и Sec-WebSocket-Accept гарантируют, что и клиент, и сервер могут фактически взаимодействовать через WebSockets. Если возникнет какая-либо проблема, связанная с этими двумя заголовками (например, в запросе клиента отсутствует Sec-WebSocket-Key), соединение WebSocket не будет установлено.

До сих пор мы рассмотрели механизмы безопасности, используемые при установлении соединения. Теперь давайте рассмотрим некоторые аспекты, влияющие на безопасность при обмене данными между клиентом и сервером. Прежде всего, чтобы снизить вероятность атак «человек посередине» и «подслушивания» (особенно при обмене критически важными конфиденциальными данными), рекомендуется использовать схему URI wss, которая использует TLS для шифрования соединения, как и https.

Ранее в этой главе мы говорили о кадрах сообщений и упоминали, что кадры, отправляемые клиентом на сервер, необходимо маскировать с помощью случайного masking-key (32-битное значение). Этот ключ содержится внутри кадра и используется для сокрытия данных полезной нагрузки. Перед дальнейшей обработкой кадры должны быть демаскированы сервером. Маскирование делает трафик WebSocket отличным от трафика HTTP, что особенно полезно при использовании прокси-серверов. Это связано с тем, что некоторые прокси-серверы могут «не понимать» протокол WebSocket и, если бы не маска, они могли бы принять его за обычный HTTP-трафик; это может привести к разного рода проблемам, например к отравлению кэша.

  1. RFC 7519: JSON Web Token (JWT)

Глава 3
API веб-сокетов

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

Обзор

API WebSocket, определенный в HTML Living Standard[28], представляет собой технологию, позволяющую открыть постоянный двусторонний полнодуплексный канал связи между веб-клиентом и веб-сервером. Интерфейс WebSocket позволяет асинхронно отправлять сообщения на сервер и получать ответы на основе событий без необходимости опроса обновлений.

Почти все современные браузеры поддерживают WebSocket API[29]. Кроме того, существует множество фреймворков и библиотек — как с открытым исходным кодом, так и коммерческих решений — которые реализуют API WebSocket. Более подробную информацию смотрите в разделе «Ресурсы» этой книги.

В оставшейся части этой главы мы рассмотрим основные возможности API WebSocket. Как вы увидите, он интуитивно понятен, спроектирован с учетом простоты и прост в использовании.

  1. Web sockets, HTML Living Standard
  2. Могу ли я использовать WebSockets?

Сервер веб-сокетов

Сервер WebSocket может быть написан на любом серверном языке программирования, поддерживающем сокеты Беркли[30]. Сервер прослушивает входящие соединения WebSocket, используя стандартный сокет TCP. После согласования начального рукопожатия сервер должен иметь возможность отправлять, получать и обрабатывать сообщения WebSocket.

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

  1. Berkeley sockets

Конструктор WebSocket

Чтобы начать работу с API WebSocket на стороне клиента, первое, что нужно сделать, — это создать экземпляр объекта WebSocket, который автоматически попытается открыть соединение с сервером:

const socket = new WebSocket('wss://example.org');

Конструктор WebSocket содержит обязательный параметр — URL-адрес сервера WebSocket. Кроме того, может быть включен дополнительный параметр протоколов, чтобы указать один или несколько подпротоколов WebSocket (протоколы уровня приложения), которые могут использоваться во время связи клиент-сервер:

const socket = new WebSocket('wss://example.org', 'myCustomProtocol');

После создания объекта WebSocket и установления соединения клиент может начать обмен данными с сервером.

События

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

API WebSocket поддерживает четыре типа событий:

Open

Событие open возникает при установке соединения WebSocket. Это указывает на то, что открывающее рукопожатие между клиентом и сервером прошло успешно, и теперь соединение WebSocket можно использовать для отправки и получения данных. Вот пример использования:

// Create WebSocket connection
const socket = new WebSocket('wss://example.org');

// Connection opened
socket.onopen = function(e) {
  console.log('Connection open!');
};

Message

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

Вот пример обработки события message:

socket.onmessage = function(msg) {
  if(msg.data instanceof ArrayBuffer) {
    processArrayBuffer(msg.data);
  } else {
    processText(msg.data);
  }
}

Error

Событие error генерируется в ответ на непредвиденные сбои или проблемы (например, не удалось отправить некоторые данные). Вот как вы прослушиваете события error:

socket.onerror = function(e) {
  console.log('WebSocket failure', e);
  handleErrors(e);
};

Close

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

Вот как вы слушаете событие close:

socket.onclose = function(e) {
  console.log('Connection closed', e);
};

Методы

API WebSocket поддерживает два метода: send() и close().

send()

Как только соединение будет установлено, вы почти готовы начать отправлять и получать сообщения на сервер WebSocket и обратно. Но прежде чем это сделать, сначала необходимо убедиться, что соединение открыто и готово к приему сообщений. Вы можете добиться этого двумя основными способами.

Первый вариант — вызвать метод send() из обработчика событий onopen, как показано в следующем примере:

socket.onopen = function(e) {
  socket.send(JSON.stringify({'msg': 'payload'}));
}

Второй способ — проверить свойство ReadyState и выбрать отправку данных только при открытом соединении WebSocket:

function processEvent(e) {
  if(socket.readyState === WebSocket.OPEN) {
    // Socket open, send!
    socket.send(e);
  } else {
    // Show an error, queue it for sending later, etc
  }
}

Два фрагмента кода выше показывают, как отправлять текстовые (строковые) сообщения. Однако помимо строк вы также можете отправлять двоичные данные (Blob или ArrayBuffer), как показано в этом примере:

var buffer = new ArrayBuffer(128);
socket.send(buffer);

var intview = new Uint32Array(buffer);
socket.send(intview);

var blob = new Blob([buffer]);
socket.send(blob);

После отправки одного или нескольких сообщений вы можете оставить соединение WebSocket открытым для дальнейшего обмена данными или вызвать метод close(), чтобы завершить его.

close()

Метод close() используется для закрытия соединения WebSocket (или попытки подключения). По сути, это эквивалент закрывающего рукопожатия, которое мы рассмотрели ранее в главе 2. После вызова этого метода больше нельзя отправлять или получать данные через соединение WebSocket.

Вот самый простой пример вызова метода close():

socket.close();

При желании вы можете передать два аргумента методу close():

Вот пример вызова метода close() с двумя необязательными параметрами:

socket.close(1003, 'Unsupported data type!');

Свойства

Объект WebSocket предоставляет несколько свойств, содержащих сведения о соединении WebSocket.

binaryType

Свойство binaryType управляет типом двоичных данных, получаемых через соединение WebSocket. Значение по умолчанию — blob; кроме того, WebSockets также поддерживают arraybuffer.

bufferedAmount

Свойство, доступное только для чтения, которое возвращает количество байтов данных, поставленных в очередь для передачи, но еще не отправленных. Значение bufferedAmount сбрасывается до нуля после отправки всех данных в очереди.

bufferedAmount наиболее полезно, особенно когда клиентское приложение передает на сервер большие объемы данных. Несмотря на то, что вызов send() происходит мгновенно, фактическая передача данных через Интернет не происходит. Браузеры будут буферизовать исходящие данные от имени вашего клиентского приложения. Свойство bufferedAmount полезно для обеспечения отправки всех данных перед закрытием соединения или для выполнения собственного регулирования на стороне клиента.

Ниже приведен пример использования bufferedAmount для отправки обновлений каждую секунду и соответствующей корректировки, если сеть не может справиться с такой скоростью:

// 10k max buffer size.
var THRESHOLD = 10240;

// Create a New WebSocket connection
const socket = new WebSocket('wss://example.org');

// Listen for the opening event
socket.onopen = function () {
  // Attempt to send update every second.
  setInterval(function () {
    // Send only if the buffer is not full
    if (socket.bufferedAmount < THRESHOLD) {
      socket.send(getApplicationState());
    }
  }, 1000);
};

extensions

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

свойства «onevent»

Эти свойства вызываются для запуска связанного кода обработчика при каждом запуске события WebSocket. Существует четыре типа свойств «onevent», по одному для каждого типа событий:

Таблица 3.1: Свойства «onevent»
СВОЙСТВО ОПИСАНИЕ
onopen Вызывается, когда свойство readyState соединения WebSocket изменяется на 1; это означает, что соединение открыто и готово отправлять и получать данные.
onmessage Вызывается при получении сообщения от сервера.
onerror Вызывается при возникновении события ошибки, влияющего на соединение WebSocket.
onclose Вызывается с событием закрытия, когда свойство readyState соединения WebSocket изменяется на 3; это означает, что соединение закрыто.

protocol

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

readyState

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

Таблица 3.2: Состояния соединения WebSocket
ЗНАЧЕНИЕ СОСТОЯНИЕ ОПИСАНИЕ
0 CONNECTING Сокет создан, но соединение еще не открыто.
1 OPEN Соединение открыто и готово к общению.
2 CLOSING Соединение находится в процессе закрытия.
3 CLOSED Соединение закрыто или не может быть открыто.

url

Свойство, доступное только для чтения, которое возвращает абсолютный URL-адрес WebSocket, разрешенный конструктором.

Глава 4
Создание веб-приложения с помощью WebSockets

Первоначальная версия этой главы была написана и опубликована Джо Франкетти[31].

В этой главе мы рассмотрим, как создать веб-приложение реального времени с помощью WebSockets и Node.js: интерактивную демонстрацию совместного использования позиции курсора. Это тот тип проекта, который требует двунаправленной мгновенной связи между клиентом и сервером — тип использования, в котором технология WebSocket действительно проявляется.

Клиенты и серверы WebSocket

Для использования технологии WebSocket на стороне сервера требуется серверное приложение. Для нашей демонстрации мы будем использовать Node.JS, легкую и эффективную асинхронную среду выполнения JavaScript, управляемую событиями. Node.js — отличный выбор для создания масштабируемых веб-приложений реального времени и поддержки многих сотен одновременных соединений WebSocket. Мы рассмотрим, как реализовать две разные библиотеки Node.js в качестве сервера WebSocket: ws и SockJS.

Использовать WebSockets во внешнем интерфейсе довольно просто благодаря API WebSocket, встроенному во все современные браузеры (мы будем использовать этот API на стороне клиента в первой части демонстрации, наряду с ws на стороне сервера). Кроме того, существует множество библиотек и решений, реализующих технологию WebSocket как на стороне клиента, так и на стороне сервера. Сюда входит SockJS, о котором мы поговорим во второй части демонстрации.

Дополнительные сведения о реализациях клиента и сервера WebSocket см. в разделе «Ресурсы».

  1. Джо Франкетти, WebSockets и Node.js — тестирование WS и SockJS путем создания веб-приложения.

ws — библиотека Node.js WebSocket

ws[32] — это сервер WebSocket для Node.js. Это довольно низкоуровневый подход: вы слушаете входящие запросы на соединение и отвечаете на необработанные сообщения в виде строк или байтовых буферов.

Чтобы продемонстрировать, как настроить WebSockets с помощью Node.js и ws, мы создадим демонстрационное приложение, которое будет обмениваться позициями курсора пользователей в реальном времени. Мы проходим через его создание ниже.

  1. ws: a Node.js WebSocket library

Создание интерактивной демонстрации совместного использования позиции курсора с помощью ws

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

Настройка сервера WebSocket

Сначала потребуется библиотека ws и используйте метод WebSocket.Server для создания нового сервера WebSocket на порту 7071 (или любом другом порту по вашему выбору):

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

Затем создадим Map для хранения метаданных клиента (любых данных, которые мы хотим связать с клиентом WebSocket):

const clients = new Map();

Подпишемся на событие подключения wss, используя функцию wss.on, которая обеспечивает обратный вызов. Она будет запускаться всякий раз, когда новый клиент WebSocket подключается к серверу:

wss.on('connection', (ws) => {
  const id = uuidv4();
  const color = Math.floor(Math.random() * 360);
  const metadata = { id, color };
  clients.set(ws, metadata);

Каждый раз, когда клиент подключается, мы генерируем новый уникальный ID, который используется для его идентификации. Клиентам также назначается цвет курсора с помощью функции Math.random(); она генерирует число от 0 до 360, которое соответствует значению оттенка цвета HSV. Затем идентификатор и цвет курсора добавляются к объекту, который мы будем называть metadata, и используем Map, чтобы связать их с нашим экземпляром WebSocket ws.

Map — это словарь: мы можем получить эти метаданные, вызвав метод get и позже предоставив экземпляр WebSocket.

Используя только что подключенный экземпляр WebSocket, мы подписываемся на событие message этого экземпляра и предоставляем функцию обратного вызова, которая будет запускаться всякий раз, когда этот конкретный клиент отправляет сообщение на сервер.

ws.on('message', (messageAsString) => {

Всякий раз, когда наш сервер получает сообщение, мы используем JSON.parse для получения содержимого сообщения и загружаем метаданные нашего клиента для этого сокета с нашей Map с помощью client.get(ws).

Мы добавим к сообщению два свойства метаданных: sender и color:

const message = JSON.parse(messageAsString);
const metadata = clients.get(ws);

message.sender = metadata.id;
message.color = metadata.color;

Затем мы снова преобразуем наше сообщение в строку (stringify) и отправляем его каждому подключенному клиенту:

const outbound = JSON.stringify(message);

  [...clients.keys()].forEach((client) => {
    client.send(outbound);
  });
});

Наконец, когда клиент закрывает соединение, мы удаляем его metadata с нашей Map:

  ws.on("close", () => {
    clients.delete(ws);
  });
});

Внизу у нас есть функция для генерации уникального ID:

function uuidv4() {
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
    var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
    return v.toString(16);
  });
}
console.log('wss up');

Эта реализация сервера выполняет многоадресную рассылку, отправляя любое полученное сообщение всем подключенным клиентам.

Теперь нам нужно написать некоторый код на стороне клиента для подключения к серверу WebSocket и передачи позиции курсора пользователя при его перемещении.

WebSockets на стороне клиента

Мы собираемся начать с некоторого стандартного шаблона HTML5:

<!DOCTYPE html>
<html lang='en'>
<head>
  <meta charset='UTF-8'>
  <meta http-equiv='X-UA-Compatible' content='IE=edge'>
  <meta name='viewport' content='width=device-width, initial-scale=1.0'>
  <title>Document</title>

Затем мы добавляем ссылку на таблицу стилей и файл index.js, который добавляем как модуль ES (используя type="module").

  <link rel='stylesheet' href='style.css'>
  <script src='index.js' type='module'></script>
</head>

Тело (body) содержит один HTML template, который содержит SVG-изображение указателя мыши. Мы собираемся использовать JavaScript для клонирования этого шаблона каждый раз, когда новый пользователь подключается к нашему серверу.

<body id='box'>
  <template id='cursor'>
    <svg viewBox='0 0 16.3 24.7' class='cursor'>
      <path stroke='#000' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' d='M15.6 15.6L.6.6v20.5l4.6-4.5 3.2 7.5 3.4-1.3-3-7.2z' />
    </svg>
  </template>
</body>
</html>

Далее нам нужно использовать JavaScript для подключения к серверу WebSocket:

(async function() {
  const ws = await connectToServer();
  ...

Мы вызываем функцию ConnectToServer(), которая разрешает промис, содержащий подключенный WebSocket (определение функции будет написано позже).

После подключения мы добавляем обработчик onmousemove в document.body. messageBody очень прост: он состоит из текущих свойств clientX и clientY из события движения мыши (горизонтальные и вертикальные координаты курсора в области просмотра приложения).

Мы преобразуем этот объект в строку (stringify) и отправляем его через наш теперь подключенный экземпляр WebSocket ws в качестве текста сообщения:

document.body.onmousemove = (evt) => {
  const messageBody = { x: evt.clientX, y: evt.clientY };
  ws.send(JSON.stringify(messageBody));
};

Теперь нам нужно добавить еще один обработчик, на этот раз для события onmessage, в экземпляр WebSocket ws. Помните, что каждый раз, когда сервер WebSocket получает сообщение, он пересылает его всем подключенным клиентам.

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

ws.onmessage = (webSocketMessage) => {
  const messageBody = JSON.parse(webSocketMessage.data);
  const cursor = getOrCreateCursorFor(messageBody);
  cursor.style.transform = `translate(${messageBody.x}px,
  ${messageBody.y}px)`;
};

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

Используя проанализированный messageBody, мы вызываем getOrCreateCursorFor. Эта функция возвращает элемент HTML, который является частью DOM. Мы посмотрим, как это работает позже.

Затем мы используем значения x и y из messageBody, чтобы настроить положение курсора с помощью CSS transform.

Наш код основан на двух служебных функциях. Первая — connectToServer, которая открывает соединение с нашим сервером WebSocket, а затем возвращает Promise, который разрешается, когда свойство WebSocket readyState имеет значение 1 — CONNECTED.

Это означает, что мы можем просто дождаться (await) этой функции и знать, что у нас есть подключенное и работающее соединение WebSocket.

async function connectToServer() {
  const ws = new WebSocket('ws://localhost:7071/ws');
  return new Promise((resolve, reject) => {
    const timer = setInterval(() => {
      if (ws.readyState === 1) {
        clearInterval(timer)
        resolve(ws);
      }
    }, 10);
  });
}

Мы также используем нашу функцию getOrCreateCursorFor. Эта функция сначала пытается найти любой существующий элемент с атрибутом данных HTML data-sender, значение которого совпадает со значением свойства sender в нашем сообщении. Если она его находит, мы знаем, что уже создали курсор для этого пользователя, и нам просто нужно вернуть его, чтобы вызывающий код мог скорректировать его положение.

function getOrCreateCursorFor(messageBody) {
  const sender = messageBody.sender;
  const existing = document.querySelector(`[data-sender='${sender}']`);
  if (existing) {
    return existing;
  }

Если мы не можем найти существующий элемент, мы клонируем наш template HTML, добавляем к нему атрибут данных с текущим ID sender и добавляем его в document.body перед его возвратом:

    const template = document.getElementById('cursor');
    const cursor = template.content.firstElementChild.cloneNode(true);
    const svgPath = cursor.getElementsByTagName('path')[0];

    cursor.setAttribute('data-sender', sender);
    svgPath.setAttribute('fill', `hsl(${messageBody.color}, 50%, 50%)`);
    document.body.appendChild(cursor);

    return cursor;
  }
}) ();

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

Запуск демо-версии

Если вы следовали инструкциям, вы можете запустить:

> npm install
> npm run start

Если нет, вы можете клонировать рабочую демо-версию по адресу: https://github.com/ally-labs/websockets-cursor-sharing.

> git clone https://github.com/ably-labs/WebSockets-cursor-sharing.git
> npm install
> npm run start

Эта демонстрация включает в себя два приложения: веб-приложение, которое мы обслуживаем через Snowpack[33], и веб-сервер Node.js. Стартовая таска NPM запускает как API, так и веб-сервер.

Демо-версия должна выглядеть так, как показано ниже:

Рисунок 4.1. Перемещение курсора в реальном времени с помощью библиотеки ws WebSockets.

Однако если вы запускаете демо-версию в браузере, который не поддерживает WebSockets (например, IE9 или ниже), или если вы ограничены особенно жесткими корпоративными прокси-серверами, вы получите сообщение об ошибке, сообщающее, что браузер не может установить соединение:

Рисунок 4.2. Сообщение об ошибке, возвращаемое браузером, когда соединение WebSocket не может быть установлено.

Это связано с тем, что библиотека ws не предлагает резервных протоколов передачи, если WebSockets недоступны. Если это требование вашего проекта или вы хотите обеспечить более высокий уровень надежности доставки ваших сообщений, вам понадобится библиотека, предлагающая несколько протоколов передачи, например SockJS.

  1. Snowpack

SockJS — библиотека JavaScript для обеспечения связи, подобной WebSocket

SockJS — это библиотека, имитирующая собственный API WebSocket в браузерах. Кроме того, он переключается на HTTP всякий раз, когда WebSocket не может подключиться или если используемый браузер не поддерживает WebSocket. Как и ws, SockJS требует наличия сервера; его сопровождающие предоставляют как клиентскую библиотеку JavaScript[34], так и серверную библиотеку Node.js[35].

Использование SockJS в клиенте похоже на собственный API WebSocket с некоторыми небольшими отличиями. Мы можем заменить ws в ранее созданной демо-версии и вместо этого использовать SockJS, чтобы включить резервную поддержку.

Обновление интерактивной демонстрации совместного использования позиции курсора для использования SockJS

Чтобы использовать SockJS в клиенте, нам сначала нужно загрузить библиотеку JavaScript SockJS из их CDN. В заголовке документа index.html, который мы создали ранее, добавьте следующую строку над включением скрипта в index.js:

<script src='https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js' defer></script>

Обратите внимание на ключевое слово defer — оно гарантирует, что библиотека SockJS будет загружена до запуска index.js.

Затем в файле app/script.js мы обновляем JavaScript для использования SockJS. Вместо объекта WebSocket мы теперь будем использовать объект SockJS. Внутри функции connectToServer мы установим соединение с сервером SockJS:

const ws = new SockJS('http://localhost:7071/ws');

Далее нам нужно обновить файл API/script.js, чтобы наш сервер использовал SockJS. Это означает изменение названий нескольких перехватчиков событий, но API очень похож на ws.

Сначала нам нужно установить sockjs-node. В вашем терминале запустите:

> npm install sockjs

Затем нам потребуется модуль sockjs и встроенный HTTP-модуль от Node. Удалите строку, требующую ws, и замените ее следующей:

const http = require('http');
const sockjs = require('sockjs');

Затем мы изменяем объявление wss следующим образом:

const wss = sockjs.createServer();

В самом низу файла API/index.js мы создадим HTTPS-сервер и добавим обработчики HTTP SockJS:

const server = http.createServer();
wss.installHandlers(server, {prefix: '/ws'});
server.listen(7071, '0.0.0.0');

Мы сопоставляем обработчики с префиксом, указанным в объекте конфигурации ('/ws'). Мы сообщаем HTTP-серверу прослушивать порт 7071 (выбранный произвольно) на всех сетевых интерфейсах машины.

Последняя задача — обновить имена событий для работы с SockJS:

ws.on('message',         will become   ws.on('data',
client.send(outbound);   will become   client.write(outbound);

И все, демо-версия теперь будет работать с WebSockets, где они поддерживаются; а там, где это не так, будет использоваться длинный опрос Comet. Этот последний запасной вариант будет показывать немного менее плавное движение курсора, но он более функционален, чем полное отсутствие соединения!

  1. SockJS-client
  2. SockJS-node

Запуск демо с помощью SockJS

Если вы следовали инструкциям, вы можете запустить:

> npm install
> npm run start

Если нет, вы можете клонировать рабочую версию демо-версии по адресу: https://github.com/ally-labs/websockets-cursor-sharing/tree/sockjs.

> git clone - b sockjs https://github.com/ably-labs/WebSockets-cursor-sharing.git
> npm install
> npm run start

Эта демонстрация включает в себя два приложения: веб-приложение, которое мы обслуживаем через Snowpack[36], и веб-сервер Node.js. Стартовая таска NPM запускает как API, так и веб-сервер.

Демо-версия должна выглядеть так, как показано ниже:

Рисунок 4.3. Перемещение курсора в реальном времени с помощью библиотеки SockJS WebSockets.

  1. Snowpack

Масштабирование веб-приложения

Вы могли заметить, что в обоих примерах мы храним состояние на сервере Node.js WebSocket — существует Map, которая отслеживает подключенные WebSocket и связанные с ними метаданные. Это означает, что для того, чтобы решение работало и чтобы каждый пользователь мог видеть друг друга, они должны быть подключены к одному и тому же серверу WebSocket.

Таким образом, количество активных пользователей, которых вы можете поддерживать, напрямую зависит от количества оборудования на вашем сервере. Node.js довольно хорошо справляется с управлением параллелизмом, но как только вы достигнете количества пользователей от нескольких сотен до нескольких тысяч, вам придется масштабировать свое оборудование, чтобы синхронизировать всех пользователей.

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

Что затрудняет масштабирование WebSockets?

WebSocket принципиально сложно масштабировать, поскольку соединения с вашим сервером WebSocket должны быть постоянными. И даже после масштабирования уровня сервера вам также необходимо предоставить решение для обмена данными между узлами. Состояние соединения необходимо хранить вне процесса — обычно это предполагает использование чего-то вроде Redis[37] или традиционной базы данных, чтобы гарантировать, что все узлы имеют одинаковое представление состояния.

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

Есть несколько способов решить эту проблему: либо использовать какую-либо форму прямого соединения между узлами кластера, обрабатывающими трафик, либо использовать внешний механизм публикации/подписки[38]. Иногда это называют «добавлением объединительной платы» в вашу инфраструктуру, и это еще одна движущаяся часть, которая затрудняет масштабирование WebSockets.

См. главу 5: WebSockets at Scale для более подробной информации о инженерных проблемах, связанных с масштабированием WebSockets.

  1. Redis
  2. Все, что вам нужно знать об публикации/подписке

Глава 5
Веб-сокеты в масштабе

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

Масштабируемость вашего серверного уровня

Существует два основных пути масштабирования уровня сервера:

Рисунок 5.1: Вертикальное и горизонтальное масштабирование

На первый взгляд вертикальное масштабирование кажется привлекательным, поскольку его легче реализовать и поддерживать, чем горизонтальное. Вы можете даже спросить себя: сколько соединений WebSocket может обрабатывать один сервер? Однако это редко бывает правильным вопросом, и это потому, что вертикальное масштабирование имеет некоторые серьезные практические ограничения.

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

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

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

Несмотря на повышенную сложность, горизонтальное масштабирование стоит использовать, поскольку оно позволяет масштабировать без ограничений (теоретически). Это делает его превосходной альтернативой вертикальному масштабированию. Итак, вместо того, чтобы спрашивать, сколько соединений может обработать сервер, лучше было бы спросить: на сколько серверов я могу распределить рабочую нагрузку?

Балансировка нагрузки

Балансировка нагрузки — это процесс распределения входящего сетевого трафика (в нашем случае соединений WebSocket) по группе внутренних серверов (обычно называемой фермой серверов). При горизонтальном масштабировании ваша стратегия балансировки нагрузки имеет основополагающее значение.

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

Рисунок 5.2: Балансировка нагрузки

Балансировщики нагрузки определяют работоспособность серверных ресурсов и не отправляют трафик на серверы, которые не могут справиться с дополнительной нагрузкой. Если сервер выходит из строя, балансировщик нагрузки перенаправляет его трафик на оставшиеся рабочие серверы. Когда в ферму добавляется новый сервер, балансировщик нагрузки автоматически начинает распределять на него трафик.

Целью эффективной стратегии балансировки нагрузки является:

Вы можете выполнить балансировку нагрузки на разных уровнях модели взаимодействия открытых систем (OSI)[39]:

  1. OSI model, Wikipedia
Алгоритмы балансировки нагрузки

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

Таблица 5.1: Алгоритмы балансировки нагрузки
АЛГОРИТМ ОПИСАНИЕ
По-круговой Включает последовательную маршрутизацию подключений к доступным серверам на циклической основе. Для упрощенного примера предположим, что у нас есть два сервера: A и B. Первое соединение идет к серверу A, второе - к серверу B, третье - к серверу A, четвертое - к серверу B и так далее. .
Наименьшее количество соединений Новое соединение направляется на сервер с наименьшим количеством активных соединений.
Наименьшая пропускная способность Новое соединение направляется на сервер, который в данный момент обслуживает наименьший объем трафика, измеряемый в мегабитах в секунду (Мбит/с).
Наименьшее время ответа Новое соединение направляется на компьютер, которому требуется наименьшее количество времени для ответа на запрос мониторинга работоспособности (скорость ответа используется для указания того, насколько загружен сервер). Некоторые балансировщики нагрузки также могут учитывать количество активных подключений на каждом сервере.
Методы хеширования Решение о маршрутизации принимается на основе хеширования различных битов данных от входящего соединения. Сюда может входить такая информация, как номер порта, имя домена и IP-адрес.
Случайный вариант с двумя вариантами Балансировщик нагрузки случайным образом выбирает два сервера из вашей фермы и направляет новое подключение к машине с наименьшим количеством активных подключений.
Пользовательская загрузка Балансировщик нагрузки запрашивает нагрузку на отдельные серверы, используя что-то вроде простого протокола сетевого управления (SNMP)[40], и назначает новое соединение машине с лучшими показателями нагрузки. Вы можете определить различные показатели для анализа, такие как загрузка ЦП, память и время отклика.

Вот другие аспекты, которые следует учитывать при балансировке нагрузки WebSockets:

Возврат к альтернативным видам транспорта

Вы можете столкнуться со сценариями, в которых вы не сможете использовать WebSockets (например, некоторые корпоративные брандмауэры и сети блокируют соединения WebSocket). Когда это происходит, ваша система должна иметь возможность вернуться к другому транспорту — обычно это метод на основе HTTP, такой как длинный опрос Comet. Это означает, что уровень вашего сервера должен «понимать» оба — WebSocket, а также все резервные варианты, которые вы используете.

Уровень вашего сервера должен быть готов к быстрой адаптации к переходу на менее эффективный транспорт, что обычно означает увеличение нагрузки. Вы должны иметь в виду, что ваша идеальная стратегия балансировки нагрузки для WebSockets не всегда может быть подходящей для HTTP-запросов; в конце концов, WebSockets с сохранением состояния и HTTP без сохранения состояния принципиально отличаются. При проектировании системы вы должны убедиться, что она способна успешно балансировать нагрузку WebSocket, а также любые резервные варианты HTTP, которые вы поддерживаете (возможно, вам захочется иметь разные фермы серверов для обработки трафика WebSocket и трафика, не относящегося к WebSocket).

Прикрепленные сеансы

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

  1. Simple Network Management Protocol, Wikipedia

Архитектура вашей системы для масштабирования

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

Коротко говоря, pub/sub обеспечивает основу для обмена сообщениями между издателями (обычно вашим сервером) и подписчиками (часто устройствами конечных пользователей). Издатели и подписчики не знают друг друга, поскольку они разделены брокером сообщений, который обычно группирует сообщения по каналам (или темам). Издатели отправляют сообщения на каналы, а подписчики получают сообщения, подписываясь на соответствующие каналы.

Рисунок 5.3: Шаблон публикации/подписки

Разделенная природа шаблона pub/sub означает, что ваши приложения теоретически могут масштабироваться для неограниченного числа подписчиков. Значительным преимуществом использования шаблона публикации/подписки является то, что у вас часто есть только один компонент, который занимается масштабированием соединений WebSocket — брокер сообщений. Пока брокер сообщений может масштабироваться предсказуемо и надежно, вам вряд ли придется добавлять дополнительные компоненты или вносить какие-либо другие изменения в вашу систему, чтобы справиться с непредсказуемым количеством одновременных пользователей, подключающихся через WebSockets.

Вот некоторые другие преимущества, которые вы получаете, используя pub/sub:

Существует множество проектов, созданных с использованием WebSockets и pub/sub[42], а также множество библиотек с открытым исходным кодом и коммерческих решений, сочетающих эти два элемента, поэтому маловероятно, что вам придется создавать свои собственные возможности WebSockets + pub/sub с нуля. Примеры решений с открытым исходным кодом, которые вы можете использовать, включают: Socket.IO с адаптером Redis pub/sub[43], SocketCluster[44] или Django Channels[45]. Конечно, выбирая open-source решение, вам придется самостоятельно его развертывать, управлять и масштабировать — это, без сомнения, сложная инженерная задача.

  1. Все, что вам нужно знать о Publish/Subscribe
  2. Проекты с открытым исходным кодом Websocket Pubsub на GitHub
  3. Redis adapter for Socket.IO
  4. SocketCluster
  5. Django Channels

Резервный транспорт

Несмотря на широкую поддержку платформ, WebSockets страдают от некоторых проблем с сетью. Вот некоторые проблемы, с которыми вы можете столкнуться:

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

Большинство решений WebSocket имеют встроенную резервную поддержку. Например, Socket.IO[46], одна из самых популярных библиотек WebSocket с открытым исходным кодом, будет непрозрачно пытаться установить соединение WebSocket, если это возможно, а в противном случае вернется к длинному опросу HTTP.

Другим примером является SockJS[47], который поддерживает большое количество резервных вариантов потоковой передачи и опроса, включая xhr-polling (длительный опрос с использованием междоменного XHR[48]) и eventsource (Server-Sent Events[49]).

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

Предположим, к вашей системе одновременно подключены тысячи или даже десятки тысяч пользователей, и произошел инцидент, из-за которого значительная часть соединений WebSocket возвращается к длинному опросу. Обработка десятков тысяч одновременных подключений WebSocket не только сама по себе является проблемой, но и еще больше усугубляется возвратом к длительному опросу, который значительно более требовательно к уровню вашего сервера. Например, хотя WebSockets позволяют передавать данные, как только они становятся доступными, через постоянные соединения, при длительном опросе вам приходится буферизовать данные и хранить их где-то до поступления следующего запроса; это гораздо более ресурсоемко (повышенное использование оперативной памяти).

  1. Socket.IO
  2. SockJS-client
  3. XMLHttpRequest Living Standard
  4. Server-Sent Events (SSE): A Conceptual Deep Dive

Управление соединениями и сообщениями WebSocket

Теперь мы рассмотрим основные моменты, которые необходимо учитывать при управлении трафиком WebSocket (соединениями и сообщениями).

Новые связи

Установление нового соединения WebSocket требует умеренных затрат — этот процесс включает в себя нетривиальную пару запрос/ответ между клиентом и сервером, известную как открывающее рукопожатие.

Теперь представьте, что вы транслируете спортивные новости в прямом эфире, и происходит широко популярное событие, например, игра чемпионата мира или финал турнира Большого шлема. У вас могут быть десятки тысяч или даже миллионы клиентских устройств, пытающихся одновременно открыть соединения WebSocket. Такой сценарий приводит к огромному всплеску трафика, и ваша система должна быть готова справиться с этим. Ситуация была бы еще более сложной, если бы все эти соединения WebSocket одновременно переключились на менее эффективный транспорт.

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

Мониторинг веб-сокетов

Общедоступный Интернет — это изменчивый и непредсказуемый источник трафика, поэтому вам нужен надежный и комплексный стек мониторинга и оповещений, который даст вам представление о вашей системе и о том, как она справляется с соединениями WebSocket.

Мы не будем вдаваться в подробности о решениях, которые вы можете использовать для создания своего стека мониторинга WebSockets — есть множество вариантов на выбор, включая инструменты с открытым исходным кодом, такие как Prometheus[50] и Grafana[51].

Вот некоторые из показателей, которые обычно отслеживаются:

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

  1. Prometheus
  2. Grafana

Сброс нагрузки

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

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

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

Восстановление соединений

Многие причины могут привести к разрыву соединений WebSocket. Пользователи могут переключиться с мобильной сети передачи данных на сеть Wi-Fi, пройти через туннель или, возможно, столкнуться с периодическими проблемами в сети. Или один из ваших серверов был перегружен и мог выйти из строя, или ему необходимо разорвать соединения. При возникновении подобных сценариев необходимо восстановить соединения WebSocket.

Автоматические переподключения

Вы можете реализовать сценарий переподключения, который позволит клиентам автоматически переподключаться. Простой вариант может выглядеть примерно так[52]:

function connect() {
  ws = new WebSocket("ws://localhost:8080");
  ws.addEventListener('close', connect);
}

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

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

var initialReconnectDelay = 1000;
var currentReconnectDelay = initialReconnectDelay;
var maxReconnectDelay = 16000;

function connect() {
  ws = new WebSocket("ws://localhost:8080");
  ws.addEventListener('open', onWebsocketOpen);
  ws.addEventListener('close', onWebsocketClose);
}

function onWebsocketOpen() {
  currentReconnectDelay = initialReconnectDelay;
}

function onWebsocketClose() {
  ws = null;
  setTimeout(() => {
    reconnectToWebsocket();
  }, currentReconnectDelay);
}

function reconnectToWebsocket() {
  if(currentReconnectDelay < maxReconnectDelay) {
    currentReconnectDelay*=2;
  }
  connect();
}

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

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

function onWebsocketClose() {
  ws = null;
  // Add anything between 0 and 3000 ms to the delay.
  setTimeout(() => {
    reconnectToWebsocket();
  }, currentReconnectDelay + Math.floor(Math.random() * 3000)));
}
  1. Йерун де Кок. Как реализовать алгоритм случайной экспоненциальной отсрочки в Javascript
Воссоединения с непрерывностью

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

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

Пульсация

Протокол WebSocket изначально поддерживает управляющие кадры[53], известные как Ping и Pong. Эти контрольные кадры представляют собой механизм контрольного сигнала на уровне приложения, позволяющий определить, активно ли соединение WebSocket. Обычно сервер отправляет кадр Ping, а по получении клиентская сторона должна отправить кадр Pong обратно в качестве ответа.

Это не особо важно в контексте всего лишь одного соединения, но наличие тысяч или даже миллионов одновременных соединений WebSocket с высокой частотой пульсации увеличит значительную нагрузку на ваш сервер. Если ваш вариант использования позволяет, возможно, имеет смысл уменьшить частоту тактовых импульсов, чтобы упростить масштабирование.

  1. RFC 6455, Section 5.5: Control Frames

Обратное давление

При потоковой передаче данных на клиентские устройства в больших масштабах через Интернет противодавление является одной из ключевых проблем, с которой вам придется столкнуться. Например, предположим, что вы передаете 20 сообщений в секунду, но клиент может обрабатывать только 15 сообщений в секунду. Что вы делаете с оставшимися 5 сообщениями в секунду, которые клиент не может обработать?

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

Типичным корректирующим действием противодавления является неизбирательное удаление пакетов. Этот подход хорошо работает, когда последнее сообщение, отправленное из потока WebSocket, всегда является самым важным — например, в таких случаях, как спортивные новости в прямом эфире, где последний результат является наиболее актуальной информацией. Чтобы уменьшить пропускную способность и задержку, помимо отбрасывания пакетов, вам также следует рассмотреть возможность использования чего-то вроде дельта-сжатия сообщений, которое обычно использует алгоритм сравнения[54] для отправки потребителю только изменений предыдущего сообщения, а не всего сообщения.

Однако отбрасывание пакетов не всегда является хорошим решением — бывают случаи, когда целостность данных имеет решающее значение, и вы просто не можете позволить себе потерять информацию. В таких сценариях вам следует использовать application-level acknowledgments (подтверждения уровня приложения) (ACK) в качестве подтверждения получения сообщения и настроить систему так, чтобы она не отправляла дополнительные пакеты сообщений до тех пор, пока она не получит ACK. Вам также необходимо подумать о том, как обеспечить непрерывность потока, даже если возникнут отключения.

  1. Цветко Йовчев, Дельта-сжатие: практическое руководство по алгоритмам сравнения и форматам дельта-файлов.

Краткое замечание об отказоустойчивости

Когда вы пытаетесь создавать масштабируемые, готовые к использованию приложения с помощью WebSockets, обслуживающие тысячи или даже миллионы потребителей, вам неизбежно нужно подумать об отказоустойчивости[55] вашей системы.

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

Не вдаваясь в подробности: чтобы сделать вашу систему отказоустойчивой, вам необходимо обеспечить ее резервирование на случай сбоев серверов и центров обработки данных. Если вы создаете облачные приложения, это, прежде всего, подразумевает возможность эластичного масштабирования уровня вашего сервера (и работу с дополнительной мощностью в режиме ожидания) и распределение вашей инфраструктуры по нескольким зонам доступности.

Однако полагаться на какой-то конкретный регион недостаточно по ряду причин[56] — иногда несколько зон доступности в регионе выходят из строя одновременно; иногда могут возникнуть проблемы с локальным подключением, из-за которых регион будет недоступен; а иногда в регионе могут быть просто ограничения пропускной способности, из-за которых невозможно поддерживать там все услуги.

В результате зачастую лучше всего развернуть инфраструктуру в нескольких регионах. Это лучший способ обеспечить статистическую независимость от сбоев и самую надежную гарантию того, что ваша система будет продолжать работать и предоставлять бесперебойное обслуживание вашим пользователям.

  1. Д-р Пэдди Байерс, Инженерная надежность и отказоустойчивость в распределенной системе.
  2. Майкл Гариффо, AWS терпит третий сбой за месяц

Контрольный список WebSockets в масштабе

Ресурсы

Рекомендации

Видео

Дополнительная литература

Библиотеки WebSocket с открытым исходным кодом

Заключительные мысли

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

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

Связаться с нами

Об Ably

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

Aly предоставляет набор API-интерфейсов для создания, расширения и предоставления мощных цифровых возможностей в реальном времени – в основном через WebSockets – для более чем 250 миллионов устройств в 80 странах каждый месяц. Такие организации, как Bloomberg, HubSpot, Verizon и Hopin, полагаются на платформу Ably, позволяющую разгрузить растущую сложность синхронизации критически важных для бизнеса данных в реальном времени в глобальном масштабе.

Создать бесплатный аккаунт