Содержание урока · 9 разделов
- Websocket клиент js: конструктор new WebSocket
- readyState: спрашивайте соединение, прежде чем писать
- Тест websocket в браузере: вкладка Network и страница-пульт
- Бинарные кадры: event.data не всегда строка
- Что браузер делает сам — а что остаётся вам
- Очередь исходящих: минимальный паттерн
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Сервер из четвёртого урока работает и ждёт соединений, но до сих пор разговаривал с ним только 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, к которому мы дойдём после событий.
Минимальный клиент: четыре события
GOconst 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, об этом ниже.
Вызов socket.send до завершения рукопожатия выбрасывает InvalidStateError: соединение в состоянии CONNECTING, отправлять нечего. Браузер не ставит такие вызовы в очередь — в отличие от многих клиентских библиотек, которые буферизуют молча. Если код теоретически может сработать раньше открытия, шлите из onopen или проверяйте readyState — второе хуже, потому что между проверкой и отправкой нет гарантий.
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 при недоступности не ждёт, а падает — очередь и повторы пишет ваш код.
Страница, открытая по https, не подключится к ws://: браузеры блокируют смешанный контент и требуют wss:// с валидным сертификатом. На localhost исключение обычно разрешено, поэтому уроки курса живут на ws://. В проде это значит: TLS-терминатор обслуживает и WebSocket, сертификат покрывает домен, а клиенты с самого первого деплоя ходят по wss — перевод на TLS на ходу неприятен.
Проверка: 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("ноль"); // <- ???Соберите приём сообщений
Заполните три пропуска: конструктор, обработчик входящих и метод отправки.
Соберите клиента от создания до отправки
Расставьте строки в правильном порядке: создание соединения, проверка состояния и первый отправленный кадр.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Очередь исходящих: минимальный паттерн#
Единственная обязанность, которую браузер свалил на ваш код, — буфер исходящих. Минимальный паттерн занимает шесть строк: массив pending, куда код складывает сообщения, пока readyState не равен единице; в onopen — цикл, который выгружает накопленное через socket.send и чистит массив; сама доставка выглядит так: соединение открыто — шлём сразу, иначе — кладём в очередь и молчим. Такой каркас уже переживает короткий обрыв связи: сообщения, набранные во время CONNECTING, уходят сразу после onopen, и пользователь ничего не замечает. Чего он не переживёт — самого обрыва: соединение, умершее с заполненной очередью, потеряет её навсегда, потому что вместе с соединением умирает и канал. Это уже территория reconnect из семнадцатого урока, где очередь переезжает в объект клиента и переигрывается после переподключения. Пока запомните форму: проверить — отправить или отложить, и никогда не глотать исключение от send молча — оно честно сообщает, что кадр потерян.
Карточки: клиент в браузере
Теперь обе половины 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.
Личные заметкиПусто
Вопросы и ответы по уроку
Можно ли передать заголовок Authorization в new WebSocket?
Нет: конструктор принимает только URL и субпротоколы. Аутентификация делается куками сессии — браузер приложит их сам, — токеном в query-строке или одноразовым билетом: первый кадр по соединению. Разбор с примерами — тринадцатый урок курса.
Почему onerror не пишет, в чём была ошибка?
Браузеры намеренно скрывают детали рукопожатия: по ним иногда можно было бы понять состояние сети и пользователя. Работающий путь — вкладка Network с фильтром WS: там видны и статус ответа, и заголовки, и момент закрытия соединения.
Нужно ли вручную закрывать соединение при уходе со страницы?
Желательно: socket.close() проводит вежливый close-рукопожатие с кодом 1000, и сервер увидит осознанный уход, а не обрыв. Без вызова браузер закроет сокет сам при выгрузке, но сервер узнает об этом только по ошибке чтения — с heartbeat из шестого урока разница будет видна в логах.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
- Ping/Pong: соединение, которое не засыпает
- Простой чат: рассылка всем
- Hub-паттерн: register, unregister, broadcast
- ReadPump и WritePump на соединение
- Первый эхо-сервер WebSocket
- gorilla/websocket: установка и Upgrader
- HTTP против WebSocket: модель соединения
- Знакомство: сервер, который звонит первым
- Все курсы по Go
Следующая ступень учебного пути
RabbitMQ в Go: очереди сообщений на amqp091-go
Интерактивный курс RabbitMQ в Go: обменники, очереди, ack, prefetch, dead letter и RPC. 25 уроков amqp091-go с примерами кода и итоговым проектом.
- Подключаться к RabbitMQ из Go и управлять connection и channel
- Объявлять очереди, обменники и привязки с нужными свойствами
- Публиковать и потреблять сообщения с ручными ack
- Настраивать prefetch и справедливое распределение между воркерами