Unity Xlua热更新框架(九):网络部分

14. 编译Xlua第三方库

14-1. 编译Xlua第三方库

通信协议:

  • protobuf、sproto、pbc、pblua、json、cjson……用于服务器与客户端通信的数据格式

Xlua不带这些第三方库
需要把第三方库下载下来,编译,,,对于网络部分已经有现成的项目https://github.com/chexiongsheng/build_xlua_with_libs
查看Xlua文档添加第三库
image.png

 public void Init()
{
    //初始化虚拟机
    LuaEnv = new LuaEnv();
    
    //添加第三方库扩展
    LuaEnv.AddBuildin("rapidjson", XLua.LuaDLL.Lua.LoadRapidJson);
    
    //外部调用require时,会自动调用loader来获取文件
    LuaEnv.AddLoader(Loader);

    m_LuaScripts = new Dictionary<string, byte[]>();

#if UNITY_EDITOR
    if (AppConst.GameMode == GameMode.EditorMode)
        EditorLoadLuaScript();
    else
#endif
        LoadLuaScript();
}

然后使用Cmake安装
上面的第三方库的build文件夹中有CMakeList,里面有#Begin lua-rapidjson的字段,因此直接编译即可。
编译过程见博客。
https://blog.csdn.net/weixin_42264818/article/details/128116856
将编译好的dll放入Xlua项目的Asset/Plugins/x86_64路径下面,覆盖原有的xlua.dll

function Main()
  print("hello main")

  local rapidjson = require('rapidjson')
  local t = rapidjson.decode('{"a":123}')--解码json字符串为table
  print(t.a)
  t.a = 456
  local s = rapidjson.encode(t)--编码json字符串
  print('json', s)
end

image.png

14-2. 网络客户端(C#)

需要完成:服务器连接、消息发送、消息接收、数据解析
创建Framework/Network/NetClient脚本

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using UnityEngine;

public class NetClient
{
    private TcpClient m_Client;//基于TCP封装好的socket
    private NetworkStream m_TcpStream;//可以从TCP里获取网络流
    private const int BufferSize = 1024 * 64;//接收数据的大小
    private byte[] m_Buffer = new byte[BufferSize];//接收数据的缓存区
    private MemoryStream m_MemStream;//MemoryStream类用于向内存而不是磁盘读写数据
    private BinaryReader m_BinaryReader;//用特定的编码将基元数据类型读作二进制值

    //构造方法,实例化m_MemStream、m_BinaryReader
    public NetClient()
    {
        m_MemStream = new MemoryStream();
        m_BinaryReader = new BinaryReader(m_MemStream);
    }
    
    /// 
    /// 连接服务器
    /// 
    /// ip地址
    /// 端口号
    public void OnConnectServer(string host, int port)
    {
        try
        {
            IPAddress[] addresses = Dns.GetHostAddresses(host);
            if (addresses.Length == 0)
            {
                Debug.LogError("host invalid");
                return;
            }
            //判断地址族是ipv6还是ipv4
            if (addresses[0].AddressFamily == AddressFamily.InterNetworkV6)
                m_Client = new TcpClient(AddressFamily.InterNetworkV6);
            else
                m_Client = new TcpClient(AddressFamily.InterNetwork);
            //判断好地址族后,设置参数
            m_Client.SendTimeout = 1000;
            m_Client.ReceiveTimeout = 1000;
            m_Client.NoDelay = true;
            //开始连接,连接成功发起异步回调OnConnect
            m_Client.BeginConnect(host, port, OnConnect, null);
        }
        catch (Exception e)
        {
            Debug.LogError(e.Message);
        }
    }

    //连接成功后执行
    private void OnConnect(IAsyncResult asyncResult)
    {
        //判断是否连接成功
        if (m_Client == null || !m_Client.Connected)
        {
            Debug.LogError("Connect server error!");
            return;
        }
        Manager.Net.OnNetConnected();
        m_TcpStream = m_Client.GetStream();
        //开始接收数据
        m_TcpStream.BeginRead(m_Buffer, 0, BufferSize, OnRead, null);
    }

    //开始接收数据执行
    private void OnRead(IAsyncResult asyncResult)
    {
        try
        {
            if (m_Client == null || m_TcpStream == null)
                return;
            //判断是否读取到了空消息,这个是有效字节数
            int length = m_TcpStream.EndRead(asyncResult);
            
            if (length < 1)
            {
                OnDisConnected();
                return;
            }
            ReceiveData(length);//解析数据
            lock (m_TcpStream)//还需要下一次接收数据,需要清空
            {
                Array.Clear(m_Buffer, 0, m_Buffer.Length);
                m_TcpStream.BeginRead(m_Buffer, 0, BufferSize, OnRead, null);
            }
        }
        catch (Exception e)
        {
            Debug.LogError(e.Message);
            OnDisConnected();
        }
    }
    
    /// 
    /// 解析数据
    /// 
    private void ReceiveData(int len)
    {
        m_MemStream.Seek(0, SeekOrigin.End);//从末尾追加数据,因为前面可能有残余
        m_MemStream.Write(m_Buffer, 0, len);//写数据
        m_MemStream.Seek(0, SeekOrigin.Begin);//移动指针到前面开始读数据
        //如果服务器有延迟等情况,发过来了很多条,需要通过While语句一条一条解析
        //如果剩余字节数>8,说明消息是完整的,,,=8说明只要id和len没有消息内容是空消息
        while (RemainingBytesLength() >= 8)
        {
            //定义了消息msgId和消息长度msgLen,,读两个int,指针往后走
            int msgId = m_BinaryReader.ReadInt32();
            int msgLen = m_BinaryReader.ReadInt32();
            if (RemainingBytesLength() >= msgLen)//说明消息完整
            {
                byte[] data = m_BinaryReader.ReadBytes(msgLen);//读这么多个字节的消息
                string message = System.Text.Encoding.UTF8.GetString(data);//从服务器读出来是json转成字符串
                
                //转到lua
                Manager.Net.Receive(msgId, message);
            }
            else//说明消息不完整或没有消息导致读取有问题,要把前面的id和len这8个字节还回去,然后break出去。
            {
                m_MemStream.Position = m_MemStream.Position - 8;
                break;
            }
        }
        //剩余字节,重新写入m_MemStream
        byte[] leftover = m_BinaryReader.ReadBytes(RemainingBytesLength());
        m_MemStream.SetLength(0);
        m_MemStream.Write(leftover, 0, leftover.Length);
    }
    
    //剩余长度,字节数
    private int RemainingBytesLength()
    {
        return (int)(m_MemStream.Length - m_MemStream.Position);
    }
    
    /// 
    /// 发送消息
    /// 
    /// 消息id
    /// json格式数据消息
    public void SendMessage(int msgID, string message)
    {
        using (MemoryStream ms = new MemoryStream())
        {
            ms.Position = 0;
            BinaryWriter bw = new BinaryWriter(ms);
            byte[] data = System.Text.Encoding.UTF8.GetBytes(message);
            //协议id
            bw.Write(msgID);
            //消息长度
            bw.Write((int)data.Length);
            //消息内容
            bw.Write(data);
            bw.Flush();
            if (m_Client != null && m_Client.Connected)
            {
                byte[] sendData = ms.ToArray();//把内存流的数据拿出来变为Byte发送出去
                m_TcpStream.BeginWrite(sendData, 0, sendData.Length, OnEndSend, null);//发送
            }
            else
            {
                Debug.LogError("服务器未连接");
            }
        }
    }

    //结束发送
    void OnEndSend(IAsyncResult ar)
    {
        try
        {
            m_TcpStream.EndWrite(ar);
        }
        catch (Exception ex)
        {
            OnDisConnected();
            Debug.LogError(ex.Message);
        }
    }

    //断开连接
    public void OnDisConnected()
    {
        if (m_Client != null && m_Client.Connected)
        {
            m_Client.Close();
            m_Client = null;
            
            m_TcpStream.Close();
            m_TcpStream = null;
        }
        Manager.Net.OnDisConnected();
    }
}

创建Framework/Manager/NetManager脚本

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class NetManager : MonoBehaviour
{
    private NetClient m_NetClient;//客户端
    //消息队列,用来接收消息
    private Queue<KeyValuePair<int, string>> m_MessageQueue = new Queue<KeyValuePair<int, string>>();
    private XLua.LuaFunction ReceiveMessage;//接收的消息发给lua的方法

    public void Init()
    {
        m_NetClient = new NetClient();
        //因为要从lua获取这个方法,肯定要在lua完成初始化之后,而lua的初始化是start之后才初始化好
        ReceiveMessage = Manager.Lua.LuaEnv.Global.Get<XLua.LuaFunction>("ReceiveMessage");
    }

    //发送消息
    public void SendMessage(int messageId, string message)
    {
        m_NetClient.SendMessage(messageId,message);
    }
    
    //连接服务器
    public void ConnectServer(string post, int port)
    {
        m_NetClient.OnConnectServer(post, port);
    }
    
    //下面两个可以自己添加逻辑,当连接到网络或者断开连接,执行逻辑
    //网络连接
    public void OnNetConnected()
    {
        
    }
    
    //被服务器断开连接
    public void OnDisConnected()
    {
        
    }
    
    //接收数据
    public void Receive(int msgId, string message)
    {
        //接收到的消息放到队列
        m_MessageQueue.Enqueue(new KeyValuePair<int, string>(msgId, message));
    }

    private void Update()
    {
        if (m_MessageQueue.Count > 0)
        {
            KeyValuePair<int, string> msg = m_MessageQueue.Dequeue();
            ReceiveMessage?.Call(msg.Key, msg.Value);
        }
    }
}

Manager中添加NetManager

private static NetManager _net;
public static NetManager Net
{
    get { return _net; }
}
public void Awake()
{
    _resource = this.gameObject.AddComponent<ResourceManager>();
    _lua = this.gameObject.AddComponent<LuaManager>();
    _ui = this.gameObject.AddComponent<UIManager>();
    _entity = this.gameObject.AddComponent<EntityManager>();
    _scene = this.gameObject.AddComponent<MySceneManager>();
    _sound = this.gameObject.AddComponent<SoundManager>();
    _event = this.gameObject.AddComponent<EventManager>();
    _pool = this.gameObject.AddComponent<PoolManager>();
    _net = this.gameObject.AddComponent<NetManager>();
}

14-3. 网络客户端(Lua)

  • 功能模块化:消息注册、消息发送、消息接收
  • 模块管理器:模块初始化、模块获取、消息接收、消息发送

不同的客户端都有类似的功能,需要在lua中将这些功能抽象出来,放进父类中。对于这么多模块,需要做一个模块管理器,将这些模块作为接口进行使用。同样这个模块管理器需要和C#进行交互

function Class(super)--super是传进来的父类
    local class = nil;--实例都是table
    --构建实例时有父类,那么实例必然有super,如果实例没有父类,实例就有一个构造函数ctor
    if super then
        class = setmetatable({}, {__index = super})--设置传入的父类为元表
        class.super = super;
    else
        class = {ctor = function()  end}--如果没有父类传进来,就直接构造一个新的表,只有一个构造函数ctor
    end
    class.__index = class--__index指向自己
    
    --new的方法为了调用子类和父类的构造函数
    function class.new(...)
        local instance = setmetatable({}, class)
        local function create(inst, ...)
            --判断如果传进来的实例有没有父类,有父类就递归,知道没有父类了,用他自己的构造方法
            if type(inst.super) == "table" then
                create(inst.super, ...);
            end
            --判断如果构造函数ctor是个方法,就用构造方法
            if type(inst.ctor) == "function" then
                inst.ctor(instance, ...);
            end
        end
        create(instance, ...);
        return instance;
    end
    return class;
end
--这个是消息的父类
local base_msg = Class();--使用Class()创建了一个类

--消息注册方法,,,,添加请求和接收,,...是参数列表,需要向服务器发送的key,没有value
function base_msg:add_req_res(msg_name, msg_id, ...)
    local keys = {...};
    
    --Class本质是个table,self[]是相当于给这个table加了一个键值
    --消息请求,,这个方法是发起request请求时调用的方法,传入一些参数
    self["req_"..msg_name] = function(self, ...)
        local values = {...};
        if #keys ~= #values then
            Log.Error("参数不正确:", msg_name);
        end
        local send_data = {};
        --keys values一一对应上,然后发送
        for i =1, #keys do
            send_data[keys[i]] = values[i];
        end
        msg_mgr.send_msg(msg_id, send_data);--manager发送消息
    end
    
    --消息接收的注册,,,,必须要先写进message接收模块,这里是直接判断,没有定义
    if type(self["res_".. msg_name]) == "function" then--如果定义了这个消息接收方法,就调用mgr注册进回调
        msg_mgr.register(msg_id,
            function(data)
                local msg = Json.decode(data);--Json解析为table
                --检查错误码
                if msg.code ~= 0 then
                    Log.Error("错误码:", msg.code);
                    return;
                end
                self["res_"..msg_name](self, msg);--将信息传入这个消息接收方法(message注册的模块方法)
            end)
    else
        Log.Error("请注册消息返回回调:".. msg_name);--有请求一定有接收
    end
end

return base_msg;
Json = require('rapidjson')
Log = require('log')
local msg_mgr = {}

local msg_module_list = {} --模块的列表,存放所有模块
local msg_responses = {} --接收消息的回调的列表

--手动添加每个模块名字
local msg_name_list =
{
    --"msg_test",
}

--遍历一下模块名字列表,require后new一下,将实例存到模块列表中
function msg_mgr.init()
    for k,v in pairs(msg_name_list) do
        msg_module_list[v] = require("message"..v).new()
    end
end

--获取消息,,通过传入模块名字,获取模块
function msg_mgr.get_msg(key)
    if not msg_module_list[key] then
        Log.Error("脚本不存在:"..key)
        return
    end
    return msg_module_list[key]
end

--注册,也就是base_msg调用的register,收到消息时调用的回调方法
function msg_mgr.register(msg_id, func)
    if msg_responses[msg_id] then
        Log.Error("消息已注册:"..msg_id)
        return
    end
    msg_responses[msg_id] = func;
end

--接收消息
function ReceiveMessage(msg_id, message)--NetManager调用了,如果有消息传进来就调用
    Log.Info("receive:<<<<<<<<<<<<<<<<<<<<<<<<:id = "..msg_id.." : "..message.."");
    --如果定义了接收这个消息的方法
    if type(msg_responses[msg_id]) == "function" then
        msg_responses[msg_id](message)
    else
        Log.Error("此消息么有res: ", msg_id)
    end
end

--发送消息
function msg_mgr.send_msg(msg_id, send_data)--base_msg调用,如果有消息请求,kv一致时,发送信息
    local str = Json.encode(send_data)
    Log.Info("receive:>>>>>>>>>>>>>>>>>>>>>>>>:id = "..msg_id.." : "..str.."");
    Manager.Net:SendMessage(msg_id, str)
end

return msg_mgr;

因为测试的时候,需要打日志,用lua打断点不方便,如果删掉日志,真机包看不见不方便,
希望日志存在,正式环境不存在,通过一个开关控制,,,,因此Debug.log不行。需要自己封装打印日志的方法,并且可以输出打印lua的table的方法。

local Log = {}

local function read_table(tab,tab_count)
    local function get_symbol(count)
        local symol = "";
        for i = 1,count do
            symol = symol .. "    ";
        end
        return symol;
    end
    local symbol = get_symbol(tab_count);
    local str = "";
    for k,v in pairs(tab) do
        if type(v) == "table" then
            str = str .. symbol .. k .. ":\n" .. symbol .."{\n"..read_table(v,tab_count + 1)..symbol.."}\n";
        elseif type(v) == "userdata" then
            str = str ..symbol .. k ..  " = userdata,\n";
        elseif type(v) == "function" then
            str = str ..symbol .. k ..  " = function,\n";
        else
            str = str ..symbol .. k ..  " = " .. tostring(v)..",\n";
        end
    end
    return str;
end


local function get_log_string(...)
    local str = "";
    local pram = {...};
    for k,v in pairs(pram) do
        if type(v) == "table" then
            str = str .. "{\n".. read_table(v,1) .."}\n";
        elseif type(v) == "function" then
            str = str .. v ..  "function,\n";
        elseif type(v) == "userdata" then
            str = str .. "userdata,\n";
        else
            str = str .. tostring(v) .. "  ";
        end
    end
    return str;
end


function Log.Info(...)
    if not AppConst.OpenLog then
        return
    end
    CS.Log.Info(get_log_string(...));
end

function Log.Warning(...)
    if not AppConst.OpenLog then
        return
    end
    CS.Log.Warning(get_log_string(...));
end

function Log.Error(...)
    if not AppConst.OpenLog then
        return
    end
    local str = get_log_string(...);
    CS.Log.Error(str .. debug.traceback());
end

return Log;

还需要去Unity中添加一下调用,因为C#中有时候也要调用,,创建Framework/Util/Log

public static class Log
{
    public static void Info(string msg)
    {
        if (!AppConst.OpenLog)
            return;
        Debug.Log(msg);
    }

    public static void Warning(string msg)
    {
        if (!AppConst.OpenLog)
            return;
        Debug.LogWarning(msg);
    }

    public static void Error(string msg)
    {
        if (!AppConst.OpenLog)
            return;
        Debug.LogError(msg);
    }
}
public static bool OpenLog = true;
public class GameStart : MonoBehaviour
{
    public bool OpenLog;

    // Start is called before the first frame update
    void Start()
    {
        AppConst.OpenLog = this.OpenLog;
    }

}

14-4. 测试网络部分

修改main.bytes开始调用网络接口。
创建LuaScripts/message/msg_test.bytes脚本用于消息测试,同时msg_mgr中给msg_name_list添加模块名字

local msg_name_list =
{
    "msg_test",
}
Manager = CS.Manager --引用C#里面定义的类
PathUtil = CS.PathUtil
Vector3 = CS.UnityEngine.Vector3
Input = CS.UnityEngine.Input
KeyCode = CS.UnityEngine.KeyCode
Time = CS.UnityEngine.Time
AppConst = CS.AppConst
Log = require('log')
Json = require('rapidjson')
require('class')
base_msg = require('message.base_msg')
msg_mgr = require('message.msg_mgr')

--定义UI层级
local ui_group = 
{
    "Main",
    "UI",
    "Box",
}

local entity_group = 
{
    "Player",
    "Monster",
    "Effect",
}

Manager.UI:SetUIGroup(ui_group)
Manager.Entity:SetEntityGroup(entity_group)
function Main()
    msg_mgr.init();--加载所有message模块
    Manager.Net:Init();--初始化TCP客户端
    Manager.Net:ConnectServer("127.0.0.1", 8000);--连接网络
    --连接网络ConnectServer调用的NetClient的OnConnectServer,设置端口
    --在调用OnConnect中GetStream和BeginRead获取网络数据流,读取到数据调用OnRead,OnRead中有ReceiveData解析数据
    --解析完继续BeginRead,获取后面的数据
    
    
    --print("hello main")
    Manager.UI:OpenUI("TestUI", "UI", "ui.TestUI")
    --Manager.Scene:LoadScene("Test01","scene.Scene01")
end

--Main()

image.png这是项目服务器
protocl是数据类型
修改TestUI

btn_pooltest:OnClickSet(
      function()
            --Manager.UI:OpenUI("Login/LoginUI", "UI", "ui.TestUI");
            --发送消息请求
            --main.bytes用msg_mgr:Init后,根据msg_name_list就require('msg_test').new()给msg_module_list赋值,require('msg_test')先执行Class(base_msg)
            --Class(base_msg)返回一个class实例,此时msg_test = class实例,msg_test.super = base_msg,然后用require('msg_test').new()就是msg_test.new()
            --new的时候创建了一个instance表,元表是msg_test,然后开始调用create(instance,...),instance没有super去找msg_test.super,由于base_msg = Class()没有super父类,因此base_msg = {ctor = function()  end},不需要递归
            --执行第二个if,instance的ctor是msg_test的ctor,msg_test脚本中创建了ctor,因此执行了构造方法。
            --在ctor中执行add_req_res("first_test", 1000, "id", "user", "password", "listTest")
            --add_req_res方法设置好了msg_test的req_first_test请求方法,然后判断res_first_test的响应回调是否已经定义,只有定义了响应回调,收到消息的时候才能ReceiveMessage执行时,响应回调
            --运行最后面的req_first_test方法,方法内调用msg_mgr的send_msg,再调用Manager.Net:SendMessage向服务器发送消息
            msg_mgr.get_msg("msg_test"):req_first_test(99999, "zhe1123", "******", {1,3,5});
        end
  )

  btn_close:OnClickSet(
      function()
          --self:Close();
      end
  )

你可能感兴趣的:(Xlua,Unity,unity,网络,lua)