WebSockets — Примеры
Пример 1 — Любое сообщение принимают за ответ
Клиент ниже отправляет команду и ждёт ответа в следующем пришедшем сообщении. Сервер при этом проталкивает уведомления, так что команды и push-сообщения перемешиваются и вызывается не тот обратный вызов. Дайте клиенту собственную схему сопоставления — тогда ответ свяжется с командой, которая его вызвала.
socket.onmessage = (event) => {
const message = JSON.parse(event.data);
showReply(message); // push и ответ здесь выглядят одинаково
};
Решение
const pending = new Map();
function send(type, payload) {
const id = crypto.randomUUID();
socket.send(JSON.stringify({ id, type, payload }));
return new Promise((resolve) => pending.set(id, resolve));
}
socket.onmessage = (event) => {
const message = JSON.parse(event.data);
const resolve = pending.get(message.id);
if (!resolve) return; // незапрошенный push, а не ответ
pending.delete(message.id);
resolve(message.payload);
};
Идентификатор в полезной нагрузке, который сервер возвращает обратно, — цена модели «запрос — ответ»: без такой пары сообщение не связать с вызвавшим его запросом. Сокет открыт запросом на обновление, где пользователь прошёл аутентификацию, поэтому аутентификация решается один раз, тогда как сопоставление нужно на каждом сообщении. А ошибка вот в чём: полагать, будто связь даёт порядок, — а он ломается в первый же раз, когда сервер заговорит сам.
Пример 2 — Переподключение, которое заваливает сервер
Клиент ниже переподключается в тот же миг, как соединение закрылось, и продолжает так, будто ничего не пропустил, — а после перезапуска так делают все клиенты разом. Перепишите путь переподключения так, чтобы повтор был распределён, а клиент узнавал, что пропустил.
function connect() {
socket = openSocket();
socket.onclose = connect; // сразу же и без восстановления
}
Решение
let attempt = 0;
let lastSeen = 0;
function connect() {
const socket = openSocket();
socket.onmessage = (event) => {
lastSeen = event.data.sequence;
render(event.data);
};
socket.onopen = () => {
attempt = 0;
replay(lastSeen); // из надёжного хранилища, а не из сокета
};
socket.onclose = () => {
const wait = Math.min(30000, 500 * 2 ** attempt) + Math.random() * 500;
attempt += 1;
setTimeout(connect, wait);
};
}
Однако соединение держит один процесс: развёртывание или падение его сбрасывает, и всё, отправленное за это время, пропадает — а replay как раз спрашивает у надёжного хранилища сообщения после lastSeen. Плотный цикл повторов превращается в поток запросов, который клиенты обрушивают на себя сами, а экспоненциальная задержка с разбросом не даёт им повторить попытку всем в одну и ту же секунду. Обычно же ошибаются, полагая, будто сообщение, отправленное пока клиент был офлайн, придёт позже, и потому не строят путь догона.