Масштабируем websocket соединения на go
Содержание:
- Пространства и «комнаты»¶
- Или getting started with WebSocket PHP без phpDaemon
- Простой пример
- Передача данных
- Idiomatic way
- Обработка разрывов соединения
- Добавление веб-сокетов в Денвер
- Реализуем часть протокола
- Массовый реконнект
- 2021: WSS Docs РД3
- Post-connection features¶
- Common patterns¶
- Установление WebSocket-соединения
- Data transfer
- Простой клиент веб-сокетов
- Заключение
Пространства и «комнаты»¶
В протоколе WebSocket существуют такие понятия, как пространства и «комнаты». По умолчанию посылаемые данные отправляются всем сокетам, но принимают эти данные лишь некоторые из них. Получается, что в определенные моменты времени будет установлено избыточное количество соединений. Чтобы избежать этого, используйте пространства.
Пространства позволяют изолировать одни сокеты от других.
app.js
В приведенном примере с помощью метода на сервере определяются два пространства: и . На клиентской стороне подключение к тому или иному пространству происходит в зависимости от текущего маршрута. Таким образом, при отправке данных, например, из пространства , об этом будут оповещены только сокеты этого пространства. По умолчанию все сокеты находятся в пространстве .
Также и в пределах пространства можно распределять сокеты по так называемым «комнатам».
Чтобы отнести сокет к определенной «комнате» используется метод пространства , который принимает имя «комнаты» (задается пользователем модуля ). Для вынесения сокета из комнаты используйте метод .
Отправка данных в «комнату» осуществляется с помощью метода .
Обработка инициируемых в пределах «комнаты» событий осуществляется с использованием метода .
Или getting started with WebSocket PHP без phpDaemon
Здравствуйте! Простите за столь длинный заголовок, но, надеюсь, что новичкам вроде меня будет легче найти эту статью, ведь мне ничего подобного найти не удалось. Несколько недель назад я принял решение переработать игровой клиент и сервер своей игры Growing Crystals с AJAX, на WebSocket, но всё оказалось не просто непросто, а очень сложно. Поэтому я и решил написать статью, которая бы помогла самым что ни на есть начинающим разработчикам на WebSocket + PHP сэкономить несколько дней времени, максимально подробно объясняя каждый свой шаг по настройке и запуску первого WebSocket скрипта на PHP.
Что у меня есть: Денвер на локальной машине, на нём я веду разработку проекта и дешевый PHP хостинг, на котором я публикую свой проект для того, чтобы получить обратную связь от Интернет-пользователей.
Что я хочу: Без установки phpDaemon (phpd), NodeJS и прочих вещей на локальную машину и хостинг, продолжить разработку своего проекта, но теперь с WebSocket, в этой статье разберем простой WebSocket эхо сервер.
Чего я не хочу: Говоря о NodeJS, не хочется переписывать серерную логику с PHP на другой язык, тем более устанавливать NodeJS, хотя и люблю JavaScript больше чем PHP.
Важно: не путайте демона написанного на php, с фреймворком асинхронных приложений phpDaemon. Который, конечно же, обязательно потребуется в случае развития проекта и многократного роста нагрузки на хостинг
Но для начала работы с WebSocket на дешевом хостинге можно обойтись и без него.
Простой пример
Чтобы открыть веб-сокет-соединение, нам нужно создать объект , указав в url-адресе специальный протокол :
Также существует протокол , использующий шифрование. Это как HTTPS для веб-сокетов.
Всегда предпочитайте
Протокол не только использует шифрование, но и обладает повышенной надёжностью.
Это потому, что данные не зашифрованы, видны для любого посредника. Старые прокси-серверы не знают о WebSocket, они могут увидеть «странные» заголовки и закрыть соединение.
С другой стороны, – это WebSocket поверх TLS (так же, как HTTPS – это HTTP поверх TLS), безопасный транспортный уровень шифрует данные от отправителя и расшифровывает на стороне получателя. Пакеты данных передаются в зашифрованном виде через прокси, которые не могут видеть, что внутри, и всегда пропускают их.
Как только объект создан, мы должны слушать его события. Их всего 4:
- – соединение установлено,
- – получены данные,
- – ошибка,
- – соединение закрыто.
…А если мы хотим отправить что-нибудь, то вызов сделает это.
Вот пример:
Для демонстрации есть небольшой пример сервера server.js, написанного на Node.js, для запуска примера выше. Он отвечает «Привет с сервера, Джон», после ожидает 5 секунд и закрывает соединение.
Так вы увидите события → → .
В общем-то, всё, мы уже можем общаться по протоколу WebSocket. Просто, не так ли?
Теперь давайте поговорим более подробно.
Передача данных
Поток данных в WebSocket состоит из «фреймов», фрагментов данных, которые могут быть отправлены любой стороной, и которые могут быть следующих видов:
- «текстовые фреймы» – содержат текстовые данные, которые стороны отправляют друг другу.
- «бинарные фреймы» – содержат бинарные данные, которые стороны отправляют друг другу.
- «пинг-понг фреймы» используется для проверки соединения; отправляется с сервера, браузер реагирует на них автоматически.
- также есть «фрейм закрытия соединения» и некоторые другие служебные фреймы.
В браузере мы напрямую работаем только с текстовыми и бинарными фреймами.
Метод WebSocket может отправлять и текстовые и бинарные данные.
Вызов принимает в виде строки или любом бинарном формате включая , и другие. Дополнительных настроек не требуется, просто отправляем в любом формате.
При получении данных, текст всегда поступает в виде строки. А для бинарных данных мы можем выбрать один из двух форматов: или .
Это задаётся свойством , по умолчанию оно равно , так что бинарные данные поступают в виде -объектов.
Blob – это высокоуровневый бинарный объект, он напрямую интегрируется с , и другими тегами, так что это вполне удобное значение по умолчанию. Но для обработки данных, если требуется доступ к отдельным байтам, мы можем изменить его на :
Idiomatic way
Посмотрим на то, как бы мы реализовали некоторые части нашего сервера, используя возможности Go, без использования системных вызовов в собственном коде.
Прежде чем рассматривать работу с , поговорим об отправке и получении данных. Данные, которые находятся над протоколом WebSocket (например, json-конверты), здесь и далее я стану называть пакетами. Давайте начнем реализацию структуры , которая будет содержать в себе логику получения и отправки пакетов через WebSocket-соединение.
2.1. Channel struct
Хочу обратить ваше внимание на запуск двух горутин чтения и записи. Для каждой горутины нужен свой стек, который в и версии Go может иметь начальный размер от 2 до 8 Кбайт
Если учесть цифру, названную ранее (3 миллиона живых соединений), то на все соединения нам потребуется 24 Гбайт памяти (при стеке в 4 Кбайт). И это без учета памяти, выделяемой на структуру , очередь исходящих пакетов и другие внутренние поля.
2.2. Горутины I/O
Посмотрим на реализацию «читателя» из соединения:
Достаточно просто, верно? Мы используем буфер, чтобы сократить количество syscall’ов на чтение и вычитывать сразу столько, сколько позволит нам размер . В бесконечном цикле мы ожидаем поступления новых данных в соединение и читаем следующий пакет. Попрошу запомнить слова ожидаем поступления новых данных: к ним мы еще вернемся позже.
Парсинг и обработка входящих пакетов останутся в стороне, поскольку это неважно для тех оптимизаций, о которых будет идти речь
А вот на все же стоит обратить внимание сейчас: по умолчанию это 4 Кбайт и значит это еще 12 Гбайт памяти. Аналогичная ситуация с «писателем»:
Мы итерируемся по каналу исходящих пакетов и пишем их в буфер. Это, как внимательный читатель уже мог догадаться, еще 4 Кбайт и 12 Гбайт памяти на наши 3 миллиона соединений.
2.3. HTTP
Простая реализация у нас есть, теперь нужно раздобыть WebSocket-соединение, с которым мы будем работать. Поскольку мы все еще находимся под заголовком Idiomatic way, то сделаем это в соответствующем ключе.
Обратим внимание на то, что внутри себя содержит буфер записи на 4 Кбайт, а для инициализации происходит выделение буфера чтения тоже на 4 Кбайт. Независимо от используемой библиотеки WebSocket после успешного ответа на Upgrade-запрос буферы I/O вместе с TCP-соединением при вызове
Независимо от используемой библиотеки WebSocket после успешного ответа на Upgrade-запрос буферы I/O вместе с TCP-соединением при вызове .
Таким образом, нам нужно еще 24 Гбайт памяти на 3 миллиона соединений.
Итого уже 72 Гбайт памяти на приложение, которое еще ничего не делает!
Обработка разрывов соединения
Если соединение разрывается, оно автоматически восстанавливается браузером. Сервер может отправить таймаут для повторного завершения или закрытия соединения. В таком случае браузер попытается подключиться после завершения таймаута или не будет ничего делать, если соединение завершено.
Реализация образца сервера
Если клиент такой простой, возможно, сложной окажется реализация сервера? Обработчик сервера для SSE может выглядеть следующим образом:
function handler(response) { // настраиваем заголовки для ответа с целью получить постоянное HTTP-соединение response.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' }); // составляем сообщение response.write('id: UniqueIDn'); response.write("data: " + data + 'nn'); // каждый раз, когда мы вводим два символа новой строки, сообщение отправляется автоматически }
Определяем функцию, которая будет обрабатывать ответ:
- Устанавливать заголовки;
- Создавать сообщение;
- Отправлять.
Обратите внимание, что здесь нет вызов метода send() или метода push(). Стандарт определяет: сообщение будет отправлено, как только в него будет добавлено два символа n n, как например: response.write(«data: » + data + ‘nn’);
В результате сообщение будет немедленно отправлено клиенту.
Составление сообщений
Сообщение может содержать несколько свойств:
1. ID
Если значение этого поля не содержит U + 0000 NULL, устанавливаем для буфера последнего идентификатора события значение поля. Иначе игнорируем поле.
2. Data
Добавляем значение поля в буфер, затем добавляем в буфер один символ U + 000A LINE FEED (LF).
3. Event
Устанавливаем для буфера тип события и значение поля. Это приводит к тому, что для event.type задается пользовательское имя события.
4. Retry
Если значение поля состоит только из цифр ASCII, тогда интерпретируем значение поля как целое число в десятичной системе исчисления. А также устанавливаем для времени повторного соединения потока событий это целое число. В противном случае игнорируем поле.
Все остальное будет проигнорировано. Мы не можем вводить собственные поля.
Пример с добавленным event:
response.write('id: UniqueIDn'); response.write('event: addn'); response.write('retry: 10000n'); response.write("data: " + data + 'nn');
В клиенте это обрабатывается с помощью addEventListener следующим образом:
source.addEventListener("add", function(event) { // выполняем действия с данными event.data; });
Вы можете отправлять несколько сообщений, разделенных символом новой строки, а также использовав для них разные идентификаторы.
... id: 54 event: add data: "" id: 55 event: remove data: JSON.stringify(some_data) id: 56 event: remove data: { data: "msg" : "JSON data"n data: "field": "value"n data: "field2": "value2"n data: }nn
Это значительно упрощает то, что мы можем сделать с нашими данными.
Добавление веб-сокетов в Денвер
Теперь задача настроить Денвер. Мне довелось работать с разными сборками Денвера. Последняя из них Denwer3_Base_2013-06-02_a2.2.22_p5.3.13_m5.5.25_pma3.5.1_xdebug.exe, к сожалению, она и другие имеющиеся в настоящий момент сборки Денвера(дело в PHP) не поддерживают сокеты по умолчанию.
Но эта проблема решается путём поиска и установки подходящей php_sockets.dll. Для того, чтобы всё заработало, достаточно разместить dll файл в каталоге Денвера \usr\local\php5\ext\php_sockets.dll и отредактировать файл \usr\local\php5\php.ini убрав точку с запятой перед строкой
extension=php_sockets.dll
перезагрузить Денвер и всё, или почти всё: в некоторых случаях при перезагрузке Денвера может возникнуть ошибка
это значит что вы используете не подходящую версию файла php_sockets.dll. Чтобы облегчить поиски предлагаю две версии – одна гарантированно подходит для PHP 5.2.12 (скачать), а вторая для гарантированно подходит для PHP 5.3.13 (скачать). Но если вам не подошел ни один из этих файлов, предлагаю скачать полный архив соответствующей версии php для windows с веб-сайта php.net и найти в архиве файл php_sockets.dll, который точно подойдёт.
Теперь можете запустить файлик sockettest.php через браузер и увидеть заветную строку “WebSockets OK”, означающую что всё необходимое для работы с Web-Socket установлено и работает хорошо.
Реализуем часть протокола
Что бы в реализации сервера была какая то цель, нужно эту цель придумать. Целью кода данной статьи будет написание WebSocket сервера, который реализует часть протокола сокетов и позволяет переписываться нескольким клиентам из консоли браузера. Для начала нужно реализовать функционал опроса клиента с помощью управляющих фреймов Ping. Нам нужно знать, что клиент еще жив и готов принимать данные с сервера. Фрейм Ping, управляющий фрейм, но он так же может содержать данные. Когда клиент получит такое сообщение по сокету, он должен отправить на сервер фрейм Pong с теми данными, которые были во фрейме Ping. До реализации этого функционала, давайте пропишем в класс сервера необходимые константы
Далее реализуем наш метод по формированию фрейма Ping
По большому счету, в данном случае, это не требуется. Нам совершенно не обязательно пересылать какие-то данные клиенту вместе с управляющим фреймом Ping. Поэтому этот метод можно удалить, а вместо него в класс добавить еще одну константу. Также для того, чтобы реализовать функционал чата, нам потребуется хранить объекты подключений. Заведем под это отдельную коллекцию в классе сервера.
Модицифируем конструктор, добавим отправку фрейма Ping подключившимся клиентам с интервалом в 5 секунд, а также добавляем новых клиентов в коллекцию.
Теперь мы можем принимать соединения по сокетам и поддерживать его с помощью пингов. Осталось научить наш сервер маршрутизировать сообщения от клиентов. В спецификации к протоколу написано, что клиенты всегда должны отправлять сообщения на сервер в маскированном виде, а сообщения сервера всегда без маски. Из этого следует, что нам нужно раскодировать сообщение, а для этого нужно понять, что за сообщение пришло на сервер, получить маску, длину сообщения и сами данные. Напишем для этого метод
- В этой строке нам нужно получить длину данных внутри фрейма. Мы делаем это с помощью операции XOR и констранты, которая представляет число 128 в двоичном виде, которое выглядит как 10000000. В данном случае мы это делаем, исходя из того, что данные от клиента всегда приходят в маскированном виде, а значит первый бит этого байта всегда будет 1.
- Согласно спецификации для фреймов с длиной 126, длина сообщения передаётся в двух следующих байтах
- Согласно спецификации для фреймов с длиной 127, длина сообщения передаётся в восьми следующих байтах
С помощью этой функции мы можем получать всю необходимую информацию для обмена сообщениями между клиентами. Напишем метод, который будет демаскировать данные
Демаскирование происходит путем применения функции XOR к каждому байту данных и соответствующему ему байту маски. Длина маски указана в спецификации и составляет 4 байта. Теперь можно написать метод для отправки коротких сообщений по сокету клиенту.
Нам осталось финализировать конструктор класса. Добавим туда рассылку полученных сообщений от клиента всем активным клиентам, а также добавим отправку всем клиентам сообщения при подключении нового клиента.
Теперь можно запустить сервер. Для проверки работоспособности можно открыть две вкладки браузера и в консоли каждой вклдаки написать следующий код.
Затем отправить сообщение в одной из вкладок
Массовый реконнект
Exponential backoff на клиенте. Rate limiter на WebSocket-сервере.JWT-аутентификация как способ не нагружать бэкенд сессий.Добивайтесь максимальной производительности соединений.
smart-batchingрепозиторииКэш сообщений, чтобы убрать пиковую нагрузку с СУБД.
- Мы добавляем сообщения в структуру данных List. Как вы видите, здесь уже три сообщения. У каждого сообщения при этом есть инкрементальный номер. Мы его увеличиваем атомарно.
- Далее мы публикуем сообщение в PUB/SUB Redis и в этот момент оно улетает в PUB/SUB. В свою очередь, PUB/SUB долетает до клиента (если он подключен). Как только клиент реконнектится, они передают номер сообщения, которое видели последним.
- Идем в Redis, смотрим в Lists. Если есть сообщения, восстанавливаем их клиенту из этой структуры данных, как будто он даже не был отключен.
- Так клиент получает весь стрим сообщений, которые ему были высланы в интервале отсутствия и так снимается пиковая нагрузка с СУБД.
Время ответа RPC при раскладке.
2021: WSS Docs РД3
1 марта 2021 года компания WSS-Consulting объявила о выходе релиза системы электронного документооборота WSS Docs РД3. В обновленной версии продукта были оптимизированы системные показатели, а также добавлены возможности, нацеленные на удобство работы с системой.
По информации компании, оптимизирована производительность системы, облегчена база оперативных документов за счет разрезания данных на оперативные и архивные документы, изменена архитектура доступа к объектам системы, исключены ошибки искажения данных при сохранении документов или принятии решений за счет создания единого сохранения карточки.
Обновленный функционал включает следующее:
На странице превью (в карточке и картотеке) появилась возможность выбора номера страницы для быстрого перехода к указанной странице.
Возможность сортировать карточки документов
- При отправке документа на повторное согласование система проверяет поля согласующих и выдает предупреждение, если нет дополнительных согласующих или решения текущих согласующих не были очищены.
- Карточки документов теперь можно сортировать по нескольким столбцам для удобства работы с большим потоком документов. Для этого нужно сделать сортировку по первому столбцу и с зажатой клавишей ctrl отсортировать документы по другим столбцам. Чтобы снять множественную сортировку, требуется отпустить клавишу ctrl и сделать сортировку по любому из столбцов.
Поручения к документу
- При создании поручений из файла добавлена возможность автоматической простановки времени для поручений, в которых указана только дата. Функциональность помогает при планировании задач в сжатые сроки.
- Изменилось окно рассылки документа. В предыдущей версии WSS Docs адресаты рассылки отображались в порядке рассылки документа. сортировка осуществляется в алфавитном порядке по возрастанию для упрощения поиска сотрудников. Также все поля окна рассылки в релизе системы имеют ограничение по высоте. Это позволит пользователям удобно работать в рамках небольшого окна. А в случае выбора большого числа адресатов, групп или адресов Email, появится скролл.
Окно рассылки документа
Добавлено информационное окно при обработке карточки, открытой из оповещения.Ранее при сохранении карточки, открытой из оповещения, отображалась пустая вкладка.
В рамках оптимизации производительности системы была снижена нагрузка на сервера, оптимизирован поиск и отображение документов в папках интерфейсов, включен полнотекстовый поиск в папках интерфейсов, в окнах выбора документов и контекстном поиске в полях, оптимизирована скорость открытия и сохранения карточки документа.
При наличии мобильного приложения WSS Docs доступна функциональность автоматического входа в приложение через систему администрирования AppCenter. В данной системе администраторы подтверждают вход пользователя, блокируют устройства в случае утери сотрудником компании и смотрят историю действий.
В релизе системы AppCenter самостоятельно проверяет в СЭД принадлежность пользователя к компании. Если проверка прошла успешно, то устройство будет автоматически зарегистрировано в AppCenter без подтверждения администратора.
Post-connection features¶
You can see a summary of this library’s supported WebSocket features by either
running the autobahn test suite against this client, or by reviewing a recently
run autobahn report, available as a .html file in the /compliance directory.
Ping/Pong Usage
The WebSocket specification defines
and
message opcodes as part of the protocol. These can serve as a way to keep a
long-lived connection active even if data is not being transmitted. However, if
a blocking event is happening, there may be some issues with ping/pong. The
below examples demonstrate how ping and pong can be sent by this library. You
can get additional debugging information by using
Wireshark
to view the ping and pong messages being sent. In order for Wireshark to
identify the WebSocket protocol properly, it should observe the initial HTTP
handshake and the HTTP 101 response in cleartext (without encryption) —
otherwise the WebSocket messages may be categorized as TCP or TLS messages.
For debugging, remember that it is helpful to enable .
WebSocket ping/pong example
This example is best for a quick test where you want to check the effect of a
ping, or where situations where you want to customize when the ping is sent.
This type of connection does not automatically respond to a “ping” with a “pong”.
import websocket websocket.enableTrace(True) ws = websocket.WebSocket() ws.connect("ws://echo.websocket.org") ws.ping() ws.pong() ws.close()
WebSocketApp ping/pong example
This example, and in general, is better for long-lived connections.
If a server needs a regular ping to keep the connection alive, this is probably
the option you will want to use. The function will automatically
send a “pong” when it receives a “ping”, per the specification.
import websocket def on_message(wsapp, message): print(message) def on_ping(wsapp, message): print("Got a ping!") def on_pong(wsapp, message): print("Got a pong! No need to respond") wsapp = websocket.WebSocketApp("wss://stream.meetup.com/2/rsvps", on_message=on_message, on_ping=on_ping, on_pong=on_pong) wsapp.run_forever(ping_interval=60, ping_timeout=10)
Common patterns¶
You will usually want to process several messages during the lifetime of a
connection. Therefore you must write a loop. Here are the basic patterns for
building a WebSocket server.
Consumer
For receiving messages and passing them to a coroutine:
async def consumer_handler(websocket, path): async for message in websocket await consumer(message)
In this example, represents your business logic for processing
messages received on the WebSocket connection.
Iteration terminates when the client disconnects.
Producer
For getting messages from a coroutine and sending them:
async def producer_handler(websocket, path): while True message = await producer() await websocket.send(message)
In this example, represents your business logic for generating
messages to send on the WebSocket connection.
raises a
exception when the client disconnects,
which breaks out of the loop.
Both
You can read and write messages on the same connection by combining the two
patterns shown above and running the two tasks in parallel:
async def handler(websocket, path): consumer_task = asyncio.ensure_future( consumer_handler(websocket, path)) producer_task = asyncio.ensure_future( producer_handler(websocket, path)) done, pending = await asyncio.wait( consumer_task, producer_task], return_when=asyncio.FIRST_COMPLETED, ) for task in pending task.cancel()
Установление WebSocket-соединения
Протокол работает над TCP.
Это означает, что при соединении браузер отправляет по HTTP специальные заголовки, спрашивая: «поддерживает ли сервер WebSocket?».
Если сервер в ответных заголовках отвечает «да, поддерживаю», то дальше HTTP прекращается и общение идёт на специальном протоколе WebSocket, который уже не имеет с HTTP ничего общего.
Пример запроса от браузера при создании нового объекта :
Описания заголовков:
- GET, Host
- Стандартные HTTP-заголовки из URL запроса
- Upgrade, Connection
- Указывают, что браузер хочет перейти на websocket.
- Origin
- Протокол, домен и порт, откуда отправлен запрос.
- Sec-WebSocket-Key
- Случайный ключ, который генерируется браузером: 16 байт в кодировке Base64.
- Sec-WebSocket-Version
- Версия протокола. Текущая версия: 13.
Все заголовки, кроме и , браузер генерирует сам, без возможности вмешательства JavaScript.
Такой XMLHttpRequest создать нельзя
Создать подобный XMLHttpRequest-запрос (подделать ) невозможно, по одной простой причине: указанные выше заголовки запрещены к установке методом .
Сервер может проанализировать эти заголовки и решить, разрешает ли он с данного домена .
Ответ сервера, если он понимает и разрешает -подключение:
Здесь строка представляет собой перекодированный по специальному алгоритму ключ . Браузер использует её для проверки, что ответ предназначается именно ему.
Затем данные передаются по специальному протоколу, структура которого («фреймы») изложена далее. И это уже совсем не HTTP.
Также возможны дополнительные заголовки и , описывающие расширения и подпротоколы (subprotocol), которые поддерживает данный клиент.
Посмотрим разницу между ними на двух примерах:
-
Заголовок означает, что браузер поддерживает модификацию протокола, обеспечивающую сжатие данных.
Это говорит не о самих данных, а об улучшении способа их передачи. Браузер сам формирует этот заголовок.
-
Заголовок говорит о том, что по WebSocket браузер собирается передавать не просто какие-то данные, а данные в протоколах SOAP или WAMP («The WebSocket Application Messaging Protocol»). Стандартные подпротоколы регистрируются в специальном каталоге IANA.
Этот заголовок браузер поставит, если указать второй необязательный параметр :
При наличии таких заголовков сервер может выбрать расширения и подпротоколы, которые он поддерживает, и ответить с ними.
Например, запрос:
Ответ:
В ответе выше сервер указывает, что поддерживает расширение , а из запрошенных подпротоколов – только SOAP.
Соединение можно открывать как или как . Протокол представляет собой WebSocket над HTTPS.
Кроме большей безопасности, у есть важное преимущество перед обычным – большая вероятность соединения. Дело в том, что HTTPS шифрует трафик от клиента к серверу, а HTTP – нет
Дело в том, что HTTPS шифрует трафик от клиента к серверу, а HTTP – нет.
Если между клиентом и сервером есть прокси, то в случае с HTTP все WebSocket-заголовки и данные передаются через него. Прокси имеет к ним доступ, ведь они никак не шифруются, и может расценить происходящее как нарушение протокола HTTP, обрезать заголовки или оборвать передачу.
А в случае с весь трафик сразу кодируется и через прокси проходит уже в закодированном виде. Поэтому заголовки гарантированно пройдут, и общая вероятность соединения через выше, чем через .
Data transfer
WebSocket communication consists of “frames” – data fragments, that can be sent from either side, and can be of several kinds:
- “text frames” – contain text data that parties send to each other.
- “binary data frames” – contain binary data that parties send to each other.
- “ping/pong frames” are used to check the connection, sent from the server, the browser responds to these automatically.
- there’s also “connection close frame” and a few other service frames.
In the browser, we directly work only with text or binary frames.
WebSocket method can send either text or binary data.
A call allows in string or a binary format, including , , etc. No settings required: just send it out in any format.
When we receive the data, text always comes as string. And for binary data, we can choose between and formats.
That’s set by property, it’s by default, so binary data comes as objects.
Blob is a high-level binary object, it directly integrates with , and other tags, so that’s a sane default. But for binary processing, to access individual data bytes, we can change it to :
Простой клиент веб-сокетов
С точки зрения веб-страницы функциональность веб-сокетов легко понять и использовать. Первый шаг — это создать объект WebSocket и передать ему URL. Код для этого подобен следующему:
Строка URL начинается с текста ws://, который идентифицирует подключение типа веб-сокет. Этот URL указывает файл веб-приложения на сервере (в данном случае это сценарий socketServer.php).
Стандарт веб-сокетов также поддерживает URL, которые начинаются с текста wss://, что указывает на требование использовать безопасное, зашифрованное подключение (точно так же, как и при запросе веб-страницы указывается URL, начинающийся с https:// вместо http://).
Веб-сокеты могут подключаться не только к своему веб-серверу. Веб-страница может открыть подключение к серверу веб-сокетов, исполняющемуся на другом веб-сервере, не требуя для этого никаких дополнительных усилий.
Само обстоятельство создания объекта WebSocket понуждает страницу пытаться подключиться к серверу. Дальше надо использовать одно из четырех событий объекта WebSocket: onOpen (при установлении подключения), onError (когда возникает ошибка), onClose (при закрытии подключения) и onMessage (когда страница получает сообщение от сервера):
Например, в случае успешного подключения неплохо бы отправить соответствующее подтверждающее сообщение. Такое сообщение доставляется с помощью метода send() объекта WebSocket, которому в качестве параметра передается обычный текст. Далее приведена функция, которая обрабатывает событие onopen и отправляет сообщение:
Предположительно, веб-сервер получит это сообщение и даст на него ответ.
События onError и onClose можно использовать для отправки извещений посетителю веб-страницы. Но безоговорочно самым важным является событие onMessage, которое срабатывает при получении новых данных от сервера. Опять же, код JavaScript для обработки этого события не представляет никаких сложностей — мы просто извлекаем текст сообщения из свойства data:
Если веб-страница решит, что вся ее работа выполнена, она может закрыть подключение, используя метод disconnect():
Из этого обзора веб-сокетов можно видеть, что использование сервера веб-сокетов стороннего разработчика не представляет никаких трудностей — нам нужно лишь знать, какие сообщения отправлять, а какие — ожидать.
Чтобы заставить подключение веб-сокетов работать, выполняется большой объем работы за кулисами. Прежде всего, веб-страница устанавливает связь по обычному стандарту HTTP. Потом это подключение нужно повысить до подключения веб-сокетов, позволяющего свободную двустороннюю связь. На этом этапе возможны проблемы, если между компьютером клиента и веб-сервером находится прокси-сервер (как, например, в типичной корпоративной сети). Прокси-сервер может отказаться сотрудничать и разорвет подключение. Эту проблему можно решить, обнаруживая неудачное подключение (посредством события onError объекта WebSocket) и применяя один из заполнителей (polyfills) для сокетов, описанных на веб-сайте GitHub. Эти заполнители применяют метод опроса, чтобы эмулировать подключение веб-сокетов.
Заключение
SSE является решением наших проблем с доставкой данных. Попутно возникли и некоторые сложности, но они оказались простыми для исправления.
Вот как выглядит окончательная рабочая установка:
Обзор окончательной архитектуры. Все конечные точки API обслуживаются NGINX, поэтому клиенты получают мультиплексированный ответ.
NGINX дает нам следующее:
- Прокси-сервер для конечных точек API в разных местах;
- HTTP / 2 и все его преимущества, такие как мультиплексирование для соединений;
- Балансировка нагрузки;
- SSL.
Таким образом, мы управляем доставкой данных и сертификатами в одном месте, вместо того, чтобы делать это отдельно на каждой конечной точке.
Основные преимущества этого подхода:
- Эффективность данных;
- Упрощенная реализация;
- Соединения автоматически мультиплексируются через HTTP / 2;
- Механизм экономии заряда аккумулятора пользовательского устройства путем разгрузки соединения с прокси-сервером.
SSE позволяет обойтись без дополнительных затрат ресурсов, в отличие от альтернатив. Что касается серверной реализации, она не сильно отличается от поллинга. Но в клиенте все намного проще, чем при поллинге, поскольку для него требуется первоначальная подписка и назначение обработчиков событий. Это очень похоже на управление WebSockets.
Здесь вы можете найти демо-версию кода простой реализации клиента и сервера.
Данная публикация является переводом статьи «Using SSE Instead Of WebSockets For Unidirectional Data Flow Over HTTP/2» , подготовленная редакцией проекта.