التطوير / ROBLOX
حدود الطلبات في Roblox: حماية تلميح الخادم من النقر المتكرر
نبني محدداً تعليمياً لتلميح الورشة: أربع محاولات فورية ثم تجدد وحدتان كل ثانية. نشرح حالة مستقلة للاعبين ووقت الخادم واستعادة الزر واختبارات محلية دون إرسال حركة اختبار حقيقية إلى Roblox.
اختر فعلاً محدداً لتقييده #
تخيل ورشة يضغط فيها المبتدئ «أين المحطة؟» لطلب تلميح. قد تكون النقرات السريعة خطأ عفوياً، أو محاولة للحصول على رد متأخر، أو تكراراً غير مرغوب. إذا بدأ كل طلب عملاً مكلفاً، فقد يولد زر صغير حملاً غير ضروري. صف أولاً عملية الخادم بعد النقر ولماذا تحتاج إلى تقييد تكرارها.
نستخدم تلميحاً نصياً، لا شراء أو مكافأة أو حفظ ملف. هذا سيناريو تعليمي أصلي، وليس ميزة أضيفت إلى ألعاب صاحب الموقع. أربع محاولات ووحدتان في الثانية إعدادات اختيرت للشرح. ليست حصص Roblox ولا توصية عامة لكل فعل. لم ننفذ اختبار حمل فعلياً متعدد اللاعبين.
زر العميل لا يحدد قاعدة الخادم #
قد يساعد تعطيل الزر مؤقتاً وعرض انتظار الرد اللاعب على فهم أن طلبه أرسل. لكن الخادم يجب أن يقرر بصورة مستقلة هل يسمح باستدعاء آخر. تحذر وثائق Roblox صراحة من الاعتماد على حد تكرار موجود في العميل وحده.
لا تقبل عدد المحاولات المتبقية الذي يرسله العميل أو وقت نقرتِه السابقة باعتباره تصريحاً. يحفظ نموذجنا الرصيد والوقت في الخادم. يطلب العميل التلميح ويتخذ الخادم القرار. تقييد التكرار لا يحول المعاملات الخاطئة إلى صحيحة؛ التحقق من النوع والفعل المسموح وحالة اللعب يظل متطلباً منفصلاً.
دلو الوحدات يسمح بسلسلة قصيرة #
تخيل لكل لاعب دلواً يتسع لأربع وحدات. تستهلك المحاولة وحدة واحدة. تعود وحدتان كل ثانية، لكن الرصيد لا يتجاوز السعة. لذلك يمكن إجراء أربع محاولات فوراً، بينما تحتاج السلسلة المستمرة إلى انتظار التجدد. هذا نهج token bucket الذي تصفه Roblox.
لا يعني ذلك «أربعة استدعاءات في كل ثانية تقويمية». قد يعيد انتظار قصير جزءاً من وحدة فقط. ما لم تتوفر وحدة كاملة، يرفض الطلب التالي. نختبر التجدد الجزئي كي يستطيع القارئ تفسير التأخير بدلاً من اعتباره خللاً عشوائياً في الزر.
| الوقت | المحاولة | النتيجة والرصيد |
|---|---|---|
| 0 | الأولى إلى الرابعة | مسموح: 4 → 0 |
| 0 | الخامسة | مرفوض: 0 |
| 0.25 s | محاولة جديدة | مرفوض: 0.5 |
| 0.5 s | محاولة جديدة | مسموح: 0 |
افصل حالة كل لاعب #
إذا أنفق A وحداته الأربع، فلا ينبغي منع طلب B التالي بسببه. تستخدم الدلاء كائناً من نوع Player يصل إلى OnServerEvent في الخادم مفتاحاً للحالة. لا تستبدله باسم أو هوية يرسلها العميل كمعامل إضافي. وإلا أصبح اختيار دلو لاعب آخر معتمداً على إدخال غير موثوق.
لا تحد الدلاء المستقلة الحركة الإجمالية لجميع اللاعبين. قد ينفق كثير من المستخدمين محاولاتهم المسموحة في الوقت نفسه. تتطلب الأفعال المكلفة أو اتصالات الخلفية تحليلاً منفصلاً للمعدل الكلي والطوابير والكلفة. لا يضم النموذج ميزانية مشتركة للخادم أو حداً للمهام المتزامنة أو حالة موزعة بين خوادم.
افهم 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 وينشئ المحدد بقيم 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 المستقل. يعيد الخمول الطويل أربع محاولات على الأكثر. لا يخلق الوقت غير الصالح أو المتراجع رصيداً إضافياً. يجري فحص الإعداد والتنظيف والمعدل الكسري. كل الاختبارات بكائنات بديلة ووقت مضبوط، دون remotes حقيقية في Roblox أو لاعبين أو قياس أداء الخادم.
| الفحص | المتوقع |
|---|---|
| محاولة خامسة فوراً | RateLimited |
| طلب لاعب آخر | دلو مستقل |
| خمول طويل | أربع وحدات على الأكثر |
| رجوع الساعة | لا تجدد |
| ساعة غير صالحة | InvalidClock |
| الحالة بعد التنظيف | دلو جديد ممتلئ |
نظف الحالة عند المغادرة #
اربط Players.PlayerRemoving مع limiter:Forget(player). وإلا يحتفظ الجدول بحالة اللاعبين المغادرين حتى ينتهي الخادم. لا تستدع Forget بعد رفض عادي: سيحصل الطلب التالي على دلو جديد ممتلئ وتزول فائدة الحد. التنظيف جزء من دورة حياة اللاعب، وليس أداة لاستعادة إذن الزر.
الدخول بعد حذف الحالة يبدأ بدلو جديد. هذا سلوك محلي متوقع، لا حماية من إعادة الاتصال ولا حد شامل للحساب. لا تستمر الحالة بين الخوادم. متطلبات تمتد إلى جلسات مختلفة تحتاج إلى تصميم خاص؛ حذف إدخال من الجدول لا يثبت تلك الضمانات.
تقييد التكرار لا يحد كل العمل اللاحق #
قد يبدأ الطلب المسموح مهمة طويلة في دالة أخرى. لا ينتظر نموذجنا انتهاءها ولا يعد المهام التي بدأت. قد يكون المعدل المسموح نفسه كبيراً لعملية مكلفة. افحص ما يحدث بعد الإذن: هل تنسخ نماذج كبيرة أو تتصل بـ API أو تؤثر في لاعبين آخرين؟
تحتاج المشتريات والحفظ والاقتصاد المشترك إلى قواعد أنظمتها، لا إلى وحدات فقط. كما لا يفحص المثال المسافة إلى محطة مادية، لأن التلميح نصي ولا يفرض هذا الشرط. عند تكييفه لتفاعل مع كائن، أضف فحوص الخادم اللازمة للموقع والحقوق والحالة. لم نغير كود ألعاب صاحب الموقع الحالية.
جهز تسليم التطوير #
احفظ معاً الفعل المحدود ومعنى إنفاق محاولة والسعة والتجدد وفحوص الخادم المطلوبة. أضف مسارات الاختبار المحلي واذكر أن اختبارات الشبكة والحمل الفعلية لم تتم. لا تقدم الرقم أربعة إعداداً عاماً لكل زر أو سلاح؛ يخص هذا التمرين فقط.
اطلب من المطور فحص لاعبين وخمول طويل ومغادرة وطلب غير صالح، واستعادة الزر بعد رفض صامت. اذكر الوظائف الغائبة منفصلة: ميزانية خادم مشتركة وعمليات متزامنة وحالة بين الجلسات وواجهة عميل. هكذا تستخدم الفكرة في اللعبة التالية مع بقاء حدود الجاهزية واضحة.
المصادر الأصلية
Roblox Creator Hub — Client-server boundaryLuau — Standard library
Roblox Creator Hub — os