在游戏项目开发中, 需要涉及协议的定义及解析, 例如服务端使用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 和 带前导长度的字符串(无\0) 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: \t\r\n, 协议例子: "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() 函数过程