第一章,我们先一起完成一个简单的登录流程。虽说简单,但也涉及到了多个服务器和数据库,所以借此功能,我们刚好了解如何通过 pitaya 建立前后端服务器、进行服务器间通信。
本节代码:xyq10612/PitayaGame at chapter1.1-前后端服务期间通信 (github.com)
注册登录服务器框架
先画一个简单的服务器框架图(只包括与注册登录有关的框架,其他服务器例如匹配、战斗、聊天等以后再加入,前期不要复杂度拉太高):
- Client 通过 socket 连接到 proxyServer(在 proxyServer 创建 Acceptor 监听);
- proxyServer 作为代理服务器,隔绝客户端与逻辑服务,更加安全,也方便做逻辑层的负载均衡。目前开发中为了方便测试,我们只开放一个 proxyServer,让客户端直连上来,之后可以多开代理服,使用 ngnix 之类的服务来做外层的负载均衡;
- 服务器内部使用 pitaya 框架提供的 RPC 功能来实现进程见通信;
- proxyServer 和 lobbyServer 都需要与 redis 建立链接,用于实现登录功能,保证登录的原子性(这个在我们实现登录逻辑的时候再细说);
- 这套实战项目采用 mongdb 作为落地服务器,所有 lobbyServer 直连 db,其他服务器不直接修改玩家存盘数据,均交由 lobbyServer 来处理。
实现前后端服务器通信
1. 建立工程
先建立工程,我这里命名是 “PitayaGame”,起名困难户……
同时把我们的两个服务器文件夹、go.mod go.work啥的都创建好:
2. 创建 proxyServer 服务器
参考 pitaya 的其他 demo 写法,创建默认的 Builder
,添加 Acceptor
等:
// main.go
var app pitaya.Pitaya
func main() {
serverType := "proxy"
port := flag.Int("port", 40000, "the port to listen")
flag.Parse()
logrus.SetLevel(logrus.DebugLevel)
config := config.NewDefaultBuilderConfig()
builder := pitaya.NewDefaultBuilder(true, serverType, pitaya.Cluster, map[string]string{}, *config)
builder.AddAcceptor(newAcceptor(*port))
app = builder.Build()
defer app.Shutdown()
app.Start()
}
func newAcceptor(port int) acceptor.Acceptor {
tcp := acceptor.NewTCPAcceptor(fmt.Sprintf(":%d", port))
return tcp
serverType
作为路由的一部分是不会修改的,在代码里可以写死,端口需要可配置,所以开放到命令行参数里。
go run
把服务器跑起来,没有问题~(注意 etcd 和 nats-server 记得开启,开启方法参考:0.准备工作)
3. 定义 proto
有些通信协议是多个服务器都需要使用的,为了方便复用,我们干脆把所有的 proto 都放在一个公共包里:
define 目录用来存放 pb 定义,proto 目录用来存放生成的 go 文件。
我们先来为 注册 功能定义相应的 pb,顺便抽取出一些可复用的部分,比如 errorCode
等。
err.proto:
syntax = "proto3";
package proto;
option go_package="../proto";
enum ErrCode {
OK = 0;
ERR = -1;
// 1000 - 1999 通用错误
UpParam = 1000; // 上行参数错误
// 2000 - 2999 用户错误
AccountRegister_NameInvalid = 2000; // 用户名不合法
AccountRegister_PwdInvalid = 2001; // 密码不合法
AccountRegister_NameExist = 2002; // 用户名已存在
}
common.proto:
syntax = "proto3";
package proto;
option go_package = "../proto";
import "err.proto";
message CommonResponse {
ErrCode err = 1;
}
message ErrorMessage {
ErrCode err = 1;
string msg = 2;
}
account.proto:
syntax = "proto3";
package proto;
option go_package = "../proto";
import "err.proto";
message RegisterRequest {
string account = 1;
string password = 2;
}
PS:后续如果再有 pb 相关的代码段,就不再给出前面几行 package、option 之类的设置,不然代码太长了。
顺便在 define 目录下写一个生成脚本:
generate.bat:
protoc --go_out=. ^
common.proto ^
err.proto ^
account.proto
pause
exit
PS: 关于如何生成 protobuf go 代码,网上也很多参考文章,我就不写了,懒……
跑完 bat 脚本,在 proto 目录下生成 go 代码。
4. proxy 账号服务 AccountService
在 proxyServer 里建立 service 文件夹,在这个示例中, service 作为服务提供方,用来处理消息,还有一种命名是 handler,这里沿用 pitaya demo 中的用法 —— service。
- 实现一个简单的服务,没有实际逻辑,仅仅测试一下通信
// proxyServer/service/accountService.go
// AccountService 账号服务
type AccountService struct {
component.Base
app pitaya.Pitaya
}
func NewAccountService(app pitaya.Pitaya) *AccountService {
return &AccountService{app: app}
}
func (s *AccountService) Register(ctx context.Context, req *proto.RegisterRequest) (*proto.CommonResponse, error) {
logrus.Debugf("register request: %v", req)
rsp := &proto.CommonResponse{Err: proto.ErrCode_OK}
return rsp, nil
- 在 main 中注册服务
// main.go
func main() {
//...
defer app.Shutdown()
initServices()
app.Start()
}
func initServices() {
account := service.NewAccountService(app)
app.Register(account,
component.WithName("account"),
component.WithNameFunc(strings.ToLower))
}
5. 测试 proxy 服务器的消息处理
开启服务器,使用 pitaya-cli 测试一下账号服务的消息处理:
>pitaya-cli
Pitaya REPL Client
>>> connect 127.0.0.1:40000
Using json client
connected!
>>> request proxy.account.register {"account":"test1", "password":"ttt"}
>>> sv->{}
客户端发送请求,服务端正常打印了日志:
level=debug msg="SID=4, Data={\"account\":\"test1\",\"password\":\"ttt\"}" requestId=ATMuGsStlRAk9pZQ26bumE route=proxy.account.register source=pitaya userId=
OK,初步调通。
6. 创建 lobbyServer 服务器
lobby 服务器作为后端服务器,是不需要对外的,不需要添加 Acceptor
监听。
// main.go
func main() {
serverType := "lobby"
logrus.SetLevel(logrus.DebugLevel)
config := config.NewDefaultBuilderConfig()
builder := pitaya.NewDefaultBuilder(false, serverType, pitaya.Cluster, map[string]string{}, *config)
app = builder.Build()
defer app.Shutdown()
initServices()
app.Start()
}
func initServices() {
account := service.NewAccountService(app)
app.Register(account,
component.WithName("account"),
component.WithNameFunc(strings.ToLower))
app.RegisterRemote(account,
component.WithName("account"),
component.WithNameFunc(strings.ToLower))
}
7. lobbyServer 账号服务 AccountService
先测试前后端服务器的通信,没有具体逻辑,打印个log:
type AccountService struct {
component.Base
app pitaya.Pitaya
}
func NewAccountService(app pitaya.Pitaya) *AccountService {
return &AccountService{
app: app,
}
}
func (s *AccountService) Register(ctx context.Context, req *proto.RegisterRequest) (*proto.CommonResponse, error) {
logrus.Debugf("register request: %v", req)
logrus.Debugf("do something in lobby")
return &proto.CommonResponse{Err: proto.ErrCode_OK}, nil
}
8. 在 proxyServer 中远程调用 lobbyServer
第 4 步里,我们是直接把注册请求返回的,现在我们修改一下代码,让 proxyServer 的 Register
远程调用 lobbyServer 的 Register
。
远程调用可以使用 RPC
和 RPCTo
,后者需要指定 ServerId
,这里使用 RPC
,底层会调用默认的路由方法 defaultRoute
随机选择一个该类型服务器。
// proxyServer/service/accountService.go
func (s *AccountService) Register(ctx context.Context, req *proto.RegisterRequest) (*proto.CommonResponse, error) {
logrus.Debugf("register request: %v", req)
logrus.Debugf("do something in proxy")
rsp := &proto.CommonResponse{}
err := s.app.RPC(ctx, "lobby.account.register", rsp, req)
if err != nil {
return nil, err
}
return rsp, nil
}
9. 测试前后端通信
记得开启 etcd 和 nats-server,框架 RPC 默认就是通过 nats 实现的,对底层实现感兴趣的可以看看这篇文章:cluster grpc demo
开启 1 个 proxyServer 和 2 个 lobbyServer,使用 2 个 pitaya-cli 测试:
pitaya-cli
Pitaya REPL Client
>>> connect 127.0.0.1:40000
Using json client
connected!
>>> request proxy.account.register {"account":"test1", "password":"ttt"}
>>> sv->{}
在 2 个 client 端都发出以上请求,根据默认的路由策略,这些请求会随机分配到 lobby1 或者 lobby2,lobby 侧输出了日志:
level=debug msg="register request: account:\"test1\" password:\"ttt\""
level=debug msg="do something in lobby"
你也可以多开几个 client,看看这些请求最终都分配到哪个 lobby 上。
小结
本节主要是对注册登录所涉及到的服务器框架有了一个大致的了解,并搭建了代理服务器 proxyServer 和大厅服务器 lobbyServer,并且测试了前后端服务器之间的通信。
下一节,我们一起来封装 mongodb 操作,完善注册逻辑,实现玩家数据入库。