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

文章目錄
寫在前面
登錄時序圖
流程分析
胡言亂語
寫在前面
這個系列的博客,主要記錄自己看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)
1
更新當前所有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年,不再那麼頹,不再那麼容易失去自己,希望自己真的能開始堅持做一件事,比如多在博客上記錄點東西,學點東西,找回持之以恆的感覺。
加油吧,動起來~
————————————————
版權聲明:本文爲CSDN博主「書影_」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/it_wjw/article/details/103755238

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