Javascript интерфейс websocket
Содержание:
- Закрытие подключения
- Установление WebSocket-соединения
- Реализация клиента на Javascript
- Сервер WS или как с сервера вызвать клиента? аля Сервер взаимодействий без него
- Простой клиент веб-сокетов
- Реализуем часть протокола
- Browser-based example¶
- Обработка разрывов соединения
- Performance is not scalability
- Basic example¶
- Полезные функции для манипуляций с сокетами в PHP
- Или getting started with WebSocket PHP без phpDaemon
- SSE (Server Sent Events / EventSource)
- Установка соединения с сервером
- Data transfer
- HTTP Streaming
- Оповещение пользователя
- REST
Закрытие подключения
Обычно, когда сторона хочет закрыть соединение (браузер и сервер имеют равные права), они отправляют «фрейм закрытия соединения» с кодом закрытия и указывают причину в виде текста.
Метод для этого:
- – специальный WebSocket-код закрытия (не обязателен).
- – строка с описанием причины закрытия (не обязательна).
Затем противоположная сторона в обработчике события получит и код и причину , например:
– это не любое число, а специальный код закрытия WebSocket.
Наиболее распространённые значения:
- – по умолчанию, нормальное закрытие,
- – невозможно установить такой код вручную, указывает, что соединение было потеряно (нет фрейма закрытия).
Есть и другие коды:
- – сторона отключилась, например сервер выключен или пользователь покинул страницу,
- – сообщение слишком большое для обработки,
- – непредвиденная ошибка на сервере,
- …и так далее.
Полный список находится в .
Коды WebSocket чем-то похожи на коды HTTP, но они разные. В частности, любые коды меньше зарезервированы. Если мы попытаемся установить такой код, то получим ошибку.
Установление 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, обрезать заголовки или оборвать передачу.
А в случае с весь трафик сразу кодируется и через прокси проходит уже в закодированном виде. Поэтому заголовки гарантированно пройдут, и общая вероятность соединения через выше, чем через .
Реализация клиента на Javascript
Протокол веб-сокет создан уже давно (приобрёл статус RFC в 11.12.2011) и поддерживается большинством браузеров.
Чтобы узнать поддерживает ли ваш браузер веб-сокеты перейдите по .
Работа в браузерах с вебсокетам проходит в несколько этапов:
- Установка соединения или рукопожатие (handshake).
- Создание обработчиков событий: onopen (соединение создано), onclose(соединение закрыто), onmessage (пришло сообщение от сервера), onerror (ошибка при работе веб-сокетов).
- Отправка сообщений (фреймов) на сервер.
Тестировать веб-сокеты мы будем на сервере websocket.org «ws://echo.websocket.org», который будет принимать от нас сообщения и отвечать на них повторением сообщением.
Этот сайт как раз существует, что лучше понять веб-сокеты, он понимает кросс-доменные запросы, поэтому страницу с JavaScript будем размещать у себя на локальном компьютере.
Этап. Рукопожатие
Чтобы создать соединение по веб-сокету достаточно создать объект WebSocket, в котором указывается урл для подключения.
Используйте протокол «ws://», если нужно не шифрованное соединение или протокол «wss://» для шифрованного соединения.
Этап. Создание обработчиков событий.
После того как мы создали объект WebSocket необходимо повесить функции-обработчики на события.
Если нужно повесить несколько функций на событие используем методы «addEventListener» и «removeEventListener». Пример:
Этап. Отправка сообщений на сервер
По веб-сокету сообщения отправляются в виде строки. Пример отправки простого текстового сообщения.
Обработка приходящих данных лежит уже на стороне сервера. Чаще для удобства работы по вебсокету отправляют JSON данные серилизованные в строку и обрабатывают приходящие данные как строка в JSON-e. Пример использования:
Удобный способ отправки сообщений по веб-сокету служит протокол «JSON-RPC» (ссылка). Это очень простой протокол, который облегчит взаимодействие браузера и сервера. Пример использования JSON RPC:
Параметры json-rpc объекта:
- jsonrpc — версия протокола, может быть «2.0» или «1.0»
- id — идентификатор запроса. Используется для идентификации ответа от сервера по своем запросу. Т.е. если отправить два запроса, то ответ от сервера по каждому запросу прийдёт в разное время, для этого и нужен id. На сервере необходимо учитывать этот параметр и в ответ прислать именно нужный id.
- method — наименование метода, любая строка, к примеру «get», «hello», «set» и др.
- params — параметры связанные с этим методом, тип переменной может быть любой, всё зависит от сервера.
Чтобы закрыть соединение используем метод close().
Сервер WS или как с сервера вызвать клиента? аля Сервер взаимодействий без него
Не так давно, в 1С появился Сервер Взаимодействия, который решает огромную проблему сервер-клиентского взаимодействия. Теперь появилась возможность отправить уведомления с сервера на клиент. Не буду углубляться во все возможности штатного (если так можно назвать) сервера взаимодействия, и кто уже успел с ним поработать — почувствовал все преимущества и недостатки. Как раз через недостатки было принято решение отказаться от него и сделать свой.
Следующая конфигурация — это серверWS (WebSocket), для любой взаимосвязи сервера с клиентом, клиента с клиентом, не только в пределах одной базы, но и различных баз, и даже размещенных удалено друг от друга.
1 стартмани
Простой клиент веб-сокетов
С точки зрения веб-страницы функциональность веб-сокетов легко понять и использовать. Первый шаг — это создать объект 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 байта. Теперь можно написать метод для отправки коротких сообщений по сокету клиенту.
Нам осталось финализировать конструктор класса. Добавим туда рассылку полученных сообщений от клиента всем активным клиентам, а также добавим отправку всем клиентам сообщения при подключении нового клиента.
Теперь можно запустить сервер. Для проверки работоспособности можно открыть две вкладки браузера и в консоли каждой вклдаки написать следующий код.
Затем отправить сообщение в одной из вкладок
Browser-based example¶
Here’s an example of how to run a WebSocket server and connect from a browser.
Run this script in a console:
#!/usr/bin/env python # WS server that sends messages at random intervals import asyncio import datetime import random import websockets async def time(websocket, path): while True now = datetime.datetime.utcnow().isoformat() + "Z" await websocket.send(now) await asyncio.sleep(random.random() * 3) start_server = websockets.serve(time, "127.0.0.1", 5678) asyncio.get_event_loop().run_until_complete(start_server) asyncio.get_event_loop().run_forever()
Then open this HTML file in a browser.
Обработка разрывов соединения
Если соединение разрывается, оно автоматически восстанавливается браузером. Сервер может отправить таймаут для повторного завершения или закрытия соединения. В таком случае браузер попытается подключиться после завершения таймаута или не будет ничего делать, если соединение завершено.
Реализация образца сервера
Если клиент такой простой, возможно, сложной окажется реализация сервера? Обработчик сервера для 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
Это значительно упрощает то, что мы можем сделать с нашими данными.
Performance is not scalability
Горизонтальное масштабирование
Производительность — конечно, это первое требование, это логично.
Сохранение порядка сообщений — тоже важное свойство, которое нам нужно.
Масштабируемость брокера. Мы хотим, чтобы брокер сам по себе масштабировался и был бы отказоустойчивым.
Миллионы топиков
Мы хотим, чтобы он поддерживал миллионы топиков одновременно, потому что это частый use case — когда у каждого соединения есть свой персональный канал. Мы хотим рассылать сообщения конкретному пользователю. И если у вас миллион WebSocket-соединений, появляется миллион топиков в брокере.
Кэш/стрим сообщений. Мы хотим, чтобы брокер поддерживал кэш или стрим сообщений в топике/канале. Что это такое и от чего это спасает, поговорим чуть позже.
Возможность писать процедуры — и это большой бонус. Я расскажу, как мне это помогло в Centrifugo и в мессенджере Авито.
Опции брокера сообщений
Redis
- Redis производительный, в том числе у него производительный PUB/SUB;
- Он стабильный и, самое главное, предсказуемый;
- У него есть Sentinel для High Availability;
- Он позволяет писать атомарные LUA-процедуры;
- Структуры данных позволяют хранить кэш сообщений. Опять возник этот магический кэш сообщений, но мы к нему скоро вернемся.
- Используйте одно или пул соединений между WebSocket-сервером и вашим брокером.
- Не используйте новое соединение на каждый коннект! Я видел часто в примерах на GitHub, как пишут код: пришло новое WebSocket-соединение, открываем новое PUB/SUB соединение с Redis или с каким-то другим брокером. Так делать не надо, это антипаттерн и это не масштабируется.
- Используйте максимально эффективный формат сериализации сообщений для общения между WebSocket-сервером и вашим брокером. Здесь не надо задумываться о том, чтобы формат был человеко-читаемым, потому что его не увидят ни ваши фронтенд-разработчики, ни тестировщики. Это сугубо внутренняя вещь, и вы можете делать ее максимально эффективной.
Basic example¶
Here’s a WebSocket server example.
It reads a name from the client, sends a greeting, and closes the connection.
#!/usr/bin/env python # WS server example import asyncio import websockets async def hello(websocket, path): name = await websocket.recv() print(f"< {name}") greeting = f"Hello {name}!" await websocket.send(greeting) print(f"> {greeting}") start_server = websockets.serve(hello, "localhost", 8765) asyncio.get_event_loop().run_until_complete(start_server) asyncio.get_event_loop().run_forever()
On the server side, executes the handler coroutine
once for each WebSocket connection. It closes the connection when the handler
coroutine returns.
Here’s a corresponding WebSocket client example.
#!/usr/bin/env python # WS client example import asyncio import websockets async def hello(): uri = "ws://localhost:8765" async with websockets.connect(uri) as websocket name = input("What's your name? ") await websocket.send(name) print(f"> {name}") greeting = await websocket.recv() print(f"< {greeting}") asyncio.get_event_loop().run_until_complete(hello())
Полезные функции для манипуляций с сокетами в PHP
- socket_accept(socketObject) — после запроса принимает соединение на стороне сервера для текущего сокета, который вставляется в параметр;
- socket_create_listen(port, connect_count) — принимает соединение на указанном порту первого параметра, а второй параметр указывает сколько максимум может быть в очереди ожидающих соединений;
- socket_get_option(socketObject, level_of_protocol, option) — получает параметры потока при соединении, где второй параметр указывается уровень протокола, на котором расположена опция из третьего параметра. Третий параметр использует одну из предопределенных констант, хранящих значения опций, ознакомиться с которыми можно в документации к PHP. Функция возвращает значение константы опции, при ошибке возвращает false;
- socket_last_error(socketObject) — возвращает последнюю возникшую ошибку при сбое соединения в числовом формате;
- socket_listen(socketObject, connect_count) — прослушивает соединения с сервером, после того как сокет был создан, а клиент присоединен к серверу. Второй параметр указывает максимальное количество ожидаемых соединений, которых может находиться в очереди. В случае успеха возвращает true, или false при ошибке;
- socket_send(socketObject, sending_data, length) — отправляет данные клиенту, который подсоединился к сокету. Третий параметр указывает максимальное количество байт, которые будут отправлены на сервер. Возвращает количество отправленных байтов или false;
- socket_sendto(socketObject, sending_data, length, adress, port) — отправка сообщения длиной length по адресу address через порт port посредством сокет-протокола даже при отсутствии подключения;
- socket_set_block(socketObject) — включает режим блокировки отправки данных до тех пор, пока сокет не получит сигнал на отправку данных – работа скрипта на сервере будет приостановлена. В случае ошибки блокировки соединения будет возвращено false;
- socket_set_option(socketObject, level_of_protocol, option) — настраивает параметры сокета. Последний параметр принимает только одну из предусмотренных в PHP констант;
- socket_strerror(code_of_error) — возвращает ошибку в формате строки. В параметр записывается код ошибки после выполнения функции socket_last_error();
- socket_write(socketObject, data, length) – осуществляет запись данных в сокет. Необязательный параметр length позволяет установить ограничение на размер записываемых данных, который указывается в количестве байт. Возвращает количество байт, записанных в сокет или false, в случае возникновения ошибки.
Или 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 на дешевом хостинге можно обойтись и без него.
SSE (Server Sent Events / EventSource)
SSE — events can be broadcast to multiple clients (Image from javaee.ch)
SSE connections can only push data to the browser. (communication is carried out from server to browser only, browsers can only subscribe to data updates originated by the server, but cannot send any data to the server)
00:00:00 CLIENT-> I need cakes 00:00:02 SERVER-> Have cake-1.00:00:04 SERVER-> Have cake-2.00:00:05 CLIENT-> Enough, I'm full.
- Sample applications: Twitter updates, stock quotes, cricket scores, notifications to browser
- Issue #1: .
- Issue #2: Maximum number of open connections is limited to 6 or 8 over HTTP/1.1 (based on the browser version). If you use HTTP/2, there won’t be an issue because one single TCP connection is enough for all requests (thanks to multiplexed support in HTTP/2).
Установка соединения с сервером
В проекте реализован метод Widget::connectToServer, он открывает диалог авторизации. Если диалог будет закрыт (result != AuthDialog::Accepted), то приложение закроется вместе с ним.
Если же ввести все авторизационные данные, то будет осуществлена попытка подключения к серверу.
void Widget::connectToServer() { AuthDialog authDialog(this); authDialog.setConnectionData(m_connectionData); int result = authDialog.exec(); if (result == AuthDialog::Accepted) { m_connectionData = authDialog.connectionData(); QString html = QString("%1 " "Установка соединения с %2:%3...") .arg(datetime()) .arg(m_connectionData.server) .arg(m_connectionData.port); ui->textBrowser->append(html); m_webSocket->open(QUrl(QString("ws://%1:%2?userName=%3&userColor=%4&gender=%5") .arg(m_connectionData.server) .arg(m_connectionData.port) .arg(m_connectionData.userName) .arg(QString(m_connectionData.userColor).replace("#","%23")) .arg(m_connectionData.gender))); } else { qApp->quit(); } }
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 :
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.
Оповещение пользователя
Если кликнуть по имени пользователя, то имя вставляется в поле ввода сообщения и заворачивается в фигурные скобки: «{» и «}».
На сервере имеется паттерн обработки такого текста (функция $worker->onMessage), который заменяет фигурные скобки на теги «<b>» и «</b>», выделяя текст жирным шрифтом.
Таким образом, когда пользователь получает сообщение, можно проверить наличие этих тегов и содержимого в них. Если в тегах содержится имя текущего пользователя, значит, в сообщении кто-то упомянул этого пользователя и надо его об этом уведомить. Это реализовано в методе обработки публичных сообщений:
void Widget::onPublicMessage(int userId, const QString &userName, const QString &userColor, const QString &text) { if (text.contains("" + m_userName + "")) { qApp->beep(); qApp->alert(this); } QString html = QString("%1 %3:" " %4") .arg(datetime()) .arg(userColor) .arg(userName) .arg(text) .arg(userId); ui->textBrowser->append(html); }
По такой же схеме можно реализовать полноценную поддержку markdown, вставку смайликов и картинок.
Расширяя функционал сервера и клиента можно также добавить:
- поддержку чат-комнат и полноценных приватных диалогов;
- сохранении истории сообщений в БД и её отправку при подключении или по запросу;
- статусы пользователей («Работаю», «Отдыхаю», «Отошёл» и др.);
- звуковые уведомления «Послать сигнал»;
- редактирование и удаление сообщений;
- цитирование сообщений других пользователей;
- передачу файлов.
Скриншот получившегося чата был в начале статьи, дополнительно приведу пример реального чата, реализованного по описанной в статье модели
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).