【Pitaya游戏服务器实战---注册登录】1.3登录逻辑与服务器路由

8 篇文章 0 订阅
4 篇文章 0 订阅

本节代码:xyq10612/PitayaGame at chapter1.3-登录逻辑与服务器路由 (github.com)

前面两节我们完成了账号注册,接下来让我们实现登录逻辑。

我们再看一下 第一节 画的架构图:
在这里插入图片描述

考虑 3 个问题:

  1. ProxyServer 只是代理服务器,真正的登录业务逻辑(如:从数据库中加载玩家数据)是在 LobbyServer 中实现的,但是 LobbyServer 是多开的一个服务器组,代理服务器是如何知道该转发给哪个大厅服的呢?
  2. 假设玩家 PlayerALobbyServer2 中登录了,由于 IO 比较慢,服务器一般会尽量少的去访问 MongoDB,所以即使玩家下线了, PlayerA 的数据也不会立刻从服务器中移除,而是会缓存一段时间。当玩家再次登录上来的时候,代理服务器将登录请求再次转发到 LobbyServer2,此时服务器上是有该玩家的缓存数据的,就不需要再去 DB 拉玩家信息了。那么 ProxyServer 是如何知道玩家之前在哪个服务器服务器登录过、保留有缓存数据呢?
  3. 如果一个账号在短时间内多次登录肯定会出问题,如何实现登录锁机制、保证登录操作的原子性?

本节我们主要解决前两个问题,通过的 SessionRedis 来记录信息,实现 LobbyServer 的路由策略。

封装 Redis Module

既然要借助 Redis 来记录登录信息,那么我们先把操作封装一下,参考对 MongoDB 的封装,实现 DataBase 接口:
PS: 完整的代码实现在 common/modules/redis 目录

type RedisStorage struct {
	modules.Base
	*redis.Client
	config RedisConfig
}

func NewRedisStorage(config RedisConfig) *RedisStorage {
	return &RedisStorage{
		config: config,
	}
}

func (r *RedisStorage) Init() error {
	r.Connect()
	return nil
}

func (r *RedisStorage) Connect() {
	uri := r.config.GetConnURI()
	opts, err := redis.ParseURL(uri)
	if err != nil {
		panic(err)
	}

	r.Client = redis.NewClient(opts)

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

func (r *RedisStorage) TestPing() error {
	_, err := r.Client.Ping(context.TODO()).Result()
	return err
}

func (r *RedisStorage) Close() {
	_ = r.Client.Close()
}

同样的,在 common/helper/redisHelper.go 也提供一个获取 Redis 模块的方法:

var r *redis.RedisStorage

func GetRedis() *redis.RedisStorage {
	if r == nil {
		module, err := pitaya.DefaultApp.GetModule(constants.RedisModule)
		if err != nil {
			panic(err)
		}

		r = module.(*redis.RedisStorage)
	}

	return r
}

定义登录消息

message LoginRequest {
  string account = 1;
  string password = 2;
}

message LoginResponse {
  ErrCode ret = 1;
  string uid = 2;
}

现在我们定义登录的回复只返回了 UID,但是实际上还需要返回其他的信息,比如:玩家的昵称、头像、等级等等。具体细节我们之后再完善,现在先聚焦于登录流程的实现。

在 LobbyServer 处理登录消息

以下 uid 的生成是临时的,但是也保证了唯一性,后面我们会使用 MongoDB 的自增 ID 来生成 UID。
顺便实现一个伪造的登录后逻辑 MockLogic,用于测试,后面我们在使用 pitaya-cli 测试的时候会用上。

// lobbyServer/service/accountService.go
func (s *AccountService) Login(ctx context.Context, req *proto.LoginRequest) (*proto.LoginResponse, error) {
	logger := pitaya.GetDefaultLoggerFromCtx(ctx)

	logger.Infof("login...%v", req)

	uid := req.Account + s.app.GetServerID()[:6]
	return &proto.LoginResponse{Ret: proto.ErrCode_OK, Uid: uid}, nil
}

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

	logger.Infof("mock logic !")

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

在 ProxyServer 处理登录消息

回到本节一开始提出的问题,当一个玩家登录的时候,我们可以将其登录信息记录到 Redis 中,这样当玩家再次登录的时候,我们就可以通过 Redis 来判断该玩家之前是否登录过,如果登录过,就可以直接将消息转发到之前登录的服务器上。如果之前登录的服务器已经下线了,那么就随机转发到一个大厅服上,并且记录下此次登录的服务器,以便下次登录的时候使用。
再进一步思考,客户端注册的时候,与 ProxyServer 的会话已经建立起来了,注册成功时,LobbyServer 可以缓存账号信息,与登录逻辑的优化思路一致,避免过多的 IO 操作,注册之后的其他消息,也应该转发到这个 LobbyServer 上,所以我们在注册成功后,将该 LobbyServer 的 ID 绑定到会话 session上。当客户端接着发送登录消息时,可以直接从会话中获取对应的 LobbyServer ID。

将上述思路分解一下:

  1. 从 session 获取上次的 lobby, 找不到就从 redis 缓存获取上次登录的 lobby
  2. 没有上次登录的, 或者上次登录的 lobby 已经不存在了, 随机分配一个 lobby
  3. 转发登录到 lobby, 由 lobby 处理登录逻辑, 初始化玩家数据等
  4. 更新 redis 缓存, 记录登录的 lobby
  5. 绑定 UID 和 lobby ID 到 Session

注册成功后,将 lobbyID 绑定到 Session

修改 Register 的实现:

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

	rsp := &proto.CommonResponse{Err: proto.ErrCode_ERR}

	lobby := router.GetRandomLobby()
	if lobby == nil {
		logger.Errorf("cannot find random lobby!")
		return rsp, nil
	}

	err := s.app.RPCTo(ctx, lobby.ID, "lobby.account.register", rsp, req)
	if err != nil {
		return nil, err
	}

	_ = session.Set(constants.SessionLobbyIdKey, lobby.ID)

	rsp.Err = proto.ErrCode_OK
	return rsp, nil
}

实现 Login,找到上次登录过的服务器

根据前面的思路分解,依次实现:

func (s *AccountService) Login(ctx context.Context, req *proto.LoginRequest) (*proto.LoginResponse, error) {
	logger := pitaya.GetDefaultLoggerFromCtx(ctx)
	session := s.app.GetSessionFromCtx(ctx)

	rsp := &proto.LoginResponse{Ret: proto.ErrCode_ERR}

	var lobbyId string

	// 1. 从session获取上次的lobby, 找不到就从redis缓存获取上次登录的lobby
	if session.HasKey(constants.SessionLobbyIdKey) {
		lobbyId = session.Get(constants.SessionLobbyIdKey).(string)
		logger.Infof("get lobby from session, account: %s", req.Account)
	}
	if lobbyId == "" {
		loginModel, err := loginModel.Get(req.Account)
		if err != nil {
			return rsp, err
		}
		lobbyId = loginModel.LobbyId
		logger.Infof("get lobby from cache, account: %s", req.Account)
	}

	// 2. 没有上次登录的, 或者上次登录的lobby已经不存在了, 随机分配一个lobby
	if lobbyId == "" || !router.IsLobbyAlive(lobbyId) {
		lobby := router.GetRandomLobby()
		if lobby == nil {
			logger.Errorf("cannot find random lobby!")
			return rsp, nil
		}
		lobbyId = lobby.ID
		logger.Infof("get lobby from random, account: %s", req.Account)
	}

	// 3. 转发登录到lobby, 由lobby处理登录逻辑, 初始化玩家数据等
	err := s.app.RPCTo(ctx, lobbyId, "lobby.account.login", rsp, req)
	if err != nil {
		logger.Errorf("rpc to lobby err: %v", err.Error())
		rsp.Ret = proto.ErrCode_ERR
		return rsp, nil
	}

	logger = logger.WithField("userId", rsp.Uid).WithField("lobby", lobbyId)

	// 4. 更新redis缓存, 记录登录的lobby
	loginModel.Save(req.Account, lobbyId)

	// 5. 绑定session-lobby, session-uid
	session.Bind(ctx, rsp.Uid)
	session.Set(constants.SessionLobbyIdKey, lobbyId)

	logger.Infof("login success account: %s", req.Account)

	return rsp, nil
}

所以在登录成功后,Session 上就有了玩家的 UID连接 上的 lobbyServer。

防止客户端绕过正常登录,直接发送消息给 LobbyServer

第二节末尾我们说过,客户端可以直接发送消息到 lobbyServer,比如通过 lobby.account.register 来注册。这里也是一样的,可以绕过代理服务器,绕过登录逻辑,直接发送 lobbyserver.account.mocklogic 给 lobbyServer,我们测试一下看看:
在这里插入图片描述

尴尬了不是,别说不用登录了,账号都不要,直接可以玩游戏了,这可不行。

先搞清楚一个问题,pitaya 的前端服务器(在我们的实战项目中就是 proxyServer )在收到一个请求时,会根据路由 URL 提取目标 ServerType 并转发。当你没有设置路由规则的时候,pitaya 会使用默认的路由规则,我们前面也提到过,默认路由规则就是返回遍历获取到的第一个服务器。
既然是这样,那我们就可以从路由规则上下功夫,定义自己的路由策略了。在上一步里,登录成功时会将玩家的 UIDlobbyServer ID 都绑定到了 Session,也就是说,没有绑定过的就肯定没正常登录。

捋清楚了,写代码:

// proxyServer/router/router.go
func LobbyRouterFunc(
	ctx context.Context,
	route *route.Route,
	payload []byte,
	servers map[string]*cluster.Server,
) (*cluster.Server, error) {
	// 转发到绑定的 lobby
	session := pitaya.GetSessionFromCtx(ctx)
	if session == nil || !session.HasKey(constants.SessionLobbyIdKey) {
		return nil, errors.New("not find binding lobby in session")
	}
	if session.UID() == "" {
		return nil, errors.New("please login")
	}

	lobbyId := session.Get(constants.SessionLobbyIdKey).(string)
	s, ok := servers[lobbyId]
	if !ok {
		return nil, errors.New("bind lobby is not exist")
	}

	return s, nil
}

main 函数中,还得将路由方法注册进来:

app.AddRoute(constants.LobbyServer, router.LobbyRouterFunc)

测试

开启 1 个 proxy 和 2 个 lobby

1. 客户端不再能绕过 proxyServer 正常的注册登录逻辑

pitaya-cli
Pitaya REPL Client
>>> connect 127.0.0.1:40000
Using json client
connected!
>>> request lobby.account.mocklogic // 不能绕过登录
>>> sv->{"code":"PIT-500","msg":"not find binding lobby in session"}
>>> request lobby.account.register {"account":"test3", "password":"pwd123456"} // 不能绕过 proxy 去 lobby 注册
>>> sv->{"code":"PIT-500","msg":"not find binding lobby in session"}
>>> request proxy.account.register {"account":"test3", "password":"pwd123456"} // 正常注册逻辑
>>> sv->{}
>>> request lobby.account.login {"account":"test3", "password":"pwd123456"} // 不能绕过 proxy 去 lobby 登录
>>> sv->{"code":"PIT-500","msg":"please login"}
>>> request proxy.account.login {"account":"test3", "password":"pwd123456"} // 正常登录逻辑
>>> sv->{"uid":"test37f5fab"}

2. 客户端登录后,每次请求都落在了同一个 lobbyServer

发送 5 次 mocklogic 请求,都落在了同一个 lobbyServer 上
在这里插入图片描述

之前在 Login 成功后,proxyServer 将 UID 绑定到了 Session,这个数据会在底层被同步到后端服务器 lobbyServer,所以 MockLogic 日志打印出了 userId=xxxxxx。pitaya 框架层确实做了非常多有用的功能~!

3. 客户端断开连接后再次登录,会登录上之前的服务器,如果之前的服务器不存在,才会登录到其他还活着的服务器

这部分我就不测试了,开启两个 lobby,关闭其中一个,查看服务器日志就可以测试。
代理服务器会优先将请求分发给上次登录过的 lobby,如果之前的 lobby 不存在了,则会随机分配一个。

小结

本节我们基本实现了登录流程,完成了代理服务器对登录请求的正确路由,解决了客户端绕过代理服务器直接发送消息给 lobbyServer 的问题,但是我们还没有保证登录操作的原子性,对于高并发、高频率的登录请求,还是有可能出现问题的,这个我们会在下一节解决。

个人能力有限,如果你对本节,或者本系列,有任何疑问和建议,欢迎提出、欢迎指正,谢谢!

  • 20
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值