寫在前面
這個系列的博客,主要記錄自己看CBE(原名KBE)源碼的一些閱讀筆記和心得,個人在看源碼前比較喜歡先那那套源碼做出個有可見性效果的產品demo來,然後根據demo在逐漸深入源碼,所以在此之前先做了個聯機版坦克大戰,想先看看CBE怎麼做遊戲服務器的具體業務功能的,可以先瞅瞅之前的那三篇博客。
基於ComblockEngine+Unity的聯機版坦克大戰(一)
基於ComblockEngine+Unity的聯機版坦克大戰(二)
基於ComblockEngine+Unity的聯機版坦克大戰(三)
我主要是爲了看源碼,實現,所以後續的博客,我應該都主要寫自己的源碼閱讀情況了~
登錄時序圖
先貼上一張新賬號登錄的時序圖。
流程分析
一次登陸請求,從客戶端發起,到服務器響應,涉及到至少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件事:-
記錄賬號數據記錄到pending_logins_中,這個map維護的是account對應的loginApp的信息。
void Baseappmgr::registerPendingAccountToBaseapp(Network::Channel* pChannel, MemoryStream& s)
-
更新當前所有Baseapp的負載,並選出負載最低的Baseapp,準備發往賬號信息。
void Baseappmgr::updateBestBaseapp() { bestBaseappID_ = findFreeBaseapp(); }
-
將賬號數據發往篩選出來的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年,不再那麼頹,不再那麼容易失去自己,希望自己真的能開始堅持做一件事,比如多在博客上記錄點東西,學點東西,找回持之以恆的感覺。
加油吧,動起來~