Медленный клиент и пределы буфера
Проверка открытого сокета защищает от отправки в закрытое соединение, но не от медленного чтения. Если сервер создаёт события быстрее, чем клиент их принимает, библиотека накапливает исходящие байты. Добавим в «Линию» предел буфера и ограничение частоты команд. После этого отдельный медленный или слишком активный клиент не должен бесконечно увеличивать используемую процессом память.
Снимок stages/14 рассчитан на локальную учебную нагрузку, а выбранные пороги являются правилами примера, не результатом измерений. Код имеет статус not_run; нагрузочные проверки не выполнялись. Рассмотрим смысл сигналов и последствия превышения, чтобы число килобайт и количество команд не воспринимались как универсальная безопасная настройка любого чата.
Буфер показывает давление на отправку
Свойство bufferedAmount сообщает количество байтов, поставленных на отправку, но ещё не переданных. Это не число непрочитанных сообщений человеком и не подтверждение доставки. Проверяем его перед созданием следующей записи в буфер. Если накоплено больше 1 MiB, закрываем соединение кодом 1013 с причиной slow_client. Клиент позже может восстановить историю по сохранённому курсору в пределах доступного окна.
Полный файл limits.js содержит транспортную проверку и простой счётчик команд текущего соединения. Он заменяет прежнюю локальную функцию send() в socket.js.
import { WebSocket } from 'ws';
export function send(client, event) {
if (client.readyState !== WebSocket.OPEN) return false;
if (client.bufferedAmount > 1024 * 1024) {
client.close(1013, 'slow_client');
return false;
}
client.send(JSON.stringify(event));
return true;
}
export function acceptRate(client) {
const now = Date.now();
if (!client.rate || now - client.rate.startedAt >= 1000) {
client.rate = { startedAt: now, count: 0 };
}
client.rate.count += 1;
return client.rate.count <= 10;
}
Проверка выполняется перед client.send(), поэтому один новый кадр может увеличить буфер поверх порога. Размер входящей команды уже ограничен maxPayload, а содержание сообщения — пределом текста. Эти правила удерживают величину разового превышения в понятных пределах. При отправке огромного снимка истории было бы недостаточно смотреть только на прежний буфер: потребовалось бы ограничить размер самого снимка или передавать его порциями.
Закрытие не удаляет принятую запись из журнала. Это намеренно: медленный получатель не должен влиять на результат команды отправителя. Он теряет текущий канал и позже читает доступную историю. В действующем сервисе может потребоваться показать предупреждение, запустить HTTP-перечитывание либо ограничить частоту событий комнаты. Любая стратегия должна ясно говорить, какой предметный результат сохраняется при разрыве и что клиент обязан восстановить.
Лимит входа относится к соединению
Функция acceptRate() разрешает не больше десяти команд в течение окна одной секунды, начинающегося первой командой сокета. По истечении секунды счётчик создаётся заново. В число входят отправка, вход в комнату и восстановление, поскольку все они занимают ресурсы сервера. При превышении возвращается прикладная ошибка rate_limit; обычный небольшой JSON не становится новым сообщением.
Это окно закреплено за сокетом, поэтому клиент, создающий много соединений, способен получить много отдельных лимитов. Для production обычно нужны ограничения по участнику, организации и источнику запросов, а также предел числа соединений. В учебном проекте мы не объявляем локальный счётчик защитой от распределённой атаки. Он показывает, куда поставить проверку и какое состояние требуется хранить для выбранной области ограничения.
Лимит частоты и предел размера решают разные задачи. Одна огромная команда нарушает размер независимо от скорости. Сотни маленьких команд нарушают частоту, хотя каждая отдельно допустима. Десять разрешённых команд могут всё же содержать дорогую операцию, если приложение начнёт обращаться к медленной базе или внешнему API. Поэтому ограничения связывают с стоимостью действий, а не только с формальным числом событий транспорта.
Ожидаемый результат быстрого повтора: первые команды окна проходят обычные проверки, следующая получает событие error. При этом соединение остаётся открытым, а через новое окно команды снова могут быть приняты. Это поведение примера, не измеренная частота обработки.
{"version":1,"type":"error","code":"rate_limit"}
Модель памяти складывается из нескольких частей
Карта сессий ограничена сотней записей, журнал каждой из двух комнат — двумястами сообщений, карта принятия — двумя тысячами команд, клиентская очередь — двадцатью неопределёнными отправками, SSE-журнал — ста уведомлениями. Отдельные пределы не заменяют расчёт общего объёма. Нужно учитывать размер строк, объектов, соединений и буферов библиотеки, а не складывать только количества записей, которые имеют разную стоимость.
Не забудьте, что pending живёт в каждом браузере, а журнал и сессии — на сервере. Ограничение одной стороны не сокращает память другой. Если интерфейс продолжит формировать новые локальные действия при закрытом канале, серверный rate limit вообще не увидит их. В «Линии» форма отключается без joined и проверяет предельный размер pending до отправки, поэтому оба участка жизненного цикла имеют отдельные барьеры.
Нельзя молча удалять случайное непрочитанное сообщение из исходящего буфера. Тогда курсор интерфейса мог бы перескочить через номер, и клиент считал бы состояние актуальным. Закрытие с последующим resume лучше сохраняет ясность: канал явно потерян, клиент обязан запросить журнал. Другой допустимый договор может разрешать пропуск промежуточных состояний, например счётчика присутствия, но для сообщений переписки это самостоятельное решение продукта.
Изменяемые события можно объединять. Несколько обновлений присутствия до чтения достаточно представить последним составом; все промежуточные составы редко нужны человеку. Сообщения истории объединять таким способом нельзя, потому что каждое имеет отдельный смысл и авторство. Перед добавлением очереди отправки решите, является ли событие записью факта или текущим состоянием, которое заменяет предыдущее. От ответа зависит безопасная политика сокращения накопленного потока.
При закрытии медленного клиента остальные получатели продолжают рассылку. Функция send возвращает false только для этого сокета, а цикл не прерывается. Если бросать исключение на первого медленного участника, последующие клиенты комнаты могли бы не получить событие. Локальная неудача отдельного канала не должна случайно стать глобальным отказом доставки всем участникам, хотя журнал остаётся источником восстановления.
Для самостоятельного разбора замените порог на значительно меньший и рассмотрите, как часто клиенту придётся восстанавливать журнал при большой истории. Это мысленное изменение показывает компромисс между памятью и повторным чтением. Реальный выбор требует измерения нагрузки, устройства клиента и инфраструктуры, которое в этой подготовке запрещено и не проводилось. Не выдавайте отсутствие запуска за подтверждение выдержанного количества пользователей.
Свойства буфера описаны в WebSocket API и документации ws. После этого состояния накопление имеет явный предел, а превышение — определённое последствие. Дальше приведём сообщения интерфейса и управление формой к доступному поведению, чтобы пользователь понимал эти переходы без угадывания по цвету кнопки.