ComblockEngine(原KBEngine)源碼剖析1——角色賬號登錄和管理

寫在前面

這個系列的博客,主要記錄自己看CBE(原名KBE)源碼的一些閱讀筆記和心得,個人在看源碼前比較喜歡先那那套源碼做出個有可見性效果的產品demo來,然後根據demo在逐漸深入源碼,所以在此之前先做了個聯機版坦克大戰,想先看看CBE怎麼做遊戲服務器的具體業務功能的,可以先瞅瞅之前的那三篇博客。
基於ComblockEngine+Unity的聯機版坦克大戰(一)
基於ComblockEngine+Unity的聯機版坦克大戰(二)
基於ComblockEngine+Unity的聯機版坦克大戰(三)

我主要是爲了看源碼,實現,所以後續的博客,我應該都主要寫自己的源碼閱讀情況了~


登錄時序圖

先貼上一張新賬號登錄的時序圖。

ClientLoginappDbmgrDBBaseappmgrBaseapplogin基本驗證觸發腳本層onRequestLoginonAccountLogin創建一個DBTaskAccountLogin線程sql查詢返回結果onLoginAccountQueryResultFromDbmgr調用腳本層onLoginCallbackFromDBregisterPendingAccountToBaseappupdateBestBaseapp 選出最優的baseappregisterPendingLogin記錄預登陸賬號信息onPendingAccountGetBaseappAddronLoginAccountQueryBaseappAddrFromBaseappmgronLoginSuccessfullyloginBaseappClientLoginappDbmgrDBBaseappmgrBaseapp


流程分析

一次登陸請求,從客戶端發起,到服務器響應,涉及到至少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年,不再那麼頹,不再那麼容易失去自己,希望自己真的能開始堅持做一件事,比如多在博客上記錄點東西,學點東西,找回持之以恆的感覺。
加油吧,動起來~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章