Использование sse вместо websockets для однонаправленного потока данных через http / 2
Содержание:
HTTP Streaming
HTTP Streaming — provides a long-lived connection for instant and continuous data push (Image from realtimeapi.io)
The client makes an HTTP request, and the server trickles out a response of indefinite length (it’s like polling infinitely).HTTP streaming is performant, easy to consume and can be an alternative to WebSockets.
Issue: Intermediaries can interrupt the connection (e.g. timeout, intermediaries serving other requests in a round-robin manner). In such cases, it cannot guarantee the complete realtimeness.
00:00:00 CLIENT-> I need cakes 00:00:01 SERVER-> Wait for a moment.00:00:01 SERVER-> Cake-1 is in process.00:00:02 SERVER-> Have cake-1.00:00:02 SERVER-> Wait for cake-2.00:00:03 SERVER-> Cake-2 is in process.00:00:03 SERVER-> You must be enjoying cake-1.00:00:04 SERVER-> Have cake-2.00:00:04 SERVER-> Wait for cake-3.00:00:05 CLIENT-> Enough, I'm full.
Пример чата
Давайте рассмотрим пример чата с использованием WebSocket API и модуля WebSocket сервера Node.js https://github.com/websockets/ws
Основное внимание мы, конечно, уделим клиентской части, но и серверная весьма проста
HTML: нам нужна форма для отправки данных и для отображения сообщений:
От JavaScript мы хотим 3 вещи:
- Открыть соединение.
- При отправке формы пользователем – вызвать для сообщения.
- При получении входящего сообщения – добавить его в .
Вот код:
Серверный код выходит за рамки этой главы. Здесь мы будем использовать Node.js, но вы не обязаны это делать. Другие платформы также поддерживают средства для работы с WebSocket.
Серверный алгоритм действий будет таким:
- Создать – набор сокетов.
- Для каждого принятого веб-сокета – добавить его в набор и поставить ему обработчик события для приёма сообщений.
- Когда сообщение получено: перебрать клиентов и отправить его всем.
- Когда подключение закрыто: .
Вот рабочий пример:
Вы также можете скачать его (верхняя правая кнопка в ифрейме) и запустить локально. Только не забудьте установить Node.js и выполнить команду до запуска.
About HTML5 WebSocket
The HTML5 WebSockets specification defines an API that enables web pages to use the WebSockets protocol for two-way communication with a remote host. It introduces the WebSocket interface and defines a full-duplex communication channel that operates through
a single socket over the Web. HTML5 WebSockets provide an enormous reduction in unnecessary network traffic and latency compared to the unscalable polling and long-polling solutions that were used to simulate a full-duplex connection by maintaining
two connections.
HTML5 WebSockets account for network hazards such as proxies and firewalls, making streaming possible over any connection, and with the ability to support upstream and downstream communications over a single connection, HTML5 WebSockets-based applications
place less burden on servers, allowing existing machines to support more concurrent connections. The following figure shows a basic WebSocket-based architecture in which browsers use a WebSocket connection for full-duplex, direct communication
with remote hosts.
One of the more unique features WebSockets provide is its ability to traverse firewalls and proxies, a problem area for many applications. Comet-style applications typically employ long-polling as a rudimentary line of defense against firewalls and proxies.
The technique is effective, but is not well suited for applications that have sub-500 millisecond latency or high throughput requirements. Plugin-based technologies such as Adobe Flash, also provide some level of socket support, but have long
been burdened with the very proxy and firewall traversal problems that WebSockets now resolve.
A WebSocket detects the presence of a proxy server and automatically sets up a tunnel to pass through the proxy. The tunnel is established by issuing an HTTP CONNECT statement to the proxy server, which requests for the proxy server to open a TCP/IP connection
to a specific host and port. Once the tunnel is set up, communication can flow unimpeded through the proxy. Since HTTP/S works in a similar fashion, secure WebSockets over SSL can leverage the same HTTP CONNECT technique. Note that WebSockets
are just beginning to be supported by modern browsers (Chrome now supports WebSockets natively). However, backward-compatible implementations that enable today’s browsers to take advantage of this emerging technology are available.
WebSockets—like other pieces of the HTML5 effort such as Local Storage and Geolocation—was originally part of the HTML5 specification, but was moved to a separate standards document to keep the specification focused. WebSockets has been submitted to the
Internet Engineering Task Force (IETF) by its creators, the Web Hypertext Application Technology Working Group (WHATWG). Authors, evangelists, and companies involved in the standardization still refer to the original set of features, including
WebSockets, as «HTML5.»
REST
The architectural style, REST (REpresentational State Transfer) is by far the most standardized way of structuring the web APIs for requests. REST is purely an architectural style based on several principles. The APIs adhering to REST principles are called RESTful APIs. REST APIs use a request/response model where every message from the server is the response to a message from the client. In general, RESTful APIs uses HTTP as its transport protocol. For such cases, lookups should use requests. , , and requests should be used for mutation, creation, and deletion respectively (avoid using requests for updating information).
Простой клиент веб-сокетов
С точки зрения веб-страницы функциональность веб-сокетов легко понять и использовать. Первый шаг — это создать объект 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. Эти заполнители применяют метод опроса, чтобы эмулировать подключение веб-сокетов.
Реализуем часть протокола
Что бы в реализации сервера была какая то цель, нужно эту цель придумать. Целью кода данной статьи будет написание WebSocket сервера, который реализует часть протокола сокетов и позволяет переписываться нескольким клиентам из консоли браузера. Для начала нужно реализовать функционал опроса клиента с помощью управляющих фреймов Ping. Нам нужно знать, что клиент еще жив и готов принимать данные с сервера. Фрейм Ping, управляющий фрейм, но он так же может содержать данные. Когда клиент получит такое сообщение по сокету, он должен отправить на сервер фрейм Pong с теми данными, которые были во фрейме Ping. До реализации этого функционала, давайте пропишем в класс сервера необходимые константы
Далее реализуем наш метод по формированию фрейма Ping
По большому счету, в данном случае, это не требуется. Нам совершенно не обязательно пересылать какие-то данные клиенту вместе с управляющим фреймом Ping. Поэтому этот метод можно удалить, а вместо него в класс добавить еще одну константу. Также для того, чтобы реализовать функционал чата, нам потребуется хранить объекты подключений. Заведем под это отдельную коллекцию в классе сервера.
Модицифируем конструктор, добавим отправку фрейма Ping подключившимся клиентам с интервалом в 5 секунд, а также добавляем новых клиентов в коллекцию.
Теперь мы можем принимать соединения по сокетам и поддерживать его с помощью пингов. Осталось научить наш сервер маршрутизировать сообщения от клиентов. В спецификации к протоколу написано, что клиенты всегда должны отправлять сообщения на сервер в маскированном виде, а сообщения сервера всегда без маски. Из этого следует, что нам нужно раскодировать сообщение, а для этого нужно понять, что за сообщение пришло на сервер, получить маску, длину сообщения и сами данные. Напишем для этого метод
- В этой строке нам нужно получить длину данных внутри фрейма. Мы делаем это с помощью операции XOR и констранты, которая представляет число 128 в двоичном виде, которое выглядит как 10000000. В данном случае мы это делаем, исходя из того, что данные от клиента всегда приходят в маскированном виде, а значит первый бит этого байта всегда будет 1.
- Согласно спецификации для фреймов с длиной 126, длина сообщения передаётся в двух следующих байтах
- Согласно спецификации для фреймов с длиной 127, длина сообщения передаётся в восьми следующих байтах
С помощью этой функции мы можем получать всю необходимую информацию для обмена сообщениями между клиентами. Напишем метод, который будет демаскировать данные
Демаскирование происходит путем применения функции XOR к каждому байту данных и соответствующему ему байту маски. Длина маски указана в спецификации и составляет 4 байта. Теперь можно написать метод для отправки коротких сообщений по сокету клиенту.
Нам осталось финализировать конструктор класса. Добавим туда рассылку полученных сообщений от клиента всем активным клиентам, а также добавим отправку всем клиентам сообщения при подключении нового клиента.
Теперь можно запустить сервер. Для проверки работоспособности можно открыть две вкладки браузера и в консоли каждой вклдаки написать следующий код.
Затем отправить сообщение в одной из вкладок
Возможности
По умолчанию пакет WSS 3 обладает следующими возможностями:
- сайты и рабочие среды
- Библиотеки документов
- Работа с документами
- Совместная правка при интеграции с Microsoft Office
- Календари
- Списки контактов
- Списки задач
- Уведомления
- Дискуссионные форумы
- Блоги
- Вики
- Объявления
- Библиотеки изображений
- Библиотеки форм
- Контроль проекта (график Гантта)
- Контроль выпусков
- Списки ссылок
- Последовательности работ
- Контроль версий
- Контроль доступа
- Настройка страниц
- Страницы Web Part
- Панели инструментов
- Поиск
- Навигация по сайту
- Бэкап и восстановление
- Управление пользователями
- Единое (центральное) администрирование веб-сайта
После установки пакетов, отдельно загружаемых как Windows Sharepoint Services 3.0 Application Templates, становятся доступны следующие дополнительные возможности:
- Управление планированием отпусков и запросами на отсутствие (Absence Request and Vacation Schedule Management)
- Служба поддержки (Help Desk)
- Составление бюджета и отслеживание нескольких проектов (Budgeting and Tracking Multiple Projects)
- Отслеживание запасов (Inventory Tracking)
- База данных ошибок (Bug Database)
- Рабочая область ИТ-группы (IT Team Workspace)
- Центр обработки (Call Center)
- Управление заявками на должности и собеседованием (Job Requisition and Interview Management)
- Управление запросами на изменение (Change Request Management)
- База знаний (Knowledge Base)
- Узел поддержки процесса определения соответствия (Compliance Process Support Site)
- Отдел абонемента в библиотеке (Lending Library)
- Управление контактами (Contacts Management)
- Отслеживание и управление физическими активами (Physical Asset Tracking and Management)
- Библиотека документов и редактирование (Document Library and Review)
- Рабочая область отслеживания проектов (Project Tracking Workspace)
- Планирование мероприятия (Event Planning)
- Резервирование оборудования и помещений (Room and Equipment Reservations)
- Возмещение и утверждение расходов (Expense Reimbursement and Approval Site)
- Канал зацепок для продаж (Sales Lead Pipeline)
- Совет директоров (Board of Directors)
- Планирование обучения сотрудников и материалы (Employee Training Scheduling and Materials)
- Отчеты об эффективности деятельности (Business Performance Rating)
- Исследование акций (Equity Research)
- Управление делами для правительственных органов (Case Management for Government Agencies)
- Объединенное отслеживание компаний маркетинга (Integrated Marketing Campaign Tracking)
- Управление учебным классом (Classroom Management)
- Управление производственным процессом (Manufacturing Process Management)
- Инициирование и управление клиническим исследованием (Clinical Trial Initiation and Management)
- Открытие нового хранилища (New Store Opening)
- Узел конкурентного анализа (Competitive Analysis Site)
- Планирование потребностей в продуктах и маркетинге (Product and Marketing Requirements Planning)
- База данных обсуждений (Discussion Database)
- Запрос предложения (Request for Proposal)
- Управление спорными счетами-фактурами (Disputed Invoice Management)
- Спортивная лига (Sports League)
- Узел мероприятий сотрудников (Employee Activities Site)
- Узел рабочей группы (Team Work Site)
- Пособия веб-сотрудников (Employee Self-Service Benefits)
- Управление карточками табельного учёта (Timecard Management)
Популярные материалы
Общая информация о веб-сокетах
Веб-сокеты, это такая технология, которая позволяет браузеру и серверу создать одно постоянное соединение и через него обмениваться данными.
Преимущества такого подхода в том что для отслеживания изменения на сайте, браузеру теперь нет необходимости постоянно «сыпать» запросы на сервер.
При постоянном соединении сервер теперь может когда ему надо отправить сообщение браузеру, т.е. связь двунаправленная, от браузера к серверу и от сервера к браузеру.
Рассмотрим классическую схему уведомления о сообщениях на сайте.
Когда пользователь авторизуется на сайте, браузер каждый 30 секунд (может и чаще) шлёт ajax-запрос на сайт, по определённому урлу.
Запрос типа — «Пришли ли мне новые сообщения».
Сервер в большинстве случаев будет отвечать «Сообщений новых нет», и только изредка долгожданное «У вас 1 новое сообщение».
Когда пользователей не много такая схема устраивает, но когда их много сервер получает до 1000 и более безсмысленных запросов.
Такая схема использовалась, потому что http построен по принципу сделал запрос, получил ответ и «давай до свидание».
В http нет возможности отправить сообщение от сервера браузеру, если браузер не спросит.
При схеме с веб-сокетами браузеру достаточно создать соединение и ждать, сервер сам ответит браузеру, когда нужно.
Преимущество на лицо — значительно снижается трафик и нагрузка на сервер, и уведомление приходит моментально.
Широта использования веб-сокетов велика: чаты, уведомления, «доставучие» online-консультанты и прочее.
Приостановка долгоживущих запросов
С Comet возникает еще одна проблема. Как серверу приостановить долгоживущий запрос без снижения производительности, а затем восстановить и выполнить его, как только на сервере произойдет событие?
Очевидно, нельзя просто задерживать запрос и ответ – это может привести к дефициту потоков и высокому потреблению памяти. Для приостановки запроса при ждущем опросе в среде неблокирующего ввода/вывода требуется специальный API. В Java такой API обеспечивает спецификация Servlet 3.0 (см. часть 1 этого цикла). Пример приведен в листинге 9.
Листинг 9. Определение асинхронного сервлета с помощью Servlet 3.0
<?xml version="1.0" encoding="UTF-8"?> <web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:j2ee="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml /ns/j2ee/web-app_3.0.xsd"> <servlet> <servlet-name>events</servlet-name> <servlet-class>ReverseAjaxServlet</servlet-class> <async-supported>true</async-supported> </servlet> <servlet-mapping> <servlet-name>events</servlet-name> <url-pattern>/ajax</url-pattern> </servlet-mapping> </web-app>
Определив асинхронный сервлет, можно использовать API Servlet 3.0 для приостановки и возобновления запроса, как показано в листинге 10.
Листинг 10. Приостановка и возобновление действия запроса
AsyncContext asyncContext = req.startAsync(); // Ссылка asyncContext где-то запоминается, // а затем, при необходимости, ее можно продолжить или завершить в другом потоке HttpServletResponse req = (HttpServletResponse) asyncContext.getResponse(); req.getWriter().write("data"); req.setContentType(); asyncContext.complete();
До появления Servlet 3.0 каждый контейнер должен был иметь (и до сих пор имеет) свой собственный механизм. Хорошо известным примером является Jetty Continuations; на Jetty Continuations опираются многие библиотеки Reverse Ajax в Java. При этом не обязательно запускать приложение в контейнере Jetty. API достаточно «умен», чтобы определить контейнер, с которым вы работаете, и вернуться к Servlet 3.0 API, если он есть, при запуске в другом контейнере, таком как Tomcat или Grizzly. Это справедливо для Comet, но если вы хотите воспользоваться преимуществами WebSockets, другого выбора, кроме использования функций, зависящих от контейнера, пока нет.
Спецификация Servlet 3.0 еще не вышла, но многие контейнеры уже реализуют этот API, так как это стандартный способ работы с Reverse Ajax.
FAQ
How to get the IP address of the client?
The remote IP address can be obtained from the raw socket.
const WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 8080 }); wss.on('connection', function connection(ws, req) { const ip = req.socket.remoteAddress; });
When the server runs behind a proxy like NGINX, the de-facto standard is to use
the header.
wss.on('connection', function connection(ws, req) { const ip = req.headers'x-forwarded-for'.split(/\s*,\s*/); });
How to detect and close broken connections?
Sometimes the link between the server and the client can be interrupted in a way
that keeps both the server and the client unaware of the broken state of the
connection (e.g. when pulling the cord).
In these cases ping messages can be used as a means to verify that the remote
endpoint is still responsive.
const WebSocket = require('ws'); function noop() {} function heartbeat() { this.isAlive = true; } const wss = new WebSocket.Server({ port: 8080 }); wss.on('connection', function connection(ws) { ws.isAlive = true; ws.on('pong', heartbeat); }); const interval = setInterval(function ping() { wss.clients.forEach(function each(ws) { if (ws.isAlive === false) return ws.terminate(); ws.isAlive = false; ws.ping(noop); }); }, 30000); wss.on('close', function close() { clearInterval(interval); });
Pong messages are automatically sent in response to ping messages as required by
the spec.
Just like the server example above your clients might as well lose connection
without knowing it. You might want to add a ping listener on your clients to
prevent that. A simple implementation would be:
const WebSocket = require('ws'); function heartbeat() { clearTimeout(this.pingTimeout); // Use `WebSocket#terminate()`, which immediately destroys the connection, // instead of `WebSocket#close()`, which waits for the close timer. // Delay should be equal to the interval at which your server // sends out pings plus a conservative assumption of the latency. this.pingTimeout = setTimeout(() => { this.terminate(); }, 30000 + 1000); } const client = new WebSocket('wss://echo.websocket.org/'); client.on('open', heartbeat); client.on('ping', heartbeat); client.on('close', function clear() { clearTimeout(this.pingTimeout); });
Установление 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, обрезать заголовки или оборвать передачу.
А в случае с весь трафик сразу кодируется и через прокси проходит уже в закодированном виде. Поэтому заголовки гарантированно пройдут, и общая вероятность соединения через выше, чем через .
Connection close
Normally, when a party wants to close the connection (both browser and server have equal rights), they send a “connection close frame” with a numeric code and a textual reason.
The method for that is:
- is a special WebSocket closing code (optional)
- is a string that describes the reason of closing (optional)
Then the other party in event handler gets the code and the reason, e.g.:
Most common code values:
- – the default, normal closure (used if no supplied),
- – no way to set such code manually, indicates that the connection was lost (no close frame).
There are other codes like:
- – the party is going away, e.g. server is shutting down, or a browser leaves the page,
- – the message is too big to process,
- – unexpected error on server,
- …and so on.
The full list can be found in .
WebSocket codes are somewhat like HTTP codes, but different. In particular, any codes less than are reserved, there’ll be an error if we try to set such a code.
Synchronization example¶
A WebSocket server can receive events from clients, process them to update the
application state, and synchronize the resulting state across clients.
Here’s an example where any client can increment or decrement a counter.
Updates are propagated to all connected clients.
The concurrency model of guarantees that updates are
serialized.
Run this script in a console:
#!/usr/bin/env python # WS server example that synchronizes state across clients import asyncio import json import logging import websockets logging.basicConfig() STATE = {"value" } USERS = set() def state_event(): return json.dumps({"type" "state", **STATE}) def users_event(): return json.dumps({"type" "users", "count" len(USERS)}) async def notify_state(): if USERS # asyncio.wait doesn't accept an empty list message = state_event() await asyncio.wait() async def notify_users(): if USERS # asyncio.wait doesn't accept an empty list message = users_event() await asyncio.wait() async def register(websocket): USERS.add(websocket) await notify_users() async def unregister(websocket): USERS.remove(websocket) await notify_users() async def counter(websocket, path): # register(websocket) sends user_event() to websocket await register(websocket) try await websocket.send(state_event()) async for message in websocket data = json.loads(message) if data"action" == "minus" STATE"value" -= 1 await notify_state() elif data"action" == "plus" STATE"value" += 1 await notify_state() else logging.error("unsupported event: {}", data) finally await unregister(websocket) start_server = websockets.serve(counter, "localhost", 6789) asyncio.get_event_loop().run_until_complete(start_server) asyncio.get_event_loop().run_forever()
Then open this HTML file in several browsers.