Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 5 из 25 ~18 мин +20 XP

Клиент: WebSocket в браузере

Websocket клиент js — это десяток строк в браузере: конструктор new WebSocket, четыре обработчика событий и готовый пульт для проверки сервера из четвёртого урока. В этом уроке напишем клиента дважды — минимальную версию из консоли и страницу-пульт с полем ввода, разберём readyState, бинарные кадры и честно разделим, что браузер делает сам, а что остаётся вашему коду.

Содержание урока · 9 разделов

Сервер из четвёртого урока работает и ждёт соединений, но до сих пор разговаривал с ним только wscat или консоль — на этом фронтенд обычно и заканчивается, потому что дальше начинается чужая территория. Территория на деле крошечная: браузерный API WebSocket занимает один конструктор и четыре события, а весь документ MDN на него — одна страница. Именно поэтому первым клиентом real-time почти всегда бывает браузер: не нужен ни билд, ни фреймворк, ни установка пакетов — открыли консоль, вставили пять строк и увидели живое соединение. В этом уроке мы используем клиента как измерительный прибор: до интеграции в настоящий фронтенд важно убедиться, что сервер корректно апгрейдит соединения, шлёт кадры и честно закрывается — и делать это удобнее всего с пультом в соседней вкладке. Весь код урока — ванильный JavaScript без зависимостей: то, что вы напишете здесь, переносится в React или Vue без изменений, потому что объект WebSocket везде один и тот же.

Websocket клиент js: конструктор new WebSocket#

Весь клиент начинается одной строкой: new WebSocket(url). В конструктор передаётся адрес со схемой ws:// — тот же сокет поверх TLS зовётся wss://, по аналогии с http и https. Дальше происходит всё, что мы разбирали вручную во втором уроке: браузер сам шлёт GET-запрос с заголовками Upgrade, Sec-WebSocket-Key и Origin, сам проверяет ответ 101 Switching Protocols — и соединение готово. Из того, что браузер берёт на себя, следуют два ограничения, которые регулярно удивляют. Первое: заголовки передать нельзя — конструктор принимает только адрес и опционально список субпротоколов; куки браузер приложит сам, а токен обычно кладут в query-строку — авторизация соединения разобрана в тринадцатом уроке. Второе: адрес обязан быть абсолютным — в отличие от fetch, где сработал бы относительный путь, WebSocket требует целиком ws://localhost:8080/ws. Рукопожатие асинхронно: конструктор возвращает объект мгновенно, а соединение ещё несколько миллисекунд устанавливается — состояние проверяется через readyState, к которому мы дойдём после событий.

Минимальный клиент: четыре события

GO
const socket = new WebSocket("ws://localhost:8080/ws");

socket.onopen = () => {
  console.log("открыто, готовность:", socket.readyState); // 1
  socket.send("привет из консоли");
};

socket.onmessage = (e) => console.log("пришло:", e.data);

socket.onclose = (e) => console.log("закрыто, код:", e.code);

socket.onerror = () => console.log("сбой: деталей не будет");

Обработчики назначают сразу после создания: браузер их запомнит и вызовет, когда придёт время, — поэтому send сидит внутри onopen, а не рядом с конструктором. onmessage получает событие, у которого data — текст кадра готовой строкой; onclose приносит код закрытия, знакомый по четвёртому уроку: 1000 — вежливое прощание, 1006 — обрыв. onerror деталей намеренно не даёт — браузер прячет причины провала рукопожатия, искать их нужно во вкладке Network, об этом ниже.

readyState: спрашивайте соединение, прежде чем писать#

У соединения четыре состояния, и их числовые значения стоит выучить, потому что в консоли и логах они мелькают именно цифрами. CONNECTING — ноль: рукопожатие в процессе, отправлять нельзя. OPEN — единица: и слать, и получать можно. CLOSING — двойка: идёт процедура закрытия. CLOSED — тройка: соединение мертво, только onclose сможет что-то рассказать о прошлом. Свойство socket.readyState читается в любой момент и мгновенно отвечает на вопрос «жив ли канал». Типичный паттерн интерфейса: кнопка отправки заблокирована, пока readyState не равен единице, и разблокируется в onopen. Важная оговорка: проверка состояния — про удобство, а не про гарантию. Между проверкой и send соединение может закрыться — сеть не обязана ждать вашего кода; единственный источник истины — события: onopen включил кнопку, onclose выключил, и гонок такой дизайн не оставляет.

Проверка: send до открытия

Код вызвал socket.send("hi") сразу после конструктора, до срабатывания onopen. Что произойдёт?

Тест websocket в браузере: вкладка Network и страница-пульт#

Для теста websocket в браузере не нужен ни проект, ни сборка. Первая остановка — DevTools: вкладка Network, фильтр WS; клик по соединению открывает ленту кадров в обеих направлениях со стрелками и размерами, а в поле пришедшего JSON его можно развернуть и почитать. Здесь же видно рукопожатие: строка с ответом 101, заголовки Upgrade и Sec-WebSocket-Accept, и здесь же — единственное место, где браузер объяснит, почему рукопожатие провалилось: onerror таких подробностей не даёт. Но лента кадров — прибор пассивный, а для проверки сервера нужен активный пульт: поле ввода, кнопка, список пришедшего. Такой пульт — полсотни строк разметки, и он же становится шаблоном будущей интеграции: всё, что вы на нём поймали — 403 от CheckOrigin, обрыв после минуты тишины, пустые кадры, — это проблемы сервера или прокси, найденные до того, как их унесёт в продакшен фронтенд-команда.

Страница-пульт: поле ввода, кнопка, журнал

GO
<ul id="log"></ul>
<input id="text" placeholder="сообщение" />
<button id="send" disabled>отправить</button>

<script>
const socket = new WebSocket("ws://localhost:8080/ws");
const log = document.getElementById("log");

socket.onopen = () => { document.getElementById("send").disabled = false; };
socket.onmessage = (e) => {
  const item = document.createElement("li");
  item.textContent = "эхо: " + e.data;
  log.appendChild(item);
};
socket.onclose = () => { document.getElementById("send").disabled = true; };

document.getElementById("send").onclick = () => {
  socket.send(document.getElementById("text").value);
};
</script>

Кнопка стартует заблокированной: disabled снимается в onopen и возвращается в onclose — состояние соединения видно пользователю без консоли. Обратите внимание на textContent вместо innerHTML: чужое сообщение с HTML-тегами не должно исполниться в вашей странице, XSS через эхо — классическая дыра первых чатов. Если сервер отвечает 403, это CheckOrigin: страница открыта с чужого Origin, и серверу нужен фильтр из третьего урока.

Бинарные кадры: event.data не всегда строка#

Для текстовых кадров e.data — готовая строка. Для бинарных браузер по умолчанию даёт объект Blob, а если заранее назначить socket.binaryType = "arraybuffer", — ArrayBuffer. Это осознанное решение спецификации: байты нельзя молча превращать в строку, потому что их смысл знает только протокол приложения. Пока протокол текстовый — JSON и команды, — бинарные кадры вам не нужны, и трогать binaryType не стоит: тогда e.data всегда строка, и JSON.parse не встречает сюрпризов. Ошибка возникает у всех одинаково: на пути к JSON.parse попадает Blob, и вместо структуры вы получаете SyntaxError с сообщением о неожиданном токене — по нему не сразу понятно, что виноват тип кадра. Лечится дисциплиной: сервер шлёт JSON только текстом, а серверный ReadJSON, кстати, тип кадра игнорирует — об этом будет отдельный разговор в десятом уроке. Бинарный режим понадобится, когда протокол перерастёт JSON: файлы, сжатые потоки, protobuf — и тогда binaryType назначается один раз при создании клиента.

Что браузер делает сам — а что остаётся вам#

  • Рукопожатие: конструктор шлёт GET с Upgrade, ключом и Origin и сам проверяет ответ 101 — фильтр CheckOrigin на сервере описывал третий урок.
  • Ответы на ping: серверные ping-кадры браузер подтверждает pong автоматически, событий в JS не возникает — heartbeat сервера не требует клиентского кода.
  • Сборка фрагментов: длинное сообщение, разбитое на несколько кадров, приходит в onmessage цельным.
  • Декодирование UTF-8: текстовые кадры вы получаете строками, перекодировок писать не нужно.
  • Вам остаётся reconnect: браузер не переподключается сам — после onclose соединение мертво навсегда, схема возврата разобрана в семнадцатом уроке.
  • Вам остаётся буфер исходящих: send при недоступности не ждёт, а падает — очередь и повторы пишет ваш код.

Проверка: ping на клиенте

Сервер отправил ping-кадр открытому соединению. Что увидит клиентский JavaScript?

Что напечатает консоль

Прочитайте код и выберите, что он выведет:

const s = new WebSocket("ws://localhost:8080/ws");

s.onopen = () => s.send("раз");
s.onmessage = (e) => console.log("got:", e.data);

s.send("ноль"); // <- ???

Соберите приём сообщений

Заполните три пропуска: конструктор, обработчик входящих и метод отправки.

const socket = new ("ws://localhost:8080/ws"); socket. = (e) => { console.log("пришло:", e.data); }; socket.onopen = () => socket.("привет");

Соберите клиента от создания до отправки

Расставьте строки в правильном порядке: создание соединения, проверка состояния и первый отправленный кадр.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Очередь исходящих: минимальный паттерн#

Единственная обязанность, которую браузер свалил на ваш код, — буфер исходящих. Минимальный паттерн занимает шесть строк: массив pending, куда код складывает сообщения, пока readyState не равен единице; в onopen — цикл, который выгружает накопленное через socket.send и чистит массив; сама доставка выглядит так: соединение открыто — шлём сразу, иначе — кладём в очередь и молчим. Такой каркас уже переживает короткий обрыв связи: сообщения, набранные во время CONNECTING, уходят сразу после onopen, и пользователь ничего не замечает. Чего он не переживёт — самого обрыва: соединение, умершее с заполненной очередью, потеряет её навсегда, потому что вместе с соединением умирает и канал. Это уже территория reconnect из семнадцатого урока, где очередь переезжает в объект клиента и переигрывается после переподключения. Пока запомните форму: проверить — отправить или отложить, и никогда не глотать исключение от send молча — оно честно сообщает, что кадр потерян.

Карточки: клиент в браузере

Термин

new WebSocket(url)

нажмите, чтобы перевернуть
Определение

Конструктор браузерного клиента: сам проводит рукопожатие по ws:// или wss://. Заголовки передать нельзя — только адрес и опциональные субпротоколы.

1 / 6

Теперь обе половины real-time видят друг друга: сервер из четвёртого урока эхо-ит пульт, и весь конвейер — рукопожатие, кадры, закрытие — проверен живьём. Честная оговорка про запуск: пульт работает только против локального сервера — поднимайте go run из четвёртого урока, песочница сайта TCP не поднимет; а если рукопожатие отбито 403, вернитесь к CheckOrigin из третьего урока. Следующая остановка — ping-pong и heartbeat: пульт это ещё не покажет, но именно там соединения научатся не засыпать после Wi-Fi. А когда клиент начнёт умирать вместе с сетью, его вернёт reconnect с backoff — схожие идеи встретятся и в уроке про reconnect. Карта всех двадцати пяти уроков лежит на странице курса — оттуда удобно возвращаться к любому материалу.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Клиент начинается с new WebSocket(url): рукопожатие браузер проводит сам, адрес — абсолютный, со схемой ws:// или wss://.
  • Четыре события: onopen, onmessage, onclose, onerror; onerror деталей не даёт — причины провала ищите во вкладке Network.
  • send до onopen выбрасывает InvalidStateError: браузер не буферизует — шлите из onopen или держите свою очередь.
  • readyState: 0 CONNECTING, 1 OPEN, 2 CLOSING, 3 CLOSED; проверка — для UX, гарантии дают только события.
  • Тест websocket в браузере: фильтр WS во вкладке Network показывает рукопожатие и каждый кадр в обе стороны.
  • Бинарные кадры приходят Blob-ом: JSON.parse на Blob падает — JSON держите текстовыми кадрами.
  • Браузер сам отвечает pong на ping и собирает фрагменты; reconnect и очередь исходящих остаются вашему коду.

Домашнее задание

Откройте страницу-пульт из урока рядом с эхо-сервером и проделайте три эксперимента: остановите сервер и посмотрите, какой код придёт в onclose; попробуйте отправить сообщение, пока кнопка заблокирована, через консоль — и разберите исключение; откройте вкладку Network и найдите кадры эха со стрелками в обе стороны. Запишите три наблюдения — они станут вашим чек-листом отладки real-time.

Личные заметкиПусто
Хранятся только в вашем браузере (WebSocket · Клиент: WebSocket в браузере)

Вопросы и ответы по уроку

Можно ли передать заголовок Authorization в new WebSocket?

Нет: конструктор принимает только URL и субпротоколы. Аутентификация делается куками сессии — браузер приложит их сам, — токеном в query-строке или одноразовым билетом: первый кадр по соединению. Разбор с примерами — тринадцатый урок курса.

Почему onerror не пишет, в чём была ошибка?

Браузеры намеренно скрывают детали рукопожатия: по ним иногда можно было бы понять состояние сети и пользователя. Работающий путь — вкладка Network с фильтром WS: там видны и статус ответа, и заголовки, и момент закрытия соединения.

Нужно ли вручную закрывать соединение при уходе со страницы?

Желательно: socket.close() проводит вежливый close-рукопожатие с кодом 1000, и сервер увидит осознанный уход, а не обрыв. Без вызова браузер закроет сокет сам при выгрузке, но сервер узнает об этом только по ошибке чтения — с heartbeat из шестого урока разница будет видна в логах.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

RabbitMQ25 уроков~11 чпрактика25 уроков уже доступны

RabbitMQ в Go: очереди сообщений на amqp091-go

Интерактивный курс RabbitMQ в Go: обменники, очереди, ack, prefetch, dead letter и RPC. 25 уроков amqp091-go с примерами кода и итоговым проектом.

  • Подключаться к RabbitMQ из Go и управлять connection и channel
  • Объявлять очереди, обменники и привязки с нужными свойствами
  • Публиковать и потреблять сообщения с ручными ack
  • Настраивать prefetch и справедливое распределение между воркерами