刚刚接触KBE,之前还是信心满满地以为很容易就能学会,但是当一切细节展现在眼前的时候,才发现这样一个庞然大物放在面前,竟然不知道该从何处下手。
与其左顾右盼,犹豫不决还不如看看源码,所谓庖丁解牛,恢恢乎游刃有余。
(本文参考的例子是kbengine-0.8.2 + kbengine_cocos2d_js_demo-0.8.0。)
废话少说,就从注册流程开始看起吧,这是最基础也是最简单的一个功能了吧。 Step by step,我只分析了简要的流程,并没有做更深入的挖掘。
首先我们看看他的主体流程:
1. 客户端发起请求
2. loginApp处理消息
3. dbmgr 处理消息,并将数据写入db,回包
4. loginApp收到回包并转发
5. 客户端收到回包
怎么样,是不是简单的不得了,只牵涉到loginApp和dbmgr两个进程。
下面我们通过源代码,仔细看看到底发生了些什么事情。
1. 客户端发起请求:
客户端首先相应用户点击“注册”
cocos2d-js-clientsrccc_scriptsStartScene.js
1 touchRegisterButtonEvent: function (sender, type) 2 { 3 switch (type) { 4 case ccui.Widget.TOUCH_BEGAN: 5 break; 6 case ccui.Widget.TOUCH_MOVED: 7 break; 8 case ccui.Widget.TOUCH_ENDED: 9 GUIDebugLayer.debug.INFO_MSG("Connect to server..."); 10 KBEngine.Event.fire("createAccount", this.usernamebox.getString(), this.passwordbox.getString(), "kbengine_cocos2d_js_demo"); 11 break; 12 case ccui.Widget.TOUCH_CANCELED: 13 break; 14 default: 15 break; 16 } 17 },
那么KBEngine.Event.fire到底是个什么鬼呢?其实这是实现在plugin/kbengine_js_plugins/kbengine.js 的本地事件的派发机制,看看它的另外一个接口: KBEngine.Event.register 应该就清楚了。
this.register = function(evtName, classinst, strCallback)
不用在意细节,反正register用来注册evntName的回调函数,fire用来派发evntName事件,从而解除发送端与响应端的紧耦合。
仔细观察代码,就不难发现,其实"createAccout"的回调早就已经注册好了
plugin/kbengine_js_plugins/kbengine.js
KBEngine.Event.register("createAccount", this, "createAccount");
再看看createAccount的实现吧,
plugin/kbengine_js_plugins/kbengine.js
this.createAccount = function(username, password, datas) { KBEngine.app.username = username; KBEngine.app.password = password; KBEngine.app.clientdatas = datas; KBEngine.app.createAccount_loginapp(true); } this.createAccount_loginapp = function(noconnect) { if(noconnect) { KBEngine.INFO_MSG("KBEngineApp::createAccount_loginapp: start connect to ws://" + KBEngine.app.ip + ":" + KBEngine.app.port + "!"); KBEngine.app.connect("ws://" + KBEngine.app.ip + ":" + KBEngine.app.port); KBEngine.app.socket.onopen = KBEngine.app.onOpenLoginapp_createAccount; } else { var bundle = new KBEngine.Bundle(); bundle.newMessage(KBEngine.messages.Loginapp_reqCreateAccount); bundle.writeString(KBEngine.app.username); bundle.writeString(KBEngine.app.password); bundle.writeBlob(KBEngine.app.clientdatas); bundle.send(KBEngine.app); } }
OK,这是把消息发送到loginApp了啊!
值得一提的是,代码在这里绕了一个弯:bundle是的作用是收集并整理数据,KBEngine.app里面封装了一个webSocket,与其说是bundle.send(KBEngine.app),还不说是KBEngine.app.send(bundle.data)更好理解。
更深的东西这里就不提了,因为我也没有去研究。。。
2.loginApp处理消息
直接看收包的代码:
void Loginapp::reqCreateAccount(Network::Channel* pChannel, MemoryStream& s) { std::string accountName, password, datas; s >> accountName >> password; s.readBlob(datas); if(!_createAccount(pChannel, accountName, password, datas, ACCOUNT_TYPE(g_serverConfig.getLoginApp().account_type))) return; }
提取数据,然后将参数转发给_createAccount。_createAccount的代码有250多行,这里只截取一些重要的,并加以解释:
kbesrcserverloginapploginapp.cpp
bool Loginapp::_createAccount(Network::Channel* pChannel, std::string& accountName, std::string& password, std::string& datas, ACCOUNT_TYPE type) { AUTO_SCOPED_PROFILE("createAccount");
/*
首先是一系列的检查,检查数据库是否开放注册,检查用户名,密码长度是否越界。这些代码并不是最重要的,已经被我移除。
*/
/*
下面这段代码做了什么事情呢? PengdingLoginMgr本质上管理着一个从AccountName到ClientInfo的映射关系。
当客户端发出注册请求的时候,loginApp将会检查AccountName是否已经存在于PendingLoginMgr当中,若已经存在,那么就发送err的回包,
如果不存在,那么允许继续处理。在后面的篇幅中,你会发现一旦用户注册成功,那么AccountName就会从PendingLoginMgr中删除。
这样做的好处在于不同用户同时请求相同的账号,能够快速地给与回复,而不需要再从数据走一圈,同时又巧妙地保存了请求端client的相关信息
*/ PendingLoginMgr::PLInfos* ptinfos = pendingCreateMgr_.find(const_cast<std::string&>(accountName)); if(ptinfos != NULL) { WARNING_MSG(fmt::format("Loginapp::_createAccount: pendingCreateMgr has {}, request create failed! ", accountName)); Network::Bundle* pBundle = Network::Bundle::ObjPool().createObject(); (*pBundle).newMessage(ClientInterface::onCreateAccountResult); SERVER_ERROR_CODE retcode = SERVER_ERR_BUSY; (*pBundle) << retcode; (*pBundle).appendBlob(retdatas); pChannel->send(pBundle); return false; }
/*
下面的代码其实是KBE的亮点之一,将一部分控制逻辑交由py脚本处理。我只保留了核心的代码。
*/ { // 把请求交由脚本处理 SERVER_ERROR_CODE retcode = SERVER_SUCCESS; SCOPED_PROFILE(SCRIPTCALL_PROFILE); PyObject* pyResult = PyObject_CallMethod(getEntryScript().get(), const_cast<char*>("onRequestCreateAccount"), const_cast<char*>("ssy#"), accountName.c_str(), password.c_str(), datas.c_str(), datas.length()); /*这里对py脚本的结果做处理,若py处理结果为err,那么loginApp将回复给client对应的消息*/ }
/*
接下来又是很长一大推代码,又是检查用户名的合法性。
可以注册的用户名类型分为ACCOUNT_TYPE_SMART, ACCOUNT_TYPE_NORMAL, ACCOUNT_TYPE_MAIL。
若客户端带上来的类型是Normal的,就做一些最简单的判断。实现在validName函数内部,用一串模式匹配字符串来验证。
若客户端带上来的类型是Email的,就做邮箱格式的判断。实现在email_isvalid函数内部。
若客户端带上来的类型是Smart的,那就先推导它的类型到底是Normal或者Email,然后在验证。
代码太长,已经被删除。
*/
/*
好了,至此我们已经通过了绝大多数的验证。可以把AccountName加入到PendingLoginMgr里面去了。
*/ ptinfos = new PendingLoginMgr::PLInfos; ptinfos->accountName = accountName; ptinfos->password = password; ptinfos->datas = datas; ptinfos->addr = pChannel->addr(); pendingCreateMgr_.add(ptinfos); Components::COMPONENTS& cts = Components::getSingleton().getComponents(DBMGR_TYPE); Components::ComponentInfos* dbmgrinfos = NULL; /*
一系列验证,略
*/ pChannel->extra(accountName); /*最后,向dbmgrApp发送注册请求*/ Network::Bundle* pBundle = Network::Bundle::ObjPool().createObject(); (*pBundle).newMessage(DbmgrInterface::reqCreateAccount); uint8 uatype = uint8(type); (*pBundle) << accountName << password << uatype; (*pBundle).appendBlob(datas); dbmgrinfos->pChannel->send(pBundle); return true; }
这里插入一段py脚本的用户名密码判断函数,
def onRequestCreateAccount(accountName, password, datas): """ KBEngine method. 请求账号创建时回调 """ INFO_MSG('onRequestCreateAccount() %s' % (accountName)) errorno = KBEngine.SERVER_SUCCESS if len(accountName) > 64: errorno = KBEngine.SERVER_ERR_NAME; if len(password) > 64: errorno = KBEngine.SERVER_ERR_PASSWORD; return (errorno, accountName, password, datas)
引擎会找到kbengine_demos_assetsscriptslogin目录下,名为kbengine.py的文件,调用里面的"onRequestCreateAccount"方法。这个函数名写死在c++里,不能随意修改。
3. dbmgr 处理消息,并将数据写入db,回包
直接看收包代码:
srcserverdbmgrdbmgr.cpp
void Dbmgr::reqCreateAccount(Network::Channel* pChannel, KBEngine::MemoryStream& s) { std::string registerName, password, datas; uint8 uatype = 0; s >> registerName >> password >> uatype; s.readBlob(datas); if(registerName.size() == 0) { ERROR_MSG("Dbmgr::reqCreateAccount: registerName is empty. "); return; } pInterfacesAccountHandler_->createAccount(pChannel, registerName, password, datas, ACCOUNT_TYPE(uatype)); numCreatedAccount_++; }
OK,很简单,没啥好说的,然后看createAccout函数
srcserverdbmgrinterfaces_handler.cpp
//------------------------------------------------------------------------------------- bool InterfacesHandler_Dbmgr::createAccount(Network::Channel* pChannel, std::string& registerName, std::string& password, std::string& datas, ACCOUNT_TYPE uatype) { std::string dbInterfaceName = Dbmgr::getSingleton().selectAccountDBInterfaceName(registerName); thread::ThreadPool* pThreadPool =DBUtil::pThreadPool(dbInterfaceName); if (!pThreadPool) { ERROR_MSG(fmt::format("InterfacesHandler_Dbmgr::createAccount: not found dbInterface({})! ", dbInterfaceName)); return false; } // 如果是email,先查询账号是否存在然后将其登记入库 if(uatype == ACCOUNT_TYPE_MAIL) { pThreadPool->addTask(new DBTaskCreateMailAccount(pChannel->addr(), registerName, registerName, password, datas, datas)); return true; } pThreadPool->addTask(new DBTaskCreateAccount(pChannel->addr(), registerName, registerName, password, datas, datas)); return true; }
也很简单,创建了一个任务,加入到线程池里面去。
selectAccountDBInterfaceName其实是获取数据库的配置信息,它记载在一个xml文件里,默认地只有一个default项目:
kbe esserverkbengine_defs.xml
<databaseInterfaces> <!-- 数据库接口名称 (可以定义多个不同的接口,但至少存在一个default) (Database interface name) --> <default> <!-- 如果为true,则为纯净的数据库,引擎不创建实体表 (If true is pure database, engine does not create the entity table) --> <pure> false </pure> <!-- 数据库类型 (mysql、redis) (Database type(mysql, redis)) --> <type> mysql </type> <!-- Type: String --> <!-- 数据库地址 (Database address) --> <host> localhost </host> <!-- Type: String --> <port> 3306 </port> <!-- Type: Integer --> <!-- 数据库账号验证 (Database auth) --> <auth> <username> kbeDataBase </username> <!-- Type: String --> <password> 123456 </password> <!-- Type: String --> <!-- 为true则表示password是加密(rsa)的, 可防止明文配置 (is true, password is RSA) --> <encrypt> true </encrypt> </auth> <!-- 数据库名称 (Database name) --> <databaseName> kbeGameDataBase </databaseName> <!-- Type: String --> <!-- 数据库允许的连接数 (Number of connections allowed by the database) --> <numConnections> 5 </numConnections> <!-- Type: Integer --> <!-- 字符编码类型 (Character encoding type) --> <unicodeString> <characterSet> utf8 </characterSet> <!-- Type: String --> <collation> utf8_bin </collation> <!-- Type: String --> </unicodeString> </default> </databaseInterfaces>
下面,我们再来看看DBTaskCreateAccount实现了写什么东西:
bool DBTaskCreateAccount::db_thread_process() { ACCOUNT_INFOS info; success_ = DBTaskCreateAccount::writeAccount(pdbi_, accountName_, password_, postdatas_, info) && info.dbid > 0; return false; } //------------------------------------------------------------------------------------- bool DBTaskCreateAccount::writeAccount(DBInterface* pdbi, const std::string& accountName, const std::string& passwd, const std::string& datas, ACCOUNT_INFOS& info) { info.dbid = 0; if(accountName.size() == 0) { return false; } // 寻找dblog是否有此账号, 如果有则创建失败 // 如果没有则向account表新建一个entity数据同时在accountlog表写入一个log关联dbid EntityTables& entityTables = EntityTables::findByInterfaceName(pdbi->name()); KBEAccountTable* pTable = static_cast<KBEAccountTable*>(entityTables.findKBETable("kbe_accountinfos")); KBE_ASSERT(pTable); ScriptDefModule* pModule = EntityDef::findScriptModule(DBUtil::accountScriptName()); if(pModule == NULL) { ERROR_MSG(fmt::format("DBTaskCreateAccount::writeAccount(): not found account script[{}], create[{}] error! ", DBUtil::accountScriptName(), accountName)); return false; } if(pTable->queryAccount(pdbi, accountName, info) && (info.flags & ACCOUNT_FLAG_NOT_ACTIVATED) <= 0) { if(pdbi->getlasterror() > 0) { WARNING_MSG(fmt::format("DBTaskCreateAccount::writeAccount(): queryAccount error: {} ", pdbi->getstrerror())); } return false; } bool hasset = (info.dbid != 0); if(!hasset) { info.flags = g_kbeSrvConfig.getDBMgr().accountDefaultFlags; info.deadline = g_kbeSrvConfig.getDBMgr().accountDefaultDeadline; } DBID entityDBID = info.dbid; if(entityDBID == 0) { // 防止多线程问题, 这里做一个拷贝。 MemoryStream copyAccountDefMemoryStream(pTable->accountDefMemoryStream()); entityDBID = EntityTables::findByInterfaceName(pdbi->name()).writeEntity(pdbi, 0, -1, ©AccountDefMemoryStream, pModule); } KBE_ASSERT(entityDBID > 0); info.name = accountName; info.email = accountName + "@0.0"; info.password = passwd; info.dbid = entityDBID; info.datas = datas; if(!hasset) { if(!pTable->logAccount(pdbi, info)) { if(pdbi->getlasterror() > 0) { WARNING_MSG(fmt::format("DBTaskCreateAccount::writeAccount(): logAccount error:{} ", pdbi->getstrerror())); } return false; } } else { if(!pTable->setFlagsDeadline(pdbi, accountName, info.flags & ~ACCOUNT_FLAG_NOT_ACTIVATED, info.deadline)) { if(pdbi->getlasterror() > 0) { WARNING_MSG(fmt::format("DBTaskCreateAccount::writeAccount(): logAccount error:{} ", pdbi->getstrerror())); } return false; } } return true; }
(这段代码关于entityTable的作用我还没有看。)
DBTaskCreateAccount继承于Task接口,Task里定义了一个纯虚函数 virtual bool process() = 0; 当添加到线程池的任务被调度到的时候,process就将被访问。
DBTaskCreateAccount的process千转百转,最终就会调用到上面的writeAccount函数。很明显这里就是在写数据库了,pTable其实就是对数据库访问函数的一层封装,pTable->logAccount将会把账号写入。
注意这里保存着一个success的返回值,这个值在main线程中有用,他是判断数据库写入成功与否,如何回包的依据。
当一个任务被完成了,那么线程池就会把它放入到已经finish的task队列里。
这个ThreadPool的原理这里就不多说了,也许以后会花时间去研究下,但大致就是这样。
dbMgr的主线程会不停调度finish的task的方法,task::presentMainThread。下面是DBTaskCreateAccount::presentMainThread实现了
thread::TPTask::TPTaskState DBTaskCreateAccount::presentMainThread() { DEBUG_MSG(fmt::format("Dbmgr::reqCreateAccount: {}. ", registerName_.c_str())); Network::Bundle* pBundle = Network::Bundle::ObjPool().createObject(); (*pBundle).newMessage(LoginappInterface::onReqCreateAccountResult); SERVER_ERROR_CODE failedcode = SERVER_SUCCESS; if(!success_) failedcode = SERVER_ERR_ACCOUNT_CREATE_FAILED; (*pBundle) << failedcode << registerName_ << password_; (*pBundle).appendBlob(getdatas_); if(!this->send(pBundle)) { ERROR_MSG(fmt::format("DBTaskCreateAccount::presentMainThread: channel({}) not found. ", addr_.c_str())); Network::Bundle::ObjPool().reclaimObject(pBundle); } return thread::TPTask::TPTASK_STATE_COMPLETED; }
看见没,之前的success变量起到作用了。dbMgr向LoginApp回包了。
4. loginApp收到回包并转发
代码很简单,无非就是把注册结果在py脚本里判断下,然后再回包给客户端。流程和发包有些类似,这里就不重复了。
5. 客户端收到回包
客户端收到回包,并把结果显示出来。
pluginskbengine_js_pluginskbengine.js
this.Client_onCreateAccountResult = function(stream) { var retcode = stream.readUint16(); var datas = stream.readBlob(); if(retcode != 0) { KBEngine.ERROR_MSG("KBEngineApp::Client_onCreateAccountResult: " + KBEngine.app.username + " create is failed! code=" + KBEngine.app.serverErrs[retcode].name + "!"); return; } KBEngine.Event.fire("onCreateAccountResult", retcode, datas); KBEngine.INFO_MSG("KBEngineApp::Client_onCreateAccountResult: " + KBEngine.app.username + " create is successfully!"); }
srccc_scriptsStartScene.js “onCreateAccountResult”的时间响应函数在下面,look,
onCreateAccountResult : function(retcode, datas) { if(retcode != 0) { GUIDebugLayer.debug.ERROR_MSG("CreateAccount is error(注册账号错误)! err=" + retcode); return; } //if(KBEngineApp.validEmail(stringAccount)) //{ // GUIDebugLayer.debug.INFO_MSG("createAccount is successfully, Please activate your Email!(注册账号成功,请激活Email!)"); //} //else { GUIDebugLayer.debug.INFO_MSG("CreateAccount is successfully!(注册账号成功!)"); } },
这就是注册账号的整个流程了。