1. परिचय
Cloud Spanner, पूरी तरह से मैनेज की जाने वाली रिलेशनल डेटाबेस सेवा है. इसे हॉरिज़ॉन्टली बढ़ाया जा सकता है और यह दुनिया भर में उपलब्ध है. यह सेवा, परफ़ॉर्मेंस और ज़्यादा अपटाइम से समझौता किए बिना, ACID ट्रांज़ैक्शन और SQL सिमैंटिक उपलब्ध कराती है.
इन सुविधाओं की वजह से, Spanner उन गेम के आर्किटेक्चर के लिए सबसे सही विकल्प है जो दुनिया भर के खिलाड़ियों को शामिल करना चाहते हैं या डेटा की स्थिरता को लेकर चिंतित हैं
इस लैब में, आपको दो Go सेवाएं बनानी होंगी. ये सेवाएं, किसी क्षेत्र के Spanner डेटाबेस के साथ इंटरैक्ट करती हैं. इससे खिलाड़ियों को साइन अप करने और गेम खेलने की सुविधा मिलती है.

इसके बाद, Python लोड फ़्रेमवर्क Locust.io का इस्तेमाल करके डेटा जनरेट करें. इससे, खिलाड़ियों के साइन अप करने और गेम खेलने की प्रोसेस को सिम्युलेट किया जा सकेगा. इसके बाद, Spanner से यह क्वेरी की जाएगी कि कितने खिलाड़ी गेम खेल रहे हैं. साथ ही, खिलाड़ियों के जीते गए गेम बनाम खेले गए गेम के बारे में कुछ आंकड़े भी मांगे जाएंगे.
आखिर में, इस लैब में बनाए गए संसाधनों को मिटा दिया जाएगा.
आपको क्या बनाना है
इस लैब में आपको ये काम करने होंगे:
- Spanner इंस्टेंस बनाना
- खिलाड़ी के साइन अप को मैनेज करने के लिए, Go में लिखी गई प्रोफ़ाइल सेवा को डिप्लॉय करना
- Go में लिखी गई मैचमेकिंग सेवा को डिप्लॉय करें, ताकि खिलाड़ियों को गेम असाइन किए जा सकें, विजेताओं का पता लगाया जा सके, और खिलाड़ियों के गेम के आंकड़ों को अपडेट किया जा सके.
आपको क्या सीखने को मिलेगा
- Cloud Spanner इंस्टेंस सेट अप करने का तरीका
- गेम का डेटाबेस और स्कीमा बनाने का तरीका
- Cloud Spanner के साथ काम करने के लिए, Go ऐप्लिकेशन डिप्लॉय करने का तरीका
- Locust का इस्तेमाल करके डेटा जनरेट करने का तरीका
- गेम और खिलाड़ियों के बारे में सवालों के जवाब पाने के लिए, Cloud Spanner में डेटा को क्वेरी करने का तरीका.
आपको किन चीज़ों की ज़रूरत होगी
2. सेटअप और ज़रूरी शर्तें
प्रोजेक्ट बनाना
अगर आपके पास पहले से कोई Google खाता (Gmail या Google Apps) नहीं है, तो आपको एक खाता बनाना होगा. Google Cloud Platform Console ( console.cloud.google.com) में साइन इन करें और एक नया प्रोजेक्ट बनाएं.
अगर आपके पास पहले से कोई प्रोजेक्ट है, तो कंसोल में सबसे ऊपर बाईं ओर मौजूद, प्रोजेक्ट चुनने वाले पुल-डाउन मेन्यू पर क्लिक करें:

इसके बाद, नया प्रोजेक्ट बनाने के लिए, डायलॉग बॉक्स में मौजूद ‘नया प्रोजेक्ट' बटन पर क्लिक करें:

अगर आपके पास पहले से कोई प्रोजेक्ट नहीं है, तो आपको अपना पहला प्रोजेक्ट बनाने के लिए इस तरह का डायलॉग दिखेगा:

इसके बाद, प्रोजेक्ट बनाने के डायलॉग बॉक्स में, नए प्रोजेक्ट की जानकारी डाली जा सकती है:

प्रोजेक्ट आईडी याद रखें. यह सभी Google Cloud प्रोजेक्ट के लिए एक यूनीक नाम होता है. ऊपर दिया गया नाम पहले ही इस्तेमाल किया जा चुका है. इसलिए, यह आपके लिए काम नहीं करेगा. माफ़ करें! इसे इस कोडलैब में बाद में PROJECT_ID के तौर पर दिखाया जाएगा.
इसके बाद, अगर आपने अब तक ऐसा नहीं किया है, तो आपको Google Cloud के संसाधनों का इस्तेमाल करने के लिए, Developers Console में बिलिंग चालू करनी होगी. साथ ही, Cloud Spanner API चालू करना होगा.

इस कोडलैब को पूरा करने में आपको कुछ डॉलर से ज़्यादा खर्च नहीं करने पड़ेंगे. हालांकि, अगर आपको ज़्यादा संसाधनों का इस्तेमाल करना है या उन्हें चालू रखना है, तो यह खर्च बढ़ सकता है. इस दस्तावेज़ के आखिर में "सफाई" सेक्शन देखें. Google Cloud Spanner की कीमत के बारे में जानकारी यहां दी गई है.
Google Cloud Platform के नए उपयोगकर्ताओं को, मुफ़्त में आज़माने के लिए 300 डॉलर मिलते हैं. इससे इस कोडलैब का इस्तेमाल बिना किसी शुल्क के किया जा सकता है.
Google Cloud Shell का सेटअप
Google Cloud और Spanner को लैपटॉप से रिमोटली ऐक्सेस किया जा सकता है. हालांकि, इस कोडलैब में हम Google Cloud Shell का इस्तेमाल करेंगे. यह क्लाउड में चलने वाला कमांड लाइन एनवायरमेंट है.
यह Debian पर आधारित वर्चुअल मशीन है. इसमें डेवलपमेंट के लिए ज़रूरी सभी टूल पहले से मौजूद हैं. यह 5 जीबी की होम डायरेक्ट्री उपलब्ध कराता है और Google Cloud में चलता है. इससे नेटवर्क की परफ़ॉर्मेंस और पुष्टि करने की प्रोसेस बेहतर होती है. इसका मतलब है कि इस कोडलैब के लिए, आपको सिर्फ़ एक ब्राउज़र की ज़रूरत होगी. हां, यह Chromebook पर भी काम करता है.
- Cloud Console से Cloud Shell को चालू करने के लिए, बस Cloud Shell चालू करें
पर क्लिक करें. इसे चालू होने और एनवायरमेंट से कनेक्ट होने में कुछ ही समय लगता है.


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 के डैशबोर्ड में जाकर इसे देखें:

Cloud Shell, कुछ एनवायरमेंट वैरिएबल को डिफ़ॉल्ट रूप से भी सेट करता है. ये वैरिएबल, आने वाले समय में कमांड चलाने के दौरान आपके काम आ सकते हैं.
echo $GOOGLE_CLOUD_PROJECT
कमांड आउटपुट
<PROJECT_ID>
कोड डाउनलोड करना
Cloud Shell में, इस लैब के लिए कोड डाउनलोड किया जा सकता है. यह v0.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, Python का लोड टेस्टिंग फ़्रेमवर्क है. यह REST API एंडपॉइंट की जांच करने के लिए काम आता है. इस कोडलैब में, ‘generators' डायरेक्ट्री में दो अलग-अलग लोड टेस्ट हैं. हम इनके बारे में हाइलाइट करेंगे:
- authentication_server.py: इसमें खिलाड़ियों को बनाने और सिंगल पॉइंट लुकअप का इस्तेमाल करने के लिए, किसी रैंडम खिलाड़ी को पाने के टास्क शामिल होते हैं.
- match_server.py: इसमें गेम बनाने और बंद करने के टास्क शामिल होते हैं. गेम बनाने पर, 100 ऐसे रैंडम खिलाड़ियों को असाइन किया जाएगा जो फ़िलहाल गेम नहीं खेल रहे हैं. गेम बंद करने पर, खेले गए गेम और जीते गए गेम के आंकड़ों को अपडेट किया जाएगा. साथ ही, उन खिलाड़ियों को आने वाले समय में होने वाले गेम में शामिल किया जा सकेगा.
Cloud Shell में Locust चलाने के लिए, आपके पास Python 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 को अपडेट करें, ताकि इंस्टॉल की गई नई locust बाइनरी को ढूंढा जा सके:
PATH=~/.local/bin":$PATH"
which locust
कमांड आउटपुट
/home/<user>/.local/bin/locust
खास जानकारी
इस चरण में, अगर आपके पास पहले से कोई प्रोजेक्ट नहीं था, तो आपने उसे सेट अप कर लिया है. साथ ही, आपने Cloud Shell को चालू कर लिया है और इस लैब के लिए कोड डाउनलोड कर लिया है.
आखिर में, लैब में लोड जनरेट करने के लिए Locust को सेट अप करें.
अगला
इसके बाद, Cloud Spanner इंस्टेंस और डेटाबेस सेट अप करें.
3. Spanner इंस्टेंस और डेटाबेस बनाना
Spanner इंस्टेंस बनाना
इस चरण में, हम कोडलैब के लिए अपना Spanner इंस्टेंस सेट अप करते हैं. सबसे ऊपर बाईं ओर मौजूद हैमबर्गर मेन्यू
में जाकर, Spanner एंट्री खोजें
या "/" दबाकर Spanner खोजें और "Spanner" टाइप करें

इसके बाद,
पर क्लिक करें. साथ ही, अपने इंस्टेंस के लिए इंस्टेंस का नाम cloudspanner-gaming डालें. इसके बाद, कॉन्फ़िगरेशन चुनें (जैसे, us-central1 जैसा कोई रीजनल इंस्टेंस चुनें) और नोड की संख्या सेट करें. इस कोडलैब के लिए, हमें सिर्फ़ 500 processing units की ज़रूरत होगी.
आखिर में, "बनाएं" पर क्लिक करें. इसके बाद, कुछ ही सेकंड में आपके पास Cloud Spanner इंस्टेंस उपलब्ध होगा.

डेटाबेस और स्कीमा बनाना
आपका इंस्टेंस चालू होने के बाद, डेटाबेस बनाया जा सकता है. Spanner में, एक इंस्टेंस पर कई डेटाबेस बनाए जा सकते हैं.
डेटाबेस में ही स्कीमा तय किया जाता है. आपके पास यह कंट्रोल करने का विकल्प भी होता है कि डेटाबेस को कौन ऐक्सेस कर सकता है. साथ ही, कस्टम एन्क्रिप्शन सेट अप करने, ऑप्टिमाइज़र को कॉन्फ़िगर करने, और डेटा सुरक्षित रखने की अवधि सेट करने का विकल्प भी होता है.
एक से ज़्यादा क्षेत्रों के इंस्टेंस पर, डिफ़ॉल्ट लीडर को भी कॉन्फ़िगर किया जा सकता है. Spanner पर डेटाबेस के बारे में ज़्यादा पढ़ें.
इस कोड-लैब के लिए, आपको डिफ़ॉल्ट विकल्पों के साथ डेटाबेस बनाना होगा. साथ ही, डेटाबेस बनाते समय स्कीमा देना होगा.
इस लैब में दो टेबल बनाई जाएंगी: players और games.

खिलाड़ी समय के साथ कई गेम में हिस्सा ले सकते हैं. हालांकि, वे एक समय पर सिर्फ़ एक गेम में हिस्सा ले सकते हैं. खिलाड़ियों के पास JSON डेटा टाइप के तौर पर आंकड़े भी होते हैं. इससे उन्हें खेले गए गेम और जीते गए गेम जैसे दिलचस्प आंकड़ों को ट्रैक करने में मदद मिलती है. बाद में अन्य आंकड़े जोड़े जा सकते हैं. इसलिए, यह खिलाड़ियों के लिए बिना स्कीमा वाला कॉलम है.
गेम में, Spanner के ARRAY डेटा टाइप का इस्तेमाल करके, गेम में हिस्सा लेने वाले खिलाड़ियों को ट्रैक किया जाता है. किसी गेम के विजेता और खत्म होने की एट्रिब्यूट वैल्यू तब तक नहीं दिखती, जब तक गेम बंद नहीं हो जाता.
यह पक्का करने के लिए कि खिलाड़ी का current_game एक मान्य गेम है, एक फ़ॉरेन की मौजूद है.
अब इंस्टेंस की खास जानकारी में जाकर, ‘डेटाबेस बनाएं' पर क्लिक करके डेटाबेस बनाएं:

इसके बाद, जानकारी भरें. डेटाबेस का नाम और डायलेक्ट, ज़रूरी विकल्प हैं. इस उदाहरण में, हमने डेटाबेस का नाम sample-game रखा है और Google Standard SQL भाषा को चुना है.
स्कीमा के लिए, इस डीडीएल को कॉपी करके बॉक्स में चिपकाएं:
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);
इसके बाद, 'बनाएं' बटन पर क्लिक करें और डेटाबेस बनने के लिए कुछ सेकंड इंतज़ार करें.
डेटाबेस बनाने वाला पेज कुछ ऐसा दिखना चाहिए:

अब आपको Cloud Shell में कुछ एनवायरमेंट वैरिएबल सेट करने होंगे, ताकि बाद में कोड लैब में उनका इस्तेमाल किया जा सके. इसलिए, instance-id को नोट करें और Cloud Shell में INSTANCE_ID और DATABASE_ID सेट करें

export SPANNER_PROJECT_ID=$GOOGLE_CLOUD_PROJECT
export SPANNER_INSTANCE_ID=cloudspanner-gaming
export SPANNER_DATABASE_ID=sample-game
खास जानकारी
इस चरण में, आपने Spanner इंस्टेंस और sample-game डेटाबेस बनाया. आपने उस स्कीमा को भी तय किया है जिसका इस्तेमाल यह सैंपल गेम करता है.
अगला
इसके बाद, प्रोफ़ाइल सेवा को डिप्लॉय करें, ताकि खिलाड़ी गेम खेलने के लिए साइन अप कर सकें!
4. प्रोफ़ाइल सेवा को डिप्लॉय करना
सेवा के बारे में खास जानकारी
प्रोफ़ाइल सेवा, Go में लिखा गया एक REST API है. यह gin फ़्रेमवर्क का इस्तेमाल करता है.

इस एपीआई में, खिलाड़ी गेम खेलने के लिए साइन अप कर सकते हैं. इसे एक सामान्य POST कमांड से बनाया जाता है. यह कमांड, खिलाड़ी का नाम, ईमेल पता, और पासवर्ड स्वीकार करती है. पासवर्ड को bcrypt का इस्तेमाल करके एन्क्रिप्ट (सुरक्षित) किया जाता है. साथ ही, हैश को डेटाबेस में सेव किया जाता है.
ईमेल को यूनीक आइडेंटिफ़ायर के तौर पर माना जाता है. वहीं, player_name का इस्तेमाल, गेम में डिसप्ले करने के लिए किया जाता है.
फ़िलहाल, यह एपीआई लॉगिन को मैनेज नहीं करता. हालांकि, इसे लागू करने का काम आपको अतिरिक्त तौर पर दिया जा सकता है.
प्रोफ़ाइल सेवा के लिए ./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())
}
साथ ही, उन एंडपॉइंट का कोड player मॉडल पर रीडायरेक्ट हो जाएगा.
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"`
}
खिलाड़ी को जोड़ने वाला फ़ंक्शन, ReadWrite लेन-देन में DML इंसर्ट का इस्तेमाल करता है. ऐसा इसलिए, क्योंकि खिलाड़ियों को बैच इंसर्ट के बजाय एक ही स्टेटमेंट में जोड़ा जाता है. फ़ंक्शन ऐसा दिखता है:
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
}
किसी खिलाड़ी को उसके यूयूआईडी के आधार पर वापस पाने के लिए, एक सामान्य रीड जारी की जाती है. इससे खिलाड़ी की playerUUID, player_name, email, और stats की जानकारी मिलती है.
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 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 एपीआई कॉल जारी करके एक नया खिलाड़ी बनाया है.
अगले चरण
अगले चरण में, मैचमेकिंग सेवा को डिप्लॉय किया जाएगा.
5. मैचमेकिंग सेवा को डिप्लॉय करना
सेवा के बारे में खास जानकारी
मैचमेकिंग सेवा, Go में लिखा गया एक REST API है. यह gin फ़्रेमवर्क का इस्तेमाल करता है.

इस एपीआई में, गेम बनाए और बंद किए जाते हैं. जब कोई गेम बनाया जाता है, तो उसमें 10 ऐसे खिलाड़ियों को शामिल किया जाता है जो फ़िलहाल कोई गेम नहीं खेल रहे हैं.
जब कोई गेम बंद कर दिया जाता है, तो विजेता को रैंडम तरीके से चुना जाता है. साथ ही, हर खिलाड़ी के games_played और games_won के आंकड़ों में बदलाव किया जाता है. साथ ही, हर खिलाड़ी के स्टेटस को अपडेट किया जाता है, ताकि यह पता चल सके कि वे अब गेम नहीं खेल रहे हैं. इससे वे आने वाले समय में गेम खेल पाएंगे.
मैचमेकिंग सेवा के लिए, ./src/golang/matchmaking-service/main.go फ़ाइल का सेटअप और कोड, profile सेवा के जैसा ही है. इसलिए, इसे यहां दोहराया नहीं गया है. यह सेवा, दो प्राइमरी एंडपॉइंट उपलब्ध कराती है. ये एंडपॉइंट यहां दिए गए हैं:
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 खिलाड़ियों को चुनती है जो फ़िलहाल कोई गेम नहीं खेल रहे हैं.
गेम बनाने और खिलाड़ियों को असाइन करने के लिए, Spanner म्यूटेशन चुने जाते हैं. ऐसा इसलिए, क्योंकि बड़े बदलावों के लिए म्यूटेशन, डीएमएल से ज़्यादा बेहतर होते हैं.
// 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 का इस्तेमाल किया जाता है. इसके लिए, GoogleSQL की TABLESPACE RESERVOIR सुविधा का इस्तेमाल किया जाता है.
गेम बंद करना थोड़ा मुश्किल होता है. इसमें गेम खेलने वाले खिलाड़ियों में से किसी एक को विजेता के तौर पर चुना जाता है. साथ ही, गेम खत्म होने का समय मार्क किया जाता है. इसके अलावा, हर खिलाड़ी के 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 पर चलती है.
इस जानकारी के साथ, अब मैचमेकिंग सेवा को चालू करने का समय आ गया है.
मैचमेकिंग सेवा को चालू करना
go कमांड का इस्तेमाल करके सेवा चलाएं. इससे पोर्ट 8082 पर चल रही सेवा शुरू हो जाएगी. इस सेवा की कई डिपेंडेंसी, profile-service की तरह ही हैं. इसलिए, नई डिपेंडेंसी डाउनलोड नहीं की जाएंगी.
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 में नया टर्मिनल खोलें:

इसके बाद, यह कर्ल कमांड जारी करें:
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"
खास जानकारी
इस चरण में, आपने मैचमेकिंग सेवा को डिप्लॉय किया है. इससे गेम बनाने और खिलाड़ियों को उस गेम में शामिल करने में मदद मिलती है. यह सेवा, गेम को बंद करने की सुविधा भी देती है. इससे रैंडम तरीके से किसी एक खिलाड़ी को विजेता चुना जाता है. साथ ही, गेम खेलने वाले सभी खिलाड़ियों के लिए, games_played और games_won के आंकड़े अपडेट किए जाते हैं.
अगले चरण
अब आपकी सेवाएं चालू हो गई हैं. इसलिए, अब खिलाड़ियों को साइन अप करने और गेम खेलने के लिए कहें!
6. चलाएं
प्रोफ़ाइल और मैचमेकिंग सेवाएं चालू होने के बाद, दिए गए लोकस्ट जनरेटर का इस्तेमाल करके लोड जनरेट किया जा सकता है.
Locust, जनरेटर चलाने के लिए वेब-इंटरफ़ेस उपलब्ध कराता है. हालांकि, इस लैब में कमांड लाइन (–headless विकल्प) का इस्तेमाल किया जाएगा.
खिलाड़ियों को साइन अप करना
सबसे पहले, आपको प्लेयर जनरेट करने होंगे.
./generators/authentication_server.py फ़ाइल में, प्लेयर बनाने के लिए Python कोड ऐसा दिखता है:
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 फ़ाइल कॉल होती है. यह फ़ाइल, एक साथ दो थ्रेड (u=2) का इस्तेमाल करके, 30 सेकंड (t=30s) के लिए नए प्लेयर जनरेट करेगी:
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 फ़ाइल में, गेम बनाने और बंद करने के लिए Python कोड ऐसा दिखता है:
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 फ़्रेमवर्क का इस्तेमाल किया गया था. इससे हमारी सेवाओं के REST API को अनुरोध भेजे गए थे.
खिलाड़ी बनाने और गेम खेलने में लगने वाले समय के साथ-साथ, एक साथ गेम खेलने वाले उपयोगकर्ताओं की संख्या (-u) में बदलाव किया जा सकता है.
अगले चरण
सिमुलेशन के बाद, आपको Spanner से क्वेरी करके अलग-अलग आंकड़े देखने होंगे.
7. गेम के आंकड़े पाना
अब हमने खिलाड़ियों के साइन अप करने और गेम खेलने की प्रोसेस को सिम्युलेट कर लिया है. इसलिए, आपको अपने आंकड़ों की जांच करनी चाहिए.
इसके लिए, Cloud Console का इस्तेमाल करके Spanner को क्वेरी के अनुरोध भेजें.

खुले और बंद किए गए गेम की जांच करना
बंद किए गए गेम में, finished टाइमस्टैंप मौजूद होता है. वहीं, चालू गेम में finished की वैल्यू 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
)
नतीजा:
|
|
|
|
|
|
यह कुकी, गेम खेलने वाले और न खेलने वाले खिलाड़ियों की संख्या की जांच करती है
अगर किसी खिलाड़ी के लिए 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
)
नतीजा:
|
|
|
|
|
|
सबसे ज़्यादा जीतने वाले लोगों का पता लगाना
जब कोई गेम बंद हो जाता है, तो किसी एक खिलाड़ी को विजेता के तौर पर चुना जाता है. गेम बंद होने के दौरान, उस खिलाड़ी के games_won आंकड़े में बढ़ोतरी होती है.
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} |
खास जानकारी
इस चरण में, आपने Spanner से क्वेरी करने के लिए Cloud Console का इस्तेमाल करके, खिलाड़ियों और गेम के अलग-अलग आंकड़े देखे.
अगले चरण
इसके बाद, बारी आती है साफ़-सफ़ाई की!
8. डेटा को व्यवस्थित करना (ज़रूरी नहीं)
क्लीन अप करने के लिए, Cloud Console के Cloud Spanner सेक्शन में जाएं. इसके बाद, ‘cloudspanner-gaming' इंस्टेंस को मिटाएं. यह इंस्टेंस, कोडलैब के "Cloud Spanner इंस्टेंस सेट अप करें" चरण में बनाया गया था.
9. बधाई हो!
बधाई हो, आपने Spanner पर सैंपल गेम को डिप्लॉय कर लिया है
आगे क्या करना है?
इस लैब में, आपको golang ड्राइवर का इस्तेमाल करके Spanner के साथ काम करने के अलग-अलग विषयों के बारे में बताया गया है. इससे आपको इन ज़रूरी कॉन्सेप्ट को बेहतर तरीके से समझने में मदद मिलेगी:
- स्कीमा डिज़ाइन
- डीएमएल बनाम म्यूटेशन
- Golang के साथ काम करना
अपने गेम के बैकएंड के तौर पर Spanner का इस्तेमाल करने का एक और उदाहरण देखने के लिए, Cloud Spanner Game Trading Post codelab देखें!