• kbengine + cocos2d-js-demo理解


    KBEngine 是国内开源的游戏服务器引擎,据说参考了 Bigworld 的架构;网上能找到的开源游戏服务器引擎很少,网易的 Pomelo 是用 Node.js 来实现的,现在还是觉得 C/C++ 更熟悉些,就先从这个开始理解游戏服务器开发吧。

    有用链接

    需要理清的问题

    带着问题来阅读源码,比毫无目的的阅读效果更好,下面列出我想要通过源码知道的问题

    • 游戏服务器是怎么运行的?
    • 服务器与客户端之间怎么交互?
    • 多个用户之间怎么看到彼此,即 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 进行交互的协议。

      1. 在 res/server/messages_fixed.xml 中,以 <Client::xxx></Client::xxx> 的形式声明客户端将要提供的函数,以<Loginapp:xxx></Loginapp::xxx> 的形式声明 loginapp 将要提供的函数
      2. 客户端(这里是 kbengins.js)实现具体的函数
      3. loginapp 实现具体的函数
      4. loginapp 加载 message_fixed.xml 并解析,首先建立 <messageId, 3 中实现的函数>的映射关系,即此时客户端可以通过一个 messageId 调用到 loginapp 的函数;其次知道客户端有哪些函数可以被调用,这些函数的 messageId 分别是什么。
      5. 客户端没法直接加载 message_fixed.xml,只能通过 loginapp 来加载协议;
      6. 客户端在加载协议之前,不知道 loginapp 的返回协议的函数对应的 id;所以协议加载函数的 id 必须在客户端写死,也就是前面提到的 messageId = 5,这个 messageId 传到 loginapp 服务器之后,就能调到 Loginapp::importClientMessage 函数;并返回协议信息。
      7. 客户端拿到协议信息,首先建立 <messageId,2 中实现的函数>的映射关系,即此时 loginapp 服务器可以通过一个 messageId 调用到客户端的函数;其次,知道 loginapp 服务器端有哪些函数可以被调用,这些函数的 messageId 分别是什么。
      8. 至此,客户端和 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 和 客户端直接通信,这个在后面会进一步讲解。
  • 相关阅读:
    BZOJ1106[POI2007]立方体大作战tet
    BZOJ4407 于神之怒加强版
    BZOJ1103: [POI2007]大都市meg
    BZOJ3170: [Tjoi2013]松鼠聚会
    Luogu 2912 [USACO08OCT]牧场散步Pasture Walking
    BZOJ1251 序列终结者- splay
    BZOJ1699: [Usaco2007 Jan]Balanced Lineup排队
    BZOJ 1005[HNOI2008]明明的烦恼
    二叉树
    [CODEVS1130]数字反转
  • 原文地址:https://www.cnblogs.com/kefeiGame/p/8242472.html
Copyright © 2020-2023  润新知