《Unity3D網絡遊戲實踐》(第2版)要點摘錄 - 「通用客戶端網絡模塊」
書名:****《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(); } } //Updatepublic 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) //字節數組長度<2,無法解析長度信息 { return ""; } //小端模式讀取長度信息 Int16 len = (Int16)((bytes[offset + 1] << 8) | bytes[offset]); if (len <= 0) //長度<=0 { return ""; } //長度不足 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); } }
-
-
-
-