پرش به محتوای اصلی

پروژهٔ ۹

Real-time Multiplayer Game Server

سرور یک بازی آنلاین چندنفره را از صفر می‌سازید: اتصال WebSocket، game loop سرورمحور، هم‌گام‌سازی وضعیت، matchmaking و آزمون بار با هزاران بازیکن مصنوعی.

Seniorزمان پیشنهادی: ۳۵ ساعتزمان مطالعه: ۹ دقیقه
در این صفحه

هدف

در پایان این پروژه یک سرور بازی دارید که:

  • چند هزار اتصال هم‌زمان WebSocket را روی یک ماشین نگه می‌دارد.
  • منطق بازی را با نرخ ثابت ۲۰ tick در ثانیه و به شکل سرورمحور اجرا می‌کند.
  • بازیکن‌ها را در اتاق‌های جدا قرار می‌دهد و هر اتاق را مستقل از بقیه پیش می‌برد.
  • قطع و وصل شدن اتصال را بدون از دست رفتن بازی تحمل می‌کند.
  • با metric، trace و آزمون بار قابل اندازه‌گیری و قابل دفاع است.

این پروژه برای کسی که از Django می‌آید مهم است، چون نقطه‌ای است که مدل request و response دیگر جواب نمی‌دهد. اینجا سرور باید خودش، بدون درخواست کاربر، هر ۵۰ میلی‌ثانیه برای همه پیام بفرستد.

چه چیزی می‌سازید

یک بازی arena دوبعدی و ساده: هر بازیکن یک دایره است، در زمین حرکت می‌کند و امتیاز جمع می‌کند. ظاهر بازی عمداً ساده است تا تمرکز روی سرور بماند. همهٔ سختی کار در این سه پرسش است:

  1. اگر دو بازیکن هم‌زمان به یک امتیاز برسند، چه کسی آن را می‌گیرد؟
  2. اگر پیام یک بازیکن ۲۰۰ میلی‌ثانیه دیر برسد، بقیه چه می‌بینند؟
  3. اگر یک کلاینت نتواند پیام‌ها را به‌موقع بخواند، سرور چه می‌کند؟

پاسخ هر سه در طراحی سرور است، نه در گرافیک.

اجزای تحویلی

جزءتوضیح
cmd/gameserverسرور اصلی: WebSocket، اتاق‌ها و game loop
cmd/botبازیکن مصنوعی برای آزمون بار
internal/protocolتعریف پیام‌ها، نسخه‌بندی و اعتبارسنجی
internal/simشبیه‌سازی قطعی بازی، بدون هیچ وابستگی به شبکه
internal/roomچرخهٔ عمر اتاق و مدیریت بازیکن‌ها
internal/matchmakingصف انتظار و ساخت اتاق
web/کلاینت مرورگر با TypeScript و Canvas
deploy/Dockerfile، compose و داشبورد metric

معماری

در حال رسم نمودار…

سه تصمیم اصلی این معماری را شکل می‌دهد:

  • هر اتاق یک goroutine مالک دارد. فقط همان goroutine وضعیت بازی را تغییر می‌دهد. ورودی بازیکن‌ها از راه channel می‌رسد و هیچ قفلی روی وضعیت بازی لازم نیست.
  • هر اتصال دو goroutine دارد. یکی فقط می‌خواند و یکی فقط می‌نویسد. صف نوشتن ظرفیت محدود دارد تا کلاینت کُند، اتاق را کُند نکند.
  • شبیه‌سازی از شبکه جداست. بستهٔ internal/sim یک تابع خالص است: وضعیت قبلی و ورودی‌ها را می‌گیرد و وضعیت بعدی را می‌دهد. برای همین بدون شبکه و بدون ساعت واقعی تست می‌شود.

مسیر یک ورودی

در حال رسم نمودار…

کلاینت منتظر سرور نمی‌ماند و حرکت را همان لحظه نشان می‌دهد (client-side prediction). وقتی snapshot رسید، وضعیت خودش را با پاسخ سرور تطبیق می‌دهد (reconciliation). حرف آخر همیشه با سرور است.

مقایسه با دنیای Python

موضوعGoPython
مدل اتصالدو goroutine برای هر اتصالیک consumer روی event loop
حلقهٔ بازییک goroutine با time.Ticker در همان پروسهیک task جدا یا worker بیرونی
ارسال گروهیchannel درون حافظهchannel layer روی Redis
استفاده از چند هستهخودکار، با یک پروسهچند پروسه و هماهنگی بیرونی
کار سنگین CPU در tickمستقیم در همان goroutineevent loop را قفل می‌کند
هزینهٔ هر اتصال بیکارچند کیلوبایتوابسته به worker و سرور ASGI
همان مسئله در Go و در Django Channels

پیش‌نیازها

  • همهٔ فاز ۳: goroutine، channel، select، context و sync.
  • از فاز ۴: net/http، middleware، logging ساخت‌یافته و graceful shutdown.
  • از فاز ۵: profiling با pprof، metric و Docker.
  • از فاز ۶: درس‌های WebSocket، طراحی سرور بی‌درنگ، game loop و هم‌گام‌سازی وضعیت.

گام‌های پروژه

  1. اتصال پایدار

    سرور WebSocket با handshake، حلقهٔ خواندن و نوشتن جدا، ping و pong، محدودیت اندازهٔ پیام و graceful shutdown.

    معیار پذیرش: هزار اتصال بیکار، بدون نشت goroutine باز و بسته می‌شوند و تست با -race سبز است.

  2. پروتکل پیام و lobby

    یک envelope نسخه‌دار برای همهٔ پیام‌ها، اعتبارسنجی ورودی و عملیات ورود و خروج از اتاق.

    معیار پذیرش: هر پیام نامعتبر با کد خطای مشخص رد می‌شود و هیچ ورودی‌ای سرور را به panic نمی‌رساند. این با fuzz test ثابت می‌شود.

  3. game loop سرورمحور

    حلقهٔ tick با نرخ ثابت، صف ورودی برای هر بازیکن و شبیه‌سازی قطعی.

    معیار پذیرش: با ورودی یکسان، خروجی شبیه‌سازی همیشه یکسان است. تست‌ها با ساعت جعلی اجرا می‌شوند و به time.Sleep نیاز ندارند.

  4. هم‌گام‌سازی وضعیت

    ارسال snapshot و سپس delta، client-side prediction، reconciliation و interpolation در کلاینت مرورگر.

    معیار پذیرش: با ۱۵۰ میلی‌ثانیه تأخیر و ۵ درصد packet loss مصنوعی، حرکت بازیکن‌ها روان می‌ماند و وضعیت کلاینت‌ها به سرور همگرا می‌شود.

  5. matchmaking و چرخهٔ عمر اتاق

    صف انتظار، ساخت و بستن اتاق، اتصال دوباره با session token و برخورد با کلاینت کُند.

    معیار پذیرش: بازیکنی که ۱۰ ثانیه قطع شده، به همان اتاق برمی‌گردد. کلاینتی که صف نوشتنش پر شود، قطع می‌شود و اتاق کُند نمی‌شود.

  6. امنیت و تاب‌آوری

    احراز هویت با token، بررسی Origin، rate limiting روی ورودی و رد کردن حرکت‌های ناممکن.

    معیار پذیرش: کلاینت دست‌کاری‌شده نمی‌تواند سریع‌تر از حد مجاز حرکت کند یا بیش از سهمش پیام بفرستد.

  7. اندازه‌گیری، آزمون بار و انتشار

    metric برای مدت هر tick و اندازهٔ صف‌ها، بازیکن مصنوعی برای آزمون بار، profiling و انتشار با Docker.

    معیار پذیرش: با ۵٬۰۰۰ بازیکن مصنوعی روی یک ماشین، صدک ۹۹ مدت tick زیر ۱۰ میلی‌ثانیه می‌ماند و نتیجه در گزارش پروژه ثبت می‌شود.

پروتکل پیام

همهٔ پیام‌ها یک envelope مشترک دارند. در گام‌های اول قالب JSON است تا اشکال‌زدایی ساده بماند:

JSON
{
  "v": 1,
  "type": "input",
  "seq": 41,
  "payload": { "dx": 1, "dy": 0 }
}
پیامجهتکاربرد
helloکلاینت به سرورمعرفی نسخهٔ پروتکل و token
joinکلاینت به سرورورود به صف matchmaking
inputکلاینت به سرورورودی بازیکن با شمارهٔ ترتیب
snapshotسرور به کلاینتوضعیت کامل یا delta در یک tick
eventسرور به کلاینترویدادهای مهم مثل گرفتن امتیاز
errorسرور به کلاینتکد و شرح خطا

تصمیم‌های مهندسی که باید از آن‌ها دفاع کنید

  • نرخ tick: چرا ۲۰ و نه ۶۰؟ هزینهٔ CPU و پهنای باند در برابر دقت.
  • ظرفیت صف نوشتن: وقتی پر شد، پیام را دور می‌ریزید یا اتصال را می‌بندید؟ برای snapshot و برای event پاسخ یکسان نیست.
  • مالکیت وضعیت: چرا یک goroutine مالک و نه sync.Mutex؟ چه زمانی این انتخاب گران می‌شود؟
  • مقیاس افقی: اتاق به یک سرور چسبیده است. بازیکن چطور به سرور درست می‌رسد و اگر آن سرور از کار بیفتد چه می‌شود؟
  • زمان: چرا شبیه‌سازی به‌جای ساعت واقعی با شمارهٔ tick کار می‌کند؟

برای هر تصمیم یک یادداشت کوتاه در پوشهٔ docs/decisions می‌نویسید. این همان چیزی است که در مصاحبهٔ Senior از شما می‌پرسند.

اشتباه‌های رایج

نتیجهٔ نهایی

در پایان، این موارد را در مخزن خود دارید:

  • سرور بازی با تست واحد، تست یکپارچه و -race سبز
  • کلاینت مرورگر قابل بازی
  • بازیکن مصنوعی و گزارش آزمون بار با عدد واقعی
  • داشبورد metric و نمونهٔ profile پیش و پس از بهینه‌سازی
  • یادداشت تصمیم‌های مهندسی
  • Dockerfile و دستور اجرای یک‌خطی

ویرایش این صفحه در GitHub