zoukankan      html  css  js  c++  java
  • Start KBE 01. 注册流程解读

    刚刚接触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,
    				&copyAccountDefMemoryStream, 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!(注册账号成功!)");
    		}    	
        },
    

      

    这就是注册账号的整个流程了。

  • 相关阅读:
    24.最优布线问题(kruskal算法)
    24.最优布线问题(kruskal算法)
    Algs4-1.4.11为StaticSETofInts添加一个实列方法howMany()
    Algs4-1.4.9预测程序运行时间
    Algs4-1.4.10二分查找找出元素所在的最小索引
    Algs4-1.4.7统计算术运算与比较次数
    Algs4-1.4.8计算输入文件中相等的整数对的数量
    Algs4-1.4.6给出以下代码段的运行时间的增长数量级
    Algs4- 1.4.4参照表1.4.4为TwoSum建立一和类似的表格
    Algs4-1.4.2修改ThreeSum防止两个int值相加可能溢出
  • 原文地址:https://www.cnblogs.com/alinne789/p/5437026.html
Copyright © 2011-2022 走看看