Введение в веб-сокеты
Содержание:
- Установление WebSocket-соединения
- Настройка VDS-сервера
- Node.js Socket Example
- Ограничения для источников WebSocket
- Node.js и socket.io¶
- Что такое протокол TCP/IP?
- Synchronization example¶
- Установка Workerman
- Методы
- Пример приложения
- Поддержка служб IIS/IIS Express
- Настройка ПО промежуточного слоя
- Краткая история веб-приложений реального времени
- Инициализация WebSocket-сервера
- Opening a websocket
Установление 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, обрезать заголовки или оборвать передачу.
А в случае с весь трафик сразу кодируется и через прокси проходит уже в закодированном виде. Поэтому заголовки гарантированно пройдут, и общая вероятность соединения через выше, чем через .
Настройка VDS-сервера
Сразу после приобретения VDS и установки операционной системы (выбрал свежую версию Ubuntu 18.04 на тарифе «Master») подключаемся к нему. На сервер можно зайти через консоль из панели управления VDS, но это не самый удобный вариант. Предпочтительнее подключаться по SSH.
Если разные способы подключения по SSH из Windows, например:
1. Воспользоваться программой Putty;
2. Воспользоваться терминалом Cygwin;
3. Воспользоваться терминалом Ubuntu из WSL (я выбрал этот способ).
В Linux намного проще, клиент для подключения по SSH, как правило, установлен во всех дистрибутивах по умолчанию, поэтому просто открываем терминал.
Независимо от выбранного способа, команда для подключения будет одна:
ssh -l root {VDS_IP_ADDRESS}
где {VDS_IP_ADDRESS} – это IP-адрес вашего сервера, который можно найти в панели управления VDS (блок «Список используемых IP-адресов»).
Окно терминала
Node.js Socket Example
Let’s say you need to write a server application, and you chose Node.js as your programming language. Your users can be any type of client, like a web browser, mobile app, IoT device, Node.js client, or anything that knows TCP.
You need to serve assets to your users over HTTP, but you also need to provide them with streams for bi-directional messaging. We can accomplish this in a single Node.js server app!
The code from the video, and also this article is available in my Node.js WebSocket Examples GitHub Repository.
First we’ll go over some plain socket code, followed by WebSocket code. If you already serve assets with something like Express.js, Hapi, or the native Node.js HTTP library, we can jump into the socket code.
Socket Server JavaScript Code
// Node.js socket server script const net = require('net'); // Create a server object const server = net.createServer((socket) => { socket.on('data', (data) => { console.log(data.toString()); }); socket.write('SERVER: Hello! This is server speaking.'); socket.end('SERVER: Closing connection now.'); }).on('error', (err) => { console.error(err); }); // Open server on port 9898 server.listen(9898, () => { console.log('opened server on', server.address().port); });
This script runs a Node.js socket server on port 9898. Whenever a client connects to this server app (IP_ADDRESS:9898) the server sends the client a string over the open socket. It says “SERVER: Hello! This is server speaking.” Whenever the client sends some data to the server, the Node.js script logs the contents in the ‘data’ event handler.
Socket Client JavaScript Code
Here we have our Node.js socket client script, which can connect to the Node.js socket server above.
// Node.js socket client script const net = require('net'); // Connect to a server @ port 9898 const client = net.createConnection({ port: 9898 }, () => { console.log('CLIENT: I connected to the server.'); client.write('CLIENT: Hello this is client!'); }); client.on('data', (data) => { console.log(data.toString()); client.end(); }); client.on('end', () => { console.log('CLIENT: I disconnected from the server.'); });
The client script attempts to connect to localhost:9898. If the connection succeeds, then the client sends a string to the server over the open socket. It says “CLIENT: Hello this is client!” Whenever the server sends some data to the client, the client logs it in the ‘data’ event handler.
This is what the output looks like for client and server Node.js socket scripts running on the command line.
Ограничения для источников WebSocket
Варианты защиты, предоставляемые CORS, не применяются к WebSocket. Браузеры не поддерживают следующие задачи:
- выполнение предварительных запросов CORS;
- использование ограничений, указанных в заголовках , при выполнении запросов WebSocket.
Однако браузеры отправляют заголовок при выпуске запросов WebSocket. Приложения должны быть настроены для проверки этих заголовков, чтобы использовались только WebSocket из ожидаемых источников.
Если вы размещаете сервер по адресу «https://server.com», а клиент — по адресу «https://client.com», добавьте «https://client.com» в список подлежащих проверке WebSocket.
Node.js и socket.io¶
Для использования в Node.js WebSocket необходимо установить npm модуль socket.io.
Рассмотрим пример.
app.js
index.html
Для подключения WebSocket на клиентской стороне используется модуль , экземпляру которого передается адрес сервера, с которым необходимо установить соединение по WebSocket.
При установке соединения между клиентом и сервером Node.js по WebSocket генерируется событие , которое обрабатывается с помощью метода модуля . Передаваемая вторым параметром методу callback-функция единственным параметром принимает экземпляр соединения (далее просто сокет).
Каждое соединение имеет свой уникальный идентификатор, зная который можно отправить сообщение конкретному клиенту (см. в примере маршрут ).
При разрыве соединения генерируется событие . Соединение разрывается, когда пользователь закрывает вкладку или когда сервер вызывает у сокета метод .
Для отправки данных от сервера Node.js к клиенту (и наоборот), используется метод , которые принимает следующие параметры:
- имя события;
- данные, которые необходимо отправить (могут быть отправлены в виде REST-аргументов);
- callback-функция (передается последним параметром), которая будет вызвана, когда вторая сторона получит сообщение.
Обработка отправляемых данных на стороне получателя происходит с использованием уже знакомого метода , первым параметром принимающего имя события, указанного в , вторым — callback-функцию с переданными данными в качестве ее параметров.
Для отправки данных всем клиентам, используйте метод применительно к объекту .
Чтобы узнать текущее количество соединений, используйте метод , вызываемый применительно к свойству экземпляра модуля (см. в примере маршрут ).
В качестве необязательного параметра методу можно передать имя «комнаты», количество соединений для который вы хотите узнать.
Что такое протокол TCP/IP?
Протокол TCP/IP (Transmission Control Protocol/Internet Protocol) представляет собой стек сетевых протоколов, повсеместно используемый для Интернета и других подобных сетей (например, данный протокол используется и в ЛВС). Название TCP/IP произошло от двух наиболее важных протоколов:
- IP (интернет протокол) — отвечает за передачу пакета данных от узла к узлу. IP пересылает каждый пакет на основе четырехбайтного адреса назначения (IP-адрес).
- TCP (протокол управления передачей) — отвечает за проверку корректной доставки данных от клиента к серверу. Данные могут быть потеряны в промежуточной сети. В протоколе TCP добавлена возможность обнаружения ошибок или потерянных данных и, как следствие, возможность запросить повторную передачу, до тех пор, пока данные корректно и полностью не будут получены.
Основные характеристики TCP/IP:
- Стандартизованные протоколы высокого уровня, используемые для хорошо известных пользовательских сервисов.
- Используются открытые стандарты протоколов, что дает возможность разрабатывать и дорабатывать стандарты независимо от программного и аппаратного обеспечения;
- Система уникальной адресации;
- Независимость от используемого физического канала связи;
Принцип работы стека протоколов TCP/IP такой же как и в модели OSI, данные верхних уровней инкапсулируются в пакеты нижних уровней.
Если пакет продвигается по уровню сверху вниз — на каждом уровне добавляется к пакету служебная информация в виде заголовка и возможно трейлера (информации помещенной в конец сообщения). Этот процесс называется инкапсуляция. Служебная информация предназначается для объекта того же уровня на удаленном компьютере. Ее формат и интерпретация определяются протоколами данного уровня.
Если пакет продвигается по уровню снизу вверх — он разделяется на заголовок и данные. Анализируется заголовок пакета, выделяется служебная информация и в соответствии с ней данные перенаправляются к одному из объектов вышестоящего уровня. Вышестоящий уровень, в свою очередь, анализирует эти данные и также их разделяет их на заголовок и данные, далее анализируется заголовок и выделяется служебная информация и данные для вышестоящего уровня. Процедура повторяется заново пока пользовательские данные, освобожденные от всей служебной информации, не дойдут до прикладного уровня.
Не исключено, что пакет так и не дойдет до прикладного уровня. В частности, если компьютер работает в роли промежуточной станции на пути между отправителем и получателем, тогда объект, на соответствующем уровне, при анализе служебной информации определит, что пакет на этом уровня адресован не ему, в следствии чего, объект проведет необходимые мероприятия для перенаправления пакета к пункту назначения или возврата отправителю с сообщением об ошибке. Но так или иначе не будет осуществлять продвижение данных на верхний уровень.
Пример инкапсуляции можно представить следующим образом:
Рассмотрим каждые функции уровней
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: %s", 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.
Установка Workerman
Чтобы скачать Workerman, сначала устанавливаем composer:
# apt update
# apt install composer
Теперь скачиваем Workerman в папку /usr/local/workerman:
# mkdir /usr/local/workerman
# cd /usr/local/workerman
# composer require workerman/workerman
И создаём php-файл, в котором будем писать код сервера чата:
touch ChatWorker.php
Далее открываем файл ChatWorker.php для редактирования. Это можно сделать разными способами. Самый хардкорный и олдскульный вариант — редактировать прямо в терминале, воспользовавшись консольными редакторами nano, mcedit, vim и др.
Если работаете в Linux, то из рабочего окружения KDE можно подключиться через файловый менеджер Dolphin по протоколу SFTP и открыть файл в любом редакторе или даже в IDE (например, в KDevelop).
Если работаете в Windows, то можете скачать Notepad++ с плагином NppFTP, либо что-то более продвинутое, вроде Sublime / Atom / Visual Studio Code, и так же подключиться по протоколу SFTP.
Методы
Отменяет соединение WebSocket и отменяет все ожидающие операции ввода-вывода. |
|
Закрывает подключение WebSocket в качестве асинхронной операции, используя подтверждение закрытия, которое определено в разделе 7 спецификации протокола WebSocket. |
|
Инициирует или завершает подтверждение закрытия, определенное в разделе 7 спецификации протокола WebSocket. |
|
Создайте буферы клиента для использования с этим экземпляром WebSocket. |
|
Этот API поддерживает инфраструктуру продукта и не предназначен для использования непосредственно из программного кода. Позволяет вызывающим объектам создать класс WebSocket на стороне клиента, который будет использовать WSPC для кадрирования. |
|
Создает новый WebSocket, работающий в указанном потоке, который представляет подключение к веб-сокету. |
|
Создает объект WebSocket , который работает с, Stream представляющим соединение через веб-сокет. |
|
Создает буфер сервера WebSocket. |
|
Используется для очистки неуправляемых ресурсов для ASP.NET и резидентных реализаций. |
|
Определяет, равен ли указанный объект текущему объекту. (Унаследовано от Object) |
|
Служит хэш-функцией по умолчанию. (Унаследовано от Object) |
|
Возвращает объект Type для текущего экземпляра. (Унаследовано от Object) |
|
Является устаревшей. Является устаревшей. Является устаревшей. возвращает значение, указывающее, предназначен ли экземпляр WebSocket платформа .NET Framework 4,5. |
|
Возвращает значение, указывающее, какое состояние экземпляра WebSocket — закрыто или прервано. |
|
Создает неполную копию текущего объекта Object. (Унаследовано от Object) |
|
Асинхронно получает данные через соединение WebSocket. |
|
Асинхронно получает данные через соединение WebSocket. |
|
Этот API поддерживает инфраструктуру продукта и не предназначен для использования непосредственно из программного кода. Является устаревшей. Разрешает вызывающим объектам регистрировать префиксы для запросов WebSocket (ws и wss). |
|
Асинхронно отправляет данные по соединению WebSocket. |
|
Асинхронно отправляет данные по соединению WebSocket. |
|
Асинхронно отправляет данные по соединению WebSocket. |
|
Проверяет, находится ли соединение в ожидаемом состоянии. |
|
Возвращает строку, представляющую текущий объект. (Унаследовано от Object) |
Пример приложения
Пример приложения в этой статье — это эхо-приложение. Оно имеет веб-страницу, которая устанавливает соединения WebSocket, а сервер перенаправляет все полученные сообщения обратно клиенту. Этот пример приложения не настроен для запуска из Visual Studio с IIS Express, поэтому его необходимо запустить в командной оболочке с и затем перейти в браузере по адресу . На этой веб-странице отображается состояние подключения.
Выберите Connect (Подключить), чтобы отправить запрос WebSocket на показанный URL-адрес. Введите тестовое сообщение и выберите Send (Отправить). После этого выберите Close Socket (Закрыть сокет). В разделе Communication Log (Журнал связи) выводится каждое выполняемое действие открытия, отправки и закрытия.
Поддержка служб IIS/IIS Express
Windows Server 2012 или более поздней версии и Windows 8 или более поздней версии с IIS и IIS Express 8 или более поздней версии поддерживают протокол WebSocket.
Примечание
Соединения WebSockets всегда включены при использовании IIS Express.
Включение WebSockets в службах IIS
Чтобы включить поддержку протокола WebSocket в Windows Server 2012 или более поздней версии:
Примечание
Эти действия не требуется выполнять при использовании IIS Express
- В меню Управление запустите мастер Добавить роли и компоненты или в окне Диспетчер серверов щелкните соответствующую ссылку.
- Выберите Установка ролей или компонентов. Выберите Далее.
- Выберите подходящий сервер (по умолчанию выбирается локальный сервер). Выберите Далее.
- Разверните Веб-сервер (IIS) в дереве Роли, разверните Веб-сервер, а затем Разработка приложений.
- Выберите протокол WebSocket. Выберите Далее.
- Если дополнительные функции не требуются, нажмите Далее.
- Нажмите кнопку Установить.
- По завершении установки выберите Закрыть, чтобы выйти из мастера.
Чтобы включить поддержку протокола WebSocket в Windows 8 или более поздней версии:
Примечание
Эти действия не требуется выполнять при использовании IIS Express
- Последовательно выберите Панель управления > Программы > Программы и компоненты > Включение или отключение компонентов Windows (в левой части экрана).
- Откройте следующие узлы: IIS > Службы Интернета > Компоненты разработки приложений.
- Выберите компонент Протокол WebSocket. Нажмите кнопку ОК.
Отключите WebSocket при использовании socket.io на Node.js
Если используется поддержка WebSocket в socket.io на Node.js, отключите модуль WebSocket IIS по умолчанию с помощью элемента в web.config или applicationHost.config. Если не выполнить этот шаг, модуль IIS WebSocket попытается обработать соединение WebSocket, а не Node.js и приложение.
Настройка ПО промежуточного слоя
Добавьте ПО промежуточного слоя WebSocket в метод класса :
Примечание
Если вы хотите принимать запросы WebSocket в контроллере, вызов должен предшествовать .
Можно настроить следующие параметры:
KeepAliveInterval — как часто нужно отправлять клиенту кадры проверки связи, чтобы прокси-серверы удерживали соединение открытым. Значение по умолчанию — две минуты.
Можно настроить следующие параметры:
- — как часто нужно отправлять клиенту кадры проверки связи, чтобы прокси-серверы удерживали соединение открытым. Значение по умолчанию — две минуты.
- — список допустимых значений заголовка Origin для запросов WebSocket. По умолчанию разрешены все источники. Дополнительные сведения см. в разделе «Ограничения для источников WebSocket» ниже.
Краткая история веб-приложений реального времени
Интернет был построен на представлении о том, что забота браузера– запрос данных с сервера, а забота сервера – обслуживание этих запросов. Эта парадигма не подвергалась сомнению несколько лет. Но с появлением AJAX в 2005 году многие начали работать над созданием двунаправленных соединений.
Веб-приложения значительно увеличивались в размере. Сдерживающим фактором для их роста была традиционная модель HTTP. Чтобы преодолеть это, были созданы несколько стратегий, позволяющих серверам «проталкивать» (push) данные клиенту. Одной из наиболее популярных стала стратегия «длинного опроса». Она подразумевает поддержание HTTP- соединения открытым до тех пор,пока у сервера есть данные для отправки клиенту.
Но все эти технологии приводят к перегрузке HTTP. Каждый раз, когда вы делаете запрос HTTP, набор заголовков и cookie передаются на сервер. Они накапливаются в большие массивы информации, которые нужно передать. Это увеличивает время ожидания, что может быть критично для равномерной работы приложения.
Чтобы решить данную проблему, нужен был способ создания постоянного соединения с минимальными задержками, которое могло бы поддерживать транзакции, инициированные как клиентом, так и сервером. Это как раз то, что предоставляют веб-сокеты.
Инициализация WebSocket-сервера
ChatWorker.php
<?php // Подключаем библиотеку Workerman require_once __DIR__ . '/vendor/autoload.php'; use Workerman\Lib\Timer; use Workerman\Worker; $connections = []; // сюда будем складывать все подключения // Стартуем WebSocket-сервер на порту 27800 $worker = new Worker("websocket://0.0.0.0:27800"); Worker::runAll();
И это всё. При запуске этого php-скрипта WebSocket-сервер будет запущен на порту 27800 и к нему уже можно будет подключиться.
Но обратите внимание: можно указать любой другой свободный порт, главное не забыть открыть его на VDS-сервере командой:
iptables -I INPUT -p tcp —dport {PORT} —syn -j ACCEPT
где {PORT} – выбранный вами порт для чата.
Запускаем WebSocket-сервер командой:
php ChatWorker.php start
Запущенный Workerman
Для проверки соединения и дальнейшей отладки можно воспользоваться плагином Simple WebSocket Client для браузера Google Chrome.
Окно плагина Simple WebSocket Client
В поле Server Location -> URL: вводим адрес сервера, начиная с названия протокола: ws:// и нажимаем кнопку Open.
При успешном подключении метка Status: CLOSED будет заменена на OPENED и разблокируется поле Request, которое в дальнейшем можно будет использовать для отправки тестовых запросов как от клиента. По сути, наш браузер уже является клиентом для сервера, просто не имеет визуального оформления и обработчиков сообщений.
Инициализировать сервер было легко, но надо ведь ещё обработать события!
Opening a websocket
When is created, it starts connecting immediately.
During the connection the browser (using headers) asks the server: “Do you support Websocket?” And if the server replies “yes”, then the talk continues in WebSocket protocol, which is not HTTP at all.
Here’s an example of browser headers for request made by .
- – the origin of the client page, e.g. . WebSocket objects are cross-origin by nature. There are no special headers or other limitations. Old servers are unable to handle WebSocket anyway, so there are no compatibility issues. But header is important, as it allows the server to decide whether or not to talk WebSocket with this website.
- – signals that the client would like to change the protocol.
- – the requested protocol is “websocket”.
- – a random browser-generated key for security.
- – WebSocket protocol version, 13 is the current one.
WebSocket handshake can’t be emulated
We can’t use or to make this kind of HTTP-request, because JavaScript is not allowed to set these headers.
If the server agrees to switch to WebSocket, it should send code 101 response:
Here is , recoded using a special algorithm. The browser uses it to make sure that the response corresponds to the request.
Afterwards, the data is transfered using WebSocket protocol, we’ll see its structure (“frames”) soon. And that’s not HTTP at all.
There may be additional headers and that describe extensions and subprotocols.
For instance:
-
means that the browser supports data compression. An extension is something related to transferring the data, functionality that extends WebSocket protocol. The header is sent automatically by the browser, with the list of all extensions it supports.
-
means that we’d like to transfer not just any data, but the data in SOAP or WAMP (“The WebSocket Application Messaging Protocol”) protocols. WebSocket subprotocols are registered in the IANA catalogue. So, this header describes data formats that we’re going to use.
This optional header is set using the second parameter of . That’s the array of subprotocols, e.g. if we’d like to use SOAP or WAMP:
The server should respond with a list of protocols and extensions that it agrees to use.
For example, the request:
Response:
Here the server responds that it supports the extension “deflate-frame”, and only SOAP of the requested subprotocols.