kbengine + cocos2d-js-demo理解
KBEngine 是国内开源的游戏服务器引擎,据说参考了 Bigworld 的架构;网上能找到的开源游戏服务器引擎很少,网易的 Pomelo 是用 Node.js 来实现的,现在还是觉得 C/C++ 更熟悉些,就先从这个开始理解游戏服务器开发吧。
有用链接
- 《KBEngine 官网》
- 《KBEngine 服务器端架构》
- 《KBEngine 论坛》
- 《Bigworld 服务器编程指导》
- 建议加入 QQ 群,里面有一些比较好的共享资料
需要理清的问题
带着问题来阅读源码,比毫无目的的阅读效果更好,下面列出我想要通过源码知道的问题
- 游戏服务器是怎么运行的?
- 服务器与客户端之间怎么交互?
- 多个用户之间怎么看到彼此,即 AOI 是怎么实现的?
- 服务器怎么实现负载均衡?
- 服务器的怎么通过心跳来维护彼此信息?
后续将分几篇文章来解释这些问题。
后续文章需要先对 KBEngine 有一定理解,大体浏览过前面提到的有用链接,至少要清楚下图中的各个组件的功用。(图片来自官网截图,具体请看《KBEngine 服务器端架构》)
官方推荐在 Windows 下编译运行,我也就不在 Linux 下折腾了。
服务器代码编译
在虚拟机里装了个 windows,再安装 VS 2013 。(2016.02.03,官方说支持 VS 2015,但我测试下来 VS 2015 会报 mysqlclient64.lib 无法连接,因为 mysqlclient64.lib 是 VS 2013 生成的)。
按官方的说明,代码 git clone 到本地之后,直接 Rebuild Solution 即可;build 成功会生成一批 exe 文件。
客户端 和 assets 文件
KBEngine 是通过资产文件在配置各个不同的项目,我这里用的是 kbengine_cocos2d_js_demo,使用 js_demo 无需再另外安装一堆客户端程序,具体配置使用可以看官方的 demo 搭建文档。
这里要注意的是,直接 git clone kbengine_cocos2d_js_demo 不能把代码一次性都捞下来,需要按说明,进入到下载下来的目录,再执行以下命令,才会把 kbengine_demos_assets 目录下载下来。因为这个目录是一个链接。
git submodule update --init --remote
安装 mysql
KBEngine 需要使用 mysql 来进行持久化,这个按说明安装。
运行服务器
把 kbengine_demos_assets 拷到服务器代码目录下,与原来的 assets 目录平行。
运行 kbengine_demos_assets 目录下的 start_server.bat,这样直接运行,不需要设置环境变量。因为 start_server.bat 会默认设置当前目录为 KBE_ROOT。
cd ..
set KBE_ROOT=%cd%
set KBE_RES_PATH=%KBE_ROOT%/kbe/res/;%curpath%/;%curpath%/scripts/;%curpath%/res/
set KBE_BIN_PATH=%KBE_ROOT%/kbe/bin/server/
运行客户端
客户端是 cocos2d_js 的程序,就运行 start_http_service.bat 即可(需要先安装 python)。
登录
服务器端和客户端都运行正常后,就可以通过浏览器来登录游戏了。
调试
先把 server 端的各个 exe 程序跑起来;在 VS2013 里,DEBUG -> Attach to Process...,选择具体的进程,如 loginapp.exe 或者 baseapp.exe 都可以。然后就可以设置断点进行调试了。
目录 kbengine_cocos2d_js_demo/cocos2d-js-client 就是一个典型的 Cocos2d-JS 项目,项目结构可以看Cocos 官方说明文档《Cocos2d-JS项目结构介绍》,我们现在只需要知道以下事实即可。
- 相关 js 文件通过 project.json 引入
- main.js 是整个项目的逻辑入口
main.js
如上图,主要的逻辑有两块
- 57~58行,即设置服务器的 ip、port
- 67行,是 Cocos 的启动 Scene
由 project.json 里面可以看到,StartScene 所在的具体路径应该在 src/cc_scripts/StartScene.js
StartScene.js
StartScene 的展现逻辑我们不去管,直接看「登录」按钮点击下去后的处理。
这里通过 fire 一个 「login」事件,把 username 和 password 发送到 plugins/kbengine_js_plugins/kbengine.js 去处理。
kbengine.js
可以看到,2377 行注册了一个事件,即 kbengine.js 里的 login 函数,会响应 「login」 事件。
由上可知,客户端在启动的时候,除了界面展示,最重要的就是在 installEvents 函数里,通过 KBEngine.Event.register 注册各个事件响应函数,即 kbengine.js 这个插件与客户端逻辑代码的交互是通过事件来完成的。
具体的 register/fire 代码这里不再贴出,有兴趣的可以自己去看。基本逻辑如下
- register 的时候,在一个 list 里存放 <事件名,回调函数>的键值对。
- fire 的时候,遍历找到这个事件名对应的回调函数,填入参数来 apply 。
「登录」前需要加载通信协议,加载过程需要在服务器与客户端之间进行函数交互调用。
通过在服务器端和客户端分别解析 res/server/messages_fixed.xml 中声明的协议来完成交互。 其实就是分别构建 <messageId,回调函数> 键值对。
那么,客户端是怎么知道如何解析这个协议的呢?
kbengine.js / login
login 函数中,3086行参数为 true 来调用 login_loginapp,所以逻辑会进入 3093~3096 行,即使用 ws 协议访问在 main.js 里配置的 ip:port;在 connect 成功后的回调函数是 kbengins.js 中的 onOpenLoginapp_login。
ws 协议介绍,这里给出部分截图,便于理解。
kbengine.js / onOpenLoginapp_login
opOpenLoginapp_login 主要有几个逻辑:
- 2600 行,设置 currentserver = 'loginapp'
- 2601 行,设置 currentstate = 'login'
- 2605~2610 行,访问服务器端的 loginapp::importClientMessages 函数,回调函数是 kbengins.js 中的 Client_onImportClientMessages 函数
为什么 2605~2607 行能访问到 loginapp 中的 importClientMessages 函数?
因为 2066 行, KBEngine.messages.Loginapp_importClientMessages 对应的 messageId 在 服务器端 loginapp 中的处理函数是 importClientMessage 函数。这个配对需要在客户端和服务器端都进行声明。
客户端在 kbengine.js 中声明了 Loginapp_importClientMessages 对应的 messageId = 5. 如下图所示:
服务器端在 res/server/messages_fixed.xml 中声明了 messageId = 5 对应的处理函数为 Loginapp 中 importClientMessage 函数。如下图所示:
loginapp.cpp / importClientMessages
服务器端的 loginapp.cpp 中的 importClientMessages 函数,向客户端发送协议信息。
有几个主要逻辑
- 5~24 行,取所有 Client 中标为 Exposed 的协议
- 26~47 行,取所有 Loginapp 中标为 Exposed 的协议
- 49 行,设置发送回客户端的 messageId,这个与客户端的 messageId 要一致,下文有进一步说明。
- 54~78 行,数据塞到流里
- 81 行,发送到客户端
- 17 行,回调函数名称,如 Loginapp::login 改为 Loginapp_login;这个很重要,这里定义了消息名称,客户端会拿这个来回调 loginapp 的函数
kbengine.js / Client_onImportClientMessages
由于前文设置了 onmessage = Client_onImportClientMessages,根据 ws 协议,服务器返回信息时,就会调用到这个函数。
Client_onImportClientMessages 有几个主要逻辑:
- 2972 行,先判断读取到的 messageId 是否为 KBEngine.messages.onImportClientMessages,可见服务器也是以 messageId 来标识客户端的处理函数的。这里确实在服务器端和客户端都做了配对。
服务器端在 res/server/messages_fixed.xml 中声明了 messageId = 518 对应的处理函数为 客户端 中 importClientMessage 函数。如下图所示:
客户端在 kbengine.js 中声明了 onImportClientMessages 对应的 messageId = 518. 如下图所示:
服务器端是如何访问客户端的,将在讲解服务器端代码时说明。 - 2983~3022行,代码先读取 msgid(2983行),然后看 msgname 是否带有 Client_(2996行)
如果有,就是 Client 回调函数(即 kbengine.js 中的以 Client_ 开头的函数,loginapp 服务器往客户端发送协议时,就会把 messages_fixed.xml 中的声明 Client::xxx 改成 Client_xxx 作为函数名),就把 msgid -> Client_xxx 添加到 KBEngine.clientmessage 中;
否则,就是服务器的函数,因为服务器分为 baseapp、loginapp 等,就要分别按 currserver 来存放,前面 (2600 行)设置了 currentserver ='loginapp',所以这里取到的,除去 client 之外,就是 loginapp 中的回调函数;
至此,在客户端中加载 client 和 loginapp 中的协议完成。 - 3026 行, Client_onImportClientMessages 的最后,会调用 onImportClientMessageCompleted 函数。
kbengine.js / onImportClientMessageCompleted
onImportClientMessageCompleted 有几个逻辑:
- 2643行,设置 socket.onmessage 为 app.onmessage
- 2644 行,hello 函数 (握手,这个不属于协议加载,将在后面细讲)
- 还记得前面 2600、2601 行设置的吧,此时 currentserver == 'loginapp',currstate == 'login',所以会执行到 2658 行,即此时才会真正开始发送用户名、密码进行登录。
kbengine.js / onmessage
由前面可知,协议加载完成后,在 onImportClientMessageCompleted 里的 2643 行,会把 socket 的 onmessage 置为上图的 onmessage 函数。
逻辑很简单,就是取到 msgid,然后在 KBEngine.clientmessage 中取到相应的 handler,然后执行即可。由此,服务器端就可以调用到客户端的函数。
小结
抛开前面各种回调,我们来说明一下 KBEngine 的协议机制。为简化起见,这里只说明 loginapp 与 client 进行交互的协议。
- 在 res/server/messages_fixed.xml 中,以 <Client::xxx></Client::xxx> 的形式声明客户端将要提供的函数,以<Loginapp:xxx></Loginapp::xxx> 的形式声明 loginapp 将要提供的函数
- 客户端(这里是 kbengins.js)实现具体的函数
- loginapp 实现具体的函数
- loginapp 加载 message_fixed.xml 并解析,首先建立 <messageId, 3 中实现的函数>的映射关系,即此时客户端可以通过一个 messageId 调用到 loginapp 的函数;其次知道客户端有哪些函数可以被调用,这些函数的 messageId 分别是什么。
- 客户端没法直接加载 message_fixed.xml,只能通过 loginapp 来加载协议;
- 客户端在加载协议之前,不知道 loginapp 的返回协议的函数对应的 id;所以协议加载函数的 id 必须在客户端写死,也就是前面提到的 messageId = 5,这个 messageId 传到 loginapp 服务器之后,就能调到 Loginapp::importClientMessage 函数;并返回协议信息。
- 客户端拿到协议信息,首先建立 <messageId,2 中实现的函数>的映射关系,即此时 loginapp 服务器可以通过一个 messageId 调用到客户端的函数;其次,知道 loginapp 服务器端有哪些函数可以被调用,这些函数的 messageId 分别是什么。
- 至此,客户端和 loginapp 服务器端都分别建立起 <message,回调函数> 的映射关系,即协议构建成功。
最后补充一下,服务器与客户端交互都是通过 MemoryStream 来传递数据,先传递 messageId,然后传递参数;
客户端/loginapp 服务器端读取网络数据时,先读取到 messageId,就知道要调用哪个函数,然后就把后面的参数取出来,进行具体调用即可。
客户端的这个过程,可以参看前面的 onmessage 函数。
协议加载完成之后,客户端就可以向服务器端发送用户名、密码进行登录
在 kbengine.js / onImportClientMessageCompleted 中,会以参数为 false 调用 login_loginapp(false) 函数。
kbengine.js / login
因为参数 noconnect 为 false,所以这里逻辑会走到 3101~3107 行;
这里就是往服务器 logapp 发送用户名、密码等信息,loginapp 的响应函数是 Loginapp::login 。
KBEngine.message.Loginapp_login 是如何对应到这个函数的,请看上方的 loginapp.cpp / importClientMessages 章节。
loginapp.cpp / login
服务器端 login 响应客户端请求,先通过 python 脚本验证,然后数据库验证,然后返回负载较低的baseapp 地址。
上面的代码,精简了各种验证逻辑,可以看到行号是不连续的。这里面有几个逻辑:
- 773 行,表示要加载的 python 脚本文件在 assets/scripts/login 下面?(这个逻辑没有跟到)
- 895~903 行,调用 assets/login/kbengine.py 脚本里的 onRequestLogin 函数。
- 978~984 行,在 pendingLoginMgr_ 中缓存登录信息及客户端地址,因为此次登录尚未处理完,后续需要跳转到 Dbmgr 这个组件去进行处理,在 Dbmgr 处理完成之后,还会跳转回来继续处理。
- 995~999 行,调用 Dbmgr 中的 onAccountLogin 函数验证用户信息。
login / kbengine.py / onRequestLogin
onRequestLogin 函数就只是简单的检查一下用户名、密码的长度就返回了。当然这里只是留了一个接口,可以自行修改逻辑。
dgmgr.cpp / onAccountLogin
这个函数调用了 interface 的 loginAccount 函数来实现。
interface_handler.cpp / loginAccount
可以看到,真正的数据库查询操作,是通过 DBTaskAcountLogin 来实现的。
dbtask.cpp / DBTaskAccountLogin::presentMainThread
数据库的具体查询逻辑我们先不管;1603~1617行,数据查到之后,会反过来调用 Loginapp::onLoginAccountQueryResultFromDbMgr 函数。
Loginapp.cpp / onLoginAccountQueryResultFromDbmgr
通过 Dbmgr 从数据库查询到数据之后,通过这个回调函数,在 Loginapp 里继续处理登录流程。
这里有几个主要逻辑:
- 1110~1117 行,先调用 assets/login/kbengine.py 脚本里的 onLoginCallbackFromDB 函数,给脚本一个嵌入处理流程的机会 ^^
- 1152~1176 行,在 baseapp 上注册;为了叙述流程简单明了,我们只考虑 1165~1176 行的逻辑,也就是用户第一次登录;后面我们会看 Baseappmgr::registerPendingAccountToBaseapp 函数。
login / kbengine.py / onLoginCallbackFromDB
可以看到,现在脚本的实现,就只是简单进行 log 输出。
由上图可可以看到 log 输出结果,在 logger_loginapp.2016-02-13.log 文件中,最前面的 S_INFO 的 S 表示是 Script 的输出。
baseappmgr.cpp / registerPendingAccountToBaseapp
这个函数,就是查找一个负载较低的 baseapp,然后在调用此 baseapp 的 registerPendingLogin 来进行注册。。
上图主要有几块逻辑:
- 367 行,找到最合适的 baseapp,其实就是通过 bestBaseappID_ 来查找的,这个是 Baseappmgr::updateBestBaseapp() 时就设置好了,使用时直接用就好。
至于各个 baseapp 之间的负载均衡,应该是在 Baseappmgr::findFreeBaseapp 函数里,基本逻辑是找到包含 entity 数最少的 baseapp,这个不在这里展开分析。 - 369~383 行,找不到 baseapp,应该是系统负载满了,那么就缓存信息,这里也不展开。
- 389~398 行,在具体的 baseapp 中注册用户信息,实际调用函数为Baseapp::registerPendingLogin。
baseapp.cpp / registerPendingLogin
上图代码有几个主要逻辑:
- 2510~2526 行,调用 BaseappMgr::onPendingAccountGetBaseappAddr,这里最主要的作用是带回 baseapp 的地址和端口。这里绕来绕去应该是因为 baseapp 不能直接和 loginapp 通信,需要通过 baseappMgr?
- 2528~2536 行,在 PendingLoingMgr 中缓存当前登录用户信息,即在 baseapp 上注册一下,后续客户端直接连 baseapp 时需要靠这个注册信息来进一步验证。
baseappmgr.cpp / onPendingAccountGetBaseappAddr
onPendingAccountGetBaseappAddr 调用 Baseappmgr::sendAllocatedBaseappAddr 来实现;最终通过调用 Loginapp::onLoginAccountQueryBaseappAddrFromBaseappmgr,把用户名,账户名,baseapp 端口、地址返回 loginapp。
loginapp.cpp / onLoginAccountQueryBaseappAddrFromBaseappmgr
绕了一大圈,终于回到 Loginapp 了。
在 1207 行取到 客户端 的 channel,然后返回 baseapp 的 ip、port 等信息,1216 行指定这个消息由客户端的 onLoginSuccessfully 来响应,客户端的 Client_onLoginSuccessfully 函数将被调用(协议相关请看前面的两篇文章)。
kbengine.js / Client_onLoginSuccessfully
终于回到客户端了 ^^
- 3238~3239 行,缓存 baseapp 地址
- 3246 行,开始 baseapp 登录
小结
- 在理清楚协议之后,再看这个登录 loginapp 的过程就比较清楚了
- 主要逻辑:loginapp 在验证用户名、密码之后,通过 baseappmgr 获取可用的 baseapp,在此 baseapp 上注册一下,然后向客户端返回 baseapp 的地址,客户端再直接连接 baseapp(需要通过前面在 baseapp 上的注册信息来验证)
- python 脚本:可以看到,在这个登录过程中,主要是通过 python 脚本暴露了几个接口,以便用户可以介入这个登录流程
- python 脚本的作用当然不限于此,后面会基于 python 脚本构造 Entity,然后基于 Mailbox 和 客户端直接通信,这个在后面会进一步讲解。
kbengine + cocos2d-js-demo理解的更多相关文章
- vue自定义指令(Directive中的clickoutside.js)的理解
阅读目录 vue自定义指令clickoutside.js的理解 回到顶部 vue自定义指令clickoutside.js的理解 vue自定义指令请看如下博客: vue自定义指令 一般在需要 DOM 操 ...
- 代码高亮插件——wangHightLighter.js——demo演示
wangHighLighter.js demo 语言: 主题: 转换 说明: wangHightLighter.js是一个可以将代码高亮显示的javascript小插件,支持常用的20多语言. ...
- 基于 Koa.js 平台的 Node.js web 快速开发框架KoaHub.js demo 可安装
KoaHub.js demo KoaHub.js KoaHub.js -- 基于 Koa.js 平台的 Node.js web 快速开发框架.可以直接在项目里使用 ES6/7(Generator Fu ...
- js面向对象理解
js面向对象理解 ECMAScript 有两种开发模式:1.函数式(过程化),2.面向对象(OOP).面向对象的语言有一个标志,那就是类的概念,而通过类可以创建任意多个具有相同属性和方法的对象.但是, ...
- cocos2d js jsb XMLHttpRequest 中文乱码
1.首先讲下怎样使用XMLHttpRequest 下面所说的是在cocos2d-x 2.2.2 或者 2.3 版本号中. 首先要明确cocos2d js事实上分两个版本号,一个是html5的版本号,另 ...
- cocos2d js的一些tip
cocos2d-js-v3.2-rc0 cc.director.end();//退出app cc.Application.getInstance().openURL("http://www. ...
- cocos2d js ClippingNode 制作标题闪亮特效
1.效果图: 之前在<Android 高仿 IOS7 IPhone 解锁 Slide To Unlock>中制作了文字上闪亮移动的效果,这次我们来看下怎样在cocos2d js 中做出类似 ...
- JS之理解继承
JS之理解继承:https://segmentfault.com/a/1190000010468293 1.call继承,也叫借用构造函数.伪造对象或是经典继承.call继承回把父类的私有属性和方法继 ...
- 微信小程序demo理解
p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 14.0px Verdana } p.p2 { margin: 0.0px 0.0px 0.0px 0.0p ...
- 一个demo 理解 vuex
相比接触vue的同学们已经看了官方文档了.这里我用一个简单的demo来阐述下vuex的知识点,虽然简单,但是容易理解.也加深自己的记忆. 用脚手架建立个项目vue init webpakc-simpl ...
随机推荐
- AME_PR采购申请单通过AME审批设定和测试(案例)
2014-06-21 Created By BaoXinjian
- android中ImageView的ScaleType属性
android中ImageView的ScaleType属性 ScaleType的值分别代表的意义: ImageView是Android中的基础图片显示控件,该控件有个重要的属性是ScaleType,该 ...
- 兔子--eclipse设置编码格式
设置编码格式 a:设置eclipse的默认编码格式:window->preferences->Workspace->Text File Encoding b:设置单个项目的编码格式: ...
- 【转】z-index层级在老版本的IE中无效
在CSS中,只能通过代码改变层级,这个属性就是z-index,要让z-index起作用有个小小前提,就是元素的position属性要是relative,absolute或是fixed. 1.第一种情况 ...
- 使用PL/Scope分析PL/SQL代码
使用PL/Scope分析你的PL/SQL代码 从11g開始Oracle引入了PL/Scope 用于编译器收集PL/SQL程序单元的全部标识符(变量名.常量名.程序名等). 收集到的信息可通过一系列静态 ...
- ControlExtensionTest(二)-----CCControlSlider
#include "../CCControlScene.h" class CCControlSliderTest : public CCControlScene { public: ...
- js 取父级 页面上的元素
var bb=window.opener.frames["contentIframe"].document.all["my:费用类别"][0].value; / ...
- Oracle PLSQL Demo - 31.执行动态SQL拿一个返回值
DECLARE v_sql ) := ''; v_count NUMBER; BEGIN v_sql := v_sql || 'select count(1) from scott.emp t'; E ...
- K-Means聚类算法的原理及实现【转】
[转]http://www.aboutyun.com/thread-18178-1-1.html 问题导读:1.如何理解K-Means算法?2.如何寻找K值及初始质心?3.如何应用K-Means算法处 ...
- axel命令 文件下载
axel是Linux下一个不错的HTTP/ftp高速下载工具.支持多线程下载.断点续传,且可以从多个地址或者从一个地址的多个连接来下载同一个文件.适合网速不给力时多线程下载提高下载速度.比如在国内VP ...