Real-time Multiplayer Game Server
سرور یک بازی آنلاین چندنفره را از صفر میسازید: اتصال WebSocket، game loop سرورمحور، همگامسازی وضعیت، matchmaking و آزمون بار با هزاران بازیکن مصنوعی.
در این صفحه
هدف
در پایان این پروژه یک سرور بازی دارید که:
- چند هزار اتصال همزمان WebSocket را روی یک ماشین نگه میدارد.
- منطق بازی را با نرخ ثابت ۲۰ tick در ثانیه و به شکل سرورمحور اجرا میکند.
- بازیکنها را در اتاقهای جدا قرار میدهد و هر اتاق را مستقل از بقیه پیش میبرد.
- قطع و وصل شدن اتصال را بدون از دست رفتن بازی تحمل میکند.
- با metric، trace و آزمون بار قابل اندازهگیری و قابل دفاع است.
این پروژه برای کسی که از Django میآید مهم است، چون نقطهای است که مدل request و response دیگر جواب نمیدهد. اینجا سرور باید خودش، بدون درخواست کاربر، هر ۵۰ میلیثانیه برای همه پیام بفرستد.
چه چیزی میسازید
یک بازی arena دوبعدی و ساده: هر بازیکن یک دایره است، در زمین حرکت میکند و امتیاز جمع میکند. ظاهر بازی عمداً ساده است تا تمرکز روی سرور بماند. همهٔ سختی کار در این سه پرسش است:
- اگر دو بازیکن همزمان به یک امتیاز برسند، چه کسی آن را میگیرد؟
- اگر پیام یک بازیکن ۲۰۰ میلیثانیه دیر برسد، بقیه چه میبینند؟
- اگر یک کلاینت نتواند پیامها را بهموقع بخواند، سرور چه میکند؟
پاسخ هر سه در طراحی سرور است، نه در گرافیک.
اجزای تحویلی
معماری
سه تصمیم اصلی این معماری را شکل میدهد:
- هر اتاق یک goroutine مالک دارد. فقط همان goroutine وضعیت بازی را تغییر میدهد. ورودی بازیکنها از راه channel میرسد و هیچ قفلی روی وضعیت بازی لازم نیست.
- هر اتصال دو goroutine دارد. یکی فقط میخواند و یکی فقط مینویسد. صف نوشتن ظرفیت محدود دارد تا کلاینت کُند، اتاق را کُند نکند.
- شبیهسازی از شبکه جداست. بستهٔ
internal/simیک تابع خالص است: وضعیت قبلی و ورودیها را میگیرد و وضعیت بعدی را میدهد. برای همین بدون شبکه و بدون ساعت واقعی تست میشود.
مسیر یک ورودی
کلاینت منتظر سرور نمیماند و حرکت را همان لحظه نشان میدهد (client-side prediction). وقتی snapshot رسید، وضعیت خودش را با پاسخ سرور تطبیق میدهد (reconciliation). حرف آخر همیشه با سرور است.
مقایسه با دنیای Python
پیشنیازها
- همهٔ فاز ۳: goroutine، channel،
select،contextوsync. - از فاز ۴:
net/http، middleware، logging ساختیافته و graceful shutdown. - از فاز ۵: profiling با
pprof، metric و Docker. - از فاز ۶: درسهای WebSocket، طراحی سرور بیدرنگ، game loop و همگامسازی وضعیت.
گامهای پروژه
اتصال پایدار
سرور WebSocket با handshake، حلقهٔ خواندن و نوشتن جدا، ping و pong، محدودیت اندازهٔ پیام و graceful shutdown.
معیار پذیرش: هزار اتصال بیکار، بدون نشت goroutine باز و بسته میشوند و تست با
-raceسبز است.پروتکل پیام و lobby
یک envelope نسخهدار برای همهٔ پیامها، اعتبارسنجی ورودی و عملیات ورود و خروج از اتاق.
معیار پذیرش: هر پیام نامعتبر با کد خطای مشخص رد میشود و هیچ ورودیای سرور را به panic نمیرساند. این با fuzz test ثابت میشود.
game loop سرورمحور
حلقهٔ tick با نرخ ثابت، صف ورودی برای هر بازیکن و شبیهسازی قطعی.
معیار پذیرش: با ورودی یکسان، خروجی شبیهسازی همیشه یکسان است. تستها با ساعت جعلی اجرا میشوند و به
time.Sleepنیاز ندارند.همگامسازی وضعیت
ارسال snapshot و سپس delta، client-side prediction، reconciliation و interpolation در کلاینت مرورگر.
معیار پذیرش: با ۱۵۰ میلیثانیه تأخیر و ۵ درصد packet loss مصنوعی، حرکت بازیکنها روان میماند و وضعیت کلاینتها به سرور همگرا میشود.
matchmaking و چرخهٔ عمر اتاق
صف انتظار، ساخت و بستن اتاق، اتصال دوباره با session token و برخورد با کلاینت کُند.
معیار پذیرش: بازیکنی که ۱۰ ثانیه قطع شده، به همان اتاق برمیگردد. کلاینتی که صف نوشتنش پر شود، قطع میشود و اتاق کُند نمیشود.
امنیت و تابآوری
احراز هویت با token، بررسی
Origin، rate limiting روی ورودی و رد کردن حرکتهای ناممکن.معیار پذیرش: کلاینت دستکاریشده نمیتواند سریعتر از حد مجاز حرکت کند یا بیش از سهمش پیام بفرستد.
اندازهگیری، آزمون بار و انتشار
metric برای مدت هر tick و اندازهٔ صفها، بازیکن مصنوعی برای آزمون بار، profiling و انتشار با Docker.
معیار پذیرش: با ۵٬۰۰۰ بازیکن مصنوعی روی یک ماشین، صدک ۹۹ مدت tick زیر ۱۰ میلیثانیه میماند و نتیجه در گزارش پروژه ثبت میشود.
پروتکل پیام
همهٔ پیامها یک envelope مشترک دارند. در گامهای اول قالب JSON است تا اشکالزدایی ساده بماند:
تصمیمهای مهندسی که باید از آنها دفاع کنید
- نرخ tick: چرا ۲۰ و نه ۶۰؟ هزینهٔ CPU و پهنای باند در برابر دقت.
- ظرفیت صف نوشتن: وقتی پر شد، پیام را دور میریزید یا اتصال را میبندید؟ برای snapshot و برای event پاسخ یکسان نیست.
- مالکیت وضعیت: چرا یک goroutine مالک و نه
sync.Mutex؟ چه زمانی این انتخاب گران میشود؟ - مقیاس افقی: اتاق به یک سرور چسبیده است. بازیکن چطور به سرور درست میرسد و اگر آن سرور از کار بیفتد چه میشود؟
- زمان: چرا شبیهسازی بهجای ساعت واقعی با شمارهٔ tick کار میکند؟
برای هر تصمیم یک یادداشت کوتاه در پوشهٔ docs/decisions مینویسید. این همان چیزی است که در مصاحبهٔ Senior از شما میپرسند.
اشتباههای رایج
نتیجهٔ نهایی
در پایان، این موارد را در مخزن خود دارید:
- سرور بازی با تست واحد، تست یکپارچه و
-raceسبز - کلاینت مرورگر قابل بازی
- بازیکن مصنوعی و گزارش آزمون بار با عدد واقعی
- داشبورد metric و نمونهٔ profile پیش و پس از بهینهسازی
- یادداشت تصمیمهای مهندسی
- Dockerfile و دستور اجرای یکخطی