写在前面
这个系列的博客,主要记录自己看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源码分析时我再去具体瞅瞅。扫描二维码关注公众号,回复: 8539242 查看本文章 -
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年,不再那么颓,不再那么容易失去自己,希望自己真的能开始坚持做一件事,比如多在博客上记录点东西,学点东西,找回持之以恒的感觉。
加油吧,动起来~