Клавиша / esc

WebSocket

Устанавливает постоянное соединение с сервером для обмена данными в реальном времени.

Время чтения: 10 мин

Кратко

Скопировано

Конструктор WebSocket — это браузерный API для работы с протоколом WebSocket. В отличие от HTTP-запросов, соединение остаётся открытым, и обе стороны могут обмениваться данными в любой момент. Сервер сам отправляет обновления, как только они появляются. Повторные запросы не нужны.

Как пишется

Скопировано
        
          
          new WebSocket(url, [protocols]?)
          new WebSocket(url, [protocols]?)

        
        
          
        
      
  • url: строка с адресом сервера, должна начинаться с ws:// или wss://;
  • protocols — строка или массив строк с названиями предпочитаемых подпротоколов WebSocket, необязательный параметр. Какой подпротокол выбрал сервер, можно узнать из свойства protocol.

Протоколы ws и wss

Скопировано

WebSocket использует два протокола:

  • ws://: незащищённое соединение (как http://);
  • wss://: защищённое соединение с шифрованием (как https://).

Браузер автоматически преобразует http:// в ws:// и https:// в wss://, если передать их вместо WebSocket-протоколов:

        
          
          const socket = new WebSocket("https://example.com/ws")console.log(socket.url) // wss://example.com/ws/
          const socket = new WebSocket("https://example.com/ws")

console.log(socket.url) // wss://example.com/ws/

        
        
          
        
      

Итоговый URL хранится в свойстве url.

Методы

Скопировано

У WebSocket всего два метода: send() и close(). Оба вызываются синхронно. Браузер сразу выполняет действие и возвращает управление, но отправка данных и закрытие соединения на уровне сети происходят асинхронно.

send()

Скопировано

Отправляет данные на сервер. Принимает один аргумент — данные, которые нужно отправить:

        
          
          socket.send('Привет, сервер!')
          socket.send('Привет, сервер!')

        
        
          
        
      

Можно отправлять разные типы данных:

        
          
          const socket = new WebSocket('wss://example.com/ws')const buffer = new ArrayBuffer(4)const view = new Uint8Array(buffer)view.set([1, 2, 3, 4])const blob = new Blob([buffer], { type: 'application/octet-stream' })if (socket.readyState === WebSocket.OPEN) {  // Отправляем строку  socket.send('Сообщение')  // Отправляем JSON  socket.send(JSON.stringify({ type: 'message', text: 'Привет' }))  // Отправляем бинарные данные  socket.send(buffer)  // Отправляем Blob  socket.send(blob)}
          const socket = new WebSocket('wss://example.com/ws')

const buffer = new ArrayBuffer(4)
const view = new Uint8Array(buffer)
view.set([1, 2, 3, 4])

const blob = new Blob([buffer], { type: 'application/octet-stream' })

if (socket.readyState === WebSocket.OPEN) {
  // Отправляем строку
  socket.send('Сообщение')

  // Отправляем JSON
  socket.send(JSON.stringify({ type: 'message', text: 'Привет' }))

  // Отправляем бинарные данные
  socket.send(buffer)

  // Отправляем Blob
  socket.send(blob)
}

        
        
          
        
      

Ещё у метода есть особенность: он складывает данные во внутреннюю очередь. Если отправляете много сообщений подряд, они не обязательно сразу улетят на сервер в том же порядке. Проверить размер очереди можно через свойство bufferedAmount:

close()

Скопировано

Закрывает соединение с сервером. Принимает два необязательных аргумента:

        
          
          socket.close(code?, reason?)
          socket.close(code?, reason?)

        
        
          
        
      
  • code: числовой код закрытия (например, 1000 для нормального закрытия), по умолчанию 1005;
  • reason: строка с причиной закрытия, не длиннее 123 байт в UTF-8.
        
          
          // Простое закрытиеsocket.close()// Закрытие с кодом и причинойsocket.close(1000, 'Работа завершена')// Пользовательский код (допустимы значения от 3000 до 4999)socket.close(4001, 'Неверный формат сообщения')
          // Простое закрытие
socket.close()

// Закрытие с кодом и причиной
socket.close(1000, 'Работа завершена')

// Пользовательский код (допустимы значения от 3000 до 4999)
socket.close(4001, 'Неверный формат сообщения')

        
        
          
        
      

Как и send(), метод close() вызывается синхронно. Сразу после вызова readyState становится WebSocket.CLOSING: браузер начинает закрывающее рукопожатие с сервером. На этом этапе соединение ещё не закрыто полностью, поэтому readyState не равен WebSocket.CLOSED. Когда обмен завершится, состояние сменится на WebSocket.CLOSED и сработает событие close.

        
          
          socket.close()console.log(socket.readyState) // WebSocket.CLOSING (2), а не CLOSED (3)
          socket.close()
console.log(socket.readyState) // WebSocket.CLOSING (2), а не CLOSED (3)

        
        
          
        
      

Коды от 1000 до 1015 зарезервированы спецификацией:

  • 1000: нормальное закрытие;
  • 1001: сервер или клиент уходит (например, закрыли вкладку);
  • 1002: ошибка протокола;
  • 1003: получены неподдерживаемые данные;
  • 1004: зарезервирован, не используется;
  • 1005: код не был передан, нельзя указывать в close();
  • 1006: соединение оборвалось без фрейма закрытия, нельзя указывать в close();
  • 1007: неверные данные во фрейме;
  • 1008: нарушение политики;
  • 1009: сообщение слишком большое;
  • 1010: клиент ожидал расширение, которое сервер не поддерживает;
  • 1011: внутренняя ошибка сервера;
  • 1012: сервер перезапускается;
  • 1013: временная перегрузка, попробуйте подключиться позже;
  • 1014: сервер выступал прокси и получил неверный ответ от upstream-сервера (аналог HTTP 502);
  • 1015: ошибка TLS-рукопожатия, нельзя указывать в close().

Полный список кодов закрытия смотрите в реестре IANA.

Слушатели событий

Скопировано

WebSocket наследует EventTarget, поэтому на события можно подписаться двумя способами:

  • через on-свойства: onopen, onmessage, onerror, onclose;
  • через addEventListener().

У WebSocket четыре события:

  • open: соединение установлено, readyState равен WebSocket.OPEN;
  • message: пришли данные от сервера. Текст приходит строкой или бинарными данными в формате, заданном binaryType;
  • error: ошибка соединения;
  • close: соединение закрыто, код и причина доступны в event.code и event.reason.

on-свойства

Скопировано

Самый короткий способ подписаться на событие — записать функцию в соответствующее свойство:

        
          
          const socket = new WebSocket('wss://example.com/ws')socket.onopen = () => {  console.log('Соединение открыто')}socket.onmessage = (event) => {  console.log('Сообщение:', event.data)}socket.onerror = () => {  console.log('Ошибка соединения')}socket.onclose = (event) => {  console.log(`Закрыто: ${event.code}, ${event.reason}`)}
          const socket = new WebSocket('wss://example.com/ws')

socket.onopen = () => {
  console.log('Соединение открыто')
}

socket.onmessage = (event) => {
  console.log('Сообщение:', event.data)
}

socket.onerror = () => {
  console.log('Ошибка соединения')
}

socket.onclose = (event) => {
  console.log(`Закрыто: ${event.code}, ${event.reason}`)
}

        
        
          
        
      

Плюсы:

  • короткая запись, удобно для простых случаев;
  • легко отписаться: socket.onmessage = null.

Минусы:

  • на каждое событие можно повесить только одну функцию, новое присваивание заменяет предыдущий обработчик;
  • нельзя добавить второй обработчик на то же событие;
  • нет опций вроде { once: true } или { signal }.

addEventListener()

Скопировано

WebSocket поддерживает подписку через addEventListener():

        
          
          const socket = new WebSocket('wss://example.com/ws')function onMessage(event) {  console.log('Сообщение:', event.data)}socket.addEventListener('open', () => {  console.log('Соединение открыто')})socket.addEventListener('message', onMessage)socket.addEventListener('close', () => {  socket.removeEventListener('message', onMessage)})
          const socket = new WebSocket('wss://example.com/ws')

function onMessage(event) {
  console.log('Сообщение:', event.data)
}

socket.addEventListener('open', () => {
  console.log('Соединение открыто')
})

socket.addEventListener('message', onMessage)

socket.addEventListener('close', () => {
  socket.removeEventListener('message', onMessage)
})

        
        
          
        
      

Плюсы:

  • несколько обработчиков на одно событие;
  • можно снять конкретный обработчик через removeEventListener();
  • поддерживаются опции: { once: true }, { signal } из AbortController.

Минусы:

  • запись длиннее;
  • чтобы отписаться, нужно сохранить ту же ссылку на функцию, что передавали в addEventListener().

Память после закрытия

Скопировано

close() закрывает соединение, но не удаляет объект WebSocket из памяти. Пока на него указывает переменная, объект живёт вместе со всеми свойствами и обработчиками.

Обработчики событий усугубляют проблему: они часто замыкают DOM-элементы и другие объекты. Пока жива ссылка на сокет, эта цепочка остаётся в памяти.

Это особенно важно для глобальных переменных и свойств объектов, которые живут долго. Если socket объявлен внутри функции и никуда не «утекает» через замыкание, после выхода из функции GC удалит объект сам, даже без socket = null.

Если ссылка на сокет хранится долго, после close() её лучше обнулить:

        
          
          let socket = new WebSocket('wss://example.com/ws')// ...socket.close()socket = null
          let socket = new WebSocket('wss://example.com/ws')
// ...
socket.close()
socket = null

        
        
          
        
      

Обнулять onopen, onmessage и остальные on-свойства по отдельности не нужно: вместе с объектом из памяти исчезнут и его обработчики.

removeEventListener() понадобится только если объект сокета остаётся в памяти, а конкретный обработчик нужно снять.

Свойства

Скопировано

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

readyState

Скопировано

Текущее состояние соединения. Возвращает число от 0 до 3. Для сравнения используйте константы на конструкторе WebSocket:

  • WebSocket.CONNECTING (0): идёт подключение;
  • WebSocket.OPEN (1): соединение открыто, можно вызывать send();
  • WebSocket.CLOSING (2): идёт закрытие после close();
  • WebSocket.CLOSED (3): соединение закрыто.
        
          
          const socket = new WebSocket('wss://example.com/ws')console.log(socket.readyState) // WebSocket.CONNECTINGsocket.onopen = () => {  console.log(socket.readyState) // WebSocket.OPEN}
          const socket = new WebSocket('wss://example.com/ws')

console.log(socket.readyState) // WebSocket.CONNECTING

socket.onopen = () => {
  console.log(socket.readyState) // WebSocket.OPEN
}

        
        
          
        
      

url

Скопировано

Строка с итоговым URL соединения после нормализации браузером. Может отличаться от аргумента конструктора. Подробнее в разделе «Протоколы ws и wss».

protocol

Скопировано

Подпротокол, который выбрал сервер при рукопожатии. Пустая строка, если подпротокол не согласовывали или сервер ничего не выбрал.

Значение доступно после события open:

        
          
          const socket = new WebSocket('wss://example.com/ws', ['soap', 'wamp'])socket.onopen = () => {  console.log(socket.protocol) // 'soap' или 'wamp'}
          const socket = new WebSocket('wss://example.com/ws', ['soap', 'wamp'])

socket.onopen = () => {
  console.log(socket.protocol) // 'soap' или 'wamp'
}

        
        
          
        
      

Список допустимых имён подпротоколов смотрите в реестре IANA.

extensions

Скопировано

Строка с расширениями протокола, которые выбрал сервер (например, сжатие permessage-deflate). Если расширений нет, вернётся пустая строка. Полный список зарегистрированных расширений смотрите в реестре IANA. Значение доступно после события open:

        
          
          socket.onopen = () => {  console.log(socket.extensions) // например, 'permessage-deflate'}
          socket.onopen = () => {
  console.log(socket.extensions) // например, 'permessage-deflate'
}

        
        
          
        
      

binaryType

Скопировано

Определяет, в каком виде бинарные сообщения попадут в event.data у события message. На текстовые сообщения не влияет: они всегда приходят как строки.

Допустимые значения:

  • 'blob' (по умолчанию): бинарные данные приходят объектом Blob;
  • 'arraybuffer': бинарные данные приходят ArrayBuffer.

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

        
          
          const socket = new WebSocket('wss://example.com/ws')socket.binaryType = 'arraybuffer'socket.onmessage = (event) => {  if (event.data instanceof ArrayBuffer) {    const view = new Uint8Array(event.data)    console.log(view)  } else {    console.log(event.data) // строка  }}
          const socket = new WebSocket('wss://example.com/ws')

socket.binaryType = 'arraybuffer'

socket.onmessage = (event) => {
  if (event.data instanceof ArrayBuffer) {
    const view = new Uint8Array(event.data)
    console.log(view)
  } else {
    console.log(event.data) // строка
  }
}

        
        
          
        
      

'arraybuffer' удобен, когда нужно сразу работать с байтами через TypedArray. 'blob' подходит, если данные удобнее обрабатывать как файл, например создать object URL или прочитать через FileReader.

bufferedAmount

Скопировано

Количество байт данных, поставленных в очередь через send(), но ещё не отправленных по сети. Удобно следить за перегрузкой при частой отправке сообщений.

        
          
          socket.send('Первое сообщение')socket.send('Второе сообщение')console.log(socket.bufferedAmount) // размер очереди в байтах
          socket.send('Первое сообщение')
socket.send('Второе сообщение')

console.log(socket.bufferedAmount) // размер очереди в байтах

        
        
          
        
      

После закрытия соединения значение может только расти: новые вызовы send() выбросят ошибку, но уже поставленные в очередь данные всё ещё учитываются. См. также close() и readyState.

Заголовки рукопожатия

Скопировано

Подключение WebSocket начинается как обычный HTTP-запрос. Браузер просит сервер «переключить протокол». Если всё прошло успешно, дальше общение идёт уже по WebSocket, а не по HTTP.

Все заголовки браузер формирует сам при вызове new WebSocket(). Из JavaScript их нельзя задать или изменить, в отличие от fetch().

Запрос клиента

Скопировано

Пример запроса на установку соединения:

        
          
          GET /ws HTTP/1.1Host: example.comUpgrade: websocketConnection: UpgradeSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==Sec-WebSocket-Version: 13Sec-WebSocket-Protocol: soap, wampSec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
          GET /ws HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: soap, wamp
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

        
        
          
        
      

Основные заголовки:

  • Upgrade: websocket и Connection: Upgrade: просьба перейти с HTTP на WebSocket;
  • Sec-WebSocket-Key: случайное значение для проверки, что сервер действительно поддерживает WebSocket;
  • Sec-WebSocket-Version: 13: версия протокола, в браузерах всегда 13;
  • Sec-WebSocket-Protocol: список подпротоколов из второго аргумента конструктора;
  • Sec-WebSocket-Extensions: расширения, которые предлагает браузер (например, сжатие).

Браузер также отправляет служебные заголовки вроде Origin и cookie для домена, как при обычном HTTP-запросе.

Ответ сервера

Скопировано

Если сервер согласен, он отвечает кодом 101 Switching Protocols:

        
          
          HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=Sec-WebSocket-Protocol: soapSec-WebSocket-Extensions: permessage-deflate
          HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: soap
Sec-WebSocket-Extensions: permessage-deflate

        
        
          
        
      
  • Sec-WebSocket-Accept: ответ на Sec-WebSocket-Key, браузер проверяет его автоматически. Если значение неверное, соединение не откроется;
  • Sec-WebSocket-Protocol: один подпротокол из списка клиента, который выбрал сервер;
  • Sec-WebSocket-Extensions: расширения, которые сервер принял.
Поддержка в браузерах:
  • Chrome 18, поддерживается
  • Edge 12, поддерживается
  • Firefox 11, поддерживается
  • Safari 6, поддерживается
О Baseline