Урок 03

Socket.IO: основы

Что такое Socket.IO

Socket.IO — библиотека для real-time с fallback на long polling.

Что даёт:

  • WebSocket — когда возможно.
  • Long polling — когда нет.
  • Автопереподключение.
  • Ack / emit с ответом.
  • Комнаты.
  • Namespaces.
  • Middleware.

Установка

Сервер:

npm install socket.io

Клиент (браузер):

npm install socket.io-client

Сервер — базовый

import { Server } from 'socket.io';

const io = new Server(8080, {
    cors: {
        origin: ['http://localhost:3000'],
        credentials: true,
    },
});

io.on('connection', (socket) => {
    console.log('Подключился:', socket.id);

    socket.on('disconnect', () => {
        console.log('Отключился:', socket.id);
    });
});

console.log('Socket.IO сервер на порту 8080');

Разбор:

  • new Server(port, options) — сервер.
  • io.on('connection', fn) — новый клиент.
  • socket — конкретный клиент.
  • socket.id — уникальный ID.

Клиент — базовый

import { io } from 'socket.io-client';

const socket = io('http://localhost:8080');

socket.on('connect', () => {
    console.log('Подключено:', socket.id);
});

socket.on('disconnect', () => {
    console.log('Отключено');
});

Socket.IO сам установит WebSocket или fallback.

Events

Socket.IO — event-driven. Основные:

Сервер:

io.on('connection', (socket) => {
    socket.on('custom:event', (data) => {
        // получить от клиента
    });
});

Клиент:

socket.on('custom:event', (data) => {
    // получить от сервера
});

Emit:

socket.emit('event', data);

Сервер → клиент

Одному клиенту:

io.on('connection', (socket) => {
    socket.emit('welcome', {
        text: 'Привет!',
    });
});

Или с задержкой:

setTimeout(() => {
    socket.emit('notification', 'Новое сообщение');
}, 1000);

Клиент → сервер

// Клиент
socket.emit('chat:message', {
    text: 'Привет всем!',
    user: 'Иван',
});
// Сервер
io.on('connection', (socket) => {
    socket.on('chat:message', (data) => {
        console.log('Получено:', data.text);
    });
});

Broadcast всем

io.emit — всем, включая отправителя:

io.emit('chat:message', {
    text: 'Всем привет',
});

socket.broadcast.emit — всем, кроме отправителя:

socket.broadcast.emit('user:joined', {
    user: socket.id,
});

socket.emit — только отправителю:

socket.emit('welcome', 'Привет лично тебе');

Ack — ответ на emit

Клиент:

socket.emit('create:post', { title: 'Hello' }, (response) => {
    if (response.error) {
        console.error('Ошибка:', response.error);
    } else {
        console.log('Создан пост:', response.id);
    }
});

Сервер:

socket.on('create:post', async (data, callback) => {
    try {
        const post = await Post.create(data);
        callback({ id: post.id });
    } catch (e) {
        callback({ error: e.message });
    }
});

Что даёт: клиент ждёт ответ от сервера, как в HTTP.

Promise-обёртка для emit

Проблема: callback не всегда удобен.

Решение:

function emitAsync(socket, event, data) {
    return new Promise((resolve) => {
        socket.emit(event, data, resolve);
    });
}

// Использование
const response = await emitAsync(socket, 'create:post', { title: 'X' });

Namespaces

Разные «каналы» на одном сервере:

const chatNsp = io.of('/chat');
const adminNsp = io.of('/admin');

chatNsp.on('connection', (socket) => {
    socket.emit('welcome', 'Чат');
});

adminNsp.on('connection', (socket) => {
    socket.emit('welcome', 'Админка');
});

Клиент:

const chat = io('http://localhost:8080/chat');
const admin = io('http://localhost:8080/admin');

Что даёт:

  • Разные middleware.
  • Разные состояния.
  • Разные комнаты.

Комнаты

socket.join('room'):

io.on('connection', (socket) => {
    socket.join('room:1');
});

Отправить в комнату:

io.to('room:1').emit('message', 'Всем в комнате 1');

Выйти:

socket.leave('room:1');

Список комнат:

socket.rooms;   // Set

⚠️ каждый сокет автоматически в комнате своего socket.id.

Middleware

Авторизация:

io.use((socket, next) => {
    const token = socket.handshake.auth.token;

    if (!token) {
        return next(new Error('Нет токена'));
    }

    try {
        const payload = jwt.verify(token, JWT_SECRET);
        socket.data.user = payload;
        next();
    } catch (e) {
        next(new Error('Неверный токен'));
    }
});

Доступ к юзеру:

io.on('connection', (socket) => {
    console.log('Подключился:', socket.data.user.id);
});

CORS

const io = new Server(8080, {
    cors: {
        origin: ['http://localhost:3000', 'https://app.example.com'],
        credentials: true,
    },
});

credentials: true — если cookies.

Транспорты

Только WebSocket:

const io = new Server(8080, {
    transports: ['websocket'],
});

По умолчанию: ['polling', 'websocket'] — сначала polling, потом upgrade.

Проверка транспорта

На клиенте:

console.log(socket.io.engine.transport.name);
// 'websocket' или 'polling'

// При upgrade
socket.io.engine.on('upgrade', (transport) => {
    console.log('Апгрейд на', transport.name);
});

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

Сервер:

import { Server } from 'socket.io';

const io = new Server(8080, {
    cors: { origin: 'http://localhost:3000' },
});

io.on('connection', (socket) => {
    console.log('Подключился:', socket.id);

    socket.on('chat:join', ({ name }) => {
        socket.data.name = name;

        io.emit('chat:system', `${name} вошёл в чат`);
    });

    socket.on('chat:message', ({ text }, callback) => {
        if (!text?.trim()) {
            return callback({ error: 'Пустое сообщение' });
        }

        io.emit('chat:message', {
            user: socket.data.name,
            text,
            time: Date.now(),
        });

        callback({ ok: true });
    });

    socket.on('disconnect', () => {
        if (socket.data.name) {
            io.emit('chat:system', `${socket.data.name} вышел`);
        }
    });
});

Клиент:

import { io } from 'socket.io-client';

const socket = io('http://localhost:8080');

socket.on('connect', () => {
    socket.emit('chat:join', { name: 'Иван' });
});

socket.on('chat:message', ({ user, text }) => {
    console.log(`${user}: ${text}`);
});

socket.on('chat:system', (msg) => {
    console.log('*** ' + msg);
});

socket.emit('chat:message', { text: 'Привет!' }, (response) => {
    if (response.error) console.error(response.error);
});

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

1. Забыли CORS:

Access to XMLHttpRequest blocked by CORS policy

Решение: cors: { origin: [...] }.

2. Использовать socket.io-client с чистым WebSocket:

Socket.IO клиент не работает с ws-сервером

3. io.emit вместо socket.emit:

io.emit('welcome');   // все получат

// Правильно:
// socket.emit('welcome'); — только один

4. Забыли credentials:

// Клиент не отправит cookies

// Правильно:
cors: { origin: '...', credentials: true }

5. Не обрабатывать disconnect:

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

Итог

  • Socket.IO — real-time с fallback.
  • new Server(port, opts) — сервер.
  • io.on('connection', fn) — клиент.
  • socket.emit, socket.on — события.
  • io.emit — всем, socket.broadcast.emit — кроме отправителя.
  • Ack — callback в emit.
  • Namespaces — каналы.
  • Комнаты — join, leave, to.
  • Middleware — авторизация.

Дальше — Socket.IO: комнаты и события.