WebSocket держит соединение открытым, поэтому сервер может отправить новое сообщение браузеру сразу, без очередного HTTP-запроса и перезагрузки. Мы поднимем сервер на Node.js, подключим две вкладки и увидим, как один JSON-пакет появляется у всех клиентов. Заодно добавим индикатор соединения, проверку входа и безопасный вывод текста.
Обычный fetch похож на вопрос и ответ: браузер обращается к серверу, получает данные и завершает запрос. Для чата нужно другое поведение. Пользователь не знает, когда собеседник напишет, поэтому серверу нужен открытый канал в обе стороны. WebSocket начинается с HTTP upgrade, затем соединение остаётся активным и передаёт сообщения небольшими фреймами.
Каждая вкладка открывает ws://localhost:8080 и получает собственный socket.
Сервер проверяет JSON и отправляет нормализованное сообщение всем открытым клиентам.
Браузер создаёт li и записывает текст через textContent, не вставляя чужой HTML.
Чем WebSocket отличается от обычного fetch
Сервер запускает WebSocketServer на порту 8080. При connection он отправляет системное сообщение только новому клиенту. Когда приходит message, сервер ограничивает размер, разбирает JSON, проверяет name и text, добавляет время и рассылает объект всем sockets со статусом OPEN.
| Этап | Что происходит | Признак результата |
|---|---|---|
| Handshake | Браузер просит upgrade до WebSocket | Событие open |
| Send | Клиент сериализует имя и текст в JSON | Сервер получил bytes |
| Broadcast | Сервер валидирует и проходит по clients | Каждый OPEN socket получил пакет |
| Render | Браузер разбирает JSON и создаёт безопасный li | Две вкладки видят сообщение |
У проекта есть один главный маршрут: получить входные данные, проверить их, выполнить действие и показать результат. Если каждый этап можно проверить отдельно, ошибка перестаёт быть загадкой.

От входных данных до видимого результата. Открытый канал работает в обе стороны
Пишем сервер и браузерный клиент
Создайте каталог, выполните npm init -y и npm install ws. Сохраните server.mjs и запустите node server.mjs. HTML отдавайте отдельным локальным сервером, например npx serve .. Откройте адрес в двух вкладках. В браузере используется встроенный WebSocket; пакет ws импортируется только в server.mjs.
import WebSocket, { WebSocketServer } from "ws";
const wss = new WebSocketServer({ port: 8080 });
function broadcast(payload) {
const json = JSON.stringify(payload);
for (const client of wss.clients) {
if (client.readyState === WebSocket.OPEN) client.send(json);
}
}
wss.on("connection", (socket) => {
socket.send(JSON.stringify({ type: "system", text: "Соединение установлено" }));
socket.on("message", (raw) => {
try {
if (raw.length > 4_000) throw new Error("Слишком длинное сообщение");
const input = JSON.parse(raw.toString());
const name = String(input.name ?? "Гость").trim().slice(0, 30);
const text = String(input.text ?? "").trim().slice(0, 500);
if (!text) return;
broadcast({ type: "chat", name: name || "Гость", text, at: Date.now() });
} catch {
socket.send(JSON.stringify({ type: "error", text: "Неверный формат сообщения" }));
}
});
});
console.log("WebSocket: ws://localhost:8080");- После загрузки клиент показывает статус «Онлайн» и системное сообщение.
- Текст из первой вкладки сразу появляется в первой и второй.
- Пустая строка не рассылается, а слишком большой или неверный JSON даёт ошибку.
- После остановки server.mjs обе вкладки получают close и меняют статус на «Отключено».
Сервер не доверяет JSON только потому, что его отправил наш интерфейс. Любой клиент может подключиться вручную, поэтому длина и типы проверяются до broadcast.

Что отличает устойчивый проект от случайного успеха. Каждый пакет проверяется до рассылки
Что происходит с одним сообщением
После события open обе стороны могут отправлять данные в любой момент. socket.send не является HTTP POST и не создаёт новый запрос. Сервер хранит набор активных wss.clients. Broadcast проходит по нему и отправляет пакет только соединениям WebSocket.OPEN. Порядок сообщений внутри одного соединения сохраняется, но глобальная история нигде не хранится.
| Часть | Ответственность | Что проверить |
|---|---|---|
| Browser WebSocket | Открывает канал и отправляет строки | Состояние open перед send |
| ws server | Принимает соединения и message events | Порт слушается один раз |
| JSON contract | Описывает name, text, type и at | Неверные данные отклонены |
| Broadcast loop | Рассылает пакет открытым clients | Закрытый socket пропущен |
| DOM renderer | Создаёт безопасные элементы списка | Используется textContent |
У соединения есть состояния CONNECTING, OPEN, CLOSING и CLOSED. Нельзя отправлять форму до OPEN, поэтому кнопка сначала disabled. Для нестабильной сети добавьте переподключение с растущей задержкой, но не запускайте бесконечный цикл каждую миллисекунду. Ping/pong помогает обнаруживать мёртвые соединения на сервере.
Проверяем две вкладки и разрыв соединения
Откройте две вкладки с разными именами и отправьте по два сообщения. Затем введите строку Привет: она должна отображаться с угловыми скобками, а не жирным HTML. Остановите сервер и проверьте статус. Запустите снова: в базовом варианте вкладка сама не подключится, поэтому добавьте кнопку «Переподключиться» или функцию reconnect с ограниченным числом попыток.
- Откройте чат в двух вкладках и подтвердите двустороннюю рассылку.
- Отправьте HTML-подобный текст и проверьте безопасный вывод.
- Заблокируйте кнопку до события open.
- Остановите сервер и покажите состояние close.
- Отправьте неверный JSON из консоли и получите error packet.
- Добавьте ручное переподключение без создания двух sockets одновременно.
- Две вкладки видят одинаковые сообщения.
- Неверный вход не роняет сервер.
- Разрыв отображается в интерфейсе.
- Чужой текст не выполняется как HTML.
Чат становится понятным, когда вы видите жизненный цикл соединения, а не только летящие сообщения. Open разрешает отправку, message приносит данные, close требует реакции, а сервер проверяет каждый пакет.

Четыре проверки перед следующим шагом. Close и неверный JSON являются обычными сценариями
Готовим чат к реальному использованию
Для следующей версии добавьте комнаты и имя пользователя, но не доверяйте им из JSON. Сессию нужно проверять на сервере. Историю сообщений храните отдельно и выдавайте с пагинацией. Добавьте ping/pong, ограничение частоты и логирование технических событий без полного текста приватных сообщений.
Как изучить тему в Кодике
В Кодике сначала закрепите события, JSON и работу с DOM. Затем сделайте эхо через один WebSocket. Только после этого добавляйте broadcast и две вкладки. Такой порядок отделяет сеть от интерфейса.
| Шаг | Что изучить в Кодике | Мини-проверка |
|---|---|---|
| 1 | События JavaScript | Обработать submit без перезагрузки |
| 2 | JSON stringify и parse | Передать name и text одной строкой |
| 3 | DOM и textContent | Безопасно вывести недоверенный текст |
| 4 | Состояния WebSocket | Обработать open, message и close |
| 5 | Серверный broadcast | Разослать пакет двум вкладкам |
Не пытайтесь запомнить весь API. В тренажёре Кодика сначала воспроизведите чистую логику без библиотеки, затем восстановите подключение и обработку ошибок. Финальный тест: объяснить проект по памяти и добавить одну свою функцию.
В браузере уже есть WebSocket. Библиотека ws нужна Node.js серверу.
Проверяйте readyState и включайте форму после события open.
Клиент можно подменить. Проверяйте тип, длину и права на сервере.
Используйте textContent, иначе чужой текст может стать разметкой и скриптом.
Что получится в итоге

Две вкладки держат открытые соединения, сервер проверяет один JSON-пакет и мгновенно рассылает безопасный текст всем подключённым клиентам.
Почему браузеру не нужен пакет ws?
Класс WebSocket встроен в браузер. ws реализует клиент и сервер для Node.js, а здесь используется его серверная часть.
Сохраняет ли сервер историю автоматически?
Нет. Он знает только активные соединения и будущие пакеты. История требует отдельного хранилища.
Зачем проверять readyState?
Отправка допустима только для открытого соединения. Клиент может ещё подключаться или уже закрываться.
Как понять, что проект действительно работает?
Проверьте основной сценарий, неверный ввод, повторный запуск и один граничный случай. Затем объясните путь данных своими словами.
Можно ли начать с готового кода из статьи?
Да. Добейтесь результата, измените одно правило и соберите ключевой файл заново без копирования.
События, DOM и JSON закрепите в курсе JavaScript. Сравните постоянный канал с обычным запросом в статье про Fetch API.
Ошибки отсутствующих элементов повторите по материалу про null, а интерфейс сообщения можно развить после Todo List на JavaScript.