如今大部分的游戏都是网络游戏,网络游戏便涉及到网络连接发起、网络数据接收等内容。LuaFramework内置了网络模块(NetworkManager、SocketClient、ByteBuffer、Converter、Protocal),本篇将会介绍该模块的调用方法以及其原理。
1、发起连接
发起连接是客户端网络通信的第一步,LuaFramewor中,只需通过LuaFramework.AppConst.SocketAddress和LuaFramework.AppConst.SocketPort设置ip和端口,然后调用NetworkManager的SendConnect方法即可发起连接。Main.lua的代码如下:
require "Network"
--主入口函数。从这里开始lua逻辑
function Main()
local LuaHelper = LuaFramework.LuaHelper
local networkMgr = LuaHelper.GetNetManager()
local AppConst = LuaFramework.AppConst
AppConst.SocketPort = 1234;
AppConst.SocketAddress = "127.0.0.1";
networkMgr:SendConnect();
end
在收到服务端回应后,LuaFramework会调用Network的OnSocket方法(写死)。新建名为Network.lua的文件,处理消息回调。在如下的代码中,Protocal代表协议号,比如“连接服务器”(Protocal.Connect)的协议号是101,在OnSocket的参数中,key便是收到的协议号,data是收到的数据。
Network = {};
--协议
Protocal = {
Connect = '101'; --连接服务器
Exception = '102'; --异常掉线
Disconnect = '103'; --正常断线
Message = '104'; --接收消息
}
--Socket消息--
function Network.OnSocket(key, data)
if key == 101 then
LuaFramework.Util.Log('OnSocket Connect');
else
LuaFramework.Util.Log('OnSocket Other');
end
end
为了测试网络功能,需要编写服务端,这里使用c#编写一套简单的服务端程序,仅为调试使用,代码如下:
using System;
using System.Net;
using System.Net.Sockets;
using System.Linq;
class MainClass
{
public static void Main(string[] args)
{
Console.WriteLine("Hello World!");
//Socket
Socket listenfd = new Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp);
//Bind
IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
IPEndPoint ipEp = new IPEndPoint(ipAdr, 1234);
listenfd.Bind(ipEp);
//Listen
listenfd.Listen(0);
Console.WriteLine("[服务器]启动成功");
while (true)
{
//Accept
Socket connfd = listenfd.Accept();
Console.WriteLine("[服务器]Accept");
}
}
}
运行服务端和客户端,客户端会发起连接,服务端accept该连接后回应,客户端会显示“OnSocket Connect”
图:服务端
图:客户端
此时把服务端关掉(断开连接),客户端会收到协议号为102的消息,即异常掉线(Exception)。
图:异常掉线
调用NetworkManager.SendConnect实际是调用BeginConnect发起连接。连接之后,回调OnConnect方法。
图:连接过程
OnConnect方法调用NetworkManager.AddEvent,排除设计模式的内容,相当于调用Network.lua的OnSocket方法。传入OnSocket的第1个参数为101(Protocal.Connect),指代协议名,第2个参数是空的字节流。网络模块中定义了101、102、103这3个固定的协议号,分别代表连接服务器、异常断线和正常断线。
图:连接回调
2、发送和接收
接下来尝试发送和接收数据。LuaFramework默认(如果不去改它的代码)使用的协议格式如下图所示,前面的2个字节为消息长度,用于处理沾包分包,随后的2个字节代表协议号(如上面的101、102、103),最后才是消息的内容。
图:协议
修改Network.lua,在连接成功后(OnSocket方法的101协议),调用send发送一串协议号为104的数据。服务端收到数据后回射给客户端,客户端在收到回应后(OnSocket方法的104协议),读取并显示出来。
send方法中新建了一个buffer,然后往buffer中添加协议号(104)和协议内容(字符串:《Unity3D网络游戏实战》是一本好书!),最后调用networkMgr:SendMessage()发送数据。networkMgr:SendMessage()会自动计算协议长度,并附加到buffer上发送出去。
--Socket消息--
function Network.OnSocket(key, data)
if key == 101 then
LuaFramework.Util.Log('OnSocket Connect');
Send()
elseif key == 104 then
LuaFramework.Util.Log('OnSocket Message ');
local str = data:ReadString();
LuaFramework.Util.Log('收到的字符串:'..str);
else
LuaFramework.Util.Log('OnSocket Other '..key);
end
end
function Send()
--组装数据
local buffer = LuaFramework.ByteBuffer.New();
buffer:WriteShort(Protocal.Message);
buffer:WriteString("《Unity3D网络游戏实战》是一本好书!");
--发送
local LuaHelper = LuaFramework.LuaHelper
local networkMgr = LuaHelper.GetNetManager()
networkMgr:SendMessage(buffer);
LuaFramework.Util.Log('数据发送完毕');
end
修改服务端程序,读出接收到的内容,并echo回去。
public static void Main(string[] args)
{
略,没有改动
while (true)
{
//Accept
Socket connfd = listenfd.Accept();
Console.WriteLine("[服务器]Accept");
//Recv 不考虑各种意外,只做测试
byte[] readBuff = new byte[100];
int count = connfd.Receive(readBuff);
//显示字节流
string showStr = "";
for (int i = 0; i < count; i++)
{
int b = (int)readBuff[i];
showStr += b.ToString() + " ";
}
Console.WriteLine("[服务器接收]字节流:"+ showStr);
//解析协议
Int16 messageLen = BitConverter.ToInt16(readBuff,0);
Int16 protocal = BitConverter.ToInt16(readBuff,2);
Int16 strLen = BitConverter.ToInt16(readBuff,4);
string str = System.Text.Encoding.UTF8.GetString(readBuff, 6, strLen);
Console.WriteLine("[服务器接收] 长度:" + messageLen);
Console.WriteLine("[服务器接收] 协议号:" + protocal);
Console.WriteLine("[服务器接收] 字符串:" + str);
//Send(echo)
byte[] writeBuff = new byte[count];
Array.Copy(readBuff,writeBuff,count);
connfd.Send(writeBuff);
}
}
运行游戏,可以看到服务端收到的如图所示的信息。字节流的前两位“53 0”表示消息长度为53字节,紧跟着的“104 0”代表协议号104。在字符串的封装中(buffer:WriteString),程序会先在buffer中添加字符串的长度,最后才是字符串的内容。“49 0”即表示“《Unity3D网络游戏实战》是一本好书!”占用49个字节(14个中文符号,每个3字节,7个英文符号,每个1字节)。协议长度53字节 = 协议号2个字节 + 字符串长度2字节 + 字符串内容49字节。
图:服务端收到的信息
客户端收到服务端回射的消息后,也会显示出来,如下图所示。
图:客户端收到的消息
在lua中调用networkMgr:SendMessage(buffer)时,实际上相当于调用了SocketClient的WriteMessage方法,该方法会计算协议的长度,然后将长度和内容组装在一起,调用BeginWrite发送数据。
图:发送数据
在建立连接后,SocketClient会调用BeginRead,当收到服务端的消息时,回调OnRead方法。OnRead又调用了OnReceive方法。
图:接收数据过程
OnReceive方法完成沾包分包处理,然后调用AddEvent方法分发消息(相当于调用了lua中NetWork表的OnSocket方法)。
图:解析数据过程
关于BeginRead、BeginConnect等方法的介绍,读者可以查看c#网络编程的资料或参照《Unity3D网络游戏实战》第6章“网络基础”。
3、消息分发
一款游戏往往涉及很多条网络通信协议,在Network.OnSocket中,如果只用ifelse语句处理不同协议,代码往往会混乱不堪。LuaFramework集成了消息分发的方法,用法如下所示。
1、引用LuaFramework\Lua\events.lua,然后使用Event.AddListener添加监听,例如“Event.AddListener(Protocal.Connect, Network.OnConnect); ”表示当收到101协议(Protocal.Connect)时,回调Network.OnConnect方法。Main.lua代码如下:
require "Network"
Event = require 'events'
--主入口函数。从这里开始lua逻辑
function Main()
local LuaHelper = LuaFramework.LuaHelper
local networkMgr = LuaHelper.GetNetManager()
local AppConst = LuaFramework.AppConst
AppConst.SocketPort = 1234;
AppConst.SocketAddress = "127.0.0.1";
Event.AddListener(Protocal.Connect, Network.OnConnect);
Event.AddListener(Protocal.Message, Network.OnMessage);
networkMgr:SendConnect();
end
2、在需要分发消息的地方调用Event.Brocast,然后编写相应的回调函数。Network.lua的部分代码如下:
--Socket消息--
function Network.OnSocket(key, data)
LuaFramework.Util.Log('OnSocket 消息分发:'..key);
Event.Brocast(tostring(key), data);
end
function Network.OnConnect(data)
LuaFramework.Util.Log('Network.OnConnect');
Send()
end
function Network.OnMessage(data)
LuaFramework.Util.Log('Network.OnMessage');
local str = data:ReadString();
LuaFramework.Util.Log('收到的字符串:'..str);
end
运行游戏,可以看到消息分发的结果。
图:消息分发
调用Event.AddListener,实际上是在一个表中添加数据,把某个协议号对应于某个方法的信息记录起来。
图:AddListener的过程
当调用Event.Brocast时,程序会查找这份表,然后执行回调方法。这里使用了协程来调用回调函数。使用协程的目的应该是不让回调逻辑阻碍主体逻辑,然而由于协程是单线程的,这点不起作用。除非回调函数也使用协程,相互配合。所以这里应该可以不用协程的。
图:Brocast的过程
至此,读者应该理解LuaFramework网络模块的使用方法了。