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 和 帶前導長度的字符串(無\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() 函數過程


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM