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:

…А если мы хотим отправить что-нибудь, то вызов 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 секунд и закрывает соединение.

Так вы увидите события openmessageclose.

Открытие веб-сокета

Когда 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

Запрос 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, описывающие расширения и подпротоколы.

Например:

Сервер должен ответить перечнем протоколов и расширений, которые он может использовать.

Например, запрос:

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]);

Затем противоположная сторона в обработчике события close получит и код code и причину reason, например:

// закрывающая сторона:
socket.close(1000, "работа закончена");

// другая сторона:
socket.onclose = event => {
  // event.code === 1000
  // event.reason === "работа закончена"
  // event.wasClean === true (закрыто чисто)
};

code – это не любое число, а специальный код закрытия WebSocket.

Наиболее распространённые значения:

Есть и другие коды:

Полный список находится в RFC6455, §7.4.1.

Коды WebSocket чем-то похожи на коды HTTP, но они разные. В частности, любые коды меньше 1000 зарезервированы. Если мы попытаемся установить такой код, то получим ошибку.

// в случае, если соединение сброшено
socket.onclose = event => {
  // event.code === 1006
  // event.reason === ""
  // event.wasClean === false (нет закрывающего кадра)
};

Состояние соединения

Чтобы получить состояние соединения, существует дополнительное свойство socket.readyState со значениями:

Пример чата

Давайте рассмотрим пример чата с использованием 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 вещи:

  1. Открыть соединение.
  2. При отправке формы пользователем – вызвать socket.send(message) для сообщения.
  3. При получении входящего сообщения – добавить его в 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.

Серверный алгоритм действий будет таким:

  1. Создать clients = new Set() – набор сокетов.
  2. Для каждого принятого веб-сокета – добавить его в набор clients.add(socket) и поставить ему обработчик события message для приёма сообщений.
  3. Когда сообщение получено: перебрать клиентов clients и отправить его всем.
  4. Когда подключение закрыто: 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;
}

Существуют и серверы веб-сокетов, предоставляющие другие возможности, включая следующие:

Серверы веб-сокетов

Чтобы иметь возможность испытать свой проект по веб-сокетам, вам нужен сервер веб-сокетов, с которым ваша страница могла бы общаться. Тестовый сервер можно найти во многих местах. Ниже даются ссылки на серверы веб-сокетов для определенных серверных языков:

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 прост.

Методы:

События:

WebSocket сам по себе не содержит такие функции, как переподключение при обрыве соединения, аутентификацию пользователей и другие механизмы высокого уровня. Для этого есть клиентские и серверные библиотеки, а также можно реализовать это вручную.

Иногда, чтобы добавить WebSocket к уже существующему проекту, WebSocket-сервер запускают параллельно с основным сервером. Они совместно используют одну базу данных. Запросы к WebSocket отправляются на wss://ws.site.com – поддомен, который ведёт к WebSocket-серверу, в то время как https://site.com ведёт на основной HTTP-сервер.

Конечно, возможны и другие пути интеграции.

Справочник JavaScript
×
Справочник JavaScript