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. Плотный цикл повторов превращается в поток запросов, который клиенты обрушивают на себя сами, а экспоненциальная задержка с разбросом не даёт им повторить попытку всем в одну и ту же секунду. Обычно же ошибаются, полагая, будто сообщение, отправленное пока клиент был офлайн, придёт позже, и потому не строят путь догона.