Cloud Spanner شروع با توسعه بازی ها

1. مقدمه

Cloud Spanner یک سرویس پایگاه داده رابطه‌ای با مقیاس‌پذیر افقی، توزیع شده در سطح جهانی و کاملاً مدیریت شده است که تراکنش‌های ACID و معنایی SQL را بدون کاهش عملکرد و در دسترس بودن بالا ارائه می‌کند.

این ویژگی‌ها باعث می‌شود تا Spanner در معماری بازی‌هایی که می‌خواهند پایگاه جهانی بازیکنان را فعال کنند یا نگران ثبات داده‌ها هستند، مناسب باشد.

در این آزمایشگاه، شما دو سرویس Go ایجاد می‌کنید که با یک پایگاه داده منطقه‌ای Spanner تعامل می‌کنند تا بازیکنان را قادر به ثبت نام و شروع بازی کنند.

413fdd57bb0b68bc.png

در مرحله بعد، داده هایی را با استفاده از چارچوب بارگذاری پایتون Locust.io برای شبیه سازی بازیکنانی که ثبت نام کرده اند و بازی را انجام می دهند، تولید می کنید. و سپس از Spanner پرس و جو می کنید تا مشخص کنید چند بازیکن در حال بازی هستند و برخی آمار در مورد بازی های برنده شده بازیکنان در مقابل بازی های انجام شده.

در نهایت، منابعی که در این آزمایشگاه ایجاد شده اند را پاکسازی خواهید کرد.

چیزی که خواهی ساخت

به عنوان بخشی از این آزمایشگاه، شما:

  • یک نمونه Spanner ایجاد کنید
  • یک سرویس نمایه که در Go to handle player signup نوشته شده است، مستقر کنید
  • یک سرویس Matchmaking نوشته شده در Go را برای اختصاص دادن بازیکنان به بازی‌ها، تعیین برندگان و به‌روزرسانی آمار بازی بازیکنان ایجاد کنید.

چیزی که یاد خواهید گرفت

  • نحوه راه اندازی یک نمونه Cloud Spanner
  • نحوه ایجاد پایگاه داده و طرحواره بازی
  • نحوه استقرار برنامه های Go برای کار با Cloud Spanner
  • نحوه تولید داده با استفاده از Locust
  • چگونه داده ها را در Cloud Spanner جستجو کنیم تا به سؤالات مربوط به بازی ها و بازیکنان پاسخ دهیم.

آنچه شما نیاز دارید

  • یک پروژه Google Cloud که به یک حساب صورت‌حساب متصل است.
  • یک مرورگر وب، مانند کروم یا فایرفاکس .

2. راه اندازی و الزامات

یک پروژه ایجاد کنید

اگر قبلاً یک حساب Google (Gmail یا Google Apps) ندارید، باید یک حساب ایجاد کنید . به کنسول Google Cloud Platform ( consol.cloud.google.com ) وارد شوید و یک پروژه جدید ایجاد کنید.

اگر قبلاً پروژه ای دارید، روی منوی کشویی انتخاب پروژه در سمت چپ بالای کنسول کلیک کنید:

6c9406d9b014760.png

و روی دکمه "پروژه جدید" در گفتگوی حاصل کلیک کنید تا یک پروژه جدید ایجاد کنید:

949d83c8a4ee17d9.png

اگر قبلاً پروژه ای ندارید، باید یک دیالوگ مانند این را ببینید تا اولین پروژه خود را ایجاد کنید:

870a3cbd6541ee86.png

گفتگوی بعدی ایجاد پروژه به شما امکان می دهد جزئیات پروژه جدید خود را وارد کنید:

6a92c57d3250a4b3.png

شناسه پروژه را به خاطر بسپارید، که یک نام منحصر به فرد در تمام پروژه های Google Cloud است (نام بالا قبلاً گرفته شده است و برای شما کار نخواهد کرد، متأسفیم!). بعداً در این آزمایشگاه کد به عنوان PROJECT_ID نامیده خواهد شد.

در مرحله بعد، اگر قبلاً این کار را انجام نداده‌اید، برای استفاده از منابع Google Cloud و فعال کردن Cloud Spanner API، باید صورت‌حساب را در Developers Console فعال کنید .

15d0ef27a8fbab27.png

گذراندن این کد نباید بیش از چند دلار هزینه داشته باشد، اما اگر تصمیم به استفاده از منابع بیشتری داشته باشید یا آنها را در حال اجرا رها کنید، ممکن است بیشتر باشد (به بخش "پاکسازی" در انتهای این سند مراجعه کنید). قیمت Google Cloud Spanner در اینجا مستند شده است.

کاربران جدید Google Cloud Platform واجد شرایط استفاده آزمایشی رایگان 300 دلاری هستند که باید این نرم افزار کد را کاملاً رایگان کند.

Google Cloud Shell Setup

در حالی که Google Cloud و Spanner را می‌توان از راه دور از لپ‌تاپ شما کار کرد، در این نرم‌افزار از Google Cloud Shell استفاده می‌کنیم، یک محیط خط فرمان که در Cloud اجرا می‌شود.

این ماشین مجازی مبتنی بر دبیان با تمام ابزارهای توسعه که شما نیاز دارید بارگذاری شده است. این دایرکتوری اصلی 5 گیگابایتی دائمی را ارائه می دهد و در Google Cloud اجرا می شود و عملکرد شبکه و احراز هویت را بسیار افزایش می دهد. این بدان معنی است که تمام چیزی که برای این کد لبه نیاز دارید یک مرورگر است (بله، روی کروم بوک کار می کند).

  1. برای فعال کردن Cloud Shell از Cloud Console، کافی است روی Activate Cloud Shell کلیک کنید. gcLMt5IuEcJJNnMId-Bcz3sxCd0rZn7IzT_r95C8UZeqML68Y1efBG_B0VRp7hc7qiZTLAF-TXD7SsOadxn8uadgHhaLeASnVS3ZHK_OgjogdOgdOg3ZHK39gdOg 2A (تهیه و اتصال به محیط فقط چند لحظه طول می کشد).

JjEuRXGg0AYYIY6QZ8d-66gx_Mtc-_jDE9ijmbXLJSAXFvJt-qUpNtsBsYjNpv2W6BQSrDc1D-ARINNQ-1EkwUhz-iUK-FUCZhJ-NtjkWkWE2C w

Screen Shot 2017-06-14 at 10.13.43 PM.png

پس از اتصال به Cloud Shell، باید ببینید که قبلاً احراز هویت شده اید و پروژه قبلاً روی PROJECT_ID شما تنظیم شده است.

gcloud auth list

خروجی فرمان

Credentialed accounts:
 - <myaccount>@<mydomain>.com (active)
gcloud config list project

خروجی فرمان

[core]
project = <PROJECT_ID>

اگر به دلایلی پروژه تنظیم نشد، به سادگی دستور زیر را صادر کنید:

gcloud config set project <PROJECT_ID>

به دنبال PROJECT_ID خود هستید؟ بررسی کنید از چه شناسه ای در مراحل راه اندازی استفاده کرده اید یا آن را در داشبورد Cloud Console جستجو کنید:

158fNPfwSxsFqz9YbtJVZes8viTS3d1bV4CVhij3XPxuzVFOtTObnwsphlm6lYGmgdMFwBJtc-FaLrZU7XHAg_ZYoCrgombMRR3h-eolLPcvOZwZw51

Cloud Shell همچنین برخی از متغیرهای محیطی را به صورت پیش‌فرض تنظیم می‌کند که ممکن است هنگام اجرای دستورات آینده مفید باشند.

echo $GOOGLE_CLOUD_PROJECT

خروجی فرمان

<PROJECT_ID>

کد را دانلود کنید

در Cloud Shell می توانید کد این آزمایشگاه را دانلود کنید. این بر اساس نسخه نسخه 0.1.0 است، بنابراین آن برچسب را بررسی کنید:

git clone https://github.com/cloudspannerecosystem/spanner-gaming-sample.git
cd spanner-gaming-sample/

# Check out v0.1.0 release
git checkout tags/v0.1.0 -b v0.1.0-branch

خروجی فرمان

Cloning into 'spanner-gaming-sample'...
*snip*
Switched to a new branch 'v0.1.0-branch'

مولد بار Locust را راه اندازی کنید

Locust یک چارچوب تست بار پایتون است که برای آزمایش نقاط انتهایی REST API مفید است. در این کد لبه، ما 2 تست بارگذاری مختلف در دایرکتوری 'generators' داریم که برجسته خواهیم کرد:

  • authentication_server.py : شامل وظایفی برای ایجاد بازیکنان و دریافت یک پخش کننده تصادفی برای تقلید از جستجوهای تک نقطه ای است.
  • match_server.py : شامل وظایفی برای ایجاد بازی و بستن بازی است. با ایجاد بازی‌ها 100 بازیکن تصادفی که در حال حاضر بازی نمی‌کنند اختصاص می‌یابد. بسته شدن بازی‌ها، آمار بازی‌های_بازی‌شده و بازی‌های_برنده را به‌روزرسانی می‌کند و به آن بازیکنان اجازه می‌دهد به بازی آینده اختصاص داده شوند.

برای اجرای Locust در Cloud Shell، به پایتون 3.7 یا بالاتر نیاز دارید. Cloud Shell با Python 3.9 ارائه می شود، بنابراین کاری جز اعتبارسنجی نسخه وجود ندارد:

python -V

خروجی فرمان

Python 3.9.12

اکنون می توانید ملزومات Locust را نصب کنید.

pip3 install -r requirements.txt

خروجی فرمان

Collecting locust==2.11.1
*snip*
Successfully installed ConfigArgParse-1.5.3 Flask-BasicAuth-0.2.0 Flask-Cors-3.0.10 brotli-1.0.9 gevent-21.12.0 geventhttpclient-2.0.2 greenlet-1.1.3 locust-2.11.1 msgpack-1.0.4 psutil-5.9.2 pyzmq-22.3.0 roundrobin-0.0.4 zope.event-4.5.0 zope.interface-5.4.0

اکنون، PATH را به‌روزرسانی کنید تا باینری ملخ تازه نصب شده پیدا شود:

PATH=~/.local/bin":$PATH"
which locust

خروجی فرمان

/home/<user>/.local/bin/locust

خلاصه

در این مرحله شما پروژه خود را راه اندازی کرده اید، اگر قبلاً یکی از آن ها را نداشتید، پوسته ابری را فعال کرده اید و کد این آزمایشگاه را دانلود کرده اید.

در نهایت، Locust را برای تولید بار بعداً در آزمایشگاه راه‌اندازی می‌کنید.

بعدی

در مرحله بعد، نمونه و پایگاه داده Cloud Spanner را تنظیم خواهید کرد.

3. یک نمونه و پایگاه داده Spanner ایجاد کنید

نمونه Spanner را ایجاد کنید

در این مرحله ما نمونه Spanner خود را برای Codelab راه اندازی کردیم. ورودی آچار را جستجو کنید 1a6580bd3d3e6783.png در سمت چپ منوی همبرگر بالا 3129589f7bc9e5ce.png یا با فشار دادن "/" عبارت "Spanner" را جستجو کنید و "Spanner" را تایپ کنید.

36e52f8df8e13b99.png

بعد، بر روی کلیک کنید 95269e75bc8c3e4d.png و با وارد کردن نام نمونه cloudspanner-gaming برای نمونه خود، انتخاب یک پیکربندی (یک نمونه منطقه ای مانند us-central1 را انتخاب کنید) و تعداد گره ها را تنظیم کنید، فرم را پر کنید. برای این کد لبه ما فقط به 500 processing units نیاز داریم.

آخرین، اما نه کم اهمیت، روی "ایجاد" کلیک کنید و در عرض چند ثانیه یک نمونه Cloud Spanner در اختیار دارید.

4457c324c94f93e6.png

پایگاه داده و طرحواره ایجاد کنید

هنگامی که نمونه شما اجرا می شود، می توانید پایگاه داده را ایجاد کنید. Spanner اجازه می دهد تا چندین پایگاه داده در یک نمونه واحد وجود داشته باشد.

پایگاه داده جایی است که شما طرحواره خود را تعریف می کنید. شما همچنین می توانید کنترل کنید که چه کسی به پایگاه داده دسترسی دارد، رمزگذاری سفارشی را تنظیم کنید، بهینه ساز را پیکربندی کنید و دوره نگهداری را تنظیم کنید.

در نمونه های چند منطقه ای، می توانید رهبر پیش فرض را نیز پیکربندی کنید. در مورد پایگاه داده در Spanner بیشتر بخوانید .

برای این آزمایشگاه کد، پایگاه داده را با گزینه های پیش فرض ایجاد می کنید و طرح را در زمان ایجاد ارائه می دهید.

این آزمایشگاه دو جدول ایجاد می کند: بازیکنان و بازی ها .

77651ac12e47fe2a.png

بازیکنان می توانند در طول زمان در بازی های زیادی شرکت کنند، اما فقط یک بازی در هر زمان. بازیکنان همچنین دارای آماری به عنوان نوع داده JSON هستند تا آمارهای جالبی مانند games_played و games_won را پیگیری کنند. از آنجایی که ممکن است آمارهای دیگری بعداً اضافه شود، این در واقع یک ستون بدون طرح برای بازیکنان است.

بازی‌ها بازیکنانی را که با استفاده از نوع داده ARRAY Spanner شرکت کرده‌اند را ردیابی می‌کنند. ویژگی های برنده و تمام شده یک بازی تا زمانی که بازی بسته نشود، پر نمی شوند.

یک کلید خارجی برای اطمینان از معتبر بودن بازی current_game بازیکن وجود دارد.

اکنون با کلیک بر روی "ایجاد پایگاه داده" در نمای کلی نمونه، پایگاه داده را ایجاد کنید:

a820db6c4a4d6f2d.png

و سپس جزئیات را پر کنید. گزینه های مهم نام پایگاه داده و گویش هستند. در این مثال، ما پایگاه داده را نمونه-بازی نامگذاری کردیم و گویش استاندارد SQL گوگل را انتخاب کردیم.

برای طرح، این DDL را کپی و در کادر قرار دهید:

CREATE TABLE games (
  gameUUID STRING(36) NOT NULL,
  players ARRAY<STRING(36)> NOT NULL,
  winner STRING(36),
  created TIMESTAMP,
  finished TIMESTAMP,
) PRIMARY KEY(gameUUID);

CREATE TABLE players (
  playerUUID STRING(36) NOT NULL,
  player_name STRING(64) NOT NULL,
  email STRING(MAX) NOT NULL,
  password_hash BYTES(60) NOT NULL,
  created TIMESTAMP,
  updated TIMESTAMP,
  stats JSON,
  account_balance NUMERIC NOT NULL DEFAULT (0.00),
  is_logged_in BOOL,
  last_login TIMESTAMP,
  valid_email BOOL,
  current_game STRING(36),
  FOREIGN KEY (current_game) REFERENCES games (gameUUID),
) PRIMARY KEY(playerUUID);

CREATE UNIQUE INDEX PlayerAuthentication ON players(email) STORING (password_hash);

CREATE INDEX PlayerGame ON players(current_game);

CREATE UNIQUE INDEX PlayerName ON players(player_name);

سپس روی دکمه ایجاد کلیک کنید و چند ثانیه صبر کنید تا پایگاه داده شما ساخته شود.

صفحه ایجاد پایگاه داده باید به شکل زیر باشد:

d39d358dc7d32939.png

اکنون، باید چند متغیر محیطی را در Cloud Shell تنظیم کنید تا بعداً در آزمایشگاه کد مورد استفاده قرار گیرند. بنابراین به instance-id توجه داشته باشید و INSTANCE_ID و DATABASE_ID آن را در Cloud Shell تنظیم کنید.

f6f98848d3aea9c.png

export SPANNER_PROJECT_ID=$GOOGLE_CLOUD_PROJECT
export SPANNER_INSTANCE_ID=cloudspanner-gaming
export SPANNER_DATABASE_ID=sample-game

خلاصه

در این مرحله شما یک نمونه Spanner و پایگاه داده نمونه بازی ایجاد کردید. شما همچنین طرحواره ای را که این نمونه بازی از آن استفاده می کند، تعریف کرده اید.

بعدی

در مرحله بعد، شما سرویس پروفایل را مستقر خواهید کرد تا به بازیکنان اجازه دهید برای بازی کردن ثبت نام کنند!

4. سرویس پروفایل را مستقر کنید

نمای کلی خدمات

سرویس پروفایل یک API REST است که در Go نوشته شده است و از چارچوب جین استفاده می کند.

4fce45ee6c858b3e.png

در این API، بازیکنان می توانند برای بازی کردن ثبت نام کنند. این توسط یک دستور POST ساده ایجاد می شود که نام بازیکن، ایمیل و رمز عبور را می پذیرد. رمز عبور با bcrypt رمزگذاری شده و هش در پایگاه داده ذخیره می شود.

ایمیل به عنوان یک شناسه منحصربه‌فرد در نظر گرفته می‌شود، در حالی که player_name برای اهداف نمایش بازی استفاده می‌شود.

این API در حال حاضر ورود به سیستم را کنترل نمی کند، اما اجرای آن می تواند به عنوان یک تمرین اضافی به شما واگذار شود.

فایل ./src/golang/profile-service/main.go برای سرویس نمایه دو نقطه پایانی اصلی را به شرح زیر نشان می دهد:

func main() {
   configuration, _ := config.NewConfig()

   router := gin.Default()
   router.SetTrustedProxies(nil)

   router.Use(setSpannerConnection(configuration))

   router.POST("/players", createPlayer)
   router.GET("/players", getPlayerUUIDs)
   router.GET("/players/:id", getPlayerByID)

   router.Run(configuration.Server.URL())
}

و کد آن نقاط پایانی به مدل پخش کننده هدایت می شود.

func getPlayerByID(c *gin.Context) {
   var playerUUID = c.Param("id")

   ctx, client := getSpannerConnection(c)

   player, err := models.GetPlayerByUUID(ctx, client, playerUUID)
   if err != nil {
       c.IndentedJSON(http.StatusNotFound, gin.H{"message": "player not found"})
       return
   }

   c.IndentedJSON(http.StatusOK, player)
}

func createPlayer(c *gin.Context) {
   var player models.Player

   if err := c.BindJSON(&player); err != nil {
       c.AbortWithError(http.StatusBadRequest, err)
       return
   }

   ctx, client := getSpannerConnection(c)
   err := player.AddPlayer(ctx, client)
   if err != nil {
       c.AbortWithError(http.StatusBadRequest, err)
       return
   }

   c.IndentedJSON(http.StatusCreated, player.PlayerUUID)
}

یکی از اولین کارهایی که سرویس انجام می دهد تنظیم اتصال Spanner است. این در سطح سرویس اجرا می شود تا استخر جلسه برای سرویس ایجاد شود.

func setSpannerConnection() gin.HandlerFunc {
   ctx := context.Background()
   client, err := spanner.NewClient(ctx, configuration.Spanner.URL())

   if err != nil {
       log.Fatal(err)
   }

   return func(c *gin.Context) {
       c.Set("spanner_client", *client)
       c.Set("spanner_context", ctx)
       c.Next()
   }
}

Player و PlayerStats ساختارهایی هستند که به صورت زیر تعریف می شوند:

type Player struct {
   PlayerUUID      string `json:"playerUUID" validate:"omitempty,uuid4"`
   Player_name     string `json:"player_name" validate:"required_with=Password Email"`
   Email           string `json:"email" validate:"required_with=Player_name Password,email"`
   // not stored in DB
   Password        string `json:"password" validate:"required_with=Player_name Email"` 
   // stored in DB
   Password_hash   []byte `json:"password_hash"`                                       
   created         time.Time
   updated         time.Time
   Stats           spanner.NullJSON `json:"stats"`
   Account_balance big.Rat          `json:"account_balance"`
   last_login      time.Time
   is_logged_in    bool
   valid_email     bool
   Current_game    string `json:"current_game" validate:"omitempty,uuid4"`
}

type PlayerStats struct {
   Games_played spanner.NullInt64 `json:"games_played"`
   Games_won    spanner.NullInt64 `json:"games_won"`
}

تابع افزودن پخش کننده از یک درج DML در داخل یک تراکنش ReadWrite استفاده می کند، زیرا افزودن پخش کننده ها به جای درج دسته ای، یک عبارت واحد است. تابع به شکل زیر است:

func (p *Player) AddPlayer(ctx context.Context, client spanner.Client) error {
   // Validate based on struct validation rules
   err := p.Validate()
   if err != nil {
       return err
   }

   // take supplied password+salt, hash. Store in user_password
   passHash, err := hashPassword(p.Password)

   if err != nil {
       return errors.New("Unable to hash password")
   }

   p.Password_hash = passHash

   // Generate UUIDv4
   p.PlayerUUID = generateUUID()

   // Initialize player stats
   emptyStats := spanner.NullJSON{Value: PlayerStats{
       Games_played: spanner.NullInt64{Int64: 0, Valid: true},
       Games_won:    spanner.NullInt64{Int64: 0, Valid: true},
   }, Valid: true}

   // insert into spanner
   _, err = client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
       stmt := spanner.Statement{
           SQL: `INSERT players (playerUUID, player_name, email, password_hash, created, stats) VALUES
                   (@playerUUID, @playerName, @email, @passwordHash, CURRENT_TIMESTAMP(), @pStats)
           `,
           Params: map[string]interface{}{
               "playerUUID":   p.PlayerUUID,
               "playerName":   p.Player_name,
               "email":        p.Email,
               "passwordHash": p.Password_hash,
               "pStats":       emptyStats,
           },
       }

       _, err := txn.Update(ctx, stmt)
       return err
   })
   if err != nil {
       return err
   }
   // return empty error on success
   return nil
}

برای بازیابی یک بازیکن بر اساس UUID، یک خواندن ساده صادر می شود. این بازیکن playerUUID، player_name، ایمیل و آمار را بازیابی می کند.

func GetPlayerByUUID(ctx context.Context, client spanner.Client, uuid string) (Player, error) {
   row, err := client.Single().ReadRow(ctx, "players",
       spanner.Key{uuid}, []string{"playerUUID", "player_name", "email", "stats"})
   if err != nil {
       return Player{}, err
   }

   player := Player{}
   err = row.ToStruct(&player)

   if err != nil {
       return Player{}, err
   }
   return player, nil
}

به طور پیش فرض، این سرویس با استفاده از متغیرهای محیطی پیکربندی می شود. به بخش مربوطه فایل ./src/golang/profile-service/config/config.go مراجعه کنید.

func NewConfig() (Config, error) {
   *snip*
   // Server defaults
   viper.SetDefault("server.host", "localhost")
   viper.SetDefault("server.port", 8080)

   // Bind environment variable override
   viper.BindEnv("server.host", "SERVICE_HOST")
   viper.BindEnv("server.port", "SERVICE_PORT")
   viper.BindEnv("spanner.project_id", "SPANNER_PROJECT_ID")
   viper.BindEnv("spanner.instance_id", "SPANNER_INSTANCE_ID")
   viper.BindEnv("spanner.database_id", "SPANNER_DATABASE_ID")

   *snip*

   return c, nil
}

می بینید که رفتار پیش فرض اجرای سرویس در localhost:8080 است.

با این اطلاعات زمان اجرای سرویس فرا رسیده است.

سرویس پروفایل را اجرا کنید

سرویس را با استفاده از دستور go اجرا کنید. این وابستگی ها را دانلود می کند و سرویسی را که روی پورت 8080 اجرا می شود ایجاد می کند:

cd ~/spanner-gaming-sample/src/golang/profile-service
go run . &

خروجی فرمان:

[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] POST   /players                  --> main.createPlayer (4 handlers)
[GIN-debug] GET    /players                  --> main.getPlayerUUIDs (4 handlers)
[GIN-debug] GET    /players/:id              --> main.getPlayerByID (4 handlers)
[GIN-debug] GET    /players/:id/stats        --> main.getPlayerStats (4 handlers)
[GIN-debug] Listening and serving HTTP on localhost:8080

با صدور دستور curl سرویس را تست کنید:

curl http://localhost:8080/players \
    --include \
    --header "Content-Type: application/json" \
    --request "POST" \
    --data '{"email": "test@gmail.com","password": "s3cur3P@ss","player_name": "Test Player"}'

خروجی فرمان:

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: <date> 18:55:08 GMT
Content-Length: 38

"506a1ab6-ee5b-4882-9bb1-ef9159a72989"

خلاصه

در این مرحله، شما سرویس نمایه ای را که به بازیکنان اجازه می دهد برای بازی شما ثبت نام کنند، مستقر کرده و با صدور یک فراخوانی POST api برای ایجاد یک بازیکن جدید، این سرویس را آزمایش کردید.

مراحل بعدی

در مرحله بعد سرویس match-making را مستقر خواهید کرد.

5. سرویس match-making را مستقر کنید

نمای کلی خدمات

سرویس match-making یک REST API است که در Go نوشته شده است و از چارچوب جین استفاده می کند.

9aecd571df0dcd7c.png

در این API، بازی ها ساخته و بسته می شوند. هنگامی که یک بازی ایجاد می شود، 10 بازیکنی که در حال حاضر در حال انجام یک بازی نیستند، به بازی اختصاص داده می شوند.

هنگامی که یک بازی بسته می شود، یک برنده به طور تصادفی انتخاب می شود و آمار هر بازیکن برای بازی های_بازی شده و بازی های_برنده تنظیم می شود. همچنین، هر بازیکن به‌روزرسانی می‌شود تا نشان دهد که دیگر بازی نمی‌کند و بنابراین برای بازی‌های آینده در دسترس است.

فایل ./src/golang/matchmaking-service/main.go برای سرویس همسریابی از تنظیمات و کد مشابهی مانند سرویس نمایه پیروی می کند، بنابراین در اینجا تکرار نمی شود. این سرویس دو نقطه پایانی اولیه را به شرح زیر نشان می دهد:

func main() {
   router := gin.Default()
   router.SetTrustedProxies(nil)

   router.Use(setSpannerConnection())

   router.POST("/games/create", createGame)
   router.PUT("/games/close", closeGame)

   router.Run(configuration.Server.URL())
}

این سرویس یک ساختار Game و همچنین ساختارهای Player و PlayerStats را ارائه می دهد:

type Game struct {
   GameUUID string           `json:"gameUUID"`
   Players  []string         `json:"players"`
   Winner   string           `json:"winner"`
   Created  time.Time        `json:"created"`
   Finished spanner.NullTime `json:"finished"`
}

type Player struct {
   PlayerUUID   string           `json:"playerUUID"`
   Stats        spanner.NullJSON `json:"stats"`
   Current_game string           `json:"current_game"`
}

type PlayerStats struct {
   Games_played int `json:"games_played"`
   Games_won    int `json:"games_won"`
}

برای ایجاد یک بازی ، سرویس خواستگاری از 100 بازیکنی که در حال حاضر در حال انجام یک بازی نیستند، به صورت تصادفی انتخاب می‌کند.

جهش‌های آچار برای ایجاد بازی و اختصاص دادن بازیکنان انتخاب می‌شوند، زیرا جهش‌ها برای تغییرات بزرگ عملکرد بیشتری نسبت به DML دارند.

// Create a new game and assign players
// Players that are not currently playing a game are eligble to be selected for the new game
// Current implementation allows for less than numPlayers to be placed in a game
func (g *Game) CreateGame(ctx context.Context, client spanner.Client) error {
   // Initialize game values
   g.GameUUID = generateUUID()

   numPlayers := 10

   // Create and assign
   _, err := client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
       var m []*spanner.Mutation

       // get players
       query := fmt.Sprintf("SELECT playerUUID FROM (SELECT playerUUID FROM players WHERE current_game IS NULL LIMIT 10000) TABLESAMPLE RESERVOIR (%d ROWS)", numPlayers)
       stmt := spanner.Statement{SQL: query}
       iter := txn.Query(ctx, stmt)

       playerRows, err := readRows(iter)
       if err != nil {
           return err
       }

       var playerUUIDs []string

       for _, row := range playerRows {
           var pUUID string
           if err := row.Columns(&pUUID); err != nil {
               return err
           }

           playerUUIDs = append(playerUUIDs, pUUID)
       }

       // Create the game
       gCols := []string{"gameUUID", "players", "created"}
       m = append(m, spanner.Insert("games", gCols, []interface{}{g.GameUUID, playerUUIDs, time.Now()}))

       // Update players to lock into this game
       for _, p := range playerUUIDs {
           pCols := []string{"playerUUID", "current_game"}
           m = append(m, spanner.Update("players", pCols, []interface{}{p, g.GameUUID}))
       }

       txn.BufferWrite(m)

       return nil
   })

   if err != nil {
       return err
   }

   return nil
}

انتخاب تصادفی بازیکنان با SQL با استفاده از قابلیت TABLESPACE RESERVOIR GoogleSQL انجام می شود.

بستن بازی کمی پیچیده تر است. این شامل انتخاب یک برنده تصادفی از بین بازیکنان بازی، مشخص کردن زمان پایان بازی، و به روز رسانی آمار هر بازیکن برای games_played و games_won است .

به دلیل این پیچیدگی و میزان تغییرات، دوباره جهش ها برای بسته شدن بازی انتخاب می شوند.

func determineWinner(playerUUIDs []string) string {
   if len(playerUUIDs) == 0 {
       return ""
   }

   var winnerUUID string

   rand.Seed(time.Now().UnixNano())
   offset := rand.Intn(len(playerUUIDs))
   winnerUUID = playerUUIDs[offset]
   return winnerUUID
}

// Given a list of players and a winner's UUID, update players of a game
// Updating players involves closing out the game (current_game = NULL) and
// updating their game stats. Specifically, we are incrementing games_played.
// If the player is the determined winner, then their games_won stat is incremented.
func (g Game) updateGamePlayers(ctx context.Context, players []Player, txn *spanner.ReadWriteTransaction) error {
   for _, p := range players {
       // Modify stats
       var pStats PlayerStats
       json.Unmarshal([]byte(p.Stats.String()), &pStats)

       pStats.Games_played = pStats.Games_played + 1

       if p.PlayerUUID == g.Winner {
           pStats.Games_won = pStats.Games_won + 1
       }
       updatedStats, _ := json.Marshal(pStats)
       p.Stats.UnmarshalJSON(updatedStats)

       // Update player
       // If player's current game isn't the same as this game, that's an error
       if p.Current_game != g.GameUUID {
           errorMsg := fmt.Sprintf("Player '%s' doesn't belong to game '%s'.", p.PlayerUUID, g.GameUUID)
           return errors.New(errorMsg)
       }

       cols := []string{"playerUUID", "current_game", "stats"}
       newGame := spanner.NullString{
           StringVal: "",
           Valid:     false,
       }

       txn.BufferWrite([]*spanner.Mutation{
           spanner.Update("players", cols, []interface{}{p.PlayerUUID, newGame, p.Stats}),
       })
   }

   return nil
}

// Closing game. When provided a Game, choose a random winner and close out the game.
// A game is closed by setting the winner and finished time.
// Additionally all players' game stats are updated, and the current_game is set to null to allow
// them to be chosen for a new game.
func (g *Game) CloseGame(ctx context.Context, client spanner.Client) error {
   // Close game
   _, err := client.ReadWriteTransaction(ctx,
       func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
           // Get game players
           playerUUIDs, players, err := g.getGamePlayers(ctx, txn)

           if err != nil {
               return err
           }

           // Might be an issue if there are no players!
           if len(playerUUIDs) == 0 {
               errorMsg := fmt.Sprintf("No players found for game '%s'", g.GameUUID)
               return errors.New(errorMsg)
           }

           // Get random winner
           g.Winner = determineWinner(playerUUIDs)

           // Validate game finished time is null
           row, err := txn.ReadRow(ctx, "games", spanner.Key{g.GameUUID}, []string{"finished"})
           if err != nil {
               return err
           }

           if err := row.Column(0, &g.Finished); err != nil {
               return err
           }

           // If time is not null, then the game is already marked as finished. 
           // That's an error.
           if !g.Finished.IsNull() {
               errorMsg := fmt.Sprintf("Game '%s' is already finished.", g.GameUUID)
               return errors.New(errorMsg)
           }

           cols := []string{"gameUUID", "finished", "winner"}
           txn.BufferWrite([]*spanner.Mutation{
               spanner.Update("games", cols, []interface{}{g.GameUUID, time.Now(), g.Winner}),
           })

           // Update each player to increment stats.games_played 
           // (and stats.games_won if winner), and set current_game 
           // to null so they can be chosen for a new game
           playerErr := g.updateGamePlayers(ctx, players, txn)
           if playerErr != nil {
               return playerErr
           }

           return nil
       })

   if err != nil {
       return err
   }

   return nil
}

پیکربندی مجدداً از طریق متغیرهای محیطی همانطور که در ./src/golang/matchmaking-service/config/config.go برای سرویس توضیح داده شده است، انجام می شود.

   // Server defaults
   viper.SetDefault("server.host", "localhost")
   viper.SetDefault("server.port", 8081)

   // Bind environment variable override
   viper.BindEnv("server.host", "SERVICE_HOST")
   viper.BindEnv("server.port", "SERVICE_PORT")
   viper.BindEnv("spanner.project_id", "SPANNER_PROJECT_ID")
   viper.BindEnv("spanner.instance_id", "SPANNER_INSTANCE_ID")
   viper.BindEnv("spanner.database_id", "SPANNER_DATABASE_ID")

برای جلوگیری از درگیری با پروفایل-سرویس، این سرویس به طور پیش فرض روی localhost:8081 اجرا می شود.

با این اطلاعات، اکنون زمان اجرای سرویس همسریابی فرا رسیده است.

سرویس match-making را اجرا کنید

سرویس را با استفاده از دستور go اجرا کنید. با این کار سرویس در حال اجرا بر روی پورت 8082 ایجاد می شود. این سرویس وابستگی های مشابهی با نمایه سرویس دارد، بنابراین وابستگی های جدید دانلود نمی شوند.

cd ~/spanner-gaming-sample/src/golang/matchmaking-service
go run . &

خروجی فرمان:

[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] POST   /games/create             --> main.createGame (4 handlers)
[GIN-debug] PUT    /games/close              --> main.closeGame (4 handlers)
[GIN-debug] Listening and serving HTTP on localhost:8081

یک بازی ایجاد کنید

سرویس را برای ایجاد یک بازی تست کنید. ابتدا یک ترمینال جدید در Cloud Shell باز کنید:

90eceac76a6bb90b.png

سپس دستور curl زیر را صادر کنید:

curl http://localhost:8081/games/create \
    --include \
    --header "Content-Type: application/json" \
    --request "POST"

خروجی فرمان:

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: <date> 19:38:45 GMT
Content-Length: 38

"f45b0f7f-405b-4e67-a3b8-a624e990285d"

بازی را ببندید

curl http://localhost:8081/games/close \
    --include \
    --header "Content-Type: application/json" \
    --data '{"gameUUID": "f45b0f7f-405b-4e67-a3b8-a624e990285d"}' \
    --request "PUT"

خروجی فرمان:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: <date> 19:43:58 GMT
Content-Length: 38

"506a1ab6-ee5b-4882-9bb1-ef9159a72989"

خلاصه

در این مرحله، سرویس matchmaking را برای مدیریت ساخت بازی و اختصاص دادن بازیکنان به آن بازی مستقر کردید. این سرویس همچنین بسته شدن یک بازی را انجام می دهد که برنده تصادفی را انتخاب می کند و تمام آمار بازیکنان بازی را برای games_played و games_won به روز می کند.

مراحل بعدی

اکنون که خدمات شما در حال اجرا است، زمان آن رسیده است که بازیکنان را وادار به ثبت نام و بازی کنید!

6. شروع به بازی کنید

اکنون که خدمات نمایه و همسریابی در حال اجرا است، می توانید با استفاده از ژنراتورهای ملخ ارائه شده بار تولید کنید.

Locust یک رابط وب برای اجرای ژنراتورها ارائه می دهد، اما در این آزمایشگاه شما از خط فرمان (گزینه headless ) استفاده خواهید کرد.

بازیکنان را ثبت نام کنید

اول، شما می خواهید بازیکن تولید کنید.

کد پایتون برای ایجاد بازیکنان در فایل ./generators/authentication_server.py به شکل زیر است:

class PlayerLoad(HttpUser):
   def on_start(self):
       global pUUIDs
       pUUIDs = []

   def generatePlayerName(self):
       return ''.join(random.choices(string.ascii_lowercase + string.digits, k=32))

   def generatePassword(self):
       return ''.join(random.choices(string.ascii_lowercase + string.digits, k=32))

   def generateEmail(self):
       return ''.join(random.choices(string.ascii_lowercase + string.digits, k=32) + ['@'] +
           random.choices(['gmail', 'yahoo', 'microsoft']) + ['.com'])

   @task
   def createPlayer(self):
       headers = {"Content-Type": "application/json"}
       data = {"player_name": self.generatePlayerName(), "email": self.generateEmail(), "password": self.generatePassword()}

       with self.client.post("/players", data=json.dumps(data), headers=headers, catch_response=True) as response:
           try:
               pUUIDs.append(response.json())
           except json.JSONDecodeError:
               response.failure("Response could not be decoded as JSON")
           except KeyError:
               response.failure("Response did not contain expected key 'gameUUID'")

نام بازیکنان، ایمیل ها و رمزهای عبور به صورت تصادفی تولید می شوند.

بازیکنانی که با موفقیت ثبت نام کرده‌اند، با کار دوم برای ایجاد بار خواندن بازیابی می‌شوند.

   @task(5)
   def getPlayer(self):
       # No player UUIDs are in memory, reschedule task to run again later.
       if len(pUUIDs) == 0:
           raise RescheduleTask()

       # Get first player in our list, removing it to avoid contention from concurrent requests
       pUUID = pUUIDs[0]
       del pUUIDs[0]

       headers = {"Content-Type": "application/json"}

       self.client.get(f"/players/{pUUID}", headers=headers, name="/players/[playerUUID]")

دستور زیر فایل ./generators/authentication_server.py را فراخوانی می کند که پخش کننده های جدیدی را برای 30 ثانیه ( t=30s) با همزمانی دو رشته در یک زمان (u=2) تولید می کند:

cd ~/spanner-gaming-sample
locust -H http://127.0.0.1:8080 -f ./generators/authentication_server.py --headless -u=2 -r=2 -t=30s

بازیکنان به بازی ها می پیوندند

اکنون که بازیکنان ثبت نام کرده اید، آنها می خواهند بازی را شروع کنند!

کد پایتون برای ایجاد و بستن بازی ها در فایل ./generators/match_server.py به شکل زیر است:

from locust import HttpUser, task
from locust.exception import RescheduleTask

import json

class GameMatch(HttpUser):
   def on_start(self):
       global openGames
       openGames = []

   @task(2)
   def createGame(self):
       headers = {"Content-Type": "application/json"}

       # Create the game, then store the response in memory of list of open games.
       with self.client.post("/games/create", headers=headers, catch_response=True) as response:
           try:
               openGames.append({"gameUUID": response.json()})
           except json.JSONDecodeError:
               response.failure("Response could not be decoded as JSON")
           except KeyError:
               response.failure("Response did not contain expected key 'gameUUID'")


   @task
   def closeGame(self):
       # No open games are in memory, reschedule task to run again later.
       if len(openGames) == 0:
           raise RescheduleTask()

       headers = {"Content-Type": "application/json"}

       # Close the first open game in our list, removing it to avoid 
       # contention from concurrent requests
       game = openGames[0]
       del openGames[0]

       data = {"gameUUID": game["gameUUID"]}
       self.client.put("/games/close", data=json.dumps(data), headers=headers)

هنگامی که این ژنراتور اجرا می شود، بازی ها را با نسبت 2:1 (باز: بسته) باز و بسته می کند. این دستور ژنراتور را به مدت 10 ثانیه اجرا می کند (-t=10s) :

locust -H http://127.0.0.1:8081 -f ./generators/match_server.py --headless -u=1 -r=1 -t=10s

خلاصه

در این مرحله، بازیکنانی را که برای بازی کردن ثبت نام کرده بودند، شبیه سازی کردید و سپس شبیه سازی هایی را برای بازیکنان انجام دادید تا با استفاده از سرویس خواستگاری بازی کنند. این شبیه‌سازی‌ها از چارچوب Locust Python برای ارسال درخواست‌ها به api REST سرویس‌های ما استفاده می‌کنند.

به راحتی می توانید زمان صرف شده برای ایجاد بازیکنان و انجام بازی ها و همچنین تعداد کاربران همزمان ( -u) را تغییر دهید.

مراحل بعدی

پس از شبیه سازی، می خواهید آمارهای مختلف را با جستجوی Spanner بررسی کنید.

7. بازیابی آمار بازی

اکنون که بازیکنان را شبیه سازی کرده ایم که می توانند ثبت نام کنند و بازی کنند، باید آمار خود را بررسی کنید.

برای انجام این کار، از Cloud Console برای ارسال درخواست‌های جستجو به Spanner استفاده کنید.

b5e3154c6f7cb0cf.png

بررسی بازی های باز و بسته

یک بازی بسته بازی‌ای است که مُهر زمانی آن تکمیل شده باشد، در حالی که یک بازی باز NULL به پایان می‌رسد. این مقدار زمانی که بازی بسته می شود تنظیم می شود.

بنابراین این پرس و جو به شما کمک می کند تا بررسی کنید چند بازی باز و چند بازی بسته شده اند:

SELECT Type, NumGames FROM
(SELECT "Open Games" as Type, count(*) as NumGames FROM games WHERE finished IS NULL
UNION ALL
SELECT "Closed Games" as Type, count(*) as NumGames FROM games WHERE finished IS NOT NULL
)

نتیجه:

Type

NumGames

Open Games

0

Closed Games

175

بررسی تعداد بازیکنان در حال بازی در مقابل عدم بازی

بازیکنی در حال انجام یک بازی است اگر ستون current_game او تنظیم شده باشد. در غیر این صورت، آنها در حال حاضر بازی نمی کنند.

بنابراین برای مقایسه تعداد بازیکنانی که در حال حاضر بازی می کنند و بازی نمی کنند، از این پرس و جو استفاده کنید:

SELECT Type, NumPlayers FROM
(SELECT "Playing" as Type, count(*) as NumPlayers FROM players WHERE current_game IS NOT NULL
UNION ALL
SELECT "Not Playing" as Type, count(*) as NumPlayers FROM players WHERE current_game IS NULL
)

نتیجه:

Type

NumPlayers

Playing

0

Not Playing

310

برندگان برتر را مشخص کنید

هنگامی که یک بازی بسته می شود، یکی از بازیکنان به طور تصادفی به عنوان برنده انتخاب می شود. آمار بازی‌های_برد آن بازیکن در طول بسته شدن بازی افزایش می‌یابد.

SELECT playerUUID, stats
FROM players
WHERE CAST(JSON_VALUE(stats, "$.games_won") AS INT64)>0
LIMIT 10;

نتیجه:

playerUUID

آمار

07e247c5-f88e-4bca-a7bc-12d2485f2f2b

{"games_played":49,"games_won":1}

09b72595-40af-4406-a000-2fb56c58fe92

{"games_played":56,"games_won":1}

1002385b-02a0-462b-a8e7-05c9b27223aa

{"games_played":66,"games_won":1}

13ec3770-7ae3-495f-9b53-6322d8e8d6c3

{"games_played":44,"games_won":1}

15513852-3f2a-494f-b437-fe7125d15f1b

{"games_played":49,"games_won":1}

17faec64-4f77-475c-8df8-6ab026cf6698

{"games_played":50,"games_won":1}

1abfcb27-037d-446d-bb7a-b5cd17b5733d

{"games_played":63,"games_won":1}

2109a33e-88bd-4e74-a35c-a7914d9e3bde

{"games_played":56,"games_won":2}

222e37d9-06b0-4674-865d-a0e5fb80121e

{"games_played":60,"games_won":1}

22ced15c-0da6-4fd9-8cb2-1ffd233b3c56

{"games_played":50,"games_won":1}

خلاصه

در این مرحله با استفاده از Cloud Console برای پرس و جوی Spanner، آمارهای مختلف بازیکنان و بازی ها را بررسی کردید.

مراحل بعدی

بعد، زمان تمیز کردن است!

8. تمیز کردن (اختیاری)

برای پاکسازی، کافی است به بخش Cloud Spanner در Cloud Console بروید و نمونه «cloudspanner-gaming» را که در مرحله کد لبه با نام «Setup a Cloud Spanner Instance» ایجاد کردیم، حذف کنید.

9. تبریک!

تبریک می‌گوییم، شما با موفقیت یک بازی نمونه را در Spanner اجرا کردید

بعدش چی؟

در این آزمایشگاه شما با موضوعات مختلف کار با Spanner با استفاده از درایور golang آشنا شده اید. این باید پایه و اساس بهتری برای درک مفاهیم مهم مانند:

  • طراحی طرحواره
  • DML در مقابل جهش
  • کار با Golang

برای نمونه دیگری از کار با Spanner به عنوان یک باطن برای بازی خود، حتماً نگاهی به نرم افزار Cloud Spanner Game Trading Post بیندازید!