Roblox Guidebook知识库
简体中文 ⌄

开发 / ROBLOX

Roblox 请求限流:保护服务器提示,避免反复点击

为工坊文字提示制作教学限流器:允许四次立即尝试,再以每秒两个令牌的速度补充。解释玩家独立状态、服务器时间、按钮恢复和本地检查,不向 Roblox 发送真实测试流量。

更新日期:

先选择需要限制的具体动作 #

设想新手在工坊点击“工作站在哪里?”请求提示。快速连续点击可能是误操作、等待延迟回复,也可能是不希望出现的重复。如果每次请求都启动昂贵工作,小按钮也会造成不必要的负担。先写明点击之后运行哪项服务器操作,以及为什么需要限制频率。

我们选择文字提示,不涉及购买、奖励或档案保存。这是原创教学场景,不是已添加到网站作者游戏中的功能。四次尝试与每秒两个令牌,是为了说明算法选择的参数,不是 Roblox 平台配额,也不是所有游戏动作的通用建议。本教程没有进行真实多人负载测试。

客户端按钮不决定服务器规则 #

暂时禁用按钮并显示等待回复,可以帮助玩家理解请求已经发出。但服务器仍必须独立判断下一次调用是否允许。Roblox 文档明确提醒,不应只依赖客户端的频率限制。界面中的等待提示解决操作理解问题,并不能替代服务器判断。

不要接受客户端声称的剩余请求次数或上次点击时间作为许可依据。我们的模块在服务器保存余额与时间。客户端请求提示,服务器作出决定。频率控制不会把错误参数变成正确参数;类型、允许的动作和游戏状态,仍要分别验证。

令牌桶允许短时间连续尝试 #

想象每名玩家都有一个最多容纳四个令牌的桶。一次尝试消耗一个令牌,每秒恢复两个,但余额不超过容量。因此可以立即尝试四次,持续请求则要等待补充。这是 Roblox 文档介绍的 token bucket 思路。

它不同于“每个日历秒内四次调用”。短暂等待可能只补充部分令牌。没有一个完整令牌时,下一次尝试仍被拒绝。我们测试小数补充,让读者能够解释等待时间,而不是把它当作随机按钮故障。

教学令牌桶:4 与每秒 2打开大图 ↗
虚构的本地测试路径,时间相对于练习开始计算。
时间尝试结果与余额
0第一次至第四次允许:4 → 0
0第五次拒绝:0
0.25 s新尝试拒绝:0.5
0.5 s新尝试允许:0

每名玩家保存独立状态 #

玩家 A 用完四个令牌,不应因此阻止 B 的下一次请求。桶表使用服务器 OnServerEvent 收到的 Player 作为键。不要用客户端额外发送的玩家名或身份编号替换这个对象,否则选择其他玩家的桶就会依赖不可信输入。

独立桶不会限制所有玩家的总流量。大量用户可能同时消耗各自允许的尝试。昂贵动作或后端请求,需要另外分析整体频率、队列和操作成本。这个模块没有共享服务器预算、并发任务上限或跨服务器状态。

理解教学 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

明确接入服务器处理函数 #

在 ReplicatedStorage 创建名为 GetHint 的 RemoteEvent,并在模块旁放置服务器 Script。Script 获取服务、通过 require 加载模块,使用 4、2 与 os.clock 创建限流器。处理函数使用 Roblox 提供的 Player,检查请求,再只向这名玩家发送预先定义的文字。

示例要求现有服务器教学逻辑维护 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 的独立桶。长时间闲置最多恢复四次尝试,无效时间与倒退不会制造额外余额。配置校验、清理和小数恢复速度也被检查。全部测试使用替代对象和受控时间,没有真实 Roblox 远程事件、玩家或服务器性能测量。

检查预期结果
立即第五次尝试RateLimited
另一玩家请求独立令牌桶
长时间闲置最多 4 个令牌
时间倒退不补充
无效时钟InvalidClock
清理后的状态新的满桶

玩家离开时清理 #

把 Players.PlayerRemoving 接到 limiter:Forget(player)。否则表会保留已离开玩家的状态,直到服务器结束。不要在普通拒绝后调用 Forget:下一次请求会得到新的满桶,取消频率限制的意义。清理属于玩家生命周期管理,不是恢复按钮许可的工具。

清理后重新进入会从新桶开始。这是预期的本地行为,不是防止反复重连或实现账号全局限流。状态不会跨服务器保存。涉及多个会话的要求需要单独设计;删除表项并不能证明这些保证。

限制频率不等于限制全部后续工作 #

允许的请求可能在其他函数中启动长任务。模块不等待任务结束,也不计算已经运行的任务数。即使符合所选频率,昂贵操作仍可能承受不了。检查许可之后发生什么:是否复制大型模型、调用 API 或影响其他玩家?

购买、数据保存和共享经济系统需要各自的规则,不能只有令牌。本例也没有检查到实体工作站的距离,因为文字提示刻意没有这种要求。适配物体交互时,应增加需要的服务器位置、权限与状态验证。我们没有修改作者现有游戏代码。

准备交接给开发者的材料 #

把受限动作、消耗尝试的含义、容量、补充速度和服务器校验一起保存。附上本地测试路径,明确标记真实网络与负载测试仍待完成。不要把四作为所有按钮或武器的统一设置;这个数字只属于当前练习。

请开发者检查两名玩家、长时间闲置、离开、错误请求,以及静默拒绝后按钮是否恢复。单独列出缺失功能:共享服务器预算、并行操作、跨会话状态和客户端界面。这样可以把思路用于下一款游戏,同时保留清楚的完成边界。

原始资料

Roblox Creator Hub — Client-server boundary
Luau — Standard library
Roblox Creator Hub — os