- Операторы
- Управляющие инструкции
- JS Объекты
- браузер BOM
- HTML DOM
- События
- HTML Объекты
- Промисы, async/await
- Сетевые запросы
- XMLHttpRequest
- Объекты URL
- Объект formData
- Fetch API
- Fetch API 2
- WebSocket
- Server Sent Events
- Бинарные данные и файлы
- Модули
- Классы
- Разное
WebSocket
Протокол WebSocket
(«веб-сокет»), описанный в спецификации RFC 6455,
обеспечивает возможность обмена данными между браузером и сервером
через постоянное соединение. Данные передаются по нему в обоих
направлениях в виде «пакетов», без разрыва соединения и дополнительных
HTTP-запросов.
WebSocket особенно хорош для сервисов, которые нуждаются в постоянном обмене данными, например онлайн игры, торговые площадки, работающие в реальном времени, и т.д.
Простой пример
Чтобы открыть веб-сокет-соединение, нам нужно создать объект new WebSocket
, указав в url-адресе специальный протокол ws
:
let socket = new WebSocket("ws://javascript.info");
Также существует протокол wss://
, использующий шифрование. Это как HTTPS для веб-сокетов.
Всегда предпочитайте wss://
Протокол wss://
не только использует шифрование, но и обладает повышенной надёжностью.
Это потому, что данные ws://
не зашифрованы, видны для
любого посредника. Старые прокси-серверы не знают о WebSocket, они могут
увидеть «странные» заголовки и закрыть соединение.
С другой стороны, wss://
– это WebSocket поверх TLS (так
же, как HTTPS – это HTTP поверх TLS), безопасный транспортный уровень
шифрует данные от отправителя и расшифровывает на стороне получателя.
Пакеты данных передаются в зашифрованном виде через прокси, которые не
могут видеть, что внутри, и всегда пропускают их.
Как только объект WebSocket
создан, мы должны слушать его события. Их всего 4:
open
– соединение установлено,message
– получены данные,error
– ошибка,close
– соединение закрыто.
…А если мы хотим отправить что-нибудь, то вызов socket.send(data)
сделает это.
Вот пример:
let socket = new WebSocket("wss://javascript.info/article/websocket/demo/hello"); socket.onopen = function(e) { alert("[open] Соединение установлено"); alert("Отправляем данные на сервер"); socket.send("Меня зовут Джон"); }; socket.onmessage = function(event) { alert(`[message] Данные получены с сервера: ${event.data}`); }; socket.onclose = function(event) { if (event.wasClean) { alert(`[close] Соединение закрыто чисто, код=${event.code} причина=${event.reason}`); } else { // например, сервер убил процесс или сеть недоступна // обычно в этом случае event.code 1006 alert('[close] Соединение прервано'); } }; socket.onerror = function(error) { alert(`[error]`); };
Для демонстрации есть небольшой пример сервера server.js, написанного на Node.js, для запуска примера выше. Он отвечает «Привет с сервера, Джон», после ожидает 5 секунд и закрывает соединение.
Так вы увидите события open
→ message
→ close
.
Открытие веб-сокета
Когда new WebSocket(url)
создан, он тут же сам начинает устанавливать соединение.
Браузер, при помощи специальных заголовков, спрашивает сервер: «Ты поддерживаешь Websocket?» и если сервер отвечает «да», они начинают работать по протоколу WebSocket, который уже не является HTTP.
Вот пример заголовков для запроса, который делает new WebSocket("wss://javascript.info/chat")
.
GET /chat Host: javascript.info Origin: https://javascript.info Connection: Upgrade Upgrade: websocket Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q== Sec-WebSocket-Version: 13
Origin
– источник текущей страницы (напримерhttps://javascript.info
). Объект WebSocket по своей природе не завязан на текущий источник. Нет никаких специальных заголовков или других ограничений. Старые сервера всё равно не могут работать с WebSocket, поэтому проблем с совместимостью нет. Но заголовокOrigin
важен, так как он позволяет серверу решать, использовать ли WebSocket с этим сайтом.Connection: Upgrade
– сигнализирует, что клиент хотел бы изменить протокол.Upgrade: websocket
– запрошен протокол «websocket».Sec-WebSocket-Key
– случайный ключ, созданный браузером для обеспечения безопасности.Sec-WebSocket-Version
– версия протокола WebSocket.
Запрос WebSocket нельзя эмулировать
Мы не можем использовать XMLHttpRequest
или fetch
для создания такого HTTP-запроса, потому что JavaScript не позволяет устанавливать такие заголовки.
Если сервер согласен переключиться на WebSocket, то он должен отправить в ответ код 101:
101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=
Здесь Sec-WebSocket-Accept
– это Sec-WebSocket-Key
, перекодированный с помощью специального алгоритма. Браузер использует его, чтобы убедиться, что ответ соответствует запросу.
После этого данные передаются по протоколу WebSocket, и вскоре мы увидим его структуру («фреймы»). И это вовсе не HTTP.
Расширения и подпротоколы
Могут быть дополнительные заголовки Sec-WebSocket-Extensions
и Sec-WebSocket-Protocol
, описывающие расширения и подпротоколы.
Например:
Sec-WebSocket-Extensions: deflate-frame
означает, что браузер поддерживает сжатие данных. Расширение – это что-то, связанное с передачей данных, расширяющее сам протокол WebSocket. ЗаголовокSec-WebSocket-Extensions
отправляется браузером автоматически со списком всевозможных расширений, которые он поддерживает.Sec-WebSocket-Protocol: soap, wamp
означает, что мы будем передавать не только произвольные данные, но и данные в протоколах SOAP или WAMP (The WebSocket Application Messaging Protocol" – «протокол обмена сообщениями WebSocket приложений»). То есть этот заголовок описывает не передачу, а формат данных, который мы собираемся использовать. Официальные подпротоколы WebSocket регистрируются в каталоге IANA.Этот необязательный заголовок ставим мы сами, передавая массив подпротоколов вторым параметром
new WebSocket
, вот так:let socket = new WebSocket("wss://javascript.info/chat", ["soap", "wamp"]);
Сервер должен ответить перечнем протоколов и расширений, которые он может использовать.
Например, запрос:
GET /chat Host: javascript.info Upgrade: websocket Connection: Upgrade Origin: https://javascript.info Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q== Sec-WebSocket-Version: 13 Sec-WebSocket-Extensions: deflate-frame Sec-WebSocket-Protocol: soap, wamp
Ответ:
101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g= Sec-WebSocket-Extensions: deflate-frame Sec-WebSocket-Protocol: soap
Здесь сервер отвечает, что поддерживает расширение – deflate-frame и может использовать только протокол SOAP из всего списка запрошенных подпротоколов.
Передача данных
Поток данных в WebSocket состоит из «фреймов», фрагментов данных, которые могут быть отправлены любой стороной, и которые могут быть следующих видов:
- «текстовые фреймы» – содержат текстовые данные, которые стороны отправляют друг другу.
- «бинарные фреймы» – содержат бинарные данные, которые стороны отправляют друг другу.
- «пинг-понг фреймы» используется для проверки соединения; отправляется с сервера, браузер реагирует на них автоматически.
- также есть «фрейм закрытия соединения» и некоторые другие служебные фреймы.
В браузере мы напрямую работаем только с текстовыми и бинарными фреймами.
Метод WebSocket .send()
может отправлять и текстовые, и бинарные данные.
Вызов socket.send(body)
принимает body
в виде строки или любом бинарном формате включая Blob
, ArrayBuffer
и другие. Дополнительных настроек не требуется, просто отправляем в любом формате.
При получении данных, текст всегда поступает в виде строки. А для бинарных данных мы можем выбрать один из двух форматов: Blob
или ArrayBuffer
.
Это задаётся свойством socket.binaryType
, по умолчанию оно равно "blob"
, так что бинарные данные поступают в виде Blob
-объектов.
Blob
– это высокоуровневый бинарный объект, он напрямую интегрируется с <a>
, <img>
и другими тегами, так что это вполне удобное значение по умолчанию. Но
для обработки данных, если требуется доступ к отдельным байтам, мы можем
изменить его на "arraybuffer"
:
socket.binaryType = "arraybuffer"; socket.onmessage = (event) => { // event.data является строкой (если текст) или arraybuffer (если двоичные данные) };
Ограничение скорости
Представим, что наше приложение генерирует много данных для отправки. Но у пользователя медленное соединение, возможно, он в интернете с мобильного телефона и не из города.
Мы можем вызывать socket.send(data)
снова и снова. Но данные будут буферизованы (сохранены) в памяти и отправлены лишь с той скоростью, которую позволяет сеть.
Свойство socket.bufferedAmount
хранит количество байт буферизованных данных на текущий момент, ожидающих отправки по сети.
Мы можем изучить его, чтобы увидеть, действительно ли сокет доступен для передачи.
// каждые 100мс проверить сокет и отправить больше данных, // только если все текущие отосланы setInterval(() => { if (socket.bufferedAmount == 0) { socket.send(moreData()); } }, 100);
Закрытие подключения
Обычно, когда сторона хочет закрыть соединение (браузер и сервер имеют равные права), они отправляют «фрейм закрытия соединения» с кодом закрытия и указывают причину в виде текста.
Метод для этого:
socket.close([code], [reason]);
code
– специальный WebSocket-код закрытия (не обязателен).reason
– строка с описанием причины закрытия (не обязательна).
Затем противоположная сторона в обработчике события close
получит и код code
и причину reason
, например:
// закрывающая сторона: socket.close(1000, "работа закончена"); // другая сторона: socket.onclose = event => { // event.code === 1000 // event.reason === "работа закончена" // event.wasClean === true (закрыто чисто) };
code
– это не любое число, а специальный код закрытия WebSocket.
Наиболее распространённые значения:
1000
– по умолчанию, нормальное закрытие,1006
– невозможно установить такой код вручную, указывает, что соединение было потеряно (нет фрейма закрытия).
Есть и другие коды:
1001
– сторона отключилась, например сервер выключен или пользователь покинул страницу,1009
– сообщение слишком большое для обработки,1011
– непредвиденная ошибка на сервере,- …и так далее.
Полный список находится в RFC6455, §7.4.1.
Коды WebSocket чем-то похожи на коды HTTP, но они разные. В частности, любые коды меньше 1000
зарезервированы. Если мы попытаемся установить такой код, то получим ошибку.
// в случае, если соединение сброшено socket.onclose = event => { // event.code === 1006 // event.reason === "" // event.wasClean === false (нет закрывающего кадра) };
Состояние соединения
Чтобы получить состояние соединения, существует дополнительное свойство socket.readyState
со значениями:
0
– «CONNECTING»: соединение ещё не установлено,1
– «OPEN»: обмен данными,2
– «CLOSING»: соединение закрывается,3
– «CLOSED»: соединение закрыто.
Пример чата
Давайте рассмотрим пример чата с использованием WebSocket API и модуля WebSocket сервера Node.js https://github.com/websockets/ws. Основное внимание мы, конечно, уделим клиентской части, но и серверная весьма проста.
HTML: нам нужна форма <form>
для отправки данных и <div>
для отображения сообщений:
<!-- форма сообщений --> <form name="publish"> <input type="text" name="message"> <input type="submit" value="Отправить"> </form> <!-- div с сообщениями --> <div id="messages"></div>
От JavaScript мы хотим 3 вещи:
- Открыть соединение.
- При отправке формы пользователем – вызвать
socket.send(message)
для сообщения. - При получении входящего сообщения – добавить его в
div#messages
.
Вот код:
let socket = new WebSocket("wss://javascript.info/article/websocket/chat/ws"); // отправка сообщения из формы document.forms.publish.onsubmit = function() { let outgoingMessage = this.message.value; socket.send(outgoingMessage); return false; }; // получение сообщения - отобразить данные в div#messages socket.onmessage = function(event) { let message = event.data; let messageElem = document.createElement('div'); messageElem.textContent = message; document.getElementById('messages').prepend(messageElem); }
Серверный код выходит за рамки этой главы. Здесь мы будем использовать Node.js, но вы не обязаны это делать. Другие платформы также поддерживают средства для работы с WebSocket.
Серверный алгоритм действий будет таким:
- Создать
clients = new Set()
– набор сокетов. - Для каждого принятого веб-сокета – добавить его в набор
clients.add(socket)
и поставить ему обработчик событияmessage
для приёма сообщений. - Когда сообщение получено: перебрать клиентов
clients
и отправить его всем. - Когда подключение закрыто:
clients.delete(socket)
.
const ws = new require('ws'); const wss = new ws.Server({noServer: true}); const clients = new Set(); http.createServer((req, res) => { // в реальном проекте здесь может также быть код для обработки отличных от websoсket-запросов // здесь мы работаем с каждым запросом как с веб-сокетом wss.handleUpgrade(req, req.socket, Buffer.alloc(0), onSocketConnect); }); function onSocketConnect(ws) { clients.add(ws); ws.on('message', function(message) { message = message.slice(0, 50); // максимальный размер сообщения 50 for(let client of clients) { client.send(message); } }); ws.on('close', function() { clients.delete(ws); }); }
Вот рабочий пример:
<!DOCTYPE html> <html><head> <meta http-equiv="content-type" content="text/html; charset=UTF-8"></head> <body><form name="publish"> <input type="text" name="message" maxlength="50"> <input type="submit" value="Send"> </form> <div id="messages"></div> <script> let url = location.host == 'localhost' ? 'ws://localhost:8080/ws' : location.host == 'javascript.local' ? `ws://javascript.local/article/websocket/chat/ws` : // интеграция для разработки с локальным сайтом `wss://javascript.info/article/websocket/chat/ws`; // боевая интеграция с javascript.info let socket = new WebSocket(url); // отправка сообщения из формы document.forms.publish.onsubmit = function() { let outgoingMessage = this.message.value; socket.send(outgoingMessage); return false; }; // прослушка входящих сообщений socket.onmessage = function(event) { let incomingMessage = event.data; showMessage(incomingMessage); }; socket.onclose = event => console.log(`Closed ${event.code}`); // отображение информации в div#messages function showMessage(message) { let messageElem = document.createElement('div'); messageElem.textContent = message; document.getElementById('messages').prepend(messageElem); } </script> </body></html>
Вы также можете скопировать код, сохранить в html-файле и запустить локально. Только не забудьте установить Node.js и выполнить команду npm install ws
до запуска.
Примеры веб-сокетов в сети
Если вы заинтересованы опробовать веб-сокеты, в сети есть много сайтов, на которых можно запустить свою разработку.
Для начала попробуйте сайт websocket.org, который предоставляет простейший сервер веб-сокетов: веб-страница отправляет ему сообщение, а он возвращает это же сообщение веб-странице:
Хотя этот сервер веб-сокетов и не представляет ничего особенного, на нем вы можете испробовать все возможности объекта WebSocket. Более того, к этому серверу можно подключиться со страницы, расположенной как на промышленном веб-сервере, так и на тестовом веб-сервере на вашем компьютере, или даже со страницы, просто запускаемой с жесткого диска:
var socket = new WebSocket("ws://echo.websocket.org"); socket.onopen = connectionOpen; socket.onmessage = messageReceived; function connectionOpen() { socket.send("UserName:alexivanov@gmail.com"); } function messageReceived(e) { var messageLog = document.getElementById("messageLog"); messageLog.innerHTML += "<br>" + "Ответ сервера: " + e.data; }
Существуют и серверы веб-сокетов, предоставляющие другие возможности, включая следующие:
- Простой чат. Чат, в котором все разговаривают со всеми. Отправляемые сообщения получают все участники чата.
- Многопользовательский альбом. Эта страница объединяет веб-сокеты с HTML5 Canvas. То, что вы рисуете на своем холсте, отображается на холсте других участников, и наоборот. Простой концепт, но очень впечатляющий на практике.
Серверы веб-сокетов
Чтобы иметь возможность испытать свой проект по веб-сокетам, вам нужен сервер веб-сокетов, с которым ваша страница могла бы общаться. Тестовый сервер можно найти во многих местах. Ниже даются ссылки на серверы веб-сокетов для определенных серверных языков:
- PHP
- Этот простой и слегка сыроватый проект будет хорошей отправной точкой для создания сервера веб-сокетов на PHP.
- Ruby
- Существует несколько образцов сервера веб-сокетов на Ruby, но этот, применяющий модель "Event—Machine", пользуется особенной популярностью.
- Python
- Сервер веб-сокетов в виде модуля расширения для Apache на языке Python.
- .NET
- Назвать простым этот всеохватывающий проект нельзя. Но он содержит завершенный сервер веб-сокетов на языке C# на основе платформы .NET корпорации Microsoft.
- Java
- По своему масштабу этот проект похож на проект .NET, но чисто на языке Java.
- node.JS
- В зависимости от того, кого вы спросите, система node.JS для разработки веб-приложений на JavaScript — это либо одна из наиболее перспективных платформ, либо просто разросшийся тестовый инструмент.
- Kaazing
- В отличие от других пунктов этого списка, Kaazing не предоставляет кода для сервера веб-сокетов. Это развитый сервер веб-сокетов, который можно лицензировать для своего веб-сайта. Для разработчиков, которые предпочитают делать все своими руками, он не будет представлять интереса. Но удобно использовать его на менее амбициозных веб-сайтах, особенно принимая во внимание то обстоятельство, что он содержит встроенную поддержку резервных решений в своих клиентских библиотеках (которые сначала пытаются применить стандарт веб-сокетов HTML5, затем Flash, а потом опрос посредством сценариев на JavaScript).
Итого
WebSocket – это современный способ иметь постоянное соединение между браузером и сервером.
- Нет ограничений, связанных с кросс-доменными запросами.
- Имеют хорошую поддержку браузерами.
- Могут отправлять/получать как строки, так и бинарные данные.
API прост.
Методы:
socket.send(data)
,socket.close([code], [reason])
.
События:
open
,message
,error
,close
.
WebSocket сам по себе не содержит такие функции, как переподключение при обрыве соединения, аутентификацию пользователей и другие механизмы высокого уровня. Для этого есть клиентские и серверные библиотеки, а также можно реализовать это вручную.
Иногда, чтобы добавить WebSocket к уже существующему проекту,
WebSocket-сервер запускают параллельно с основным сервером. Они
совместно используют одну базу данных. Запросы к WebSocket отправляются
на wss://ws.site.com
– поддомен, который ведёт к WebSocket-серверу, в то время как https://site.com
ведёт на основной HTTP-сервер.
Конечно, возможны и другие пути интеграции.