zoukankan      html  css  js  c++  java
  • Lua 自适应协议解析器开发记录(一)

      在游戏项目开发中, 需要涉及协议的定义及解析, 例如服务端使用c++底层, 前端使用 as进行 flash显示, 前后段数据通信采用 socket, 这就需要协议的定制了. 服务端使用 c++ 做底层网络维护, 搭配 lua 脚本处理逻辑 和 协议解析处理; 使用这种方式的好处时, 指定新协议或修改时, 无需重新编译 C++ 的底层, 只需要修改 lua 脚本, 并重启 服务端程序或 重新加载 lua脚本即可. 唯一的问题时, 当前项目在立项时, 被设计的不友好, 每个模块分配不同同事开发, 每个同事都需要了解协议的格式, 例如"交换背包内两个物品时", lua 这边需要有如

    function ChangePos( user, sockRawData )
        local operation = CParse( sockRawData, "i:operation" )
        if operation == 1 then --删除物品
            local deleteItemID = CParse( sockRawData, "i:itemid" )
            ... delete item 
        elseif operation == 2 then --交换位置
            local firstItemID, secondItemID = CParse( sockRawData, "i:first|i:second" )
            ... change firstItemID and secondItemID
        else ...
            ...
        end
    
    end

    即负责 ChangePos() 模块的开发人员, 必须直到 CParse() 的使用方式, 以及 协议的构成 : i:packetid|i:operation|?:?

    理想的的 ChangePos() 应该能够这样

    function ChangePos( user, readable )
        if readable.option == 1 then 
            delete ( readable.itemid)
        elseif readable.operation == 2 then 
            change( readable.first, readable.second )
        else ...
            ...
        end
    
    end

    即无需在关注 socket 是如何构成原始数据的, 如何进行进行解析; 只需要直到 逻辑数据的构成, 例如 case option == 1 , 后面的 第一个数据就是 要删除的 itemid, case option == 2 , 后面的 两个数据分别是 将要进行交换的 物品 first 和 second 更多工作放在 业务逻辑上.

    确实, json, messagepack, protobuf 都能够实现这种需求; 但目前, 我只在 c++ 底层实现了 protobuf 的使用, 并且想再 用现有的 protobuf-for-lua 按照 个lua 版本的 protobuf, 但是太麻烦了: 还需要 安装 python, 等其他库; 并且 c++ 的protobuf 是 通过 预先使用 protc 生成 各个协议的 静态类, 再拷贝到 实际项目中进行引入的 但(我的浅薄经验来看) 我人 希望很多不是很底层的代码都能够自己掌控, 并且 能够使用设计模式 设计类 , 例如 抽象工厂模式 管理 各个协议等等.

    好吧, 服务端用 C++ 开发, 确定的协议规则有: 1.每个协议数据包分为两部分 包头(指代包体的长度) 和 包体 2.数据类型只有两种: int32 和 带前导长度的字符串(无) 3.没有数据压缩

    在服务端, 不适用 线程的 protobuf 等应用库, 使用最简单的数据发送方式; 在前端, 使用 Love2D , 使用 lua 脚本进行编写, 解析协议数据 , 也不使用 protobuf 为了在 Love2D 前端能够使用 :

    function ChangePos( user, readable )
        if readable.option == 1 then 
            delete ( readable.itemid)
        elseif readable.operation == 2 then 
            change( readable.first, readable.second )
        else ...
            ...
        end
    
    end

    这种方式进行开发, 必须有一个机制, 能够将 原始的网络数据解析成 readable 的 lua-table 虽然, 在 Love2D 的网络通信中, 我使用的是 自带的 socket 库: require( "socket"), 并且接收到的数据也是 字符串数据类型(虽然游戏字符无法解析), 即便也能够使用 lua 语言进行解析; 为了练习, 我还是将 网络数据 定义成  userdata , 以备后来 前端修改为 C++; 呃, 虽然 Love2D 也是C++编写的, 能够重新编译或者修改 Love2D 源代码, 生成自己的 Love2D; 我不要, 我当前的目标是快速的开发前端, 毕竟服务端设计才是我应该关注的重点; 于是我现在使用的Love2D 是 exe 已编译成功的. 既然, 前端的 "使用C++编写的" Love2D 引擎不能被修改, 又不使用 Love2D 的 Lua 进行协议解析, 只能是 用 C++ 开发一个 protocal.dll 供 Love2D 的 lua 脚本使用 rquire( "protocal" ) 预期的 Love2D 前端使用 效果将如:

    packet_format_list = 
    {
        [1001] = "i:packetid|i:option{{i:itemid}{i:first}{i:second}}", --背包相关协议
        [1002] = ...
        ...
    }
    
    function Underlying_on_network_receive_data( user, sockRawUserData )
    
        local packet_id     = nil
        local packet_format = nil 
        
        ...
        --假设经过上面处理, 确定该协议 是  1001 
        packet_id = 1001
        ...
    
        local packet_format = packet_format[ packet_id]
    
        local protocal = require( "protocal" )
        local readable = protocal.unpack( sockRawData, packet_format_list )
    
    
        --因为已经假设该协议是 1001 的背包相关协议
        ChangePos( user, readable )
    
    end

    所以, 本篇随笔的 主要工作就是 protocal.dll 的开发. 为了介绍 protocal.unpack() 函数的实际工作过程, 有必要先规定 通信协议 及 协议的编写, 其中的 三点已在前面介绍:

    --数据规则
    1.每个协议数据包分为两部分 包头(指代包体的长度) 和 包体
    2.数据类型只有两种: int32 和 带前导长度的字符串(无0)
    3.没有数据压缩
    
    --协议编写规则
    关键字符/或短语:
    
    i:                    int32                
                        例如     i:goldAdd
    s:                    带长度的字符创       
                        例如     s:newname
    {}                  数据块               
                        例如     {i:goldAdd|i:copperAdd}
    |                   分隔符               
                        例如分隔不同的字段     i:goldAdd|i:copperAdd
                        数据块之间不需要该分隔符, 例如{i:goldAdd|i:copperAdd}|{s:newname} 会被自动整理成 {i:goldAdd|i:copperAdd}{s:newname}, 
                因为 {} 自带
    ""(及分隔)的意义 控制字符( 下面介绍的 o: 和 r:) 后面不需要该分隔符号, 例如 o:option|{{i:goldAdd|i:copperAdd}}
                会被整理成 o:option{{i:goldAdd|i:copperAdd}} 因为 {} 自带
    ""(及分隔)的意义 o: 开关标记, 开关数值 范围为 [1,...) 大于0的连续整数,后面会附带一组 {} 标记的数据块列表, 举例来说 o:option{ {i:goldAdd|i:copperAdd},{s:newname},{i:hpAdd},{r:mpAdjustList{i:adj}} } switch option : case 1: 后面有两个数据 i:goldAdd|i:copperAdd 生成的lua 结构: { option = 1, option_list = { goldAdd = ?, copperAdd = ?, }, } case 2: 后面有一个数据 s:newname 生成的lua 结构: { option = 2, option_list = { newname = ?, }, } case 3:后面有一个数据 i:hpAdd 生成的lua 结构: { option = 3, option_list = { hpAdd = ?, }, } case 4:后面是一个循环数据块 {r:mpAdjustList{i:adj} } 生成的lua 结构: { option = 4, option_list = { ??? }, } 即 当前 option 的值,附带 列表的值 以 _list 结尾 r: 表示后面多少相同模式的数据, 举例来说 {r:mpAdjustList{i:adj} } 此数据块有 mpAdjustList 个循环数据, 每个循环内 有一个数据 i:adj --能够被忽略,但为了易查看的字符 ignored characters: , 协议例子: "i:packetid|i:userid|i:username|i:gold|i:copper|i:hp|i:mp|o:switch{{i:goldAdd|i:copperAdd},{s:newname},{i:hpAdd},{r:mpAdjustList{i:adj}} }" 可转化为层次结构: "i:packetid|i:userid|i:username|i:gold|i:copper|i:hp|i:mp|o:switch { {i:goldAdd|i:copperAdd}, {s:newname}, {i:hpAdd}, {r:mpAdjustList {i:adj} } }"

    介绍完协议规则后, 下面开始 protocal.dll 的实际开发

    首先, 使用 vs2010 创建 protocal.dll

    extern "C"
    {
    #include "lua/lua.h"
    #include "lua/lualib.h"
    #include "lua/lauxlib.h"
    }
    
    #include <Windows.h>
    #include <WinCrypt.h>
    
    extern "C" int unpack( lua_State* L)
    {
        printf( "hello, pig! ready to unpack ...");
        return 0;
    }
    
    struct luaL_reg protocalFunctions[] = 
    {
        { "unpack", unpack},
        { 0, 0}
    };
    
    extern "C" int luaopen_protocal( lua_State* L)
    {
        luaL_register( L, "protocal", protocalFunctions);
        return 1;
    }
    
    生成 protocal.dll

    注意, 当前版本的lua 我遇到一个问题, 如果将 protocal.dll 改名为 otherName.dll 在lua 进行 require( "otherName") 时会报错:找不到指定过的模块 解决方案:

    1. dll 导出给 lua 的模块名:extern "C" int luaopen_MYLIBRARY( lua_State* L) 
    
    2. vs 生成的 MYLIBRARY.dll 
    
    3. require( "MYLIBRARY" )

    这三者的 MYLIBRARY 要一致, 本项目为:

    1. dll 导出给 lua 的模块名:extern "C" int luaopen_protocal( lua_State* L) 
    
    2. vs 生成的 protocal.dll 
    
    3. require( "protocal" )

    另外对于vs2010 还要设置 "模块定义文件" < "输入" < "链接器" < "配置属性" 添加 .def 文件:

    例如:

    EXPORTS luaopen_netpack

    (最后, 使用 Love2D 时, 需要将 生成的 protocal.dll 放在love.exe 同目录内)

    使用时:

    --test.lua 
    
    require( "protocal" )
    
    protocal.unpack() --将会输出: hello, pig! ready to unpack ...

    到此, 使用 vs2010 导出 dll 给 lua 使用的框架暂且搭好了

    备注:

    1.这是线程不安全的, 因为使用 strtok 进行切割 
    
    2.未考虑大端小端问题 
    
    3.字符创采用 length-based , 并且给 sizeof(前导长度) == sizeof(int)

    ...后续: 具体 unpack() 函数过程

  • 相关阅读:
    包含游标、数组的例子
    团队开发的注意点
    养成逻辑的习惯
    C# DEV 右键出现菜单
    C#中ToString数据类型格式大全 千分符(转)
    Oracle系统查询的语句
    PLSQLDevelop 查询当前未完成的会话
    Oracle 异常工作中出现的
    Oracle 返回结果集 sys_refcursor
    DevExpress.XtraEditors.TextEdit,设定为必须number类型的。
  • 原文地址:https://www.cnblogs.com/Wilson-Loo/p/3302419.html
Copyright © 2011-2022 走看看