Урок 02

WebSocket в Node.js (ws)

Библиотека ws

ws — минималистичная и быстрая реализация WebSocket для Node.js.

npm install ws

Что даёт:

  • Сервер и клиент.
  • Чистый WebSocket — без fallback.
  • Минимум накладных.
  • Работает в любом браузере.

Сервер

server.js:

import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', (ws) => {
    console.log('Новый клиент');

    ws.on('message', (data) => {
        console.log('Получено:', data.toString());
    });

    ws.send('Привет от сервера!');
});

console.log('Сервер на ws://localhost:8080');

Что здесь:

  • WebSocketServer — класс сервера.
  • connection — новое соединение.
  • ws — объект конкретного клиента.
  • message — входящее сообщение.
  • ws.send — отправить.

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

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

ws.onopen = () => {
    console.log('Соединение открыто');
    ws.send('Привет!');
};

ws.onmessage = (event) => {
    console.log('Получено:', event.data);
};

ws.onclose = () => {
    console.log('Соединение закрыто');
};

ws.onerror = (error) => {
    console.error('Ошибка:', error);
};

Четыре обработчика:

  • onopen — открытие.
  • onmessage — сообщение.
  • onclose — закрытие.
  • onerror — ошибка.

Клиент в Node.js

import WebSocket from 'ws';

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

ws.on('open', () => {
    ws.send('Привет!');
});

ws.on('message', (data) => {
    console.log('Получено:', data.toString());
});

Синтаксис другой — Node-версия использует .on().

JSON-сообщения

Отправка:

ws.send(JSON.stringify({
    type: 'chat',
    text: 'Привет',
    user: 'Иван',
}));

Чтение:

ws.on('message', (data) => {
    try {
        const message = JSON.parse(data.toString());

        switch (message.type) {
            case 'chat':
                handleChat(message);
                break;
            case 'typing':
                handleTyping(message);
                break;
            default:
                console.warn('Неизвестный тип:', message.type);
        }
    } catch (e) {
        console.error('Некорректный JSON:', data.toString());
    }
});

Правило: всегда проверяй JSON — клиент может прислать что угодно.

Broadcast всем

Отправить всем клиентам:

wss.on('connection', (ws) => {
    ws.on('message', (data) => {
        const text = data.toString();

        // Отправить всем
        wss.clients.forEach((client) => {
            if (client.readyState === WebSocket.OPEN) {
                client.send(text);
            }
        });
    });
});

Что здесь:

  • wss.clients — Set клиентов.
  • client.readyState — состояние.
  • WebSocket.OPEN — константа.

Состояния соединения

WebSocket.CONNECTING;   // 0
WebSocket.OPEN;         // 1
WebSocket.CLOSING;      // 2
WebSocket.CLOSED;       // 3

Проверка:

if (ws.readyState === WebSocket.OPEN) {
    ws.send('OK');
}

⚠️ Не отправляй в закрытое соединение — будет ошибка.

Ping/Pong

Проверка живого соединения:

const wss = new WebSocketServer({ port: 8080 });

const interval = setInterval(() => {
    wss.clients.forEach((ws) => {
        if (ws.isAlive === false) {
            return ws.terminate();
        }

        ws.isAlive = false;
        ws.ping();
    });
}, 30000);

wss.on('connection', (ws) => {
    ws.isAlive = true;
    ws.on('pong', () => {
        ws.isAlive = true;
    });
});

wss.on('close', () => {
    clearInterval(interval);
});

Что происходит:

  1. Каждые 30 сек — ping.
  2. Клиент отвечает pong автоматически.
  3. Если pong не пришёл — соединение мёртвое, terminate.

Зачем: TCP-соединение может быть разорвано (Wi-Fi, мобильная сеть), но сервер этого не знает.

Закрытие

Сервер:

ws.close(1000, 'Нормальное закрытие');

Клиент:

ws.close();

Коды:

  • 1000 — нормальное закрытие.
  • 1001 — уход.
  • 1008 — нарушение политики.
  • 1011 — ошибка сервера.
  • 4000–4999 — свои коды.

HTTP + WebSocket на одном порту

Используя http.createServer:

import http from 'node:http';
import { WebSocketServer } from 'ws';

const server = http.createServer((req, res) => {
    res.writeHead(200);
    res.end('HTTP работает');
});

const wss = new WebSocketServer({ server });

wss.on('connection', (ws) => {
    ws.send('WebSocket работает');
});

server.listen(8080, () => {
    console.log('http://localhost:8080 — и HTTP, и WebSocket');
});

Что даёт:

  • Один порт для HTTP и WebSocket.
  • Один сервер.
  • Проще с прокси.

Уникальные ID клиентов

import { randomUUID } from 'node:crypto';

wss.on('connection', (ws) => {
    ws.id = randomUUID();
    ws.userId = null;

    ws.on('message', (data) => {
        const message = JSON.parse(data.toString());
        // используем ws.id
    });
});

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

Хранение состояния

const clients = new Map();

wss.on('connection', (ws) => {
    const id = randomUUID();
    clients.set(id, { ws, userId: null });

    ws.on('close', () => {
        clients.delete(id);
    });
});

В памяти сервера.

⚠️ Не масштабируется на несколько серверов.

Практика: echo-сервер

import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', (ws) => {
    console.log('Клиент подключился');

    ws.on('message', (data) => {
        const text = data.toString();
        console.log('Получено:', text);

        ws.send(`Эхо: ${text}`);
    });

    ws.on('close', () => {
        console.log('Клиент отключился');
    });

    ws.send('Привет! Отправь что-нибудь.');
});

Проверка:

npx wscat -c ws://localhost:8080
# > hello
# < Эхо: hello

Практика: broadcast-чат

import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', (ws) => {
    ws.send(JSON.stringify({
        type: 'system',
        text: 'Добро пожаловать в чат',
    }));

    ws.on('message', (data) => {
        const message = JSON.parse(data.toString());

        const out = JSON.stringify({
            type: 'chat',
            user: message.user,
            text: message.text,
            time: new Date().toISOString(),
        });

        wss.clients.forEach((client) => {
            if (client.readyState === client.OPEN) {
                client.send(out);
            }
        });
    });
});

Частые ошибки

1. Отправка в закрытое соединение:

ws.send('x');   // если readyState !== OPEN → ошибка

// Правильно:
if (ws.readyState === ws.OPEN) {
    ws.send('x');
}

2. Не парсить JSON:

const msg = data;   // Buffer, не объект

// Правильно:
const msg = JSON.parse(data.toString());

3. Забыть закрыть соединение:

Клиент отключился, а сервер всё ещё хранит его

Решение: ws.on('close', () => clients.delete(id)).

4. Мёртвые соединения:

Клиент в метро, соединение порвано, а сервер думает, что живой

Решение: ping/pong.

5. Хардкод порта:

new WebSocketServer({ port: 8080 });

// Правильно:
const port = Number(process.env.PORT) || 8080;
new WebSocketServer({ port });

Итог

  • ws — минималистичная библиотека.
  • WebSocketServer — сервер.
  • connection, message, close, error — события.
  • ws.send — отправить.
  • wss.clients — все клиенты.
  • Ping/pong — проверка жизни.
  • HTTP + WS на одном порту — через http.createServer.
  • Состояние — в памяти сервера (не масштабируется).

Дальше — Socket.IO: основы.