Почему WebSockets — это магия real-time?
Представьте, что вы открыли чат с поддержкой, отправили сообщение и... ждете. Секунда, две, пять. Знакомо? Это классический HTTP-запрос: вы просите, сервер отвечает. А что если сервер сам хочет что-то сказать? Например, новый заказ в CRM или голос в онлайн-игре? Тут HTTP пасует — ему нужен постоянный пинг-понг.
WebSockets решают эту проблему кардинально. Они создают постоянный двусторонний канал между клиентом и сервером. Один раз «рукопожатие» — и данные летят в обе стороны мгновенно. Никакого лишнего трафика, никаких задержек. Только чистая магия real-time.
В этой статье мы на TypeScript соберем полноценное real-time приложение. Научимся работать с WebSocket сервером на Node.js, настроим клиент и даже добавим обработку ошибок. К концу вы сможете написать свой чат, систему уведомлений или многопользовательскую игру.
Что такое WebSockets и зачем они нужны?
WebSocket — это протокол поверх TCP, который обеспечивает постоянное соединение между браузером и сервером. В отличие от HTTP, где каждый запрос открывает и закрывает соединение, WebSocket держит его открытым.
Где это критично?
- Чаты и мессенджеры — сообщения приходят без задержки.
- Онлайн-игры — позиции игроков обновляются в реальном времени.
- Биржевые котировки — цены меняются каждую миллисекунду.
- Совместное редактирование — как Google Docs.
TypeScript добавляет к этому строгую типизацию. Вы не отправите случайно строку вместо числа — компилятор поймает ошибку на этапе разработки. Это спасает от багов в продакшене.
Настройка проекта: Node.js + TypeScript + WebSocket
Начнем с инициализации. Убедитесь, что у вас установлены Node.js (версия 18+) и npm.
mkdir websocket-ts-chat
cd websocket-ts-chat
npm init -y
npm install ws @types/ws typescript ts-node @types/node
npx tsc --init
В tsconfig.json включите строгий режим и укажите outDir: "./dist". Это база для любого TypeScript проекта.
Пишем WebSocket сервер на TypeScript
Создайте файл server.ts. Здесь мы поднимем WebSocket сервер на порту 8080.
import { WebSocketServer, WebSocket } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
interface CustomWebSocket extends WebSocket {
id: string;
username?: string;
}
wss.on('connection', (ws: CustomWebSocket) => {
ws.id = generateId();
console.log(`Клиент подключился: ${ws.id}`);
ws.on('message', (data: Buffer) => {
const message = data.toString();
console.log(`Получено: ${message}`);
// Рассылаем всем, кроме отправителя
wss.clients.forEach((client) => {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(`[${ws.id}]: ${message}`);
}
});
});
ws.on('close', () => {
console.log(`Клиент отключился: ${ws.id}`);
});
ws.send('Добро пожаловать в чат!');
});
function generateId(): string {
return Math.random().toString(36).substring(2, 9);
}
console.log('Сервер запущен на ws://localhost:8080');
Разберем ключевые моменты:
- WebSocketServer — создает сервер.
- connection — событие при подключении нового клиента.
- message — получаем данные от клиента.
- send — отправляем данные обратно.
Запустите сервер командой npx ts-node server.ts. Он будет слушать порт 8080.
Клиентская часть: подключаемся к WebSocket
Теперь создадим client.ts. Это будет простой скрипт, который подключается к серверу и отправляет сообщения.
import WebSocket from 'ws';
const ws = new WebSocket('ws://localhost:8080');
ws.on('open', () => {
console.log('Подключено к серверу');
ws.send('Привет, сервер!');
});
ws.on('message', (data: Buffer) => {
console.log(`Сервер: ${data.toString()}`);
});
ws.on('close', () => {
console.log('Соединение закрыто');
});
ws.on('error', (error: Error) => {
console.error('Ошибка:', error.message);
});
// Отправляем сообщение каждые 5 секунд
setInterval(() => {
ws.send(`Сообщение в ${new Date().toLocaleTimeString()}`);
}, 5000);
Запустите клиент в другом терминале: npx ts-node client.ts. Вы увидите, как сервер отвечает на каждое сообщение. Это и есть real-time магия.
Добавляем типизацию сообщений
Одно из главных преимуществ TypeScript — строгие типы. Давайте определим формат сообщений.
interface ChatMessage {
type: 'message' | 'system' | 'error';
username: string;
content: string;
timestamp: number;
}
// Отправка типизированного сообщения
const msg: ChatMessage = {
type: 'message',
username: 'Alice',
content: 'Привет!',
timestamp: Date.now()
};
ws.send(JSON.stringify(msg));
Теперь, когда клиент или сервер получают данные, они знают точную структуру. Никаких сюрпризов.
Обработка ошибок и переподключение
В real-time приложениях соединение может оборваться. Клиент должен уметь переподключаться.
function connectWithRetry(url: string, retries: number = 5): WebSocket {
let ws = new WebSocket(url);
ws.on('close', () => {
if (retries > 0) {
console.log(`Попытка переподключения... Осталось ${retries} попыток`);
setTimeout(() => {
ws = connectWithRetry(url, retries - 1);
}, 2000);
} else {
console.error('Не удалось восстановить соединение');
}
});
ws.on('error', (err: Error) => {
console.error('Ошибка соединения:', err.message);
});
return ws;
}
const ws = connectWithRetry('ws://localhost:8080');
Этот паттерн используется в продакшене. Он гарантирует, что приложение не упадет при временных сбоях сети.
Расширяем функционал: комнаты и авторизация
Наш чат пока один на всех. Добавим комнаты — пользователи смогут общаться в изолированных группах.
interface RoomMessage {
room: string;
sender: string;
text: string;
}
// Сервер: обработка комнат
const rooms = new Map>();
ws.on('message', (data: Buffer) => {
const parsed: RoomMessage = JSON.parse(data.toString());
const { room, sender, text } = parsed;
if (!rooms.has(