刚刚接触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!(注册账号成功!)");
}
},
这就是注册账号的整个流程了。