Разработка / ROBLOX
Ограничение запросов в Roblox: защита серверной подсказки от частых нажатий
Делаем учебный ограничитель для запроса подсказки в мастерской: четыре попытки сразу, затем пополнение по две в секунду. Разбираем отдельное состояние игроков, серверное время, поведение кнопки и локальные проверки без настоящей нагрузки на Roblox.
Выбери конкретное действие для ограничения #
Представь мастерскую, где новичок нажимает «Где станция?» и получает подсказку. Несколько быстрых нажатий могут быть случайностью, попыткой дождаться ответа или нежелательным повтором. Если каждый запрос запускает тяжёлую работу, одна кнопка способна создавать лишнюю нагрузку. Сначала запиши, какая серверная операция происходит после нажатия и почему её частоту нужно ограничить.
Мы используем выдачу текстовой подсказки, а не покупку, награду или сохранение профиля. Это собственный учебный сценарий, не функция, добавленная в игры автора сайта. Параметры «четыре» и «две в секунду» выбраны для объяснения. Они не являются лимитами Roblox или рекомендацией для любого игрового действия. Настоящий многопользовательский нагрузочный тест здесь не выполнялся.
Кнопка клиента не определяет правило сервера #
Можно временно отключить кнопку на устройстве игрока и показать ожидание ответа. Это помогает человеку понять, что запрос уже отправлен. Но сервер всё равно должен самостоятельно решать, разрешён ли новый вызов. Документация Roblox отдельно подчёркивает, что нельзя полагаться только на ограничение частоты на стороне клиента.
Не принимай от клиента число «сколько запросов осталось» или время «когда я нажал в прошлый раз» как основание разрешения. В нашем примере остаток и время хранятся в серверном модуле. Клиент запрашивает подсказку; сервер принимает своё решение. Ограничение не превращает плохие параметры в хорошие: проверка типа, допустимого действия и игрового состояния остаётся отдельной задачей.
Корзина жетонов разрешает короткую серию #
Для каждого игрока воображаем корзину вместимостью четыре жетона. Одна попытка тратит один жетон. Со временем жетоны возвращаются со скоростью два в секунду, но их количество не превышает вместимость. Поэтому сразу можно выполнить четыре попытки, а непрерывная длинная серия должна ждать пополнения. Это схема token bucket, которую Roblox описывает как распространённый подход.
Не путай её с правилом «четыре вызова в каждом календарном отрезке секунды». После короткого периода ожидания может вернуться только часть жетона. Пока нет целого жетона, новая попытка не разрешается. Мы проверяем дробное пополнение, чтобы читатель видел, откуда получается задержка, а не воспринимал её как случайную ошибку кнопки.
| Время | Попытка | Результат и остаток |
|---|---|---|
| 0 | Первая–четвёртая | Разрешены: 4 → 0 |
| 0 | Пятая | Отказ: 0 |
| 0.25 s | Новая попытка | Отказ: 0,5 |
| 0.5 s | Новая попытка | Разрешена: 0 |
У каждого игрока своё состояние #
Если A потратил все четыре жетона, новый запрос B не должен блокироваться из-за A. Поэтому корзины находятся в таблице, где ключ — объект Player, полученный серверным OnServerEvent. Не используй переданное клиентом имя или идентификатор в качестве замены этого объекта. Иначе выбор чужой корзины окажется частью входных данных, которым ты доверяешь.
Отдельные корзины не ограничивают суммарную нагрузку всех игроков. Много пользователей могут одновременно исчерпать свои разрешённые попытки. Если действие дорогое или обращается к backend, нужен отдельный анализ общей частоты, очереди и стоимости операции. В этом модуле нет общего бюджета сервера, ограничения параллельных задач или распределённого состояния нескольких серверов.
Как устроен учебный ModuleScript #
Помести HintRequestLimiter в ServerScriptService. Конструктор new(capacity, refillPerSecond, clock) задаёт вместимость, скорость и серверную функцию часов. Allow(player) возвращает разрешение и короткий код результата. Forget(player) удаляет локальное состояние после ухода. Модуль не обращается к DataStore, не создаёт RemoteEvent и не выдаёт подсказку самостоятельно.
Вместимость должна быть целой от 1 до 1000, скорость — положительным конечным числом не выше 1000. Это выбранные ограничения нашего примера, а не платформенные квоты. Дробная скорость допустима. Ошибка или некорректное число часов приводит к отказу; при движении часов назад модуль не добавляет жетоны. Сам ограничитель использует только локальное состояние текущего сервера.
os.clock не имеет заданной абсолютной начальной точки. Сравнивай разницу значений, а не календарную дату. Учебный модуль допускает конечное отрицательное начало; отдельный локальный тест проверяет такое пополнение.
-- Original teaching limiter for server-side hint requests.
-- Pass the Player supplied by OnServerEvent, never a client-supplied identity.
-- Frequency control does not validate permissions or grant rewards.
local Limiter = {}
local function finite(value)
return type(value) == "number" and value == value
and value > -math.huge and value < math.huge
end
function Limiter.new(capacity, refillPerSecond, clock)
assert(finite(capacity) and capacity == math.floor(capacity)
and capacity >= 1 and capacity <= 1000, "Invalid capacity")
assert(finite(refillPerSecond) and refillPerSecond > 0
and refillPerSecond <= 1000, "Invalid refill rate")
if clock == nil then clock = os.clock end
assert(type(clock) == "function", "Invalid clock")
local buckets = {}
local adapter = {}
function adapter:Allow(player)
if player == nil then return false, "MissingPlayer" end
local ok, now = pcall(clock)
if not ok or not finite(now) then
return false, "InvalidClock"
end
local bucket = buckets[player]
if not bucket then
bucket = {tokens = capacity, last = now}
buckets[player] = bucket
elseif now < bucket.last then
return false, "ClockWentBackwards"
else
bucket.tokens = math.min(capacity,
bucket.tokens + (now - bucket.last) * refillPerSecond)
bucket.last = now
end
if bucket.tokens < 1 then return false, "RateLimited" end
bucket.tokens -= 1
return true, "Allowed"
end
function adapter:Forget(player)
buckets[player] = nil
end
return adapter
end
return Limiter
Подключи серверный обработчик осмысленно #
Для примера создай RemoteEvent GetHint в ReplicatedStorage и Script сервера рядом с модулем. Script получает сервисы, подключает модуль через require и создаёт limiter с параметрами 4, 2 и os.clock. Обработчик использует Player, который передаёт Roblox, затем проверяет запрос и отправляет только заранее определённый текст подсказки этому игроку.
Пример ожидает атрибут TutorialStep со значением 1, назначенный существующей серверной логикой обучения. Он сам не начинает обучение и не меняет этот атрибут по запросу клиента. Если такого состояния нет, подсказка не отправится. Клиентский интерфейс тоже не включён в пример: его нужно отдельно подключить к GetHint и обработке ответа. Серверный Script показан для интеграции, а не как проверенная готовая игра.
-- Server Script; create ReplicatedStorage.GetHint as a RemoteEvent first.
-- TutorialStep must be maintained by your existing SERVER gameplay logic.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Limiter = require(
game:GetService("ServerScriptService"):WaitForChild("HintRequestLimiter")
)
local request = ReplicatedStorage:WaitForChild("GetHint")
assert(request:IsA("RemoteEvent"), "GetHint must be a RemoteEvent")
local limiter = Limiter.new(4, 2, os.clock)
request.OnServerEvent:Connect(function(player, hintName)
-- Each attempt consumes quota before further processing.
if not limiter:Allow(player) then return end
if type(hintName) ~= "string" or #hintName > 32 then return end
if hintName ~= "FindWorkshop" then return end
if player:GetAttribute("TutorialStep") ~= 1 then return end
local character = player.Character
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
if not humanoid or humanoid.Health <= 0 then return end
request:FireClient(player, "Hint", "Look for the workshop sign.")
end)
Players.PlayerRemoving:Connect(function(player)
limiter:Forget(player)
end)
Реши, что именно расходует попытку #
В нашем обработчике Allow вызывается до проверки строки, поэтому попытка расходует жетон даже при неверном имени подсказки или неподходящем этапе обучения. Это сознательное правило учебного примера. Его удобно объяснить так: ограничивается число обращений к обработчику, а не только число успешно выданных подсказок. Некорректный ввод не получает бесплатный бесконечный путь к последующим проверкам.
Не возвращай потраченный жетон автоматически после каждого отказа, иначе поток неправильных запросов может обходить выбранное правило. При этом игровой смысл действия всё равно должен проверяться отдельно. Наличие жетона не означает доступ к награде, владение предметом, завершение задания или право изменять чужой объект. Для другой задачи порядок и стоимость попытки нужно явно пересмотреть.
Не отвечай на каждый отвергнутый повтор #
Учебный обработчик молча прекращает ограниченный запрос. Если вместо этого рассылать ответ или писать подробную строку журнала при каждом отказе, поток повторов может создавать новую работу уже на пути отказа. Не делай из ограничителя источник бесконечных уведомлений. Для наблюдения лучше заранее выбрать ограниченный способ сводить количество отказов, а не сохранять любой входной пакет.
Молчаливый отказ означает, что интерфейс должен уметь вернуть кнопку в рабочее состояние по разумному локальному ожиданию и объяснить пользователю повторную попытку. В этой статье клиентский таймер не реализован. Не оставляй кнопку навсегда отключённой в ожидании ответа, который сервер намеренно не отправляет. Разделяй удобство интерфейса и решение сервера о разрешении.
Проверка с управляемыми часами #
Локальный Luau-тест передаёт в конструктор собственную функцию часов. В момент 0 игрок A делает четыре разрешённые попытки; пятая получает RateLimited. В момент 0,25 накоплено полжетона, поэтому новая попытка ещё запрещена. В момент 0,5 доступен один жетон: одна попытка разрешается, следующая — нет. Числа описывают тест, а не посещения реальной игры.
Параллельно в том же локальном тесте проверяется независимая корзина B. После большого простоя A получает не больше четырёх попыток. Некорректные часы и движение назад не создают запас. Мы проверили ошибки настройки, очистку и дробную скорость пополнения. Эти проверки выполнялись с подставными объектами и временем, без RemoteEvent Roblox, настоящих игроков и измерения производительности сервера.
| Проверка | Ожидание |
|---|---|
| Пятая попытка сразу | RateLimited |
| Запрос другого игрока | Независимая корзина |
| Длинный простой | Не более 4 жетонов |
| Часы идут назад | Нет пополнения |
| Некорректные часы | InvalidClock |
| Состояние после очистки | Новая полная корзина |
Уход игрока требует очистки #
Подключи Players.PlayerRemoving к limiter:Forget(player). Иначе таблица продолжит удерживать состояние ушедших игроков до завершения сервера. Не вызывай Forget при обычном отказе: это создаст новую полную корзину на следующей попытке и отменит смысл ограничения. Очистка — часть жизненного цикла игрока, а не способ вернуть кнопке разрешение.
Новый вход после удаления состояния начинает новую корзину. Это ожидаемое поведение локального примера, но не защита от повторных подключений и не глобальное ограничение аккаунта. Состояние не сохраняется между серверами. Для задачи, которая требует учитывать разные сеансы, нужно отдельное проектирование; не обещай такую гарантию на основании очистки таблицы.
Ограничение частоты не ограничивает всю работу #
После разрешённого вызова другая функция может начать длительную задачу. Наш модуль не ждёт её завершения и не контролирует число уже запущенных задач. Даже допустимая частота может быть слишком высокой для тяжёлого действия. Посмотри отдельно, что происходит после проверки: создаются ли модели, запускаются ли запросы к API, затрагиваются ли другие игроки.
Для обработки покупок, сохранения данных или общей экономики нужны правила соответствующих систем, а не только жетоны. Также этот пример не проверяет расстояние до физической станции: подсказка специально является текстовым действием без такого требования. Если переносишь ограничитель на взаимодействие с предметом, добавь необходимые серверные проверки позиции, прав и состояния. Мы не меняли код существующих игр автора.
Что передать чату разработки #
Сохрани действие, которое ограничиваешь, смысл расхода попытки, вместимость, скорость и список серверных проверок. Добавь маршруты локального теста и отдельную пометку о том, что реальный сетевой и нагрузочный тест ещё нужен. Не передавай число четыре как универсальную настройку для любых кнопок или оружия: оно связано только с этим упражнением.
Попроси разработчика проверить поведение двух игроков, длинного простоя, ухода и неподходящего запроса, а также возврат кнопки после молчаливого отказа. Отдельно перечисли не реализованные задачи: общий бюджет сервера, параллельные операции, разные сеансы и клиентский интерфейс. Такой список позволяет использовать идею в следующей игре, сохраняя понятные границы готовности.
Первоисточники
Roblox Creator Hub — Client-server boundaryLuau — Standard library
Roblox Creator Hub — os