ComblockEngine源码剖析1——角色账号登录和管理

写在前面

这个系列的博客,主要记录自己看CBE(原名KBE)源码的一些阅读笔记和心得,个人在看源码前比较喜欢先那那套源码做出个有可见性效果的产品demo来,然后根据demo在逐渐深入源码,所以在此之前先做了个联机版坦克大战,想先看看CBE怎么做游戏服务器的具体业务功能的,可以先瞅瞅之前的那三篇博客。
基于ComblockEngine+Unity的联机版坦克大战(一)
基于ComblockEngine+Unity的联机版坦克大战(二)
基于ComblockEngine+Unity的联机版坦克大战(三)

我主要是为了看源码,实现,所以后续的博客,我应该都主要写自己的源码阅读情况了~


登录时序图

先贴上一张新账号登录的时序图。

Client Loginapp Dbmgr DB Baseappmgr Baseapp login 基本验证 触发脚本层onRequestLogin onAccountLogin 创建一个DBTaskAccountLogin线程 sql查询 返回结果 onLoginAccountQueryResultFromDbmgr 调用脚本层onLoginCallbackFromDB registerPendingAccountToBaseapp updateBestBaseapp 选出最优的baseapp registerPendingLogin 记录预登陆账号信息 onPendingAccountGetBaseappAddr onLoginAccountQueryBaseappAddrFromBaseappmgr onLoginSuccessfully loginBaseapp Client Loginapp Dbmgr DB Baseappmgr Baseapp


流程分析

一次登陆请求,从客户端发起,到服务器响应,涉及到至少5个进程间的交互通信。

  • Client最先向Loginapp发起登录请求
    具体代码参见Loginapp::login
    Loginapp会对账号名、消息包体数据做基本的合法性验证。
    由于在之后的流程中需要dbmgr来完成角色数据从db的读取,以及baseappmgr和baseapp的响应,所以,在此,必须保证dbmgr和baseappmgr进程已经启动完毕。
    对于这些进程的状态数据,CBE都是由Components这个单例类来维护。

    Components::ComponentInfos* baseappmgrinfos = Components::getSingleton().getBaseappmgr();
    if(baseappmgrinfos == NULL || baseappmgrinfos->pChannel == NULL || baseappmgrinfos->cid == 0)
    {
    	datas = "";
    	_loginFailed(pChannel, loginName, SERVER_ERR_SRV_NO_READY, datas, true);
    	s.done();
    	return;
    }
    
    • Q1: 如何避免用户连续多次发起登录请求?
      一次完整的登录验证是需要一定时长的,在这个流程中如何避免多次流程的重入,只要在最开始的入口处做一次防重入处理就好。
      在Loginapp中有一个pendingLoginMgr_对象,就是用来干这件事的,这个对象会将此账号的相关数据进行记录,这一类账号属于连上了服务器,但是还未处理完所有流程。维护这份数据,可以有效的避免一次登陆流程中,同一账号多次连续的请求,也可以为后续流程验证做准备。

      PendingLoginMgr::PLInfos* ptinfos = pendingLoginMgr_.find(loginName);
      if(ptinfos != NULL)
      {
      	datas = "";
      	_loginFailed(pChannel, loginName, SERVER_ERR_BUSY, datas, true);
      	return;
      }
      
      ptinfos = new PendingLoginMgr::PLInfos;
      ptinfos->ctype = ctype;
      ptinfos->datas = datas;
      ptinfos->accountName = loginName;
      ptinfos->password = password;
      ptinfos->addr = pChannel->addr();
      ptinfos->forceInternalLogin = forceInternalLogin;
      pendingLoginMgr_.add(ptinfos);
      
  • 将用户信息发送给Dbmgr,进行账号有效性验证
    Dbmgr主要是根据账号从数据库中查找账号信息,由于sql的交互通常比较慢,如果在主线程同步等待sql返回,会严重影响Dbmgr进程的处理效率。这部分CBE采用的是多线程处理,它维护了一个名为pThreadPoolMaps_的线程池,关于线程池和sql的具体操作在后续单独文章里面再写。这里Dbmgr会创建一个DBTaskAccountLogin的Task对象,并把这个Task丢到线程池中去跑。

    具体代码可以参考Dbmgr::onAccountLogin和InterfacesHandler_Dbmgr::loginAccount。

    bool InterfacesHandler_Dbmgr::loginAccount(Network::Channel* pChannel, 
    											std::string& loginName,
    										 	std::string& password, 
    										 	std::string& datas)
    {
    	std::string dbInterfaceName = Dbmgr::getSingleton().selectAccountDBInterfaceName(loginName);
    
    	thread::ThreadPool* pThreadPool = DBUtil::pThreadPool(dbInterfaceName);
    	if (!pThreadPool)
    	{
    		ERROR_MSG(fmt::format("InterfacesHandler_Dbmgr::loginAccount: not found dbInterface({})!\n",
    			dbInterfaceName));
    
    		return false;
    	}
    
    	pThreadPool->addTask(new DBTaskAccountLogin(pChannel->addr(),
    		loginName, loginName, password, SERVER_SUCCESS, datas, datas, true));
    
    	return true;
    }
    
    • Q1: 如何判断账号是否在线?
      根据账号表中的componentID字段来判断,可以参考KBEEntityLogTableMysql::queryEntity这个方法。具体componentID的设置和读取,在DB源码分析时我再去具体瞅瞅。

    • Q2: 在坦克大战demo中,为啥不需要角色账号创建?
      这个原因就在于db查找账号这一步,CBE允许在配置了自动创建账号的情况下,对于一个新账号,会自动进行账号数据的创建,具体代码如下:

      bool DBTaskAccountLogin::db_thread_process()
      {
      	// 这里省略了一大堆别的代码
      	
      	if (g_kbeSrvConfig.getDBMgr().notFoundAccountAutoCreate || 
      		(g_kbeSrvConfig.interfacesAddrs().size() > 0 && !needCheckPassword_/*第三方处理成功则自动创建账号*/))
      	{
      		if(!DBTaskCreateAccount::writeAccount(pdbi_, accountName_, password_, postdatas_, info) || info.dbid == 0 || info.flags != ACCOUNT_FLAG_NORMAL)
      		{
      			ERROR_MSG(fmt::format("DBTaskAccountLogin::db_thread_process(): writeAccount[{}] is error!\n",
      				accountName_));
      
      			retcode_ = SERVER_ERR_DB;
      			return false;
      		}
      
      		INFO_MSG(fmt::format("DBTaskAccountLogin::db_thread_process(): not found account[{}], autocreate successfully!\n", 
      			accountName_));
      
      		info.password = KBE_MD5::getDigest(password_.data(), (int)password_.length());
      	}
      	else
      	{
      		ERROR_MSG(fmt::format("DBTaskAccountLogin::db_thread_process(): not found account[{}], login failed!\n", 
      			accountName_));
      
      		retcode_ = SERVER_ERR_NOT_FOUND_ACCOUNT;
      		return false;
      	}
      
      	return false;
      }
      
  • Dbmgr返回查询数据给Loginapp
    Loginapp在收到Dbmgr返回的账号数据后会做账号有效性验证,比如角色是否被冷冻、是否被封号等等都会在这一步完成,判断是根据flags作为标志位来完成。同时会触发python层的onLoginCallbackFromDB方法,会告知到python层对应的loginName、accountName等数据。loginName是请求登录Loginapp时的登录名,accountName是不一定都等于loginName的,因为一个账号可以由多个三方账号来登录。最后实际进入游戏,访问baseapp的都是accoutName。
    最后,Loginapp会把数据转发到Baseappmgr上,让Baseappmgr转发数据到合适的Baseapp进程中。

  • Baseappmgr处理
    Baseappmgr上主要做3件事:

    1. 记录账号数据记录到pending_logins_中,这个map维护的是account对应的loginApp的信息。

      void Baseappmgr::registerPendingAccountToBaseapp(Network::Channel* pChannel, MemoryStream& s)
      
    2. 更新当前所有Baseapp的负载,并选出负载最低的Baseapp,准备发往账号信息。

      void Baseappmgr::updateBestBaseapp()
      {
      	bestBaseappID_ = findFreeBaseapp();
      }
      
    3. 将账号数据发往筛选出来的Baseapp进程

  • Baseapp处理
    Baseapp其实就有点类似于别的游戏服务器里面的GateServer的概念啦,这里做的事情就非常简单,就是把这个账号数据记录到pendingLoginMgr_中,pendingLoginMgr_也是PendingLoginMgr类的一个对象,用来记录表示,那个已经连上服务器但是还没真实进入游戏的账号信息。
    记录完毕后,Baseapp会以消息onPendingAccountGetBaseappAddr通知Baseappmgr进程。

    void Baseapp::registerPendingLogin(Network::Channel* pChannel, KBEngine::MemoryStream& s)
    {
    	// ...省略一堆数据读取逻辑
    
    	Network::Bundle* pBundle = Network::Bundle::createPoolObject(OBJECTPOOL_POINT);
    	(*pBundle).newMessage(BaseappmgrInterface::onPendingAccountGetBaseappAddr);
    	
    	// ... 省略部分逻辑
    	
    	pChannel->send(pBundle);
    
    	PendingLoginMgr::PLInfos* ptinfos = new PendingLoginMgr::PLInfos;
    	// ...省略相关赋值逻辑
    	pendingLoginMgr_.add(ptinfos);
    }
    
  • Baseappmgr接着要做啥?
    Baseappmgr这会会从pending_logins_这个map中找到这个账号对于的Loginapp进程,然后把Baseapp返回的地址、端口等数据发送回Loginapp,然后把账号信息从pending_logins_中移除。

  • Loginapp最后的处理
    走了老大一圈,就是为了得到账号对于的accountName、Baseapp的地址和端口,拿到数据后,Loginapp就把这重要的信息返回给对应的客户端,整个流程到此就结束了。
    Client之后的通信便是根据拿到的Baseapp地址和端口,用accountName之前向Baseapp发起登录请求。

胡言乱语

这都2020.1.11了,从写下标题到发布,拖了11天,发现自己是真的懒…
真的很想能在2020年,不再那么颓,不再那么容易失去自己,希望自己真的能开始坚持做一件事,比如多在博客上记录点东西,学点东西,找回持之以恒的感觉。
加油吧,动起来~

发布了47 篇原创文章 · 获赞 7 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/it_wjw/article/details/103755238