【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 来获取。

// common/modules/db/database.go
package db

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

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

基础层:Config 通用字段

// common/modules/db/config.go
package db

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

MongoDB实现层:MongoConfig 定义

// 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 接口定义了数据库文档的数据库名和集合名,业务层的数据库文档(可以理解为关系数据库中的 )都需要实现这个接口。

package mongodb

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

MongoDB 实现层:MongoStorage 实现

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

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 目录下,这个看个人习惯,也可以放在需要测试的包内。

// 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: 之后这类的代码,为了控制篇幅,就不在文章中给出了,直接在代码里查看即可。

// 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。

// 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。

// 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 数据库文档

// 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: 关于测试部分,考虑到篇幅,后续的测试代码就不在文章中给出了

// 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:

// 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 处理方法,检查账号是否合法、是否重复,然后入库。

// 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,然后注册两个账号,看看是否正常入库。

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

>>> request proxy.account.register {"account":"test3", "password":"ttt"}

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

小结

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

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值