ToLua熱更新之LuaFramework框架之網路(6)
如今大部分的遊戲都是網路遊戲,網路遊戲便涉及到網路連線發起、網路資料接收等內容。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的過程