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() 函数过程

原文地址:https://www.cnblogs.com/Wilson-Loo/p/3302419.html