Express.jsSeniorSystem design
Как реализовать WebSockets совместно с сервером Express?
WebSocket в Express реализуется через библиотеку ws или socket.io: создать http.Server, передать его в WebSocketServer, навесить обработчик upgrade. Socket.io добавляет комнаты, неймспейсы и fallback на polling.
WebSockets совместно с Express
Почему нужен отдельный подход
WebSocket — это протокол поверх TCP, отличный от HTTP. Express обрабатывает только HTTP-запросы. Для WebSocket нужно перехватить upgrade событие на уровне http.Server, которое Express использует внутри.
Вариант 1: библиотека ws (низкоуровневый)
import express from 'express';
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
const app = express();
const server = createServer(app);
const wss = new WebSocketServer({ server }); // разделяем один порт
// HTTP роуты работают как обычно
app.get('/api/status', (req, res) => res.json({ ok: true }));
// WebSocket подключения
wss.on('connection', (ws, req) => {
const clientIp = req.headers['x-forwarded-for'] ?? req.socket.remoteAddress;
console.log(`Client connected: ${clientIp}`);
ws.on('message', (data) => {
const message = data.toString();
console.log(`Received: ${message}`);
// Echo back
ws.send(JSON.stringify({ echo: message, ts: Date.now() }));
});
ws.on('close', (code, reason) => {
console.log(`Client disconnected: ${code} ${reason}`);
});
ws.on('error', (err) => {
console.error('WebSocket error:', err);
});
// Отправить приветствие
ws.send(JSON.stringify({ type: 'connected' }));
});
server.listen(3000, () => console.log('Server on :3000'));
Broadcast всем клиентам
const broadcast = (data) => {
const message = JSON.stringify(data);
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
};
// Из HTTP-роута отправить WS всем подключённым
app.post('/api/notify', express.json(), (req, res) => {
broadcast({ type: 'notification', payload: req.body });
res.json({ sent: wss.clients.size });
});
Аутентификация WebSocket
WS handshake — обычный HTTP-запрос, можно использовать cookie или query-токен:
import { parse } from 'cookie';
import jwt from 'jsonwebtoken';
wss.on('connection', (ws, req) => {
// Вариант 1: из cookie
const cookies = parse(req.headers.cookie ?? '');
const token = cookies.auth_token;
// Вариант 2: из query string (?token=...)
const url = new URL(req.url, 'http://localhost');
const queryToken = url.searchParams.get('token');
try {
const user = jwt.verify(token ?? queryToken, process.env.JWT_SECRET);
ws.userId = user.id; // прикрепить к ws объекту
} catch (err) {
ws.close(4001, 'Unauthorized');
return;
}
ws.on('message', (data) => {
console.log(`Message from user ${ws.userId}:`, data.toString());
});
});
Вариант 2: socket.io (высокоуровневый)
import express from 'express';
import { createServer } from 'http';
import { Server as SocketIO } from 'socket.io';
const app = express();
const server = createServer(app);
const io = new SocketIO(server, {
cors: { origin: process.env.FRONTEND_URL, credentials: true },
});
// Неймспейсы
const chatNs = io.of('/chat');
chatNs.on('connection', (socket) => {
socket.on('join-room', (roomId) => {
socket.join(roomId);
// Отправить всем в комнате
chatNs.to(roomId).emit('user-joined', { userId: socket.id });
});
socket.on('send-message', ({ roomId, text }) => {
chatNs.to(roomId).emit('new-message', {
userId: socket.id,
text,
ts: Date.now(),
});
});
});
server.listen(3000);
Масштабирование: Redis Pub/Sub
При нескольких инстансах сервера нужен брокер сообщений. Socket.io поддерживает Redis adapter:
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';
const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));
Подводные камни
- Не использовать
app.listen()— он создаёт внутреннийhttp.Server, недоступный для WebSocketServer. Нужно явно создаватьcreateServer(app). - Отсутствие heartbeat — мёртвые соединения не определяются. Использовать
ws.ping()каждые 30 сек и закрывать клиентов безpong. - Хранить состояние WebSocket в памяти процесса — при нескольких инстансах клиенты на разных серверах не видят сообщения друг друга. Нужен Redis adapter.
- Не ограничивать размер входящих сообщений — DoS через огромные payload. Параметр
maxPayloadв конструктореWebSocketServer. - Не обрабатывать
errorсобытие на ws объекте — необработанный error event падает весь процесс. - Socket.io клиент и чистый ws — несовместимы. Socket.io использует собственный протокол поверх WebSocket.
- Nginx по умолчанию не проксирует WebSocket — нужны заголовки
UpgradeиConnectionв конфиге.
Common mistakes
- Дает общий ответ про Node.js и не называет конкретные API Express.js.
- Не объясняет, где в lifecycle находится WebSockets совместно с Express.
- Не разделяет validation, authorization, business logic и persistence.
- Игнорирует ошибки, лимиты входных данных, observability и тестирование.
What the interviewer is testing
- Может объяснить WebSockets совместно с Express на примере кода.
- Называет ключевые API: http.createServer(app), WebSocketServer.
- Использует точные API Express.js, а не вымышленные hooks/decorators/methods.
- Видит production-риски: безопасность, отказоустойчивость, логирование и тесты.