【Pitaya游戏服务器实战】注册登录 --> 1.2 完善账号注册与数据入库

上一节我们通过内置支持的 RPC 实现了服务器内部通信,这一节我们继续完善流程,将数据库 API 封装成 pitaya 框架的 Module,然后实现玩家账号注册的数据入库。

本节代码:xyq10612/PitayaGame at chapter1.2-完善账号注册与数据入库 (github.com)

封装 MongoDB Module

db 模块可能在多个服务器都要使用,所以这些我们也是放在 common 包里,层级结构如下:

基础层:

  • db/database: 所有 db 都需实现的接口,实战项目中计划接入 MongoDBRedis
  • db/config: db 配置的通用字段提取; mongo 实现:
  • db/mongodb/config: MongoDB 模块的配置定义;
  • db/mongodb/mongo: MongoDB 对于 DataBase 接口的实现;
  • db/mongodb/collection: MongoDB 的 Collection 接口,业务层的数据库文档都需要实现这个接口。

基础层:Database 接口

将 database 实现为 pitaya 框架的 Module,在 app.Start() 之前注册到框架里,这样可以在服务器启动时自动初始化,而不需要手动调用。也可以在其他地方通过 GetModule 来获取。

go 复制代码
 // common/modules/db/database.go
 package db

 import "github.com/topfreegames/pitaya/v2/interfaces"

 type DataBase interface {
     interfaces.Module
     Connect()
     Close()
     TestPing() error
 }

基础层:Config 通用字段

go 复制代码
 // common/modules/db/config.go
 package db

 type Config struct {
     Host     string
     Port     int
     Username string
     Password string
 }

MongoDB实现层:MongoConfig 定义

go 复制代码
 // common/modules/db/mongodb/config.go
 package mongodb

 import (
     "common/modules/db"
     "fmt"
 )

 type MongoConfig struct {
     db.Config
     MaxPoolSize int // 连接池大小
 }

 func (c *MongoConfig) GetConnURI() string {
     return fmt.Sprintf("mongodb://%s:%d", c.Host, c.Port)
 }

 func NewDefaultMongoConfig() *MongoConfig {
     return &MongoConfig{
         Config: db.Config{
             Host: "localhost",
             Port: 27017,
         },
         MaxPoolSize: 10,
     }
 }

MongoDB 实现层:Collection 接口

Collection 接口定义了数据库文档的数据库名和集合名,业务层的数据库文档(可以理解为关系数据库中的 )都需要实现这个接口。

go 复制代码
 package mongodb

 type Collection interface {
     GetDBName() string
     GetCollectionName() string
 }

MongoDB 实现层:MongoStorage 实现

MongoStorage 实现了 DataBase 接口,嵌入 mongo.Client 结构来处理 MongoDB,这里我们使用 go.mongodb.org/mongo-driver 作为数据库驱动,具体的使用方法可以参考官方文档:docs.mongodb.com/drivers/go/。 目前只封装了 InsertOneExist 两个操作数据库的方法,后续可以根据业务需求继续封装。

go 复制代码
 package mongodb

 import (
     "context"
     "errors"
     "github.com/topfreegames/pitaya/v2/modules"
     "go.mongodb.org/mongo-driver/mongo"
     "go.mongodb.org/mongo-driver/mongo/options"
     "go.mongodb.org/mongo-driver/mongo/readpref"
 )

 type MongoStorage struct {
     modules.Base
     *mongo.Client
     config MongoConfig
 }

 func NewMongoStorage(config MongoConfig) *MongoStorage {
     return &MongoStorage{
         config: config,
     }
 }

 func (m *MongoStorage) Init() error {
     m.Connect()
     return nil
 }

 func (m *MongoStorage) Connect() {
     opt := options.Client().ApplyURI(m.config.GetConnURI())
     if m.config.Username != "" && m.config.Password != "" {
         opt.SetAuth(options.Credential{
             Username: m.config.Username,
             Password: m.config.Password,
         })
     }

     client, err := mongo.Connect(context.TODO(), opt)
     if err != nil {
         panic(err)
     }

     m.Client = client

     if err = m.TestPing(); err != nil {
         panic(err)
     }
 }

 func (m *MongoStorage) TestPing() error {
     return m.Ping(context.TODO(), readpref.Primary())
 }

 func (m *MongoStorage) Close() {
     _ = m.Disconnect(context.TODO())
 }

 func (m *MongoStorage) GetCollection(dbName, collection string) *mongo.Collection {
     return m.Database(dbName).Collection(collection)
 }

 func (m *MongoStorage) InsertOne(c Collection) error {
     _, err := m.GetCollection(c.GetDBName(), c.GetCollectionName()).InsertOne(context.TODO(), c)
     if err != nil {
         return err
     }

     return nil
 }

 func (m *MongoStorage) Exist(dbName, collection string, filter interface{}) bool {
     opt := &options.FindOneOptions{Projection: filter}
     r := m.GetCollection(dbName, collection).FindOne(context.TODO(), filter, opt)
     return !errors.Is(mongo.ErrNoDocuments, r.Err())
 }

这一节里我们暂时还不需要 Redis,就先不封装 Redis Module 了。

单元测试

我把所有测试代码都放在 tests 目录下,这个看个人习惯,也可以放在需要测试的包内。

go 复制代码
 // tests/mongo_test.go
 package tests

 import (
     "common/modules/db"
     "common/modules/db/mongodb"
     "context"
     "github.com/stretchr/testify/assert"
     "go.mongodb.org/mongo-driver/bson"
     "testing"
 )

 type Account struct {
     Name string `bson:"name"`
     Pwd  string `bson:"pwd"`
     Uid  string `bson:"uid"`
 }

 func registerAccount(mongo *mongodb.MongoStorage, name, pwd, uid string) error {
     account := &Account{
         name, pwd, uid,
     }
     _, err := mongo.GetCollection("tests", "account").InsertOne(context.TODO(), account)
     return err
 }

 func queryAccount(mongo *mongodb.MongoStorage, name string) (Account, error) {
     var account Account
     err := mongo.GetCollection("tests", "account").FindOne(context.TODO(), bson.M{"name": name}).Decode(&account)
     return account, err
 }

 func TestRegister(t *testing.T) {
     config := &mongodb.MongoConfig{
         Config: db.Config{
             Host:     "localhost",
             Port:     27017,
             Username: "debugeve", // 这些是我本机的mongodb配置,请根据自己的设置来修改
             Password: "develop2023",
         },
     }
     s := mongodb.NewMongoStorage(*config)
     s.Init()
     err := registerAccount(s, "test", "pwdtest", "test001uid")
     assert.NoError(t, err)
     account, err := queryAccount(s, "test")
     assert.NoError(t, err)
     assert.Equal(t, "test001uid", account.Uid)
 }

跑通测试,使用可视化工具连上 MongoDB 看一下数据正常入库了。 PS: 可视化工具推荐使用 Studio 3T

实现数据入库

定义相关常量

数据库名、集合名、Module 名字都定义在公共包里,这样方便后续调用修改。 PS: 之后这类的代码,为了控制篇幅,就不在文章中给出了,直接在代码里查看即可。

go 复制代码
 // common/constants/moduleConstant.go
 package constants

 const (
   MongoDBModule = "MongoDB"
 )

 // common/constants/dbConstant.go
 const (
   // ------------------ DB ------------------
   MongoGameDB    = "game"
   MongoAccountDB = "account"

   // ------------------ Collection ------------------
   MongoAccountCollection = "account"
 )

将 MongoDB Module 注册到 pitaya 应用

在 main 函数中调用 registerModules 函数注册 MongoDB Module。

go 复制代码
 // lobbyServer/main.go
 func registerModules() {
     // TODO: 测试中 直接写死 后续需改成读配置文件
     mongo := mongodb.NewMongoStorage(mongodb.MongoConfig{
         Config: db.Config{
             Host:     "localhost",
             Port:     27017,
             Username: "debugeve",
             Password: "develop2023",
         },
         MaxPoolSize: 10,
     })
     app.RegisterModule(mongo, constants.MongoDBModule)
 }

添加一个 helper 包,用来封装一些公共的方法,比如获取 MongoDB Module。

go 复制代码
// lobbyServer/helper/mongoHelper.go
package helper

import (
  "common/constants"
  "common/modules/db/mongodb"
  "github.com/topfreegames/pitaya/v2"
  "go.mongodb.org/mongo-driver/mongo"
)

var db *mongodb.MongoStorage

func GetMongo() *mongodb.MongoStorage {
  if db == nil {
    m, err := pitaya.DefaultApp.GetModule(constants.MongoDBModule)
    if err != nil {
      panic(err)
    }

    db = m.(*mongodb.MongoStorage)
  }

  return db
}

func GetGameDB() *mongo.Database {
  return GetMongo().Database(constants.MongoGameDB)
}

func GetAccountDB() *mongo.Database {
  return GetMongo().Database(constants.MongoAccountDB)
}

定义 AccountModel 数据库文档

go 复制代码
// lobbyServer/accountModel/account.go
package accountModel

import (
  "common/constants"
  "context"
  "go.mongodb.org/mongo-driver/bson"
  "lobbyServer/helper"
)

type AccountModel struct {
  Name     string `bson:"name"`
  Password string `bson:"password"`
  Uid      string `bson:"uid"`
}

func (a *AccountModel) GetDBName() string {
  return constants.MongoAccountDB
}

func (a *AccountModel) GetCollectionName() string {
  return constants.MongoAccountCollection
}

func (a *AccountModel) New() error {
  return helper.GetMongo().InsertOne(a)
}

func Exist(name string) bool {
  return helper.GetMongo().Exist(constants.MongoAccountDB, constants.MongoAccountCollection, bson.M{"name": name})
}

func FindOne(name string) (AccountModel, error) {
  var model AccountModel
  err := helper.GetAccountDB().Collection(constants.MongoAccountCollection).FindOne(context.TODO(), bson.M{"name": name}).Decode(&model)
  return model, err
}

测试:获取 MongoDB Module 并完成 Account 相关的数据库操作

这里只展示了部分代码,import 部分就不展示了,完整代码可以去 github 仓库查看。 PS: 关于测试部分,考虑到篇幅,后续的测试代码就不在文章中给出了

go 复制代码
// lobbyServer/test/account_test.go
func mockApp() *pitaya.App {
  c := config.NewDefaultBuilderConfig()
  app := pitaya.NewDefaultApp(false, "testServer", pitaya.Cluster, map[string]string{}, *c).(*pitaya.App)

  return app
}

func TestAccountModel(t *testing.T) {
  app := mockApp()
  pitaya.DefaultApp = app

  mongo := mongodb.NewMongoStorage(mongodb.MongoConfig{
    Config: db.Config{
      Host:     "localhost",
      Port:     27017,
      Username: "debugeve",
      Password: "develop2023",
    },
    MaxPoolSize: 10,
  })
  app.RegisterModule(mongo, constants.MongoDBModule)

  go func() {
    app.Start()
  }()

  helpers.ShouldEventuallyReturn(t, func() bool {
    return app.IsRunning()
  }, true, time.Second, time.Second*10)

  name := fmt.Sprintf("test%s", time.Now().Format("20060102150405"))
  model := accountModel.AccountModel{
    Name:     name,
    Password: "pwd123456",
    Uid:      fmt.Sprintf("uid-%s", name),
  }
  err := model.New()
  assert.NoError(t, err)

  exist := accountModel.Exist(name)
  assert.Equal(t, true, exist)

  find, err := accountModel.FindOne(name)
  assert.NoError(t, err)
  assert.Equal(t, model.Uid, find.Uid)
}

这里有个很好用的方法 helpers.ShouldEventuallyReturn,可以用来判断某个条件是否成立,如果不成立就会一直重试,直到超时。 helpers 包还有其他类似的方法,写单元测试的时候比较好用。

完善注册流程,完成 Register 处理方法

在注册新账号同时生成玩家的唯一id,这里使用了 github.com/google/uuid 包来生成唯一id:

go 复制代码
// lobbyServer/helper/helper.go
package helper

import (
    "github.com/google/uuid"
    "strings"
)

func GenerateUid() string {
    id := uuid.New()
    uuid := strings.Replace(id.String(), "-", "", -1)
    return uuid
}

完成 Register 处理方法,检查账号是否合法、是否重复,然后入库。

go 复制代码
// lobbyServer/service/accountService.go
// 长度限制 4 - 10
// 合法字符限制 a-z A-Z 0-9
func checkNameValid(name string) bool {
  re := regexp.MustCompile("^[a-zA-Z0-9]{4,10}$")
  return re.MatchString(name)
}

func checkPwdValid(pwd string) bool {
  return true
}

func (s *AccountService) Register(ctx context.Context, req *proto.RegisterRequest) (*proto.CommonResponse, error) {
  logger := pitaya.GetDefaultLoggerFromCtx(ctx)

  // check params
  if req.Account == "" || req.Password == "" {
    logger.Error("Account or password is empty!")
    return &proto.CommonResponse{Err: proto.ErrCode_UpParam}, nil
  }

  // 合法性
  if !checkNameValid(req.Account) {
    return &proto.CommonResponse{Err: proto.ErrCode_AccountRegister_NameInvalid}, nil
  }

  // 重复性
  if accountModel.Exist(req.Account) {
    return &proto.CommonResponse{Err: proto.ErrCode_AccountRegister_NameExist}, nil
  }

  uid := helper.GenerateUid()
  if uid == "" {
    logger.Errorf("Failed to generate uid!")
    return &proto.CommonResponse{Err: proto.ErrCode_ERR}, nil
  }

  // 注册
  model := &accountModel.AccountModel{
    Name:     req.Account,
    Password: req.Password,
    Uid:      uid,
  }

  err := model.New()
  if err != nil {
    logger.Errorf("Failed to create account! name: %s", req.Account)
    return &proto.CommonResponse{Err: proto.ErrCode_ERR}, nil
  }

  return &proto.CommonResponse{Err: proto.ErrCode_OK}, nil
}

测试:注册流程

开启 proxyServer 和 lobbyServer,使用 pitaya-cli 连接 proxyServer,然后注册两个账号,看看是否正常入库。

sh 复制代码
pitaya-cli
Pitaya REPL Client
>>> connect 127.0.0.1:40000
Using json client
connected!
>>> request proxy.account.register {"account":"test1", "password":"ttt"}
>>> sv->{}
>>> request proxy.account.register {"account":"test2", "password":"ttt"}
>>> sv->{}

注册完成,使用可视化工具查看数据库,数据正常入库了。

我们还可以修改一下请求路由,使用 lobby.account.register

sh 复制代码
>>> request proxy.account.register {"account":"test3", "password":"ttt"}

可以发现,test3 也注册成功了,proxyServer 在收到请求后,会将请求转发给 lobby 处理。但是我们不应该直接使用 lobby.account.register 来直接注册账号,因为这样会绕过 proxyServer,而我们其实需要在代理服做一些其他的处理,目前在账号注册的流程中并没有体现,下一节,我们进入登录流程,就会发现 proxyServer 的作用了。

小结

本节我们完成了账号注册的流程,包括:数据库模块的封装、注册 MongoDB Module、实现数据入库、完成 Register 处理方法、测试注册流程。

相关推荐
优梦创客16 小时前
《黑神话悟空》开发框架与战斗系统解析
unity·游戏开发·黑神话悟空·战斗系统·解包
蒙娜丽宁3 天前
Go语言错误处理详解
ios·golang·go·xcode·go1.19
qq_172805593 天前
GO Govaluate
开发语言·后端·golang·go
littleschemer4 天前
Go缓存系统
缓存·go·cache·bigcache
程序者王大川5 天前
【GO开发】MacOS上搭建GO的基础环境-Hello World
开发语言·后端·macos·golang·go
charon87785 天前
虚幻引擎 | 实时语音转口型 Multilingual lipsync
人工智能·游戏·语音识别·游戏开发
Grassto5 天前
Gitlab 中几种不同的认证机制(Access Tokens,SSH Keys,Deploy Tokens,Deploy Keys)
go·ssh·gitlab·ci
高兴的才哥5 天前
kubevpn 教程
kubernetes·go·开发工具·telepresence·bridge to k8s
少林码僧6 天前
sqlx1.3.4版本的问题
go
蒙娜丽宁6 天前
Go语言结构体和元组全面解析
开发语言·后端·golang·go