Написание клиентских приложений с помощью веб-сокетов

Веб-сокеты - технология, которая позволяет открыть интерактивную сессию общения между браузером пользователя и сервером. Соединяясь через веб-сокеты, веб-приложения могут осуществлять взаимодействие в реальном времени вместо того, чтобы делать запросы к клиенту о входящих/исходящих изменениях.

Замечание: У нас есть работающий пример чата, части кода из которого используются в статье. Пример будет доступен, когда инфраструктура сайта сможет должным образом поддерживать хостинг примеров с использованием веб-сокетов.

Доступность веб-сокетов

API веб-сокетов доступно в Javascript коде, область видимости которого включает объект DOM Window или любой объект, реализующий WorkerUtils; это означает, что вы можете использовать Web Workers.

Замечание: API веб-сокетов (как и протокол лежащий в его основе) всё ещё проходят этап активной разработки; в настоящее время существует много проблем совместимости с разными браузерами (и даже с разными релизами одного и того же браузера).

Создание объекта WebSocket

Чтобы общаться через протокол веб-сокетов необходимо создать объект WebSocket; при его создании автоматически происходит попытка открыть соединение с сервером.

Конструктор WebSocket принимает один обязательный и один необязательный параметр:

WebSocket WebSocket(
  in DOMString url,
  in optional DOMString protocols
);

WebSocket WebSocket(
  in DOMString url,
  in optional DOMString[] protocols
);
url
URL, с которым происходит соединение; это должен быть URL веб-сокет-сервера.
protocols Необязательный
Может быть одной строкой протокола или массивом таких строк. Эти строки используют для индикации под-протоколов; таким образом, один сервер может реализовывать несколько под-протоколов веб-сокетов (к примеру, вам может потребоваться, чтобы сервер мог обрабатывать разные типы взаимодействий в зависимости от определённого под-протокола). Если вы не укажете строку протокола, то будет передана пустая строка.

В конструкторе могут возникать следующие исключения:

SECURITY_ERR
Порт, к которому проводится подключение, заблокирован.

Ошибки подключения

Если ошибка случается во время попытки подключения, то в объект WebSocket сначала посылается простое событие с именем «error» (таким образом, задействуя обработчик onerror), потом - событие CloseEvent  (таким образом, задействуя обработчик onclose) чтобы обозначить причину закрытия соединения.

Однако, начиная с версии Firefox 11, типичным является получение в консоль от платформы Mozilla расширенного сообщения об ошибке и кода завершения, как то определено в RFC 6455, Section 7.4 посредством CloseEvent.

Примеры

Этот простой пример создаёт новый WebSocket, подключаемый к серверу ws://www.example.com/socketserver. В данном примере в конструктор сокета в качестве дополнительного параметра передаётся пользовательский протокол "protocolOne", хотя эта часть может быть опущена.

var exampleSocket = new WebSocket("ws://www.example.com/socketserver", "protocolOne");

После выполнения функции, exampleSocket.readyState будет иметь значение CONNECTING. readyState изменится на OPEN как только соединение станет готовым к передаче данных.

Если нужно открыть соединение, поддерживающее несколько протоколов, можно передать массив протоколов:

var exampleSocket = new WebSocket("ws://www.example.com/socketserver", ["protocolOne", "protocolTwo"]);

Когда соединение установлено (что соответствует, readyState OPEN), exampleSocket.protocol сообщит, какой протокол выбрал сервер.

В приведенных выше примерах ws заменяет http, аналогично wss заменяет https. Установка соединения через WebSocket зависит от механизма обновления HTTP, таким образом запрос на обновление неявный, когда мы обращаемся к серверу HTTP с помощью ws://www.example.com или wss://www.example.com.

Отправка данных на сервер

Однажды открыв соединение, вы можете передавать данные на сервер. Для осуществления этого, вызовите метод send() объекта WebSocket  для каждого сообщение, которое желаете отправить:

exampleSocket.send("Вот текст, который будет отправлен серверу.");

Вы можете пересылать данные в виде строки, Blob, так и ArrayBuffer.

Замечание: До версии 11, Firefox поддерживал отправку данных только в виде строки.

Так как установка соедиения асинхронна и подвержена сбоям, то нет никакой гарантии, что вызов метода send(), после создания объекта WebSocket, будет завершен успешно. По крайней мере, мы можем быть уверены, что попытка отправить данные будет иметь место только после того, как соединение будет установлено, определив обработчик onopen для выполнения этого действия:

exampleSocket.onopen = function (event) {
  exampleSocket.send("Вот текст, который будет отправлен серверу.");
};

Использование JSON для передачи объектов

Одна удобная вещь которую вы можете сделать, это использовать JSON для пересылки сложных данных на сервер. Например, приложение-чат может взаимодействовать с сервером, используя протокол, реализованный с использованием пакетов данных, инкапсулированных в JSON:

// Отправьте текст всем пользователям через сервер
function sendText() {
  // Создайте объект содержащий данные, необходимые серверу для обрабоки сообщения от клиента чата.
  var msg = {
    type: "message",
    text: document.getElementById("text").value,
    id:   clientID,
    date: Date.now()
  };

  // Отправьте объект в виде JSON строки.
  exampleSocket.send(JSON.stringify(msg));

  // Очистите элемент ввода текста, чтобы получить следующую строку текста от пользователя.
  document.getElementById("text").value = "";
}

Получение сообщений от сервера

WebSockets — это API, управляемый событиями; когда сообщения получены, событие "message" доставлено в функцию onmessage. Чтобы начать прослушивание входящих данных, вы можете сделать что-то вроде этого:

exampleSocket.onmessage = function (event) {
  console.log(event.data);
}

Получение и интерпретация JSON объектов

Давайте рассмотрим клиентское приложение чата, которое впервые упоминалось в разделе Использование JSON для передачи объектов. Есть разные типы пакетов данных, которые может получить клиент, например:

  • Вход в систему
  • Текст сообщения
  • Обновление списка пользователей

Код обрабатывающий эти входящие сообщения, может выглядеть так:

exampleSocket.onmessage = function(event) {
  var f = document.getElementById("chatbox").contentDocument;
  var text = "";
  var msg = JSON.parse(event.data);
  var time = new Date(msg.date);
  var timeStr = time.toLocaleTimeString();

  switch(msg.type) {
    case "id":
      clientID = msg.id;
      setUsername();
      break;
    case "username":
      text = "<b>User <em>" + msg.name + "</em> signed in at " + timeStr + "</b><br>";
      break;
    case "message":
      text = "(" + timeStr + ") <b>" + msg.name + "</b>: " + msg.text + "<br>";
      break;
    case "rejectusername":
      text = "<b>Your username has been set to <em>" + msg.name + "</em> because the name you chose is in use.</b><br>"
      break;
    case "userlist":
      var ul = "";
      for (i=0; i < msg.users.length; i++) {
        ul += msg.users[i] + "<br>";
      }
      document.getElementById("userlistbox").innerHTML = ul;
      break;
  }

  if (text.length) {
    f.write(text);
    document.getElementById("chatbox").contentWindow.scrollByPages(1);
  }
};

Здесь мы используем JSON.parse() чтобы преобразовать JSON строку в объект, затем обработайте его.

Формат текстовых данных

Текст, полученный через WebSocket должен иметь кодировку UTF-8

До Gecko 9.0 (Firefox 9.0 / Thunderbird 9.0 / SeaMonkey 2.6), некоторые не символьные значения в допустимом тексте UTF-8 могут привести к разрыву соединения. Теперь Gecko допускает эти значения.

Закрытие соединения

Когда вы закончили использовать соединение WebSocket, закройте его используя метод close():

exampleSocket.close();

Перед попыткой закрыть соединение может быть полезно проверить атрибут bufferedAmount чтобы определить, не переданы ли еще какие-либо данные по сети.

Безопасность

WebSocket не следует использовать в среде со смешанным содержимым: то есть вы не должны открывать незащищенное соединение WebSocket со страницы, загруженной с использованием HTTPS, или наоборот. Фактически, некоторые браузеры явно запрещают это, например Firefox 8 и выше.