通信协议:
Xlua不带这些第三方库
需要把第三方库下载下来,编译,,,对于网络部分已经有现成的项目https://github.com/chexiongsheng/build_xlua_with_libs
查看Xlua文档添加第三库
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
需要完成:服务器连接、消息发送、消息接收、数据解析
创建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>();
}
不同的客户端都有类似的功能,需要在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;
}
}
修改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()
这是项目服务器
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
)