書名:《Unity3D網絡遊戲實踐》(第2版)
作者:羅培羽
所讀版本:機械工業出版社
Client Module Code unitypackage(Google Drive):
https://drive.google.com/file/d/1C6ykannRduaaJf0LdeuehGP_ATKRzYJe/view?usp=sharing
框架部分 – NetManager 網絡管理器
網絡事件分發模塊
網絡事件
連接成功/失敗/關閉
維護事件監聽列表,記錄每種網絡事件對應的回調方法
//網絡事件監聽列表
private static Dictionary<NetEvent, EventListener> eventListeners = new Dictionary<NetEvent, EventListener>();
//添加網絡事件監聽
public static void AddEventListener(NetEvent netEvent, EventListener listener){//...}
//移除網絡事件監聽
public static void RemoveEventListener(NetEvent netEvent, EventListener listener){//...}
//分發網絡事件
public static void FireEvent(NetEvent netEvent, string err){//...}
服務器連接模塊
使用BeginConnect發起連接
考慮發起連接後,回調返回前再次連接的情況
static bool isConnecting = false;
//連接
public static void Connect(string ip, int port)
{
//...
isConnecting = true;
socket.BeginConnect(ip, port, ConnectCallback, socket);
}
//連接回調
private static void ConnectCallback(IAsyncResult ar)
{
//...
//終止連接
socket.EndConnect(ar);
//分發連接消息
FireEvent(NetEvent.ConnectSucc, "");
//重置連接狀態
isConnecting = false;
//繼續接收
socket.BeginReceive(readBuff.bytes, readBuff.writeIdx, readBuff.remain, 0, ReceiveCallback, socket);
//...
}
數據傳輸模塊
把協議名和協議體分別編碼後組合成新的編碼數據後發送
//發送數據
public static void Send(MsgBase msg)
{
//...
byte[] nameBytes = MsgBase.EncodeName(msg); //協議名數據:長度信息+協議名2進制數據
byte[] bodyBytes = MsgBase.Encode(msg); //協議體數據:長度信息 + 協議體2進制數據
//消息長度計算及組裝
//...
//寫入隊列
ByteArray ba = new ByteArray(sendBytes);
writeQueue.Enqueue(ba);
//...
//發送
socket.BeginSend(sendBytes, 0, sendBytes.Length, 0, SendCallback, socket);
}
//發送數據回調
static void SendCallback(IAsyncResult ar)
{
//...
//獲取成功發送的數據長度
int count = socket.EndSend(ar);
//獲取寫入隊列的第一條數據
//...
writeQueue.Dequeue();
ba = writeQueue.First();
//...
//還有數據就繼續發送
socket.BeginSend(ba.bytes, ba.readIdx, ba.length, 0, SendCallback, socket);
//...
}
消息事件分發模塊
與網絡事件類似,唯一不同的是不通過枚舉值,而是通過協議名去分發消息
同樣需要維護一個事件監聽列表
數據處理模塊
連接回調後開啟異步接收數據:BeginReceive
在接收回調中對數據進行處理
接收回調
//消息列表
static List<MsgBase> msgList = new List<MsgBase>();
//消息列表長度
static int msgCount = 0;
//每一次Update處理的消息量
readonly static int MAX_MESSAGE_FIRE = 10;
//Receive回調
private static void ReceiveCallback(IAsyncResult ar)
{
Socket socket = (Socket)ar.AsyncState;
//獲取接收數據長度
int count = socket.EndReceive(ar);
//收到FIN信號(count == 0),斷開連接
//...
//更新寫入索引
readBuff.writeIdx += count;
//數據處理
OnReceiveData();
//剩餘空間即將不足,進行數據移位與擴容
//...
//繼續接收
socket.BeginReceive(readBuff.bytes, readBuff.writeIdx, readBuff.remain, 0, ReceiveCallback, socket);
//...
}
數據處理
解析協議
對粘包半包、大小端問題進行處理
根據協議前2個字節判斷是否收到一條完整協議
根據協議格式解析出協議對象,根據協議名獲得消息
把消息放在消息列表msgList中
由主線程Update讀取列表並進行消息處理
優化:每次Update處理多條消息
//數據處理
private static void OnReceiveData()
{
if (readBuff.length <= 2) { return; } //消息長度不足解析長度信息
//...
//解析協議名
//...
//解析協議體
//...
//檢查接收緩沖區剩餘空間
readBuff.CheckAndMoveBytes();
//把解析出來的消息加到消息列表中
lock (msgList){msgList.Add(msgBase);}
msgCount++;
//繼續讀取消息
if(readBuff.length > 2){OnReceiveData();}
}
//Update
public static void Update(){MsgUpdate();}
//更新消息
public static void MsgUpdate()
{
if (msgCount == 0) { return; }
//處理多條消息
for (int i = 0; i < MAX_MESSAGE_FIRE; i++)
{
//分發消息
}
}
心跳機制
判斷當前時間與上次發送ping時間的間隔,超過指定時間後再發送一次ping
判斷當前時間與上次收到pong時間的間隔,超過指定時間後斷開連接
//心跳間隔時間
public static int pingInterval = 10;
//上一次發送Ping的時間
static float lastPingTime = 0;
//上一次收到Pong的時間
static float lastPongTime = 0;
//發送Ping協議
private static void PingUpdate()
{
//Check Ping Signal Overtime and try ping again
if(Time.time - lastPingTime > pingInterval){//...}
//Check Pong Signal Overtime
if(Time.time - lastPongTime > pingInterval * 4){//...}
}
//監聽Pong協議
private static void OnMsgPong(MsgBase msgBase)
{
lastPongTime = Time.time;
}
ByteArray 緩沖區類
MsgBase 協議基類
協議部分
客戶端和服務端通信的數據格式
參數解析
把一個協議對象轉換成2進制數據(編碼),再把2進制數據轉換成協議對象(解碼)
Json/ProtoBuf
Json協議
消息長度 + 協議名長度 + 協議名 + 協議體
消息長度 = 協議名長度描述字節數 + 協議名字節數 + 協議體字節數
協議名編/解碼
MsgBase.EncodeName
//編碼協議名
public static byte[] EncodeName(MsgBase msgBase)
{
//協議名和長度
byte[] nameBytes = System.Text.Encoding.UTF8.GetBytes(msgBase.protoName);
Int16 len = (Int16)nameBytes.Length;
byte[] bytes = new byte[2 + len]; //2字節用於描述協議名長度
//小端組裝長度信息
bytes[0] = (byte)(len % 256);
bytes[1] = (byte)(len / 256);
Array.Copy(nameBytes, 0, bytes, 2, len);
return bytes;
}
MsgBase.DecodeName
//解碼協議名
public static string DecodeName(byte[] bytes, int offset, out int count)
{
count = 0;
if(offset + 2 > bytes.Length) { return ""; } //字節數組長度<2,無法解析長度信息
//小端模式讀取長度信息
Int16 len = (Int16)((bytes[offset + 1] << 8) | bytes[offset]);
if (len <= 0) { return ""; } //長度<=0
if(offset + 2 + len > bytes.Length) { return ""; } //長度不足
count = 2 + len;
string name = System.Text.Encoding.UTF8.GetString(bytes, offset + 2, len);
return name;
}
協議體編/解碼
MsgBase.Encode
//編碼協議體
public static byte[] Encode(MsgBase msgBase)
{
string s = JsonUtility.ToJson(msgBase);
return System.Text.Encoding.UTF8.GetBytes(s);
}
MsgBase.Decode
//解碼協議體
public static MsgBase Decode(string protoName, byte[] bytes, int offset, int count)
{
string s = System.Text.Encoding.UTF8.GetString(bytes, offset, count);
MsgBase msgBase = (MsgBase)JsonUtility.FromJson(s, Type.GetType(protoName));
return msgBase;
}
ProtoBuf協議
Google發布的一套協議格式,規定了一系列的編解碼方法
編碼後的數據量較小
獲取protobuf-net.dll
編寫proto文件
message MsgMove{
optional int32 x = 1;
optional int32 y = 2;
optional int32 z = 3;
}
使用protogen,根據proto文件生成對應的協議類
協議基類為global::ProtoBuf.IExtensible
Encode接口改為接受ProtoBuf.IExtensible參數,使用ProtoBuf.Serializer.Serialize將協議對象轉為字節流
//使用pb編碼
public static byte[] ProtobufEncode(ProtoBuf.IExtensible msgBase)
{
using(MemoryStream ms = new MemoryStream())
{
ProtoBuf.Serializer.Serialize(ms, msgBase);
return ms.ToArray();
}
}
Decode接口改為接受協議名、解碼對象byte數組、起始位置和長度參數,使用ProtoBuf.Serializer.NonGeneric.Deserialize將字節流轉為基於ProtoBuf.IExtensible基類的對象返回
//使用pb解碼
public static ProtoBuf.IExtensible ProtobufDecode(string protoName, byte[] bytes, int offset, int count)
{
using (MemoryStream ms = new MemoryStream(bytes, offset, count))
{
System.Type t = System.Type.GetType(protoName);
return (ProtoBuf.IExtensible)ProtoBuf.Serializer.NonGeneric.Deserialize(t, ms);
}
}