Протокол ws: WebSockets — полноценный асинхронный веб / Хабр
Введение ввеб-сокеты
В статье рассказывается, как использовать веб-сокеты для создания веб-приложений. Но перед тем как погрузиться в изучение протокола и API веб-сокетов, рассмотрим проблемы, с которыми сталкиваются веб-приложения, и как WebSocket помогает их решению.
Интернет был построен на представлении о том, что забота браузера– запрос данных с сервера, а забота сервера – обслуживание этих запросов. Эта парадигма не подвергалась сомнению несколько лет. Но с появлением AJAX в 2005 году многие начали работать над созданием двунаправленных соединений.
Веб-приложения значительно увеличивались в размере. Сдерживающим фактором для их роста была традиционная модель HTTP. Чтобы преодолеть это, были созданы несколько стратегий, позволяющих серверам «проталкивать» (push) данные клиенту. Одной из наиболее популярных стала стратегия «длинного опроса». Она подразумевает поддержание HTTP- соединения открытым до тех пор,пока у сервера есть данные для отправки клиенту.
Но все эти технологии приводят к перегрузке HTTP. Каждый раз, когда вы делаете запрос HTTP, набор заголовков и cookie передаются на сервер. Они накапливаются в большие массивы информации, которые нужно передать. Это увеличивает время ожидания, что может быть критично для равномерной работы приложения.
Чтобы решить данную проблему, нужен был способ создания постоянного соединения с минимальными задержками, которое могло бы поддерживать транзакции, инициированные как клиентом, так и сервером. Это как раз то, что предоставляют веб-сокеты.
Веб-сокет создает постоянное соединение между клиентом и сервером, которое обе стороны могут использовать для отправки данных.
Браузер устанавливает соединение по веб-сокету при помощи «рукопожатия». Этот процесс начинается с отправки клиентом обычного запроса HTTP на сервер. В этот запрос включается заголовок Upgrade, который сообщает серверу, что браузер хочет установить соединение по веб-сокету.
Вот упрощённый пример первоначальных заголовков запроса.
GET ws://websocket.example.com/ HTTP/1.1
Origin: http://example.com
Connection: Upgrade
Host: websocket.example.com
Upgrade: websocket
Замечание: URL-адреса веб-сокетов используют протокол ws. Также существует протокол wss для безопасных соединений, который является эквивалентом HTTPS.
Если сервер поддерживает протокол WebSocket, он сообщает об этом с помощью заголовка Upgrade в ответе.
HTTP/1.1 101 WebSocket Protocol Handshake
Date: Wed, 16 Oct 2013 10:07:34 GMT
Connection: Upgrade
Upgrade: WebSocket
После того, как рукопожатие выполнено, первоначальное соединение HTTP заменяется соединением по веб-сокету, которое использует то же соединение TCP/IP. На этом этапе любая из сторон может начать отправку данных.
С помощью веб-сокетов можно передавать неограниченный объем информации без добавления данных, связанных с запросами (как в HTTP). Данные передаются через веб-сокет как сообщения, каждое из которых состоит из одного или большего количества фрагментов.
Чтобы убедиться, что сообщение будет правильно интерпретировано на стороне клиента, каждому фрагменту предшествуют от 4 до 12 байт данных о полезной нагрузке. Использование обмена сообщениями на основе фрагментов позволяет снизить объем дополнительных данных, что приводит к сокращению задержек.
Замечание: Стоит отметить, что клиент будет уведомлен о новом сообщении только, когда сервер передаст все его фрагменты.
Мы создадим простое приложение, которое соединяется с сервером по веб-сокету. Перед тем, как мы углубимся в детали API, нужно создать несколько файлов.
Посмотреть пример
Загрузить код
Посмотреть на CodePen
Создайте файл index.html и добавьте в него следующую разметку.
<!DOCTYPE html> <html lang="en"> <head> <metacharset="utf-8"> <title>WebSockets Demo</title> <link rel="stylesheet" href="style.css"> </head> <body> <div> <h2>WebSockets Demo</h2> <div>Connecting...</div> <ul></ul> <form action="#" method="post"> <textarea placeholder="Write your message here..." required></textarea> <button type="submit">Send Message</button> <button type="button">Close Connection</button> </form> </div> <script src="app.js"></script> </body> </html>
Разница между веб-сокетами и Socket.IO / Хабр
Доброго времени суток, друзья!
Веб-сокеты и Socket.IO, вероятно, являются двумя наиболее распространенными средствами коммуникации в режиме реального времени (далее — живое общение). Но чем они отличаются?
При построении приложения для живого общения наступает момент, когда необходимо выбрать средство для обмена данными между клиентом и сервером. Веб-сокеты и Socket.IO являются самыми популярными средствами живого общения в современном вебе. Какое из них выбрать? В чем разница между этими технологиями? Давайте выясним.
Веб-сокеты
Говоря о веб-сокетах, мы имеем ввиду протокол веб-коммуникации, представляющий полнодуплексный канал коммуникации поверх простого TCP-соединения. Проще говоря, эта технология позволяет установить связь между клиентом и сервером с минимальными затратами, позволяя создавать приложения, использующие все преимущества живого общения.
Например, представьте, что вы создаете чат: вам необходимо получать и отправлять данные как можно быстрее, верно? С этим прекрасно справляются веб-сокеты! Вы можете открыть TCP-соединение и держать его открытым сколько потребуется.
Веб-сокеты появились в 2010 году в Google Chrome 4, первый RFC (6455) опубликован в 2011.
Веб-сокеты используются в следующих случаях:
- Чаты
- Многопользовательские игры
- Совместное редактирование
- Социальные (новостные) ленты
- Приложения, работающие на основе местоположения
и т.д.
Socket.IO
Socket.IO — библиотека JavaScript, основанная (написанная поверх) на веб-сокетах… и других технологиях. Она использует веб-сокеты, когда они доступны, или такие технологии, как Flash Socket, AJAX Long Polling, AJAX Multipart Stream, когда веб-сокеты недоступны. Легкой аналогией может служить сравнение Fetch API и Axios.
Разница между веб-сокетами и Socket.IO
Главными преимуществами Socket.IO является следующее:
- В отличие от веб-сокетов, Socket.IO позволяет отправлять сообщения всем подключенным клиентам. Например, вы пишете чат и хотите уведомлять всех пользователей о подключении нового пользователя. Вы легко можете это реализовать с помощью одной операции. При использовании веб-сокетов, для реализации подобной задачи вам потребуется список подключенных клиентов и отправка сообщений по одному.
- В веб-сокетах сложно использовать проксирование и балансировщики нагрузки. Socket.IO поддерживает эти технологии из коробки.
- Как отмечалось ранее, Socket.IO поддерживает постепенную (изящную) деградацию.
- Socket.IO поддерживает автоматическое переподключение при разрыве соединения.
- С Socket.IO легче работать.
Может показаться, что Socket.IO — лучшее средство для живого общения. Однако существует несколько ситуаций, когда лучше использовать веб-сокеты.
Во-первых, веб-сокеты поддерживаются всеми современными браузерами. Поэтому вы редко нуждаетесь в поддержке других технологий, предоставляемой Socket.IO.
Если говорить о сетевом трафике, то веб-сокеты отправляют всего два запроса:
- GET для получения HTML страницы
- UPGRADE для соединения с веб-сокетами
Это позволяет установить соединение с сервером. А что насчет Socket.IO?
- GET для получения HTML страницы
- Клиентская библиотека Socket.IO (207кб)
- Три long polling (длинные опросы) Ajax-запроса
- UPGRADE для соединения с веб-сокетами
В мире JS 207кб — это много. Какое нерациональное использование сетевого трафика!
В npm существует пакет «websocket-vs-socket.io», который позволяет сравнить сетевой трафик этих технологий:
Сетевой трафик веб-сокетов:
Сетевой трафик Socket.IO:
Разница очевидна!
Пишем код
Простой сервер на веб-сокетах
В нашей программе на Node.js мы создадим сервер, работающий на порту 3001. При каждом подключении клиента мы будем присваивать ему уникальный ID. При отправке сообщения клиентом мы будем уведомлять его об успехе: []:
const WebSocket = require('ws')
const UUID = require('uuid')
const wss = new WebSocket.Server({ port: 3001 })
wss.on('connection', ws => {
ws.id = UUID()
ws.on('message', message => {
ws.send(`[${ws.id}]: ${message}`)
})
})
Отлично! Но что если мы хотим рассылать сообщение каждому подключенному клиенту? Веб-сокеты не поддерживают рассылку по умолчанию. Это можно реализовать следующим образом:
const WebSocket = require("ws")
const UUID = require("uuid")
const wss = new WebSocket.Server({ port: 3001 })
function broadcast(clientId, message) {
wss.clients.forEach(client => {
if(client.readyState === WebSocket.OPEN) {
client.send(`[${clientId}]: ${message}`)
}
})
}
wss.on('conection', ws => {
ws.id = UUID()
ws.on('message', message => broadcast(ws.id, message))
})
Легко и просто! Как видите, WebSocket.Server хранит записи о каждом подключенном клиенте, поэтому мы можем сделать итерацию и отправить сообщение каждому. Вы можете протестировать код на компьютере (MacOS) или в браузере (Chrome).
Простой сервер на Socket.IO
Это было не сложно. Может ли Socket.IO сделать это еще проще? Как нам написать такой же сервер на Socket.IO?
const io = require('socket.io')
const server = io.listen(3002)
server.on('connection', socket => {
socket.on('message', message => {
socket.emit(`[${socket.id}]: ${message}`)
socket.broadcast.emit(`[${socket.id}]: ${message}`)
})
})
Код получился почти наполовину короче! Как видите, метод «broadcast» не отправляет уведомление отправителю, поэтому мы вынуждены делать это вручную.
Существует проблема: код нельзя протестировать на обычном клиенте веб-сокетов. Это связано с тем, что, как отмечалось ранее, Socket.IO использует не чистые веб-сокеты, а множество технологий для поддержки всех возможных клиентов. Так как же нам проверить его работоспособность?
// head
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/socket.io.slim.js"></script>
// body
<script>
ioClient = io.connect('http://localhost:3002')
ioClient.on('connect', socket => {
ioClient.send('hello world')
ioClient.on('message', msg => console.log(msg))
})
</script>
Необходимо использовать специальный клиент. В приведенном примере мы загружаем его из CDN. Этот клиент позволяет нам провести быстрые (грязные) тесты в браузере.
Как видите, наши примеры не сильно отличаются. Однако, если говорить о совместимости, следует помнить о том, что Socket.IO работает с собственной библиотекой и его нельзя использовать в целях, не связанных с веб-разработкой. В тоже время веб-сокеты могут использоваться для решения широкого спектра задач, таких как P2P коммуникация, обмен данными между серверами в режиме реального времени и т.д.
На заметку
Горизонтальное масштабирование. Допустим, ваш чат обрел популярность и вам необходимо добавить еще один сервер и балансировщик нагрузки для обработки запросов. Ну, если вы открываете соединение на «server 1», затем балансировщик переключает вас на «server 2», вы получите ошибку: «Error during WebSocket handshake: Unexpected response code: 400». Socket.IO решает эту проблему с помощью cookie (или с помощью маршрутизации соединений на основе исходных адресов), а у веб-сокетов не существует подобного механизма.
Производительность. Как отмечалось ранее, Socket.IO предоставляет несколько абстрактных уровней над транспортным уровнем веб-сокетов. Также здесь используется упаковывание данных в формат JSON, так что возможность отправлять на сервер (и наоборот) бинарные данные отсутствует. Если вам необходим такой функционал, придется «поколдовать» над кодом библиотеки с целью обеспечения нужного поведения. С веб-сокетами таких проблем не возникает.
Так что же выбрать?
Решать вам.
Socket.IO облегчает жизнь, вам не нужно заботиться о проблемах, связанных с балансировкой нагрузки, разрывом соединений или рассылкой сообщений… но необходим ли вам такой функционал? Клиентская библиотека Socket.IO весит больше, чем пакеты React, Redux и React-Redux вместе взятые. Уверены ли вы, что не можете ограничиться веб-сокетами?
Еще одной важной вещью, о которой следует помнить, является то, что при использовании Socket.IO на стороне сервера, большая часть кода будет написана на абстракциях, предоставляемых этой библиотекой. Если у вас возникнет необходимость переписать Node.js-микросервисы на Go, Elixir, Java или другой язык программирования, вам придется переписывать почти всю логику. Например, для рассылки сообщений в Socket.IO используется метод «broadcast» (который в веб-сокетах реализуется вручную), поэтому при рефакторинге придется понять, как этот метод работает. В этом случае следует предпочесть веб-сокеты, поскольку их легче адаптировать.
Благодарю за внимание.
WebSocket | Chrome Полная поддержка 4 | Edge Полная поддержка 12 | Firefox Полная поддержка 11
| IE Полная поддержка 10 | Opera Полная поддержка 12.1 | Safari Полная поддержка 5 | WebView Android Полная поддержка ≤37 | Chrome Android Полная поддержка 18 | Firefox Android Полная поддержка 14
| Opera Android Полная поддержка 12.1 | Safari iOS Полная поддержка 4.2 | Samsung Internet Android Полная поддержка 1.0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
WebSocket() constructor | Chrome Полная поддержка Да | Edge Полная поддержка ≤79 | Firefox Полная поддержка 7
| IE ? | Opera Полная поддержка Да | Safari ? | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка 7 | Opera Android ? | Safari iOS ? | Samsung Internet Android Полная поддержка Да |
binaryType | Chrome Полная поддержка Да | Edge Полная поддержка 12 | Firefox Полная поддержка Да | IE ? | Opera Полная поддержка Да | Safari Полная поддержка Да | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android Полная поддержка Да | Safari iOS Полная поддержка Да | Samsung Internet Android Полная поддержка Да |
bufferedAmount | Chrome Полная поддержка Да | Edge Полная поддержка 12 | Firefox Полная поддержка Да | IE ? | Opera Полная поддержка Да | Safari Полная поддержка Да | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android Полная поддержка Да | Safari iOS Полная поддержка Да | Samsung Internet Android Полная поддержка Да |
close | Chrome Полная поддержка 4 | Edge Полная поддержка 12 | Firefox Полная поддержка 8
| IE Полная поддержка 10 | Opera Полная поддержка 12.1 | Safari Полная поддержка 5 | WebView Android Полная поддержка ≤37 | Chrome Android Полная поддержка 18 | Firefox Android Полная поддержка 8
| Opera Android Полная поддержка 12.1 | Safari iOS Полная поддержка 4.2 | Samsung Internet Android Полная поддержка 1.0 |
close event | Chrome Полная поддержка Да | Edge Полная поддержка 12 | Firefox Полная поддержка Да | IE ? | Opera Полная поддержка Да | Safari ? | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android ? | Safari iOS ? | Samsung Internet Android Полная поддержка Да |
error event | Chrome Полная поддержка Да | Edge Полная поддержка 12 | Firefox Полная поддержка Да | IE ? | Opera Полная поддержка Да | Safari ? | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android ? | Safari iOS ? | Samsung Internet Android Полная поддержка Да |
extensions | Chrome Полная поддержка Да | Edge Полная поддержка 12 | Firefox Полная поддержка 8 | IE ? | Opera Полная поддержка Да | Safari Полная поддержка Да | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка 8 | Opera Android Полная поддержка Да | Safari iOS Полная поддержка Да | Samsung Internet Android Полная поддержка Да |
message event | Chrome Полная поддержка Да | Edge Полная поддержка 12 | Firefox Полная поддержка Да | IE ? | Opera Полная поддержка Да | Safari ? | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android ? | Safari iOS ? | Samsung Internet Android Полная поддержка Да |
onclose | Chrome Полная поддержка Да | Edge Полная поддержка 12 | Firefox Полная поддержка Да | IE ? | Opera Полная поддержка Да | Safari Полная поддержка Да | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android Полная поддержка Да | Safari iOS Полная поддержка Да | Samsung Internet Android Полная поддержка Да |
onerror | Chrome Полная поддержка Да | Edge Полная поддержка 12 | Firefox Полная поддержка Да | IE ? | Opera Полная поддержка Да | Safari Полная поддержка Да | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android Полная поддержка Да | Safari iOS Полная поддержка Да | Samsung Internet Android Полная поддержка Да |
onmessage | Chrome Полная поддержка Да | Edge Полная поддержка 12 | Firefox Полная поддержка Да | IE ? | Opera Полная поддержка Да | Safari Полная поддержка Да | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android Полная поддержка Да | Safari iOS Полная поддержка Да | Samsung Internet Android Полная поддержка Да |
onopen | Chrome Полная поддержка Да | Edge Полная поддержка 12 | Firefox Полная поддержка Да | IE ? | Opera Полная поддержка Да | Safari Полная поддержка Да | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android Полная поддержка Да | Safari iOS Полная поддержка Да | Samsung Internet Android Полная поддержка Да |
open event | Chrome Полная поддержка Да | Edge Полная поддержка 12 | Firefox Полная поддержка Да | IE ? | Opera Полная поддержка Да | Safari ? | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android ? | Safari iOS ? | Samsung Internet Android Полная поддержка Да |
protocol | Chrome Полная поддержка Да | Edge Полная поддержка 12 | Firefox Полная поддержка Да | IE ? | Opera Полная поддержка Да | Safari Полная поддержка Да | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android Полная поддержка Да | Safari iOS Полная поддержка Да | Samsung Internet Android Полная поддержка Да |
Supports protocol as specified by RFC 6455 | Chrome Полная поддержка 16 | Edge Полная поддержка 12 | Firefox Полная поддержка 11 | IE Полная поддержка 10 | Opera Полная поддержка 15 | Safari Полная поддержка 6 | WebView Android Полная поддержка Да | Chrome Android Полная поддержка 18 | Firefox Android Полная поддержка 14 | Opera Android Полная поддержка 14 | Safari iOS Полная поддержка 6 | Samsung Internet Android Полная поддержка 1.0 |
readyState | Chrome Полная поддержка 43 | Edge Полная поддержка 12 | Firefox Полная поддержка 19 | IE Полная поддержка 10 | Opera Полная поддержка 30 | Safari Полная поддержка 10 | WebView Android Полная поддержка 43 | Chrome Android Полная поддержка 43 | Firefox Android Полная поддержка 19 | Opera Android Полная поддержка 30 | Safari iOS Полная поддержка 10 | Samsung Internet Android Полная поддержка 4.0 |
send | Chrome Полная поддержка 4 | Edge Полная поддержка 12 | Firefox Полная поддержка 18
| IE Полная поддержка 10 | Opera Полная поддержка 12.1 | Safari Полная поддержка 5 | WebView Android Полная поддержка ≤37 | Chrome Android Полная поддержка 18 | Firefox Android Полная поддержка 18
| Opera Android Полная поддержка 12.1 | Safari iOS Полная поддержка 4.2 | Samsung Internet Android Полная поддержка 1.0 |
url | Chrome Полная поддержка Да | Edge Полная поддержка 12 | Firefox Полная поддержка Да | IE ? | Opera Полная поддержка Да | Safari Полная поддержка Да | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android Полная поддержка Да | Safari iOS Полная поддержка Да | Samsung Internet Android Полная поддержка Да |
Available in workers | Chrome Полная поддержка Да | Edge Полная поддержка ≤18 | Firefox Полная поддержка 37 | IE ? | Opera ? | Safari ? | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка 37 | Opera Android ? | Safari iOS ? | Samsung Internet Android Полная поддержка Да |
Web-разработка • PHP и MySQL
Протокол WebSocket предназначен для решения разных задач и снятия ограничений обмена данными между браузером и сервером. Он позволяет пересылать любые данные, на любой домен, безопасно и почти без лишнего сетевого трафика. Для установления соединения WebSocket клиент и сервер используют протокол, похожий на HTTP. Клиент формирует особый HTTP-запрос, на который сервер отвечает определенным образом.
Простой сокет-сервер
В первую очередь надо в файле php.ini
расскомментировать строку, позволяющую работать с сокетами и перезапустить сервер:
extension = php_sockets.dll
Вот как выглядит простейший сокет-сервер:
<?php function SocketServer($limit = 0) { $starttime = time(); echo 'SERVER START' . PHP_EOL; echo 'Socket create...' . PHP_EOL; $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); if (false === $socket) { die('Error: ' . socket_strerror(socket_last_error()) . PHP_EOL); } echo 'Socket bind...' . PHP_EOL; $bind = socket_bind($socket, '127.0.0.1', 7777); // привязываем к ip и порту if (false === $bind) { die('Error: ' . socket_strerror(socket_last_error()) . PHP_EOL); } echo 'Set options...' . PHP_EOL; // разрешаем использовать один порт для нескольких соединений $option = socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1); if (false === $option) { die('Error: ' . socket_strerror(socket_last_error()) . PHP_EOL); } echo 'Listening socket...' . PHP_EOL; $listen = socket_listen($socket); // слушаем сокет if (false === $listen) { die('Error: ' . socket_strerror(socket_last_error()) . PHP_EOL); } while (true) { // бесконечный цикл ожидания подключений echo 'Waiting for connections...' . PHP_EOL; $connect = socket_accept($socket); // зависаем, пока не получим ответа if ($connect !== false) { echo 'Client connected...' . PHP_EOL; echo 'Send message to client...' . PHP_EOL; socket_write($connect, 'Hello, Client!'); } else { echo 'Error: ' . socket_strerror(socket_last_error()) . PHP_EOL; usleep(1000); } // останавливаем сервер после $limit секунд if ($limit && (time() - $starttime > $limit)) { echo 'Closing connection...' . PHP_EOL; socket_close($socket); echo 'SERVER STOP' . PHP_EOL; return; } } } error_reporting(E_ALL); // выводим все ошибки и предупреждения set_time_limit(0); // бесконечное время работы скрипта ob_implicit_flush(); // включаем вывод без буферизации // Запускаем сервер в работу, завершение работы через 60 секунд SocketServer(60);
Запустим его в работу:
> php.exe -f simple.php SERVER START Socket create... Socket bind... Set option... Listening socket... Waiting for connections...
Попробуем пообщаться с сервером с помощью telnet
:
> telnet
Получив приглашение telnet
, даем команду:
> open 127.0.0.1 7777
И видим сообщение от сервера:
Наш сервер в другом окне тоже встрепенулся:
WebSocket сервер
Протокол WebSocket работает над TCP. Это означает, что при соединении браузер отправляет по HTTP специальные заголовки, спрашивая: «Поддерживает ли сервер WebSocket?». Если сервер в ответных заголовках отвечает «Да, поддерживаю», то дальше HTTP прекращается и общение идёт на специальном протоколе WebSocket, который уже не имеет с HTTP ничего общего.
GET /chat HTTP/1.1 Host: websocket.server.com Upgrade: websocket Connection: Upgrade Origin: http://www.example.com Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q== Sec-WebSocket-Version: 13
Здесь GET
и Host
— стандартные HTTP-заголовки, а Upgrade
и Connection
указывают, что браузер хочет перейти на WebSocket.
Сервер может проанализировать эти заголовки и решить, разрешает ли он WebSocket с данного домена Origin
. Ответ сервера, если он понимает и разрешает WebSocket-подключение:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=
Для тестирования работы сервера нам нужен клиент:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Простой WebSocket клиент</title> <link rel="stylesheet" href="style.css" type="text/css" /> <script src="socket.js" type="text/javascript"></script> </head> <body> <div> <span>Сервер</span> <input type="text" value="" /> </div> <div> <input type="button" value="Установить соединение" /> <input type="button" value="Разорвать соединение" /> </div> <div> <span>Сообщение</span> <input type="text" value="" /> <input type="button" value="Отправить сообщение" /> </div> <div> <span>Информация</span> <div></div> </div> </body> </html>
window.addEventListener('DOMContentLoaded', function () { var socket; // показать сообщение в #socket-info function showMessage(message) { var div = document.createElement('div'); div.appendChild(document.createTextNode(message)); document.getElementById('socket-info').appendChild(div); } /* * Установить соединение с сервером и назначить обработчики событий */ document.getElementById('connect').onclick = function () { // новое соединение открываем, если старое соединение закрыто if (socket === undefined || socket.readyState !== 1) { socket = new WebSocket(document.getElementById('server').value); } else { showMessage('Надо закрыть уже имеющееся соединение'); } /* * четыре функции обратного вызова: одна при получении данных и три – при изменениях в состоянии соединения */ socket.onmessage = function (event) { // при получении данных от сервера showMessage('Получено сообщение от сервера: ' + event.data); } socket.onopen = function () { // при установке соединения с сервером showMessage('Соединение с сервером установлено'); } socket.onerror = function(error) { // если произошла какая-то ошибка showMessage('Произошла ошибка: ' + error.message); }; socket.onclose = function(event) { // при закрытии соединения с сервером showMessage('Соединение с сервером закрыто'); if (event.wasClean) { showMessage('Соединение закрыто чисто'); } else { showMessage('Обрыв соединения'); // например, «убит» процесс сервера } showMessage('Код: ' + event.code + ', причина: ' + event.reason); }; }; /* * Отправка сообщения серверу */ document.getElementById('send-msg').onclick = function () { if (socket !== undefined && socket.readyState === 1) { var message = document.getElementById('message').value; socket.send(message); showMessage('Отправлено сообщение серверу: ' + message); } else { showMessage('Невозможно отправить сообщение, нет соединения'); } }; /* * Закрыть соединение с сервером */ document.getElementById('disconnect').onclick = function () { if (socket !== undefined && socket.readyState === 1) { socket.close(); } else { showMessage('Соединение с сервером уже было закрыто'); } }; });
body > div { margin-bottom: 15px; overflow: hidden; } span { display: block; margin-bottom: 2px; } input { padding: 5px; box-sizing: border-box; } input[type="text"] { width: 100%; } input[type="button"] { width: 25%; float: left; margin-top: 5px; margin-right: 5px; } div#socket-info { padding: 5px; border: 1px solid #ddd; }
Проверим его в работе. Открываем HTML-страницу в браузере и заполняем первое поле «Сервер»:
ws://echo.websocket.org
Это гарантированно работающий WebSocket echo-сервер, которые отправляет все сообщения обратно. Жмем кнопку «Установить соединение», набираем текст сообщения в поле «Сообщение», жмем кнопку «Отправить сообщение»:
А теперь код WebSocket сервера на PHP:
<?php /** * Класс WebSocket сервера */ class WebSocketServer { /** * Функция вызывается, когда получено сообщение от клиента */ public $handler; /** * IP адрес сервера */ private $ip; /** * Порт сервера */ private $port; /** * Сокет для принятия новых соединений, прослушивает указанный порт */ private $connection; /** * Для хранения всех подключений, принятых слушающим сокетом */ private $connects; /** * Ограничение по времени работы сервера */ private $timeLimit = 0; /** * Время начала работы сервера */ private $startTime; /** * Выводить сообщения в консоль? */ private $verbose = false; /** * Записывать сообщения в log-файл? */ private $logging = false; /** * Имя log-файла */ private $logFile = 'ws-log.txt'; /** * Ресурс log-файла */ private $resource; public function __construct($ip = '127.0.0.1', $port = 7777) { $this->ip = $ip; $this->port = $port; // эта функция вызывается, когда получено сообщение от клиента; // при создании экземпляра класса должна быть переопределена $this->handler = function($connection, $data) { $message = '[' . date('r') . '] Получено сообщение от клиента: ' . $data . PHP_EOL; if ($this->verbose) { echo $message; } if ($this->logging) { fwrite($this->resource, $message); } }; } public function __destruct() { if (is_resource($this->connection)) { $this->stopServer(); } if ($this->logging) { fclose($this->resource); } } /** * Дополнительные настройки для отладки */ public function settings($timeLimit = 0, $verbose = false, $logging = false, $logFile = 'ws-log.txt') { $this->timeLimit = $timeLimit; $this->verbose = $verbose; $this->logging = $logging; $this->logFile = $logFile; if ($this->logging) { $this->resource = fopen($this->logFile, 'a'); } } /** * Выводит сообщение в консоль и/или записывает в лог-файл */ private function debug($message) { $message = '[' . date('r') . '] ' . $message . PHP_EOL; if ($this->verbose) { echo $message; } if ($this->logging) { fwrite($this->resource, $message); } } /** * Отправляет сообщение клиенту */ public static function response($connect, $data) { socket_write($connect, self::encode($data)); } /** * Запускает сервер в работу */ public function startServer() { $this->debug('Try start server...'); $this->connection = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); if (false === $this->connection) { $this->debug('Error socket_create(): ' . socket_strerror(socket_last_error())); return; } $bind = socket_bind($this->connection, $this->ip, $this->port); // привязываем к ip и порту if (false === $bind) { $this->debug('Error socket_bind(): ' . socket_strerror(socket_last_error())); return; } // разрешаем использовать один порт для нескольких соединений $option = socket_set_option($this->connection, SOL_SOCKET, SO_REUSEADDR, 1); if (false === $option) { $this->debug('Error socket_set_option(): ' . socket_strerror(socket_last_error())); return; } $listen = socket_listen($this->connection); // слушаем сокет if (false === $listen) { $this->debug('Error socket_listen(): ' . socket_strerror(socket_last_error())); return; } $this->debug('Server is running...'); $this->connects = array($this->connection); $this->startTime = time(); while (true) { $this->debug('Waiting for connections...'); // создаем копию массива, так что массив $this->connects не будет изменен функцией socket_select() $read = $this->connects; $write = $except = null; /* * Сокет $this->connection только прослушивает порт на предмет новых соединений. Как только поступило * новое соединение, мы создаем новый ресурс сокета с помощью socket_accept() и помещаем его в массив * $this->connects для дальнейшего чтения из него. */ if ( ! socket_select($read, $write, $except, null)) { // ожидаем сокеты, доступные для чтения (без таймаута) break; } // если слушающий сокет есть в массиве чтения, значит было новое соединение if (in_array($this->connection, $read)) { // принимаем новое соединение и производим рукопожатие if (($connect = socket_accept($this->connection)) && $this->handshake($connect)) { $this->debug('New connection accepted'); $this->connects[] = $connect; // добавляем его в список необходимых для обработки } // удаляем слушающий сокет из массива для чтения unset($read[ array_search($this->connection, $read) ]); } foreach ($read as $connect) { // обрабатываем все соединения, в которых есть данные для чтения $data = socket_read($connect, 100000); $decoded = self::decode($data); // если клиент не прислал данных или хочет разорвать соединение if (false === $decoded || 'close' === $decoded['type']) { $this->debug('Connection closing'); socket_write($connect, self::encode(' Closed on client demand', 'close')); socket_shutdown($connect); socket_close($connect); unset($this->connects[ array_search($connect, $this->connects) ]); $this->debug('Closed successfully'); continue; } // получено сообщение от клиента, вызываем пользовательскую // функцию, чтобы обработать полученные данные if (is_callable($this->handler)) { call_user_func($this->handler, $connect, $decoded['payload']); } } // если истекло ограничение по времени, останавливаем сервер if ($this->timeLimit && time() - $this->startTime > $this->timeLimit) { $this->debug('Time limit. Stopping server.'); $this->stopServer(); return; } } } /** * Останавливает работу сервера */ public function stopServer() { // закрываем слушающий сокет socket_close($this->connection); if (!empty($this->connects)) { // отправляем все клиентам сообщение о разрыве соединения foreach ($this->connects as $connect) { if (is_resource($connect)) { socket_write($connect, self::encode(' Closed on server demand', 'close')); socket_shutdown($connect); socket_close($connect); } } } } /** * Для кодирования сообщений перед отправкой клиенту */ private static function encode($payload, $type = 'text', $masked = false) { $frameHead = array(); $payloadLength = strlen($payload); switch ($type) { case 'text': // first byte indicates FIN, Text-Frame (10000001): $frameHead[0] = 129; break; case 'close': // first byte indicates FIN, Close Frame(10001000): $frameHead[0] = 136; break; case 'ping': // first byte indicates FIN, Ping frame (10001001): $frameHead[0] = 137; break; case 'pong': // first byte indicates FIN, Pong frame (10001010): $frameHead[0] = 138; break; } // set mask and payload length (using 1, 3 or 9 bytes) if ($payloadLength > 65535) { $payloadLengthBin = str_split(sprintf('%064b', $payloadLength), 8); $frameHead[1] = ($masked === true) ? 255 : 127; for ($i = 0; $i < 8; $i++) { $frameHead[$i + 2] = bindec($payloadLengthBin[$i]); } // most significant bit MUST be 0 if ($frameHead[2] > 127) { return array('type' => '', 'payload' => '', 'error' => 'frame too large (1004)'); } } elseif ($payloadLength > 125) { $payloadLengthBin = str_split(sprintf('%016b', $payloadLength), 8); $frameHead[1] = ($masked === true) ? 254 : 126; $frameHead[2] = bindec($payloadLengthBin[0]); $frameHead[3] = bindec($payloadLengthBin[1]); } else { $frameHead[1] = ($masked === true) ? $payloadLength + 128 : $payloadLength; } // convert frame-head to string: foreach (array_keys($frameHead) as $i) { $frameHead[$i] = chr($frameHead[$i]); } if ($masked === true) { // generate a random mask: $mask = array(); for ($i = 0; $i < 4; $i++) { $mask[$i] = chr(rand(0, 255)); } $frameHead = array_merge($frameHead, $mask); } $frame = implode('', $frameHead); // append payload to frame: for ($i = 0; $i < $payloadLength; $i++) { $frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i]; } return $frame; } /** * Для декодирования сообщений, полученных от клиента */ private static function decode($data) { if ( ! strlen($data)) { return false; } $unmaskedPayload = ''; $decodedData = array(); // estimate frame type: $firstByteBinary = sprintf('%08b', ord($data[0])); $secondByteBinary = sprintf('%08b', ord($data[1])); $opcode = bindec(substr($firstByteBinary, 4, 4)); $isMasked = ($secondByteBinary[0] == '1') ? true : false; $payloadLength = ord($data[1]) & 127; // unmasked frame is received: if (!$isMasked) { return array('type' => '', 'payload' => '', 'error' => 'protocol error (1002)'); } switch ($opcode) { // text frame: case 1: $decodedData['type'] = 'text'; break; case 2: $decodedData['type'] = 'binary'; break; // connection close frame: case 8: $decodedData['type'] = 'close'; break; // ping frame: case 9: $decodedData['type'] = 'ping'; break; // pong frame: case 10: $decodedData['type'] = 'pong'; break; default: return array('type' => '', 'payload' => '', 'error' => 'unknown opcode (1003)'); } if ($payloadLength === 126) { $mask = substr($data, 4, 4); $payloadOffset = 8; $dataLength = bindec(sprintf('%08b', ord($data[2])) . sprintf('%08b', ord($data[3]))) + $payloadOffset; } elseif ($payloadLength === 127) { $mask = substr($data, 10, 4); $payloadOffset = 14; $tmp = ''; for ($i = 0; $i < 8; $i++) { $tmp .= sprintf('%08b', ord($data[$i + 2])); } $dataLength = bindec($tmp) + $payloadOffset; unset($tmp); } else { $mask = substr($data, 2, 4); $payloadOffset = 6; $dataLength = $payloadLength + $payloadOffset; } /** * We have to check for large frames here. socket_recv cuts at 1024 bytes * so if websocket-frame is > 1024 bytes we have to wait until whole * data is transferd. */ if (strlen($data) < $dataLength) { return false; } if ($isMasked) { for ($i = $payloadOffset; $i < $dataLength; $i++) { $j = $i - $payloadOffset; if (isset($data[$i])) { $unmaskedPayload .= $data[$i] ^ $mask[$j % 4]; } } $decodedData['payload'] = $unmaskedPayload; } else { $payloadOffset = $payloadOffset - 4; $decodedData['payload'] = substr($data, $payloadOffset); } return $decodedData; } /** * «Рукопожатие», т.е. отправка заголовков согласно протоколу WebSocket */ private function handshake($connect) { $info = array(); $data = socket_read($connect, 1000); $lines = explode("\r\n", $data); foreach ($lines as $i => $line) { if ($i) { if (preg_match('/\A(\S+): (.*)\z/', $line, $matches)) { $info[$matches[1]] = $matches[2]; } } else { $header = explode(' ', $line); $info['method'] = $header[0]; $info['uri'] = $header[1]; } if (empty(trim($line))) break; } // получаем адрес клиента $ip = $port = null; if ( ! socket_getpeername($connect, $ip, $port)) { return false; } $info['ip'] = $ip; $info['port'] = $port; if (empty($info['Sec-WebSocket-Key'])) { return false; } // отправляем заголовок согласно протоколу вебсокета $SecWebSocketAccept = base64_encode(pack('H*', sha1($info['Sec-WebSocket-Key'] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'))); $upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" . "Upgrade: websocket\r\n" . "Connection: Upgrade\r\n" . "Sec-WebSocket-Accept:".$SecWebSocketAccept."\r\n\r\n"; socket_write($connect, $upgrade); return true; } }
Для тестирования напишем небольшой PHP-скрипт, который запускает в работу сервер и все сообщения клиента отправляет обратно (echo-сервер):
<?php error_reporting(E_ALL); set_time_limit(0); ob_implicit_flush(); require 'WebSocketServer.class.php'; $server = new WebSocketServer('127.0.0.1', 7777); // максимальное время работы 100 секунд, выводить сообщения в консоль $server->settings(100, true); // эта функция вызывается, когда получено сообщение от клиента $server->handler = function($connect, $data) { // полученные от клиента данные отправляем обратно WebSocketServer::response($connect, $data); }; $server->startServer();
Запускаем скрипт в работу:
> php.exe -f echo-server.php [Fri, 12 Oct 2018 15:08:13 +0300] Try start server... [Fri, 12 Oct 2018 15:08:13 +0300] Server is running... [Fri, 12 Oct 2018 15:08:13 +0300] Waiting for connections...
Еще один пример использования сервера — клиент отправляет команды, а сервер их выполняет:
<?php error_reporting(E_ALL); set_time_limit(0); ob_implicit_flush(); require 'WebSocketServer.class.php'; $server = new WebSocketServer('127.0.0.1', 7777); // максимальное время работы 100 секунд, выводить сообщения в консоль $server->settings(100, true); // эта функция вызывается, когда получено сообщение от клиента $server->handler = function($connect, $data) { // анализируем поступившую команду и даем ответ if ( ! in_array($data, array('date', 'time', 'country', 'city'))) { WebSocketServer::response($connect, 'Неизвестная команда'); return; } switch ($data) { case 'date' : $response = date('d.m.Y'); break; case 'time' : $response = date('H:i:s'); break; case 'country': $response = 'Россия'; break; case 'city' : $response = 'Москва'; break; } WebSocketServer::response($connect, $response); }; $server->startServer();
Альтернативная реализация WebSocket сервера с использованием функций для работы с потоками:
<?php /** * Класс WebSocket сервера */ class WebSocketServer { /** * Функция вызывается, когда получено сообщение от клиента */ public $handler; /** * IP адрес сервера */ private $ip; /** * Порт сервера */ private $port; /** * Для хранения слушающего сокета потока */ private $connection; /** * Для хранения всех подключений */ private $connects; /** * Ограничение по времени работы сервера */ private $timeLimit = 0; /** * Время начала работы сервера */ private $startTime; /** * Выводить сообщения в консоль? */ private $verbose = false; /** * Записывать сообщения в log-файл? */ private $logging = false; /** * Имя log-файла */ private $logFile = 'ws-log.txt'; /** * Ресурс log-файла */ private $resource; public function __construct($ip = '127.0.0.1', $port = 7777) { $this->ip = $ip; $this->port = $port; // эта функция вызывается, когда получено сообщение от клиента; // при создании экземпляра класса должна быть переопределена $this->handler = function($connection, $data) { $message = '[' . date('r') . '] Получено сообщение от клиента: ' . $data . PHP_EOL; if ($this->verbose) { echo $message; } if ($this->logging) { fwrite($this->resource, $message); } }; } public function __destruct() { if (is_resource($this->connection)) { $this->stopServer(); } if ($this->logging) { fclose($this->resource); } } /** * Дополнительные настройки для отладки */ public function settings($timeLimit = 0, $verbose = false, $logging = false, $logFile = 'ws-log.txt') { $this->timeLimit = $timeLimit; $this->verbose = $verbose; $this->logging = $logging; $this->logFile = $logFile; if ($this->logging) { $this->resource = fopen($this->logFile, 'a'); } } /** * Выводит сообщение в консоль или записывает в лог-файл */ private function debug($message) { $message = '[' . date('r') . '] ' . $message . PHP_EOL; if ($this->verbose) { echo $message; } if ($this->logging) { fwrite($this->resource, $message); } } /** * Отправляет сообщение клиенту */ public static function response($connect, $data) { fwrite($connect, self::encode($data)); } /** * Запускает сервер в работу */ public function startServer() { $this->debug('Try start server...'); $this->connection = stream_socket_server('tcp://' . $this->ip . ':' . $this->port, $errno, $errstr); if ( ! $this->connection) { $this->debug('Cannot start server: ' .$errstr. '(' .$errno. ')'); return false; } $this->debug('Server is running...'); $this->connects = array(); $this->startTime = time(); while (true) { $this->debug('Waiting for connections...'); // формируем массив прослушиваемых сокетов $read = $this->connects; $read[] = $this->connection; $write = $except = null; if ( ! stream_select($read, $write, $except, null)) { // ожидаем сокеты доступные для чтения (без таймаута) break; } if (in_array($this->connection, $read)) { // есть новое соединение // принимаем новое соединение и производим рукопожатие if (($connect = stream_socket_accept($this->connection, -1)) && $this->handshake($connect)) { $this->debug('New connection accepted'); $this->connects[] = $connect; // добавляем его в список необходимых для обработки } unset($read[ array_search($this->connection, $read) ]); } foreach ($read as $connect) { // обрабатываем все соединения $data = fread($connect, 100000); $decoded = self::decode($data); // если клиент не прислал данных или хочет разорвать соединение if (false === $decoded || 'close' === $decoded['type']) { $this->debug('Connection closing'); fwrite($connect, self::encode(' Closed on client demand', 'close')); fclose($connect); unset($this->connects[ array_search($connect, $this->connects) ]); $this->debug('Closed successfully'); continue; } // получено сообщение от клиента, вызываем пользовательскую // функцию, чтобы обработать полученные данные if (is_callable($this->handler)) { call_user_func($this->handler, $connect, $decoded['payload']); } } // если истекло ограничение по времени, останавливаем сервер if ($this->timeLimit && time() - $this->startTime > $this->timeLimit) { $this->debug('Time limit. Stopping server.'); $this->stopServer(); return; } } } /** * Останавливает работу сервера */ public function stopServer() { fclose($this->connection); // закрываем слушающий сокет if (!empty($this->connects)) { // отправляем все клиентам сообщение о разрыве соединения foreach ($this->connects as $connect) { if (is_resource($connect)) { fwrite($connect, self::encode(' Closed on server demand', 'close')); fclose($connect); } } } } /** * Для кодирования сообщений перед отправкой клиенту */ private static function encode($payload, $type = 'text', $masked = false) { $frameHead = array(); $payloadLength = strlen($payload); switch ($type) { case 'text': // first byte indicates FIN, Text-Frame (10000001): $frameHead[0] = 129; break; case 'close': // first byte indicates FIN, Close Frame(10001000): $frameHead[0] = 136; break; case 'ping': // first byte indicates FIN, Ping frame (10001001): $frameHead[0] = 137; break; case 'pong': // first byte indicates FIN, Pong frame (10001010): $frameHead[0] = 138; break; } // set mask and payload length (using 1, 3 or 9 bytes) if ($payloadLength > 65535) { $payloadLengthBin = str_split(sprintf('%064b', $payloadLength), 8); $frameHead[1] = ($masked === true) ? 255 : 127; for ($i = 0; $i < 8; $i++) { $frameHead[$i + 2] = bindec($payloadLengthBin[$i]); } // most significant bit MUST be 0 if ($frameHead[2] > 127) { return array('type' => '', 'payload' => '', 'error' => 'frame too large (1004)'); } } elseif ($payloadLength > 125) { $payloadLengthBin = str_split(sprintf('%016b', $payloadLength), 8); $frameHead[1] = ($masked === true) ? 254 : 126; $frameHead[2] = bindec($payloadLengthBin[0]); $frameHead[3] = bindec($payloadLengthBin[1]); } else { $frameHead[1] = ($masked === true) ? $payloadLength + 128 : $payloadLength; } // convert frame-head to string: foreach (array_keys($frameHead) as $i) { $frameHead[$i] = chr($frameHead[$i]); } if ($masked === true) { // generate a random mask: $mask = array(); for ($i = 0; $i < 4; $i++) { $mask[$i] = chr(rand(0, 255)); } $frameHead = array_merge($frameHead, $mask); } $frame = implode('', $frameHead); // append payload to frame: for ($i = 0; $i < $payloadLength; $i++) { $frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i]; } return $frame; } /** * Для декодирования сообщений, полученных от клиента */ private static function decode($data) { if ( ! strlen($data)) { return false; } $unmaskedPayload = ''; $decodedData = array(); // estimate frame type: $firstByteBinary = sprintf('%08b', ord($data[0])); $secondByteBinary = sprintf('%08b', ord($data[1])); $opcode = bindec(substr($firstByteBinary, 4, 4)); $isMasked = ($secondByteBinary[0] == '1') ? true : false; $payloadLength = ord($data[1]) & 127; // unmasked frame is received: if (!$isMasked) { return array('type' => '', 'payload' => '', 'error' => 'protocol error (1002)'); } switch ($opcode) { // text frame: case 1: $decodedData['type'] = 'text'; break; case 2: $decodedData['type'] = 'binary'; break; // connection close frame: case 8: $decodedData['type'] = 'close'; break; // ping frame: case 9: $decodedData['type'] = 'ping'; break; // pong frame: case 10: $decodedData['type'] = 'pong'; break; default: return array('type' => '', 'payload' => '', 'error' => 'unknown opcode (1003)'); } if ($payloadLength === 126) { $mask = substr($data, 4, 4); $payloadOffset = 8; $dataLength = bindec(sprintf('%08b', ord($data[2])) . sprintf('%08b', ord($data[3]))) + $payloadOffset; } elseif ($payloadLength === 127) { $mask = substr($data, 10, 4); $payloadOffset = 14; $tmp = ''; for ($i = 0; $i < 8; $i++) { $tmp .= sprintf('%08b', ord($data[$i + 2])); } $dataLength = bindec($tmp) + $payloadOffset; unset($tmp); } else { $mask = substr($data, 2, 4); $payloadOffset = 6; $dataLength = $payloadLength + $payloadOffset; } /** * We have to check for large frames here. socket_recv cuts at 1024 bytes * so if websocket-frame is > 1024 bytes we have to wait until whole * data is transferd. */ if (strlen($data) < $dataLength) { return false; } if ($isMasked) { for ($i = $payloadOffset; $i < $dataLength; $i++) { $j = $i - $payloadOffset; if (isset($data[$i])) { $unmaskedPayload .= $data[$i] ^ $mask[$j % 4]; } } $decodedData['payload'] = $unmaskedPayload; } else { $payloadOffset = $payloadOffset - 4; $decodedData['payload'] = substr($data, $payloadOffset); } return $decodedData; } /** * «Рукопожатие», т.е. отправка заголовков согласно протоколу WebSocket */ private function handshake($connect) { $info = array(); $line = fgets($connect); $header = explode(' ', $line); $info['method'] = $header[0]; $info['uri'] = $header[1]; // считываем заголовки из соединения while ($line = rtrim(fgets($connect))) { if (preg_match('/\A(\S+): (.*)\z/', $line, $matches)) { $info[$matches[1]] = $matches[2]; } else { break; } } // получаем адрес клиента $address = explode(':', stream_socket_get_name($connect, true)); $info['ip'] = $address[0]; $info['port'] = $address[1]; if (empty($info['Sec-WebSocket-Key'])) { return false; } // отправляем заголовок согласно протоколу вебсокета $SecWebSocketAccept = base64_encode(pack('H*', sha1($info['Sec-WebSocket-Key'] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'))); $upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" . "Upgrade: websocket\r\n" . "Connection: Upgrade\r\n" . "Sec-WebSocket-Accept:".$SecWebSocketAccept."\r\n\r\n"; fwrite($connect, $upgrade); return $info; } }
Дополнительно
Поиск:
HandShake • JavaScript • PHP • Server • Socket • Web-разработка • WebSocket • Клиент • Протокол • Сервер • Сокет
Что такое веб-сокет и чем он отличается от HTTP?
HTTP и WebSocket — это протоколы связи, используемые в коммуникации клиент-сервер.
Протокол HTTP: HTTP является однонаправленным, когда клиент отправляет запрос, а сервер отправляет ответ. Давайте рассмотрим пример, когда пользователь отправляет запрос на сервер, этот запрос поступает в виде HTTP или HTTPS, после приема сервером запроса отправляет ответ клиенту, каждый запрос связывается с соответствующим ответом, после отправки ответа соединение закрывается, каждый запрос HTTP или HTTPS каждый раз устанавливает новое соединение с сервером, и после получения ответа соединение прерывается само собой.
HTTP — это протокол без сохранения состояния, работающий поверх TCP, который является протоколом, ориентированным на установление соединения, он гарантирует доставку пакетов данных с использованием трехсторонних методов установления связи и повторную передачу потерянных пакетов.
HTTP может работать поверх любого надежного протокола с установлением соединения, такого как TCP, SCTP. Когда клиент отправляет HTTP-запрос на сервер, между клиентом и сервером открывается TCP-соединение, и после получения ответа TCP-соединение прерывается, каждый HTTP-запрос открывает отдельное TCP-соединение с сервером, например, если клиент отправляет 10 запросов На сервере будет открыто 10 отдельных HTTP-соединений. и закройте после получения ответа / отступления.
Информация сообщения HTTP, закодированная в ASCII, каждое сообщение запроса HTTP состоит из версии протокола HTTP (HTTP / 1.1, HTTP / 2), методов HTTP (GET / POST и т. Д.), Заголовков HTTP (тип содержимого, длина содержимого), информации о хосте и т. Д. и тело, которое содержит фактическое сообщение, которое передается на сервер. Заголовки HTTP варьируются от 200 байтов до 2 КБ, общий размер заголовка HTTP составляет 700-800 байтов. Когда веб-приложение использует больше файлов cookie и других инструментов на стороне клиента, которые расходуют средства хранения агента, оно уменьшает полезную нагрузку HTTP-заголовка.
WebSocket: WebSocket является двунаправленным, полнодуплексным протоколом, который используется в том же сценарии взаимодействия клиент-сервер, в отличие от HTTP, он начинается с ws: // или wss: // . Это протокол с отслеживанием состояния, который означает, что соединение между клиентом и сервером будет поддерживаться до тех пор, пока оно не будет прервано какой-либо стороной (клиентом или сервером). после закрытия соединения одним из клиентов и сервера соединение прекращается с обоих концов.
Давайте возьмем пример взаимодействия клиент-сервер: есть клиент, который является веб-браузером и сервером, всякий раз, когда мы инициируем соединение между клиентом и сервером, клиент-сервер совершает рукопожатие и решает создать новое соединение, и это соединение будет сохранить в живых до тех пор, пока не будет прекращен ни одним из них. Когда соединение установлено и активно, соединение происходит по одному и тому же каналу соединения, пока оно не будет прервано.
Таким образом, после установления связи между клиентом и сервером клиент-сервер принимает решение о новом соединении, чтобы поддерживать его, это новое соединение будет называться WebSocket. Как только установление линии связи и соединение будут открыты, обмен сообщениями будет происходить в двунаправленном режиме до тех пор, пока между клиент-сервером не будет установлено соединение. Если кто-либо из них (клиент-сервер) умирает или решает закрыть, соединение закрывается обеими сторонами. То, как работает сокет, немного отличается от того, как работает HTTP, код состояния 101 обозначает протокол переключения в WebSocket.
Когда можно использовать веб-сокет:
- Веб-приложение в режиме реального времени. Веб-приложение в режиме реального времени использует веб-сокет для отображения данных на стороне клиента, которые непрерывно отправляются внутренним сервером. В WebSocket данные непрерывно проталкиваются / передаются в одно и то же соединение, которое уже открыто, поэтому веб-сокет быстрее и повышает производительность приложения.
Например, на торговом веб-сайте или в торговле биткойнами, это наиболее изменчивая вещь, которая происходит там, для отображения колебаний цен и данных о движении, которые непрерывно передаются внутренним сервером клиентскому концу с помощью канала веб-сокета.
- Игровое приложение. В игровом приложении вы можете сосредоточиться на том, что данные постоянно поступают на сервер и без обновления пользовательского интерфейса будут действовать на экране; пользовательский интерфейс автоматически обновляется, даже не устанавливая новое соединение, поэтому оно очень полезно в игровом приложении.
- Приложение чата. Приложение чата использует WebSocket для установления соединения только один раз для обмена, публикации и трансляции сообщения среди подписчика. он использует одно и то же соединение WebSocket для отправки и получения сообщения и передачи сообщения один на один.
Когда не следует использовать WebSocket: WebSocket можно использовать, если нам нужны обновленные в реальном времени или непрерывные потоки данных, которые передаются по сети. Если мы хотим получить старые данные или получить данные только один раз, чтобы обработать их с помощью приложения, мы должны использовать протокол HTTP , старые данные, которые не требуются очень часто или выбираются только один раз, могут быть запрошены простым HTTP-запросом, поэтому в этом сценарии лучше не использовать WebSocket.
Примечание: веб-сервисов RESTful достаточно для получения данных с сервера, если мы загружаем данные только один раз.
Различия между HTTP и WebSocket Connection:
WebSocket Connection | HTTP Connection |
---|---|
WebSocket is a bidirectional communication protocol that can send the data from the client to the server or from the server to the client by reusing the established connection channel. The connection is kept alive until terminated by either the client or the server. | The HTTP protocol is unidirectional protocol works on the top of TCP protocol which is a connection-oriented transport layer protocol, we can create the connection by using HTTP request methods after getting the response HTTP connection get closed. |
Almost all the real-time application like (trading, monitoring, notification) services uses WebSocket to receiving the data on a single communication channel. | Simple RESTful application uses HTTP protocol which is stateless. |
All the frequently updated applications used WebSocket because it is faster than HTTP Connection. | When we do not want to retain a connection for a particular amount of time or reusing the single connection for transmitting the data, HTTP connection is slower than the WebSocket.. |
Примечание: в зависимости от вашего проекта вы должны выбрать, где это будет WebSocket или HTTP Connection.
Рекомендуемые посты:
Что такое веб-сокет и чем он отличается от HTTP?
0.00 (0%) 0 votes
Исследователи предупредили, что протокол WS-Discovery используется для DDoS-атак — «Хакер»
ИБ-специалисты обеспокоены тем, что злоумышленники используют WS-Discovery (Web Services Dynamic Discovery) для DDoS-атак, так как протокол может давать коэффициент амплификации равный 300 и даже 500.
Издание ZDNet сообщает, что первые атаки такого рода начались еще в мае текущего года, однако тогда специалисты и издание решили не привлекать внимания к происходящему, и не подавать злоумышленникам идеи. Но в последнее время уже несколько хак-групп начали злоупотреблять WS-Discovery, и такие DDoS-атаки, к сожалению, становятся постоянным явлением.
WS-Discovery представляет собой многоадресный протокол, который можно использовать для обнаружения других устройств, которые обмениваются данными через определенный протокол или интерфейс. Так, он применяется для обнаружения и обмена данными посредством SOAP с использованием пакетов UDP, поэтому иногда WS-Discovery называют SOAP-over-UDP.
И хотя WS-Discovery нельзя назвать широко распространенным или общеизвестным протоколом, он одобрен отраслевой организацией ONVIF, членами которой являются такие компании, как Axis, Sony, Bosch и так далее. В итоге в настоящее время WS-Discovery поддерживают множество устройств, от IP-камер до принтеров, от бытовых приборов до DVR. По статистике поисковика BinaryEdge, в сети можно обнаружить примерно 630 000 таких девайсов.
По мнению ИБ-специалистов, WS-Discovery идеально подходит для проведения DDoS-атак по ряду причин. Во-первых, протокол основан на UDP, а значит, возможен спуфинг места назначения пакетов. Атакующие могут отправить UDP-пакет WS-Discovery-службе устройства, использовав при этом поддельный обратный IP-адрес. В результате устройство направит свой ответ на этот поддельный адрес, позволяя злоумышленникам манипулировать WS-Discovery-трафиком и направляя его на желаемую цель. Во-вторых, ответ WS-Discovery во много раз больше изначального запроса. Это позволяет атакующим без труда усиливать DDoS-атаки.
По информации ZDNet, WS-Discovery использовался в нескольких DDoS-атаках, коэффициент амплификации которых доходил до 300 и даже до 500. Это очень тревожные цифры, так как обычно коэффициент усиления для других UDP-протоколов равен в среднем 10. К счастью, исследователи полагают, что такие высокие коэффициенты амплификации – это скорее исключение из правила, а не норма. Так, по данным ИБ-компании ZeroBS GmbH, которая в этом месяце отслеживала волну DDoS-атак с использованием WS-Discovery, более распространенный коэффициент усиления все же составляет примерно 10.
Тем не менее, журналисты отмечают, что PoC-эксплоит для проведения DDoS-атак с использованием WS-Discovery, опубликованный на GitHub еще в конце 2018 года, помогает достичь коэффициента амплификации от 70 до 150 (ссылку издание по понятным причинам не приводит).
О первых масштабных атаках с использованием протокола WS-Discovery в мае текущего года сообщил ИБ-специалист Такер Престон (Tucker Preston). Тогда он наблюдал более 130 DDoS-атак, некоторые из которых достигли мощности более 350 Гбит/с. Позднее об этих атаках написали и эксперты компании Netscout в отчете, опубликованном в прошлом месяце.
Атаки, зафиксированные Такером Престоном
И хотя, по данным ZeroBS GmbH, потом атаки почти прекратились, в августе 2019 года они усилились снова. В отличие от первых волн, эти атаки были гораздо «скромнее», и исследователи полагают, что их проводили группировки, которые не до конца осведомлены о возможностях протокола или не имеют технических средств для его использования в полную силу. По информации ZeroBS GmbH, последние атаки достигали максимум 40 Гбит/с и коэффициентом усиления не более 10. Для этих атак использовалось всего порядка 5000 устройств (в основном IP-камеры и принтеры), входящих в ботнеты.
Данные ZeroBS GmbH
Эксперты предупреждают, что в настоящее время DDoS-атаки с применением WS-Discovery еще не используются ежедневно и в полную силу. Пока злоумышленники эксплуатируют возможности только небольшой части WS-Discovery-устройств, доступных в сети, и добиваются весьма небольших коэффициентов амплификации. Однако уже в ближайшие месяцы этот протокол может стать любимым инструментом операторов ботнетов, и ситуация существенно ухудшится.
Обзор способов и протоколов аутентификации в веб-приложениях
Я расскажу о применении различных способов аутентификации для веб-приложений, включая аутентификацию по паролю, по сертификатам, по одноразовым паролям, по ключам доступа и по токенам. Коснусь технологии единого входа (Single Sign-On), рассмотрю различные стандарты и протоколы аутентификации.
Перед тем, как перейти к техническим деталям, давайте немного освежим терминологию.
- Идентификация — это заявление о том, кем вы являетесь. В зависимости от ситуации, это может быть имя, адрес электронной почты, номер учетной записи, итд.
- Аутентификация — предоставление доказательств, что вы на самом деле есть тот, кем идентифицировались (от слова “authentic” — истинный, подлинный).
- Авторизация — проверка, что вам разрешен доступ к запрашиваемому ресурсу.
Например, при попытке попасть в закрытый клуб вас идентифицируют (спросят ваше имя и фамилию), аутентифицируют (попросят показать паспорт и сверят фотографию) и авторизуют (проверят, что фамилия находится в списке гостей), прежде чем пустят внутрь.
Аналогично эти термины применяются в компьютерных системах, где традиционно под идентификацией понимают получение вашей учетной записи (identity) по username или email; под аутентификацией — проверку, что вы знаете пароль от этой учетной записи, а под авторизацией — проверку вашей роли в системе и решение о предоставлении доступа к запрошенной странице или ресурсу.
Однако в современных системах существуют и более сложные схемы аутентификации и авторизации, о которых я расскажу далее. Но начнем с простого и понятного.
Аутентификация по паролю
Этот метод основывается на том, что пользователь должен предоставить username и password для успешной идентификации и аутентификации в системе. Пара username/password задается пользователем при его регистрации в системе, при этом в качестве username может выступать адрес электронной почты пользователя.
Применительно к веб-приложениям, существует несколько стандартных протоколов для аутентификации по паролю, которые мы рассмотрим ниже.
HTTP authentication
Этот протокол, описанный в стандартах HTTP 1.0/1.1, существует очень давно и до сих пор активно применяется в корпоративной среде. Применительно к веб-сайтам работает следующим образом:
- Сервер, при обращении неавторизованного клиента к защищенному ресурсу, отсылает HTTP статус “401 Unauthorized” и добавляет заголовок “WWW-Authenticate” с указанием схемы и параметров аутентификации.
- Браузер, при получении такого ответа, автоматически показывает диалог ввода username и password. Пользователь вводит детали своей учетной записи.
- Во всех последующих запросах к этому веб-сайту браузер автоматически добавляет HTTP заголовок “Authorization”, в котором передаются данные пользователя для аутентификации сервером.
- Сервер аутентифицирует пользователя по данным из этого заголовка. Решение о предоставлении доступа (авторизация) производится отдельно на основании роли пользователя, ACL или других данных учетной записи.
Весь процесс стандартизирован и хорошо поддерживается всеми браузерами и веб-серверами. Существует несколько схем аутентификации, отличающихся по уровню безопасности:
- Basic — наиболее простая схема, при которой username и password пользователя передаются в заголовке Authorization в незашифрованном виде (base64-encoded). Однако при использовании HTTPS (HTTP over SSL) протокола, является относительно безопасной.
Пример HTTP аутентификации с использованием Basic схемы.
- Digest — challenge-response-схема, при которой сервер посылает уникальное значение nonce, а браузер передает MD5 хэш пароля пользователя, вычисленный с использованием указанного nonce. Более безопасная альтернативв Basic схемы при незащищенных соединениях, но подвержена man-in-the-middle attacks (с заменой схемы на basic). Кроме того, использование этой схемы не позволяет применить современные хэш-функции для хранения паролей пользователей на сервере.
- NTLM (известная как Windows authentication) — также основана на challenge-response подходе, при котором пароль не передается в чистом виде. Эта схема не является стандартом HTTP, но поддерживается большинством браузеров и веб-серверов. Преимущественно используется для аутентификации пользователей Windows Active Directory в веб-приложениях. Уязвима к pass-the-hash-атакам.
- Negotiate — еще одна схема из семейства Windows authentication, которая позволяет клиенту выбрать между NTLM и Kerberos аутентификацией. Kerberos — более безопасный протокол, основанный на принципе Single Sign-On. Однако он может функционировать, только если и клиент, и сервер находятся в зоне intranet и являются частью домена Windows.
Стоит отметить, что при использовании HTTP-аутентификации у пользователя нет стандартной возможности выйти из веб-приложения, кроме как закрыть все окна браузера.
Forms authentication
Для этого протокола нет определенного стандарта, поэтому все его реализации специфичны для конкретных систем, а точнее, для модулей аутентификации фреймворков разработки.
Работает это по следующему принципу: в веб-приложение включается HTML-форма, в которую пользователь должен ввести свои username/password и отправить их на сервер через HTTP POST для аутентификации. В случае успеха веб-приложение создает session token, который обычно помещается в browser cookies. При последующих веб-запросах session token автоматически передается на сервер и позволяет приложению получить информацию о текущем пользователе для авторизации запроса.
Пример forms authentication.
Приложение может создать session token двумя способами:
- Как идентификатор аутентифицированной сессии пользователя, которая хранится в памяти сервера или в базе данных. Сессия должна содержать всю необходимую информацию о пользователе для возможности авторизации его запросов.
- Как зашифрованный и/или подписанный объект, содержащий данные о пользователе, а также период действия. Этот подход позволяет реализовать stateless-архитектуру сервера, однако требует механизма обновления сессионного токена по истечении срока действия. Несколько стандартных форматов таких токенов рассматриваются в секции «Аутентификация по токенам».
Необходимо понимать, что перехват session token зачастую дает аналогичный уровень доступа, что и знание username/password. Поэтому все коммуникации между клиентом и сервером в случае forms authentication должны производиться только по защищенному соединению HTTPS.
Другие протоколы аутентификации по паролю
Два протокола, описанных выше, успешно используются для аутентификации пользователей на веб-сайтах. Но при разработке клиент-серверных приложений с использованием веб-сервисов (например, iOS или Android), наряду с HTTP аутентификацией, часто применяются нестандартные протоколы, в которых данные для аутентификации передаются в других частях запроса.
Существует всего несколько мест, где можно передать username и password в HTTP запросах:
- URL query — считается небезопасным вариантом, т. к. строки URL могут запоминаться браузерами, прокси и веб-серверами.
- Request body — безопасный вариант, но он применим только для запросов, содержащих тело сообщения (такие как POST, PUT, PATCH).
- HTTP header —оптимальный вариант, при этом могут использоваться и стандартный заголовок Authorization (например, с Basic-схемой), и другие произвольные заголовки.
Распространенные уязвимости и ошибки реализацииАутентификации по паролю считается не очень надежным способом, так как пароль часто можно подобрать, а пользователи склонны использовать простые и одинаковые пароли в разных системах, либо записывать их на клочках бумаги. Если злоумышленник смог выяснить пароль, то пользователь зачастую об этом не узнает. Кроме того, разработчики приложений могут допустить ряд концептуальных ошибок, упрощающих взлом учетных записей.
Ниже представлен список наиболее часто встречающихся уязвимостей в случае использования аутентификации по паролю:
- Веб-приложение позволяет пользователям создавать простые пароли.
- Веб-приложение не защищено от возможности перебора паролей (brute-force attacks).
- Веб-приложение само генерирует и распространяет пароли пользователям, однако не требует смены пароля после первого входа (т.е. текущий пароль где-то записан).
- Веб-приложение допускает передачу паролей по незащищенному HTTP-соединению, либо в строке URL.
- Веб-приложение не использует безопасные хэш-функции для хранения паролей пользователей.
- Веб-приложение не предоставляет пользователям возможность изменения пароля либо не нотифицирует пользователей об изменении их паролей.
- Веб-приложение использует уязвимую функцию восстановления пароля, которую можно использовать для получения несанкционированного доступа к другим учетным записям.
- Веб-приложение не требует повторной аутентификации пользователя для важных действий: смена пароля, изменения адреса доставки товаров и т. п.
- Веб-приложение создает session tokens таким образом, что они могут быть подобраны или предсказаны для других пользователей.
- Веб-приложение допускает передачу session tokens по незащищенному HTTP-соединению, либо в строке URL.
- Веб-приложение уязвимо для session fixation-атак (т. е. не заменяет session token при переходе анонимной сессии пользователя в аутентифицированную).
- Веб-приложение не устанавливает флаги HttpOnly и Secure для browser cookies, содержащих session tokens.
- Веб-приложение не уничтожает сессии пользователя после короткого периода неактивности либо не предоставляет функцию выхода из аутентифицированной сессии.
Аутентификация по сертификатам
Сертификат представляет собой набор атрибутов, идентифицирующих владельца, подписанный certificate authority (CA). CA выступает в роли посредника, который гарантирует подлинность сертификатов (по аналогии с ФМС, выпускающей паспорта). Также сертификат криптографически связан с закрытым ключом, который хранится у владельца сертификата и позволяет однозначно подтвердить факт владения сертификатом.
На стороне клиента сертификат вместе с закрытым ключом могут храниться в операционной системе, в браузере, в файле, на отдельном физическом устройстве (smart card, USB token). Обычно закрытый ключ дополнительно защищен паролем или PIN-кодом.
В веб-приложениях традиционно используют сертификаты стандарта X.509. Аутентификация с помощью X.509-сертификата происходит в момент соединения с сервером и является частью протокола SSL/TLS. Этот механизм также хорошо поддерживается браузерами, которые позволяют пользователю выбрать и применить сертификат, если веб-сайт допускает такой способ аутентификации.
Использование сертификата для аутентификации.
Во время аутентификации сервер выполняет проверку сертификата на основании следующих правил:
- Сертификат должен быть подписан доверенным certification authority (проверка цепочки сертификатов).
- Сертификат должен быть действительным на текущую дату (проверка срока действия).
- Сертификат не должен быть отозван соответствующим CA (проверка списков исключения).
Пример X.509 сертификата.
После успешной аутентификации веб-приложение может выполнить авторизацию запроса на основании таких данных сертификата, как subject (имя владельца), issuer (эмитент), serial number (серийный номер сертификата) или thumbprint (отпечаток открытого ключа сертификата).
Использование сертификатов для аутентификации — куда более надежный способ, чем аутентификация посредством паролей. Это достигается созданием в процессе аутентификации цифровой подписи, наличие которой доказывает факт применения закрытого ключа в конкретной ситуации (non-repudiation). Однако трудности с распространением и поддержкой сертификатов делает такой способ аутентификации малодоступным в широких кругах.
Аутентификация по одноразовым паролям
Аутентификация по одноразовым паролям обычно применяется дополнительно к аутентификации по паролям для реализации two-factor authentication (2FA). В этой концепции пользователю необходимо предоставить данные двух типов для входа в систему: что-то, что он знает (например, пароль), и что-то, чем он владеет (например, устройство для генерации одноразовых паролей). Наличие двух факторов позволяет в значительной степени увеличить уровень безопасности, что м. б. востребовано для определенных видов веб-приложений.
Другой популярный сценарий использования одноразовых паролей — дополнительная аутентификация пользователя во время выполнения важных действий: перевод денег, изменение настроек и т. п.
Существуют разные источники для создания одноразовых паролей. Наиболее популярные:
- Аппаратные или программные токены, которые могут генерировать одноразовые пароли на основании секретного ключа, введенного в них, и текущего времени. Секретные ключи пользователей, являющиеся фактором владения, также хранятся на сервере, что позволяет выполнить проверку введенных одноразовых паролей. Пример аппаратной реализаций токенов — RSA SecurID; программной — приложение Google Authenticator.
- Случайно генерируемые коды, передаваемые пользователю через SMS или другой канал связи. В этой ситуации фактор владения — телефон пользователя (точнее — SIM-карта, привязанная к определенному номеру).
- Распечатка или scratch card со списком заранее сформированных одноразовых паролей. Для каждого нового входа в систему требуется ввести новый одноразовый пароль с указанным номером.
Аппаратный токен RSA SecurID генерирует новый код каждые 30 секунд.
В веб-приложениях такой механизм аутентификации часто реализуется посредством расширения forms authentication: после первичной аутентификации по паролю, создается сессия пользователя, однако в контексте этой сессии пользователь не имеет доступа к приложению до тех пор, пока он не выполнит дополнительную аутентификацию по одноразовому паролю.
Аутентификация по ключам доступа
Этот способ чаще всего используется для аутентификации устройств, сервисов или других приложений при обращении к веб-сервисам. Здесь в качестве секрета применяются ключи доступа (access key, API key) — длинные уникальные строки, содержащие произвольный набор символов, по сути заменяющие собой комбинацию username/password.
В большинстве случаев, сервер генерирует ключи доступа по запросу пользователей, которые далее сохраняют эти ключи в клиентских приложениях. При создании ключа также возможно ограничить срок действия и уровень доступа, который получит клиентское приложение при аутентификации с помощью этого ключа.
Хороший пример применения аутентификации по ключу — облако Amazon Web Services. Предположим, у пользователя есть веб-приложение, позволяющее загружать и просматривать фотографии, и он хочет использовать сервис Amazon S3 для хранения файлов. В таком случае, пользователь через консоль AWS может создать ключ, имеющий ограниченный доступ к облаку: только чтение/запись его файлов в Amazon S3. Этот ключ в результате можно применить для аутентификации веб-приложения в облаке AWS.
Пример применения аутентификации по ключу.
Использование ключей позволяет избежать передачи пароля пользователя сторонним приложениям (в примере выше пользователь сохранил в веб-приложении не свой пароль, а ключ доступа). Ключи обладают значительно большей энтропией по сравнению с паролями, поэтому их практически невозможно подобрать. Кроме того, если ключ был раскрыт, это не приводит к компрометации основной учетной записи пользователя — достаточно лишь аннулировать этот ключ и создать новый.
С технической точки зрения, здесь не существует единого протокола: ключи могут передаваться в разных частях HTTP-запроса: URL query, request body или HTTP header. Как и в случае аутентификации по паролю, наиболее оптимальный вариант — использование HTTP header. В некоторых случаях используют HTTP-схему Bearer для передачи токена в заголовке (Authorization: Bearer [token]). Чтобы избежать перехвата ключей, соединение с сервером должно быть обязательно защищено протоколом SSL/TLS.
Пример аутентификации по ключу доступа, переданного в HTTP заголовке.
Кроме того, существуют более сложные схемы аутентификации по ключам для незащищенных соединений. В этом случае, ключ обычно состоит их двух частей: публичной и секретной. Публичная часть используется для идентификации клиента, а секретная часть позволяет сгенерировать подпись. Например, по аналогии с digest authentication схемой, сервер может послать клиенту уникальное значение nonce или timestamp, а клиент — возвратить хэш или HMAC этого значения, вычисленный с использованием секретной части ключа. Это позволяет избежать передачи всего ключа в оригинальном виде и защищает от replay attacks.
Аутентификация по токенам
Такой способ аутентификации чаще всего применяется при построении распределенных систем Single Sign-On (SSO), где одно приложение (service provider или relying party) делегирует функцию аутентификации пользователей другому приложению (identity provider или authentication service). Типичный пример этого способа — вход в приложение через учетную запись в социальных сетях. Здесь социальные сети являются сервисами аутентификации, а приложение доверяет функцию аутентификации пользователей социальным сетям.
Реализация этого способа заключается в том, что identity provider (IP) предоставляет достоверные сведения о пользователе в виде токена, а service provider (SP) приложение использует этот токен для идентификации, аутентификации и авторизации пользователя.
На общем уровне, весь процесс выглядит следующим образом:
- Клиент аутентифицируется в identity provider одним из способов, специфичным для него (пароль, ключ доступа, сертификат, Kerberos, итд.).
- Клиент просит identity provider предоставить ему токен для конкретного SP-приложения. Identity provider генерирует токен и отправляет его клиенту.
- Клиент аутентифицируется в SP-приложении при помощи этого токена.
Пример аутентификации «активного» клиента при помощи токена, переданного посредством Bearer схемы.
Процесс, описанный выше, отражает механизм аутентификации активного клиента, т. е. такого, который может выполнять запрограммированную последовательность действий (например, iOS/Android приложения). Браузер же — пассивный клиент в том смысле, что он только может отображать страницы, запрошенные пользователем. В этом случае аутентификация достигается посредством автоматического перенаправления браузера между веб-приложениями identity provider и service provider.
Пример аутентификации «пассивного» клиента посредством перенаправления запросов.
Существует несколько стандартов, в точности определяющих протокол взаимодействия между клиентами (активными и пассивными) и IP/SP-приложениями и формат поддерживаемых токенов. Среди наиболее популярных стандартов — OAuth, OpenID Connect, SAML, и WS-Federation. Некоторая информация об этих протоколах — ниже в статье.
Сам токен обычно представляет собой структуру данных, которая содержит информацию, кто сгенерировал токен, кто может быть получателем токена, срок действия, набор сведений о самом пользователе (claims). Кроме того, токен дополнительно подписывается для предотвращения несанкционированных изменений и гарантий подлинности.
При аутентификации с помощью токена SP-приложение должно выполнить следующие проверки:
- Токен был выдан доверенным identity provider приложением (проверка поля issuer).
- Токен предназначается текущему SP-приложению (проверка поля audience).
- Срок действия токена еще не истек (проверка поля expiration date).
- Токен подлинный и не был изменен (проверка подписи).
В случае успешной проверки SP-приложение выполняет авторизацию запроса на основании данных о пользователе, содержащихся в токене.
Форматы токенов
Существует несколько распространенных форматов токенов для веб-приложений:
- Simple Web Token (SWT) — наиболее простой формат, представляющий собой набор произвольных пар имя/значение в формате кодирования HTML form. Стандарт определяет несколько зарезервированных имен: Issuer, Audience, ExpiresOn и HMACSHA256. Токен подписывается с помощью симметричного ключа, таким образом оба IP- и SP-приложения должны иметь этот ключ для возможности создания/проверки токена.
Пример SWT токена (после декодирования).
Issuer=http://auth.myservice.com&
Audience=http://myservice.com&
ExpiresOn=1435937883&
UserName=John Smith&
UserRole=Admin&
HMACSHA256=KOUQRPSpy64rvT2KnYyQKtFFXUIggnesSpE7ADA4o9w - JSON Web Token (JWT) — содержит три блока, разделенных точками: заголовок, набор полей (claims) и подпись. Первые два блока представлены в JSON-формате и дополнительно закодированы в формат base64. Набор полей содержит произвольные пары имя/значения, притом стандарт JWT определяет несколько зарезервированных имен (iss, aud, exp и другие). Подпись может генерироваться при помощи и симметричных алгоритмов шифрования, и асимметричных. Кроме того, существует отдельный стандарт, отписывающий формат зашифрованного JWT-токена.
Пример подписанного JWT токена (после декодирования 1 и 2 блоков).
{ «alg»: «HS256», «typ»: «JWT» }.
{ «iss»: «auth.myservice.com», «aud»: «myservice.com», «exp»: «1435937883», «userName»: «John Smith», «userRole»: «Admin» }.
S9Zs/8/uEGGTVVtLggFTizCsMtwOJnRhjaQ2BMUQhcY - Security Assertion Markup Language (SAML) — определяет токены (SAML assertions) в XML-формате, включающем информацию об эмитенте, о субъекте, необходимые условия для проверки токена, набор дополнительных утверждений (statements) о пользователе. Подпись SAML-токенов осуществляется при помощи ассиметричной криптографии. Кроме того, в отличие от предыдущих форматов, SAML-токены содержат механизм для подтверждения владения токеном, что позволяет предотвратить перехват токенов через man-in-the-middle-атаки при использовании незащищенных соединений.
Стандарт SAML
Стандарт Security Assertion Markup Language (SAML) описывает способы взаимодействия и протоколы между identity provider и service provider для обмена данными аутентификации и авторизации посредством токенов. Изначально версии 1.0 и 1.1 были выпущены в 2002 – 2003 гг., в то время как версия 2.0, значительно расширяющая стандарт и обратно несовместимая, опубликована в 2005 г.
Этот основополагающий стандарт — достаточно сложный и поддерживает много различных сценариев интеграции систем. Основные «строительные блоки» стандарта:
- Assertions — собственный формат SAML токенов в XML формате.
- Protocols — набор поддерживаемых сообщений между участниками, среди которых — запрос на создание нового токена, получение существующих токенов, выход из системы (logout), управление идентификаторами пользователей, и другие.
- Bindings — механизмы передачи сообщений через различные транспортные протоколы. Поддерживаются такие способы, как HTTP Redirect, HTTP POST, HTTP Artifact (ссылка на сообщения), SAML SOAP, SAML URI (адрес получения сообщения) и другие.
- Profiles — типичные сценарии использования стандарта, определяющие набор assertions, protocols и bindings необходимых для их реализации, что позволяет достичь лучшей совместимости. Web Browser SSO — один из примеров таких профилей.
Кроме того, стандарт определяет формат обмена метаинформацией между участниками, которая включает список поддерживаемых ролей, протоколов, атрибутов, ключи шифрования и т. п.
Рассмотрим краткий пример использования SAML для сценария Single Sign-On. Пользователь хочет получить доступ на защищенный ресурс сервис-провайдера (шаг № 1 на диаграмме аутентификации пассивных клиентов). Т. к. пользователь не был аутентифицирован, SP отправляет его на сайт identity provider’а для создания токена (шаг № 2). Ниже приведен пример ответа SP, где последний использует SAML HTTP Redirect binding для отправки сообщения с запросом токена:
В случае такого запроса, identity provider аутентифицирует пользователя (шаги №3-4), после чего генерирует токен. Ниже приведен пример ответа IP с использованием HTTP POST binding (шаг № 5):
После того как браузер автоматически отправит эту форму на сайт service provider’а (шаг № 6), последний декодирует токен и аутентифицирует пользователя. По результатам успешной авторизации запроса пользователь получает доступ к запрошенному ресурсу (шаг № 7).
Стандарты WS-Trust и WS-Federation
WS-Trust и WS-Federation входят в группу стандартов WS-*, описывающих SOAP/XML-веб сервисы. Эти стандарты разрабатываются группой компаний, куда входят Microsoft, IBM, VeriSign и другие. Наряду с SAML, эти стандарты достаточно сложные, используются преимущественно в корпоративных сценариях.
Стандарт WS-Trust описывает интерфейс сервиса авторизации, именуемого Secure Token Service (STS). Этот сервис работает по протоколу SOAP и поддерживает создание, обновление и аннулирование токенов. При этом стандарт допускает использование токенов различного формата, однако на практике в основном используются SAML-токены.
Стандарт WS-Federation касается механизмов взаимодействия сервисов между компаниями, в частности, протоколов обмена токенов. При этом WS-Federation расширяет функции и интерфейс сервиса STS, описанного в стандарте WS-Trust. Среди прочего, стандарт WS-Federation определяет:
- Формат и способы обмена метаданными о сервисах.
- Функцию единого выхода из всех систем (single sign-out).
- Сервис атрибутов, предоставляющий дополнительную информацию о пользователе.
- Сервис псевдонимов, позволяющий создавать альтернативные имена пользователей.
- Поддержку пассивных клиентов (браузеров) посредством перенаправления.
Можно сказать, что WS-Federation позволяет решить те же задачи, что и SAML, однако их подходы и реализация в некоторой степени отличаются.
Стандарты OAuth и OpenID Connect
В отличие от SAML и WS-Federation, стандарт OAuth (Open Authorization) не описывает протокол аутентификации пользователя. Вместо этого он определяет механизм получения доступа одного приложения к другому от имени пользователя. Однако существуют схемы, позволяющие осуществить аутентификацию пользователя на базе этого стандарта (об этом — ниже).
Первая версия стандарта разрабатывалась в 2007 – 2010 гг., а текущая версия 2.0 опубликована в 2012 г. Версия 2.0 значительно расширяет и в то же время упрощает стандарт, но обратно несовместима с версией 1.0. Сейчас OAuth 2.0 очень популярен и используется повсеместно для предоставления делегированного доступа и третье-сторонней аутентификации пользователей.
Чтобы лучше понять сам стандарт, рассмотрим пример веб-приложения, которое помогает пользователям планировать путешествия. Как часть функциональности оно умеет анализировать почту пользователей на наличие писем с подтверждениями бронирований и автоматически включать их в планируемый маршрут. Возникает вопрос, как это веб-приложение может безопасно получить доступ к почте пользователей, например, к Gmail?
> Попросить пользователя указать данные своей учетной записи? — плохой вариант.
> Попросить пользователя создать ключ доступа? — возможно, но весьма сложно.
Как раз эту проблему и позволяет решить стандарт OAuth: он описывает, как приложение путешествий (client) может получить доступ к почте пользователя (resource server) с разрешения пользователя (resource owner). В общем виде весь процесс состоит из нескольких шагов:
- Пользователь (resource owner) дает разрешение приложению (client) на доступ к определенному ресурсу в виде гранта. Что такое грант, рассмотрим чуть ниже.
- Приложение обращается к серверу авторизации и получает токен доступа к ресурсу в обмен на свой грант. В нашем примере сервер авторизации — Google. При вызове приложение дополнительно аутентифицируется при помощи ключа доступа, выданным ему при предварительной регистрации.
- Приложение использует этот токен для получения требуемых данных от сервера ресурсов (в нашем случае — сервис Gmail).
Взаимодействие компонентов в стандарте OAuth.
Стандарт описывает четыре вида грантов, которые определяют возможные сценарии применения:
- Authorization Code — этот грант пользователь может получить от сервера авторизации после успешной аутентификации и подтверждения согласия на предоставление доступа. Такой способ наиболее часто используется в веб-приложениях. Процесс получения гранта очень похож на механизм аутентификации пассивных клиентов в SAML и WS-Federation.
- Implicit — применяется, когда у приложения нет возможности безопасно получить токен от сервера авторизации (например, JavaScript-приложение в браузере). В этом случае грант представляет собой токен, полученный от сервера авторизации, а шаг № 2 исключается из сценария выше.
- Resource Owner Password Credentials — грант представляет собой пару username/password пользователя. Может применяться, если приложение является «интерфейсом» для сервера ресурсов (например, приложение — мобильный клиент для Gmail).
- Client Credentials — в этом случае нет никакого пользователя, а приложение получает доступ к своим ресурсам при помощи своих ключей доступа (исключается шаг № 1).
Стандарт не определяет формат токена, который получает приложение: в сценариях, адресуемых стандартом, приложению нет необходимости анализировать токен, т. к. он лишь используется для получения доступа к ресурсам. Поэтому ни токен, ни грант сами по себе не могут быть использованы для аутентификации пользователя. Однако если приложению необходимо получить достоверную информацию о пользователе, существуют несколько способов это сделать:
- Зачастую API сервера ресурсов включает операцию, предоставляющую информацию о самом пользователе (например, /me в Facebook API). Приложение может выполнять эту операцию каждый раз после получения токена для идентификации клиента. Такой метод иногда называют псевдо-аутентификацией.
- Использовать стандарт OpenID Connect, разработанный как слой учетных данных поверх OAuth (опубликован в 2014 г.). В соответствии с этим стандартом, сервер авторизации предоставляет дополнительный identity token на шаге № 2. Этот токен в формате JWT будет содержать набор определенных полей (claims) с информацией о пользователе.
Стоит заметить, что OpenID Connect, заменивший предыдущие версии стандарта OpenID 1.0 и 2.0, также содержит набор необязательных дополнений для поиска серверов авторизации, динамической регистрации клиентов и управления сессией пользователя.
Заключение
В этой статье мы рассмотрели различные методы аутентификации в веб-приложениях. Ниже — таблица, которая резюмирует описанные способы и протоколы:
Способ | Основное применение | Протоколы |
По паролю | Аутентификация пользователей | HTTP, Forms |
По сертификатам | Аутентификация пользователей в безопасных приложениях; аутентификация сервисов | SSL/TLS |
По одноразовым паролям | Дополнительная аутентификация пользователей (для достижения two-factor authentication) | Forms |
По ключам доступа | Аутентификация сервисов и приложений | — |
По токенам | Делегированная аутентификация пользователей; делегированная авторизация приложений | SAML, WS-Federation, OAuth, OpenID Connect |
Надеюсь, что информация оказалась полезна, и вы сможете применить ее при дизайне и разработке новых приложений. До новых встреч!
Автор: Дмитрий Выростков, Solutions Architect в DataArt.
Как работают веб-сокеты? — Кевин Сукочефф
WebSocket — это постоянное соединение между клиентом и сервером.
WebSockets обеспечивает двунаправленный полнодуплексный канал связи.
который работает через HTTP через одно соединение сокета TCP / IP. На своем
core протокол WebSocket упрощает передачу сообщений между клиентом
и сервер. Эта статья представляет собой введение в WebSocket.
протокол, в том числе проблемы, которые решают WebSockets, и обзор того, как
WebSockets описываются на уровне протокола.
Почему именно WebSocket?
Идея WebSockets возникла из-за ограничений HTTP-технологий.
технологии. При использовании HTTP клиент запрашивает ресурс, а сервер
отвечает запрошенными данными. HTTP — строго однонаправленный
протокол — любые данные, отправленные с сервера клиенту, должны быть первыми
по запросу клиента. Долгое голосование традиционно действовало как
обходной путь для этого ограничения. При длительном опросе клиент делает
HTTP-запрос с длительным периодом ожидания, и сервер использует это время.
тайм-аут для отправки данных клиенту.Длинный опрос работает, но есть
недостаток — ресурсы на сервере связаны на всем протяжении
длинный опрос, даже если нет данных для отправки.
WebSockets, с другой стороны, позволяют отправлять данные на основе сообщений,
аналогично UDP, но с надежностью TCP. WebSocket использует HTTP как
начальный транспортный механизм, но поддерживает соединение TCP после
получен ответ HTTP, чтобы его можно было использовать для отправки сообщений
между клиентом и сервером. WebSockets позволяют нам создавать «в реальном времени»
приложения без использования длительного опроса.
Обзор протокола
Протокол состоит из открывающего рукопожатия, за которым следует базовое сообщение.
кадрирование, наложенное на TCP.— RFC 6455 — Протокол WebSocket
WebSockets начинают свою жизнь как стандартный HTTP-запрос и ответ. Внутри этого
цепочка ответов на запросы, клиент просит открыть соединение WebSocket,
и сервер отвечает (если может). Если это первоначальное рукопожатие
успешно, клиент и сервер согласились использовать существующий TCP / IP
соединение, которое было установлено для HTTP-запроса как WebSocket
подключение.Теперь данные могут передаваться через это соединение с использованием базового фреймового
протокол сообщений. Как только обе стороны признают, что WebSocket
соединение должно быть закрыто, TCP соединение разорвано.
Установление соединения WebSocket — Открытое рукопожатие WebSocket
WebSockets не используют схему http: //
или https: //
(потому что они
не следовать протоколу HTTP). Скорее, URI WebSocket используют новую схему
ws:
(или wss:
для безопасного WebSocket).Остальная часть URI — это
то же, что и HTTP URI: хост, порт, путь и любые параметры запроса.
"ws:" "//" хост [":" порт] путь ["?" запрос]
"wss:" "//" хост [":" порт] путь ["?" запрос]
Соединения WebSocket могут быть установлены только для URI, следующих за этим
схема. То есть, если вы видите URI со схемой ws: //
(или wss: //
),
тогда и клиент, и сервер ДОЛЖНЫ следовать соединению WebSocket.
протокол в соответствии со спецификацией WebSocket.
подключений WebSocket
установлено при обновлении пары HTTP-запрос / ответ. Клиент, который
поддерживает WebSockets и хочет установить соединение, отправит HTTP
запрос, который включает несколько обязательных заголовков:
-
Подключение: обновление
- Заголовок
Connection
обычно определяет,
сетевое соединение остается открытым после текущей транзакции
отделка. Обычное значение для этого заголовка —keep-alive
, чтобы
убедитесь, что соединение является постоянным, чтобы разрешить последующие запросы
на тот же сервер.Во время рукопожатия открытия WebSocket мы установили
заголовок доОбновите
, сигнализируя, что мы хотим сохранить соединение
жив, и использовать его для запросов, отличных от HTTP.
- Заголовок
-
Обновление: websocket
- Заголовок
Upgrade
используется клиентами, чтобы попросить сервер переключиться
к одному из перечисленных протоколов в порядке убывания предпочтения. Мы
укажите здесьwebsocket
, чтобы указать, что клиент хочет
установить соединение WebSocket.
- Заголовок
-
Sec-WebSocket-ключ: q4xkcO32u266gldTuKaSOw ==
-
Sec-WebSocket-Key
— одноразовое случайное значение (nonce)
генерируется клиентом.Значение — это случайно выбранное 16-байтовое значение, имеющее
был закодирован base64.
-
-
Sec-WebSocket-Версия: 13
- Единственная допустимая версия протокола WebSocket — 13. Любая другая
версия, указанная в этом заголовке, недействительна.
- Единственная допустимая версия протокола WebSocket — 13. Любая другая
Вместе эти заголовки приведут к HTTP-запросу GET от
клиент к URI ws: //
, как в следующем примере:
ПОЛУЧИТЬ ws: //example.com: 8181 / HTTP / 1.1
Хост: localhost: 8181
Подключение: Обновление
Прагма: без кеширования
Cache-Control: без кеша
Обновление: websocket
Sec-WebSocket-Версия: 13
Sec-WebSocket-ключ: q4xkcO32u266gldTuKaSOw ==
Когда клиент отправляет начальный запрос на открытие соединения WebSocket, он
ждет ответа от сервера.Ответ должен иметь код ответа HTTP 101 Switching Protocols
. Ответ HTTP 101 Switching Protocols
указывает, что сервер переключается на протокол, который клиент
запрошен в заголовке запроса Upgrade
. Кроме того, сервер должен
включить заголовки HTTP, подтверждающие, что соединение было успешно
улучшено:
HTTP / 1.1 101 Протоколы коммутации
Обновление: websocket
Подключение: Обновление
Sec-WebSocket-Accept: fA9dggdnMPU79lJgAE3W4TRnyDM =
-
Подключение: обновление
- Подтверждает, что соединение было обновлено.
-
Обновление: websocket
- Подтверждает, что соединение было обновлено.
-
Sec-WebSocket-Accept
: fA9dggdnMPU79lJgAE3W4TRnyDM = `-
Sec-WebSocket-Accept
имеет кодировку base64, хешированное значение SHA-1. Вы
сгенерировать это значение путем объединения клиентовSec-WebSocket-Key
nonce и статическое значение258EAFA5-E914-47DA-95CA-C5AB0DC85B11
определено в RFC 6455. ХотяSec-WebSocket-Key и
Sec-WebSocket-Accept кажутся сложными, они существуют, так что оба
клиент и сервер могут знать, что их коллега поддерживает
WebSockets.Поскольку WebSocket повторно использует HTTP-соединение, там
являются потенциальными проблемами безопасности, если любая из сторон интерпретирует WebSocket
данные как HTTP-запрос.
-
После того, как клиент получит ответ сервера, соединение WebSocket
открыть, чтобы начать передачу данных.
Протокол WebSocket
WebSocket — это протокол с фреймами , что означает, что фрагмент данных (сообщение)
делится на несколько дискретных частей, размер которых
закодировано в кадре.Кадр включает тип кадра, длину полезной нагрузки,
и часть данных. Обзор кадра приведен в RFC.
6455 и воспроизведено
Вот.
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ - + - + - + - + ------- + - + ------------- + ----------------- -------------- +
| F | R | R | R | код операции | M | Длина полезной нагрузки | Увеличенная длина полезной нагрузки |
| I | S | S | S | (4) | A | (7) | (16/64) |
| N | V | V | V | | S | | (если полезная нагрузка len == 126/127) |
| | 1 | 2 | 3 | | K | | |
+ - + - + - + - + ------- + - + ------------- + - - - - - - - - - - - - - - - +
| Увеличенная длина полезной нагрузки продолжается, если полезная нагрузка len == 127 |
+ - - - - - - - - - - - - - - - + ------------------------------- +
| | Маскирующий ключ, если МАСКА установлена в 1 |
+ ------------------------------- + ----------------- -------------- +
| Маскирующий ключ (продолжение) | Данные полезной нагрузки |
+ -------------------------------- - - - - - - - - - - - - - - - - +
: Данные полезной нагрузки продолжение...:
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Данные полезной нагрузки продолжение ... |
+ ------------------------------------------------- -------------- +
Я не буду здесь останавливаться на всех деталях протокола кадров. Обратитесь к RFC
6455 для получения полной информации. Скорее,
Я расскажу о самых важных моментах, чтобы мы могли понять
протокола WebSocket.
Ребро-бит
Первый бит заголовка WebSocket — это бит Fin.Этот бит устанавливается, если
этот фрейм — последние данные для завершения этого сообщения.
Биты RSV1, RSV2, RSV3
Эти биты зарезервированы для использования в будущем.
код операции
У каждого кадра есть код операции, который определяет, как интерпретировать
данные полезной нагрузки.
Значение кода операции | Описание |
---|---|
0x00 | Этот кадр продолжает полезную нагрузку из предыдущего кадра. |
0x01 | Обозначает текстовый фрейм.Текстовые фреймы декодируются сервером в кодировке UTF-8. |
0x02 | Обозначает двоичный фрейм. Двоичные фреймы доставляются сервером без изменений. |
0x03-0x07 | Зарезервировано для использования в будущем. |
0x08 | Обозначает, что клиент хочет закрыть соединение. |
0x09 | Фрейм ping. Служит механизмом подтверждения связи, гарантирующим, что соединение все еще живо. Получатель должен ответить понгом. |
0x0a | Рама для понга. Служит механизмом подтверждения связи, гарантирующим, что соединение все еще живо. Получатель должен ответить фреймом ping. |
0x0b-0x0f | Зарезервировано для использования в будущем. |
Маска
Установка этого бита в 1 включает маскирование . WebSockets требует, чтобы все
полезная нагрузка обфусцирована с использованием случайного ключа (маски), выбранного клиентом.
Ключ маскировки объединяется с данными полезной нагрузки с помощью операции XOR.
перед отправкой данных в полезную нагрузку.Эта маскировка предотвращает появление кешей.
неверная интерпретация фреймов WebSocket как кэшируемых данных. Почему мы должны предотвращать
кеширование данных WebSocket? Безопасность.
При разработке протокола WebSocket было показано, что если
развертывается скомпрометированный сервер, и клиенты подключаются к этому серверу, он
можно иметь промежуточные прокси или кеш инфраструктуры
ответы скомпрометированного сервера, чтобы будущие клиенты, запрашивающие
data получают неверный ответ. Эта атака называется cache.
отравление , и является результатом того факта, что мы не можем контролировать, как
прокси-серверы ведут себя в дикой природе.Это особенно проблематично
при внедрении нового протокола, такого как WebSocket, который должен взаимодействовать с
существующая инфраструктура интернета.
Длина полезной нагрузки
Поле Payload len
и Extended payload length поле
используются для
кодировать общую длину данных полезной нагрузки для этого кадра. Если полезная нагрузка
данные небольшие (менее 126 байт), длина кодируется в поле Payload len
. По мере роста данных полезной нагрузки мы используем дополнительные поля для
закодировать длину полезной нагрузки.
Маскирующий ключ
Как обсуждалось с битом MASK
, все кадры, отправленные от клиента к
серверы маскируются 32-битным значением, содержащимся в кадре.
Это поле присутствует, если бит маски установлен в 1, и отсутствует, если
бит маски установлен на 0.
Данные полезной нагрузки
Данные полезной нагрузки
включают произвольные данные приложения и любые расширения
данные, согласованные между клиентом и сервером.
Расширения согласовываются во время первоначального рукопожатия и позволяют
расширить протокол WebSocket для дополнительных целей.
Закрытие соединения WebSocket — рукопожатие закрытия WebSocket
Чтобы закрыть соединение WebSocket, отправляется закрывающий кадр (код операции 0x08
).
Помимо кода операции, закрывающий кадр может содержать тело, которое
указывает причину закрытия. Если одна из сторон соединения получает
закрывающий кадр, он должен отправить закрывающий кадр в ответ, и больше никаких данных
должны быть отправлены через соединение. После получения кадра закрытия
обе стороны разрывают TCP-соединение.Сервер всегда
инициирует закрытие TCP-соединения.
Дополнительные ссылки
Эта статья представляет собой введение в протокол WebSocket и
покрывает много земли. Однако полный протокол содержит больше деталей, чем
что я мог бы вписать в этот пост в блоге. Если хотите узнать больше, там
есть несколько отличных ресурсов на выбор:
См. Также
.
WebSocket | Chrome Полная поддержка 4 | Край Полная поддержка 12 | Firefox Полная поддержка 11
| IE Полная поддержка 10 | Опера Полная поддержка 12.1 | Safari Полная поддержка 5 | WebView Android Полная поддержка ≤37 | Chrome Android Полная поддержка 18 | Firefox Android Полная поддержка 14
| Опера Android Полная поддержка 12.1 | Safari iOS Полная поддержка 4.2 | Samsung Интернет Android Полная поддержка 1.0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
WebSocket () конструктор | Chrome Полная поддержка Да | Край Полная поддержка ≤79 | Firefox Полная поддержка 7
| IE ? | Опера Полная поддержка Да | Safari ? | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка 7 | Опера Android ? | Safari iOS ? | Samsung Интернет Android Полная поддержка Да |
двоичный Тип | Хром Полная поддержка Да | Край Полная поддержка 12 | Firefox Полная поддержка Есть | IE ? | Опера Полная поддержка Да | Safari Полная поддержка Да | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android Полная поддержка Да | Safari iOS Полная поддержка Да | Samsung Internet Android Полная поддержка Да |
с буферизацией Количество | Хром Полная поддержка Да | Край Полная поддержка 12 | Firefox Полная поддержка Есть | IE ? | Опера Полная поддержка Да | Safari Полная поддержка Да | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android Полная поддержка Да | Safari iOS Полная поддержка Да | Samsung Internet Android Полная поддержка Да |
закрыть | Хром Полная поддержка 4 | Край Полная поддержка 12 | Firefox Полная поддержка 8
| IE Полная поддержка 10 | Опера Полная поддержка 12.1 | Safari Полная поддержка 5 | WebView Android Полная поддержка ≤37 | Chrome Android Полная поддержка 18 | Firefox Android Полная поддержка 8
| Опера Android Полная поддержка 12.1 | Safari iOS Полная поддержка 4.2 | Samsung Интернет Android Полная поддержка 1.0 |
закрыть событие | Chrome Полная поддержка Да | Край Полная поддержка 12 | Firefox Полная поддержка Есть | IE ? | Опера Полная поддержка Да | Safari ? | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android ? | Safari iOS ? | Samsung Интернет Android Полная поддержка Да |
ошибка событие | Chrome Полная поддержка Да | Край Полная поддержка 12 | Firefox Полная поддержка Есть | IE ? | Опера Полная поддержка Да | Safari ? | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android ? | Safari iOS ? | Samsung Интернет Android Полная поддержка Да |
Удлинители | Хром Полная поддержка Да | Край Полная поддержка 12 | Firefox Полная поддержка 8 | IE ? | Опера Полная поддержка Да | Safari Полная поддержка Да | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка 8 | Опера Android Полная поддержка Да | Safari iOS Полная поддержка Да | Samsung Internet Android Полная поддержка Да |
сообщение событие | Chrome Полная поддержка Да | Край Полная поддержка 12 | Firefox Полная поддержка Есть | IE ? | Опера Полная поддержка Да | Safari ? | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android ? | Safari iOS ? | Samsung Интернет Android Полная поддержка Да |
вкл. | Хром Полная поддержка Да | Край Полная поддержка 12 | Firefox Полная поддержка Есть | IE ? | Опера Полная поддержка Да | Safari Полная поддержка Да | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android Полная поддержка Да | Safari iOS Полная поддержка Да | Samsung Internet Android Полная поддержка Да |
ошибка | Chrome Полная поддержка Да | Край Полная поддержка 12 | Firefox Полная поддержка Есть | IE ? | Опера Полная поддержка Да | Safari Полная поддержка Да | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android Полная поддержка Да | Safari iOS Полная поддержка Да | Samsung Internet Android Полная поддержка Да |
в сообщении | Хром Полная поддержка Да | Край Полная поддержка 12 | Firefox Полная поддержка Есть | IE ? | Опера Полная поддержка Да | Safari Полная поддержка Да | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android Полная поддержка Да | Safari iOS Полная поддержка Да | Samsung Internet Android Полная поддержка Да |
onopen | Chrome Полная поддержка Да | Край Полная поддержка 12 | Firefox Полная поддержка Есть | IE ? | Опера Полная поддержка Да | Safari Полная поддержка Да | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android Полная поддержка Да | Safari iOS Полная поддержка Да | Samsung Internet Android Полная поддержка Да |
открыто событие | Chrome Полная поддержка Да | Край Полная поддержка 12 | Firefox Полная поддержка Есть | IE ? | Опера Полная поддержка Да | Safari ? | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android ? | Safari iOS ? | Samsung Интернет Android Полная поддержка Да |
протокол | Хром Полная поддержка Да | Край Полная поддержка 12 | Firefox Полная поддержка Есть | IE ? | Опера Полная поддержка Да | Safari Полная поддержка Да | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android Полная поддержка Да | Safari iOS Полная поддержка Да | Samsung Internet Android Полная поддержка Да |
Поддерживает протокол согласно RFC 6455 | Chrome Полная поддержка 16 | Край Полная поддержка 12 | Firefox Полная поддержка 11 | IE Полная поддержка 10 | Опера Полная поддержка 15 | Safari Полная поддержка 6 | WebView Android Полная поддержка Да | Chrome Android Полная поддержка 18 | Firefox Android Полная поддержка 14 | Опера Android Полная поддержка 14 | Safari iOS Полная поддержка 6 | Samsung Интернет Android Полная поддержка 1.0 |
Готово Состояние | Хром Полная поддержка 43 | Край Полная поддержка 12 | Firefox Полная поддержка 19 | IE Полная поддержка 10 | Опера Полная поддержка 30 | Safari Полная поддержка 10 | WebView Android Полная поддержка 43 | Chrome Android Полная поддержка 43 | Firefox Android Полная поддержка 19 | Опера Android Полная поддержка 30 | Safari iOS Полная поддержка 10 | Samsung Интернет Android Полная поддержка 4.0 |
отправить | Хром Полная поддержка 4 | Край Полная поддержка 12 | Firefox Полная поддержка 18
| IE Полная поддержка 10 | Опера Полная поддержка 12.1 | Safari Полная поддержка 5 | WebView Android Полная поддержка ≤37 | Chrome Android Полная поддержка 18 | Firefox Android Полная поддержка 18
| Опера Android Полная поддержка 12.1 | Safari iOS Полная поддержка 4.2 | Samsung Интернет Android Полная поддержка 1.0 |
url | Chrome Полная поддержка Да | Край Полная поддержка 12 | Firefox Полная поддержка Есть | IE ? | Опера Полная поддержка Да | Safari Полная поддержка Да | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android Полная поддержка Да | Safari iOS Полная поддержка Да | Samsung Internet Android Полная поддержка Да |
В наличии у рабочих | Хром Полная поддержка Да | Край Полная поддержка ≤18 | Firefox Полная поддержка 37 | IE ? | Opera ? | Safari ? | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка 37 | Опера Android ? | Safari iOS ? | Samsung Интернет Android Полная поддержка Да |
.
WebSocket | Chrome Полная поддержка 4 | Край Полная поддержка 12 | Firefox Полная поддержка 11
| IE Полная поддержка 10 | Опера Полная поддержка 12.1 | Safari Полная поддержка 5 | WebView Android Полная поддержка ≤37 | Chrome Android Полная поддержка 18 | Firefox Android Полная поддержка 14
| Опера Android Полная поддержка 12.1 | Safari iOS Полная поддержка 4.2 | Samsung Интернет Android Полная поддержка 1.0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
WebSocket () конструктор | Chrome Полная поддержка Да | Край Полная поддержка ≤79 | Firefox Полная поддержка 7
| IE ? | Опера Полная поддержка Да | Safari ? | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка 7 | Опера Android ? | Safari iOS ? | Samsung Интернет Android Полная поддержка Да |
двоичный Тип | Хром Полная поддержка Да | Край Полная поддержка 12 | Firefox Полная поддержка Есть | IE ? | Опера Полная поддержка Да | Safari Полная поддержка Да | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android Полная поддержка Да | Safari iOS Полная поддержка Да | Samsung Internet Android Полная поддержка Да |
с буферизацией Количество | Хром Полная поддержка Да | Край Полная поддержка 12 | Firefox Полная поддержка Есть | IE ? | Опера Полная поддержка Да | Safari Полная поддержка Да | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android Полная поддержка Да | Safari iOS Полная поддержка Да | Samsung Internet Android Полная поддержка Да |
закрыть | Хром Полная поддержка 4 | Край Полная поддержка 12 | Firefox Полная поддержка 8
| IE Полная поддержка 10 | Опера Полная поддержка 12.1 | Safari Полная поддержка 5 | WebView Android Полная поддержка ≤37 | Chrome Android Полная поддержка 18 | Firefox Android Полная поддержка 8
| Опера Android Полная поддержка 12.1 | Safari iOS Полная поддержка 4.2 | Samsung Интернет Android Полная поддержка 1.0 |
закрыть событие | Chrome Полная поддержка Да | Край Полная поддержка 12 | Firefox Полная поддержка Есть | IE ? | Опера Полная поддержка Да | Safari ? | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android ? | Safari iOS ? | Samsung Интернет Android Полная поддержка Да |
ошибка событие | Chrome Полная поддержка Да | Край Полная поддержка 12 | Firefox Полная поддержка Есть | IE ? | Опера Полная поддержка Да | Safari ? | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android ? | Safari iOS ? | Samsung Интернет Android Полная поддержка Да |
Удлинители | Хром Полная поддержка Да | Край Полная поддержка 12 | Firefox Полная поддержка 8 | IE ? | Опера Полная поддержка Да | Safari Полная поддержка Да | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка 8 | Опера Android Полная поддержка Да | Safari iOS Полная поддержка Да | Samsung Internet Android Полная поддержка Да |
сообщение событие | Chrome Полная поддержка Да | Край Полная поддержка 12 | Firefox Полная поддержка Есть | IE ? | Опера Полная поддержка Да | Safari ? | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android ? | Safari iOS ? | Samsung Интернет Android Полная поддержка Да |
вкл. | Хром Полная поддержка Да | Край Полная поддержка 12 | Firefox Полная поддержка Есть | IE ? | Опера Полная поддержка Да | Safari Полная поддержка Да | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android Полная поддержка Да | Safari iOS Полная поддержка Да | Samsung Internet Android Полная поддержка Да |
ошибка | Chrome Полная поддержка Да | Край Полная поддержка 12 | Firefox Полная поддержка Есть | IE ? | Опера Полная поддержка Да | Safari Полная поддержка Да | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android Полная поддержка Да | Safari iOS Полная поддержка Да | Samsung Internet Android Полная поддержка Да |
в сообщении | Хром Полная поддержка Да | Край Полная поддержка 12 | Firefox Полная поддержка Есть | IE ? | Опера Полная поддержка Да | Safari Полная поддержка Да | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android Полная поддержка Да | Safari iOS Полная поддержка Да | Samsung Internet Android Полная поддержка Да |
onopen | Chrome Полная поддержка Да | Край Полная поддержка 12 | Firefox Полная поддержка Есть | IE ? | Опера Полная поддержка Да | Safari Полная поддержка Да | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android Полная поддержка Да | Safari iOS Полная поддержка Да | Samsung Internet Android Полная поддержка Да |
открыто событие | Chrome Полная поддержка Да | Край Полная поддержка 12 | Firefox Полная поддержка Есть | IE ? | Опера Полная поддержка Да | Safari ? | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android ? | Safari iOS ? | Samsung Интернет Android Полная поддержка Да |
протокол | Хром Полная поддержка Да | Край Полная поддержка 12 | Firefox Полная поддержка Есть | IE ? | Опера Полная поддержка Да | Safari Полная поддержка Да | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android Полная поддержка Да | Safari iOS Полная поддержка Да | Samsung Internet Android Полная поддержка Да |
Поддерживает протокол согласно RFC 6455 | Chrome Полная поддержка 16 | Край Полная поддержка 12 | Firefox Полная поддержка 11 | IE Полная поддержка 10 | Опера Полная поддержка 15 | Safari Полная поддержка 6 | WebView Android Полная поддержка Да | Chrome Android Полная поддержка 18 | Firefox Android Полная поддержка 14 | Опера Android Полная поддержка 14 | Safari iOS Полная поддержка 6 | Samsung Интернет Android Полная поддержка 1.0 |
Готово Состояние | Хром Полная поддержка 43 | Край Полная поддержка 12 | Firefox Полная поддержка 19 | IE Полная поддержка 10 | Опера Полная поддержка 30 | Safari Полная поддержка 10 | WebView Android Полная поддержка 43 | Chrome Android Полная поддержка 43 | Firefox Android Полная поддержка 19 | Опера Android Полная поддержка 30 | Safari iOS Полная поддержка 10 | Samsung Интернет Android Полная поддержка 4.0 |
отправить | Хром Полная поддержка 4 | Край Полная поддержка 12 | Firefox Полная поддержка 18
| IE Полная поддержка 10 | Опера Полная поддержка 12.1 | Safari Полная поддержка 5 | WebView Android Полная поддержка ≤37 | Chrome Android Полная поддержка 18 | Firefox Android Полная поддержка 18
| Опера Android Полная поддержка 12.1 | Safari iOS Полная поддержка 4.2 | Samsung Интернет Android Полная поддержка 1.0 |
url | Chrome Полная поддержка Да | Край Полная поддержка 12 | Firefox Полная поддержка Есть | IE ? | Опера Полная поддержка Да | Safari Полная поддержка Да | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка Да | Opera Android Полная поддержка Да | Safari iOS Полная поддержка Да | Samsung Internet Android Полная поддержка Да |
В наличии у рабочих | Хром Полная поддержка Да | Край Полная поддержка ≤18 | Firefox Полная поддержка 37 | IE ? | Opera ? | Safari ? | WebView Android Полная поддержка Да | Chrome Android Полная поддержка Да | Firefox Android Полная поддержка 37 | Опера Android ? | Safari iOS ? | Samsung Интернет Android Полная поддержка Да |
.
Начало работы — документация по websockets 8.1
Требования
веб-сокетов
требует Python ≥ 3.6.1.
По возможности следует использовать последнюю версию Python. Если вы используете
старая версия, имейте в виду, что для каждой дополнительной версии (3.x) только последняя
Выпуск bugfix (3.x.y) официально поддерживается.
Установка
Установите веб-сокетов
с:
Базовый пример
Вот пример сервера WebSocket.
Считывает имя клиента, отправляет приветствие и закрывает соединение.
#! / Usr / bin / env python # Пример сервера WS импортировать asyncio импортировать веб-сокеты async def привет (веб-сокет, путь): name = ждать websocket.recv () print (f "<{имя}") welcome = f "Привет, {имя}!" ждать websocket.send (приветствие) print (f "> {приветствие}") start_server = websockets.serve (привет, "localhost", 8765) asyncio.get_event_loop (). run_until_complete (начальный_сервер) asyncio.get_event_loop (). run_forever ()
На стороне сервера websockets
выполняет сопрограмму обработчика hello
один раз для каждого подключения WebSocket. Он закрывает соединение, когда обработчик
сопрограмма возвращается.
Вот соответствующий пример клиента WebSocket.
#! / Usr / bin / env python # Пример клиента WS импортировать asyncio импортировать веб-сокеты async def hello (): uri = "ws: // localhost: 8765" async с websockets.connect (uri) как websocket: name = input ("Как вас зовут?") жду веб-сокета.отправить (имя) print (f "> {имя}") приветствие = ждать websocket.recv () print (f "<{приветствие}") asyncio.get_event_loop (). run_until_complete (привет ())
Использование connect ()
в качестве асинхронного диспетчера контекста обеспечивает
соединение закрывается перед выходом из сопрограммы hello
.
Безопасный пример
Secure WebSocket-соединения повышают конфиденциальность, а также надежность
потому что они снижают риск вмешательства плохих прокси.
Протокол WSS для WS то же, что HTTPS для HTTP: соединение зашифровано
с безопасностью транспортного уровня (TLS), который часто называют безопасным
Уровень сокетов (SSL). WSS требует сертификатов TLS, таких как HTTPS.
Вот как можно адаптировать пример сервера для обеспечения безопасных соединений. Увидеть
документация модуля ssl
для безопасной настройки контекста.
#! / Usr / bin / env python # Пример сервера WSS (WS over TLS) с самоподписанным сертификатом импортировать asyncio импортировать pathlib импортировать ssl импортировать веб-сокеты async def привет (веб-сокет, путь): name = ждать веб-сокета.recv () print (f "<{имя}") welcome = f "Привет, {имя}!" ждать websocket.send (приветствие) print (f "> {приветствие}") ssl_context = ssl.SSLContext (ssl.PROTOCOL_TLS_SERVER) localhost_pem = pathlib.Path (__ файл __). with_name ("localhost.pem") ssl_context.load_cert_chain (localhost_pem) start_server = websockets.serve ( привет, "localhost", 8765, ssl = ssl_context ) asyncio.get_event_loop (). run_until_complete (начальный_сервер) asyncio.get_event_loop (). run_forever ()
Вот как адаптировать клиента.
#! / Usr / bin / env python # Пример клиента WSS (WS over TLS) с самоподписанным сертификатом импортировать asyncio импортировать pathlib импортировать ssl импортировать веб-сокеты ssl_context = ssl.SSLContext (ssl.PROTOCOL_TLS_CLIENT) localhost_pem = pathlib.Path (__ файл __). with_name ("localhost.pem") ssl_context.load_verify_locations (localhost_pem) async def hello (): uri = "wss: // localhost: 8765" асинхронный с websockets.connect ( uri, ssl = ssl_context ) как веб-сокет: name = input ("Как вас зовут?") жду веб-сокета.отправить (имя) print (f "> {имя}") приветствие = ждать websocket.recv () print (f "<{приветствие}") asyncio.get_event_loop (). run_until_complete (привет ())
Этому клиенту нужен контекст, потому что сервер использует самозаверяющий сертификат.
Клиент подключается к защищенному серверу WebSocket с действующим сертификатом.
(т. е. подписанный центром сертификации, которому доверяет ваша установка Python), можно просто передать
ssl = True от
до connect ()
вместо создания контекста.
Пример на основе браузера
Вот пример того, как запустить сервер WebSocket и подключиться из браузера.
Запустите этот скрипт в консоли:
#! / Usr / bin / env python # WS-сервер, который отправляет сообщения через случайные промежутки времени импортировать asyncio дата и время импорта случайный импорт импортировать веб-сокеты async def time (веб-сокет, путь): в то время как True: сейчас = datetime.datetime.ut
.