上一节我们通过内置支持的 RPC 实现了服务器内部通信,这一节我们继续完善流程,将数据库 API 封装成 pitaya 框架的 Module,然后实现玩家账号注册的数据入库。
本节代码:xyq10612/PitayaGame at chapter1.2-完善账号注册与数据入库 (github.com)
封装 MongoDB Module
db 模块可能在多个服务器都要使用,所以这些我们也是放在 common 包里,层级结构如下:
基础层:
- db/database: 所有 db 都需实现的接口,实战项目中计划接入 MongoDB 和 Redis;
- 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/。 目前只封装了 InsertOne
和 Exist
两个操作数据库的方法,后续可以根据业务需求继续封装。
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 处理方法、测试注册流程。