---
title: "Клиент: WebSocket в браузере"
description: "Websocket клиент js: new WebSocket, события onopen и onmessage, readyState и тест websocket в браузере. Соберём пульт и проверим эхо-сервер живьём."
course: "WebSocket в Go: real-time чаты и уведомления"
url: "https://golangmaster.ru/courses/websocket/lessons/05-js-klient/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

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

*Курс «WebSocket в Go: real-time чаты и уведомления», урок 5 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/websocket/lessons/05-js-klient/*

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

Сервер из четвёртого урока работает и ждёт соединений, но до сих пор разговаривал с ним только 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, об этом ниже.

> **send до открытия — исключение, а не очередь.** Вызов socket.send до завершения рукопожатия выбрасывает InvalidStateError: соединение в состоянии CONNECTING, отправлять нечего. Браузер не ставит такие вызовы в очередь — в отличие от многих клиентских библиотек, которые буферизуют молча. Если код теоретически может сработать раньше открытия, шлите из onopen или проверяйте readyState — второе хуже, потому что между проверкой и отправкой нет гарантий.

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

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

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

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

- Кадр уйдёт сразу после открытия соединения
- Браузер поставит кадр в очередь и утихомирит разработчика
- ✅ Исключение InvalidStateError: соединение в состоянии CONNECTING
- Соединение закроется с кодом 1000

Браузерный send не буферизует: в состоянии CONNECTING он выбрасывает InvalidStateError. Очередь и повторы — забота вашего кода, и первый шаг в этом направлении — слать из 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 при недоступности не ждёт, а падает — очередь и повторы пишет ваш код.

> **wss и смешанный контент.** Страница, открытая по https, не подключится к ws://: браузеры блокируют смешанный контент и требуют wss:// с валидным сертификатом. На localhost исключение обычно разрешено, поэтому уроки курса живут на ws://. В проде это значит: TLS-терминатор обслуживает и WebSocket, сертификат покрывает домен, а клиенты с самого первого деплоя ходят по wss — перевод на TLS на ходу неприятен.

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

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

- Событие onmessage с текстом ping
- ✅ Ничего: браузер сам ответит pong, события в JS не будет
- Событие onerror: ping считается ошибкой протокола
- Только onclose: ping закрывает соединение

Ping и pong — управляющие кадры уровня протокола: браузер отвечает на них сам и в JavaScript ничего не доставляет. Сервер проверяет живость по pong-обработчику и дедлайну чтения — вся механика heartbeat разобрана в шестом уроке, клиентского кода она не требует.

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

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

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

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

**Что выведет программа?**

- got: ноль, затем got: раз
- ✅ Исключение InvalidStateError; позже got: раз
- got: ноль: браузер подождёт открытия
- Ничего: соединение не откроется

Последний send выполнится в состоянии CONNECTING и выбросит InvalidStateError — очередь у браузера нет. Обработчики уже назначены, поэтому рукопожатие завершится, onopen отправит раз, и сервер вернёт его эхом: got: раз. Порядок важен: назначайте обработчики до первой попытки отправки.

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

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

```go
const socket = new ___("ws://localhost:8080/ws");

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

socket.onopen = () => socket.___("привет");
```

Подсказка: Класс называется как протокол; входящие кадры доставляет onmessage, а метод send уходит в открытое соединение.

Правильные ответы в пропусках: `WebSocket`, `onmessage`, `send`

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

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

Правильный порядок строк:

1. `const socket = new WebSocket("ws://localhost:8080/ws");`
2. `console.log("состояние сразу:", socket.readyState); // 0`
3. `socket.onopen = () => {`
4. `  console.log("открыто:", socket.readyState); // 1`
5. `  socket.send("привет из клиента");`
6. `};`

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

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

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

- **new WebSocket(url)** — Конструктор браузерного клиента: сам проводит рукопожатие по ws:// или wss://. Заголовки передать нельзя — только адрес и опциональные субпротоколы.
- **event.data** — Полезная нагрузка входящего кадра: для текстового — строка, для бинарного — Blob, либо ArrayBuffer при binaryType = arraybuffer.
- **readyState** — Состояние соединения: 0 CONNECTING, 1 OPEN, 2 CLOSING, 3 CLOSED. send валиден только в OPEN, иначе InvalidStateError.
- **ws и wss** — Схемы адреса: ws — открытый канал, wss — тот же протокол поверх TLS. Страница https к ws:// не подключится — блокировка смешанного контента.
- **Что делает браузер сам** — Рукопожатие, ответы pong на серверные ping, сборку фрагментированных сообщений, декодирование UTF-8.
- **Что остаётся клиенту** — Reconnect после обрыва, собственная очередь исходящих, реакция на коды закрытия — браузер ничего из этого не делает за вас.

Теперь обе половины real-time видят друг друга: сервер из [четвёртого урока](/courses/websocket/lessons/04-pervyj-server/) эхо-ит пульт, и весь конвейер — рукопожатие, кадры, закрытие — проверен живьём. Честная оговорка про запуск: пульт работает только против локального сервера — поднимайте go run из четвёртого урока, песочница сайта TCP не поднимет; а если рукопожатие отбито 403, вернитесь к CheckOrigin из [третьего урока](/courses/websocket/lessons/03-ustanovka-gorilla/). Следующая остановка — [ping-pong и heartbeat](/courses/websocket/lessons/06-ping-pong/): пульт это ещё не покажет, но именно там соединения научатся не засыпать после Wi-Fi. А когда клиент начнёт умирать вместе с сетью, его вернёт [reconnect с backoff](/courses/websocket/lessons/17-reconnect/) — схожие идеи встретятся и в уроке про reconnect. Карта всех двадцати пяти уроков лежит на [странице курса](/courses/websocket/) — оттуда удобно возвращаться к любому материалу.

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

- Клиент начинается с 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 из шестого урока разница будет видна в логах.
