所讀版本:機械工業出版社
這一篇在做什麼
前面已經有了連接、收發與異步回調,接下來要面對的是「資料收到了之後要怎麼組織」。
這一篇主要處理兩件事:
- 怎樣定義一個簡單可解析的通信協議
- 怎樣在 Unity 裡用消息隊列,把異步線程收到的資料安全地交回主線程處理
為什麼需要通信協議
Socket 只負責傳送字節流,不會幫你理解這串字節到底代表:
- 聊天消息
- 登錄請求
- 移動同步
- 系統通知
因此,客戶端和服務端必須事先約定資料格式,也就是通信協議。
最簡單的做法之一,是把消息整理成固定格式的字符串,例如:
消息名|客戶端IP與端口,參數1,參數2,參數n
這樣收包後就能先拆出:
- 消息類型
- 參數內容
再交給對應的處理函數去執行。
為什麼還要消息隊列
在 Unity 裡,大部分 UI 與遊戲對象操作都要求在主線程執行。
但 Socket 的異步回調通常不保證回到 Unity 主線程。實際上,BeginReceive 等異步 API 多半會由線程池處理,所以:
- 第一次接收回調不一定在主線程
- 下一次回調也不一定和上一次在同一條線程
- 如果直接在回調裡動 UI,很容易出現各種錯亂或例外
因此,一個很實用的辦法是:
- 異步線程只負責把收到的消息放進隊列
- 主線程每幀從隊列取消息
- 真正的業務邏輯與 UI 更新在主線程完成

這個管理器想解決什麼
這一篇的 NetManager 可以看成一個很小的中控:
- 管理 Socket 連接
- 記錄不同消息名對應的處理函數
- 保存待處理消息隊列
- 在主線程中逐條消費消息
NetManager 範例
public static class NetManager {
static Socket socket;
//接收緩沖區
static byte[] readBuff = new byte[1024];
//委托類型
public delegate void MsgListener(string str);
//監聽列表(各個消息名對應的處理方法)
private static Dictionary<string, MsgListener> listeners = new Dictionary<string, MsgListener>();
//消息隊列
static List<string> msgList = new List<string>();
//記錄消息對應的處理方法
public static void AddListener(string msgName, MsgListener listener)
{
listeners[msgName] = listener;
}
public static void Connect(string ip, int port)
{
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.Connect(ip, port); //同步連接
socket.BeginReceive(readBuff, 0, 1024, 0, ReceiveCallback, socket); //異步接收信息
}
static void ReceiveCallback(IAsyncResult ar)
{
try
{
Socket socket = (Socket)ar.AsyncState;
int count = socket.EndReceive(ar);
string recvStr = System.Text.Encoding.UTF8.GetString(readBuff, 0, count);
//消息隊列更新
msgList.Add(recvStr);
socket.BeginReceive(readBuff, 0, 1024, 0, ReceiveCallback, socket);
}
catch(SocketException ex)
{
Debug.Log("Socket Receive Failed: " + ex.ToString());
}
}
public static void Send(string sendStr)
{
if (socket == null) { return; }
if (!socket.Connected) { return; }
//同步發送
byte[] sendBytes = System.Text.Encoding.UTF8.GetBytes(sendStr);
socket.Send(sendBytes);
}
//每幀更新,持續處理消息隊列中的消息
public static void Update()
{
if (msgList.Count <= 0) { return; }
string msgStr = msgList[0];
msgList.RemoveAt(0);
string[] split = msgStr.Split('|');
string msgName = split[0];
string msgArgs = split[1];
if (listeners.ContainsKey(msgName))
{
listeners[msgName](msgArgs);
}
}
}
逐段拆開看
監聽器字典
public delegate void MsgListener(string str);
private static Dictionary<string, MsgListener> listeners = new Dictionary<string, MsgListener>();
這部分的作用是把「消息名」映射到「對應處理函數」。
例如:
Move對應角色移動更新Chat對應聊天顯示Login對應登錄結果處理
只要調用 AddListener,就能把某個消息名註冊到對應的處理邏輯上。
消息隊列
static List<string> msgList = new List<string>();
這個列表充當最基本的消息隊列。異步接收線程只往尾部追加新消息,而主線程則從頭部取出處理。
雖然這裡用的是 List<string>,但思路本質上就是 FIFO 隊列。
連接與首次接收
public static void Connect(string ip, int port)
{
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.Connect(ip, port); //同步連接
socket.BeginReceive(readBuff, 0, 1024, 0, ReceiveCallback, socket); //異步接收信息
}
這裡是同步 Connect 加異步 BeginReceive 的組合。也就是說:
- 連接建立本身仍然是同步的
- 收包則交給異步回調處理
ReceiveCallback 在做什麼
static void ReceiveCallback(IAsyncResult ar)
{
try
{
Socket socket = (Socket)ar.AsyncState;
int count = socket.EndReceive(ar);
string recvStr = System.Text.Encoding.UTF8.GetString(readBuff, 0, count);
//消息隊列更新
msgList.Add(recvStr);
socket.BeginReceive(readBuff, 0, 1024, 0, ReceiveCallback, socket);
}
catch(SocketException ex)
{
Debug.Log("Socket Receive Failed: " + ex.ToString());
}
}
這裡的工作非常克制,只做三件事:
- 把接收到的字節轉成字符串
- 放進消息列表
- 重新掛起下一次接收
刻意不在這裡直接處理消息內容,正是為了避免在線程池回調裡亂動 Unity 主線程資源。
Send 做了哪些保護
public static void Send(string sendStr)
{
if (socket == null) { return; }
if (!socket.Connected) { return; }
//同步發送
byte[] sendBytes = System.Text.Encoding.UTF8.GetBytes(sendStr);
socket.Send(sendBytes);
}
這裡先檢查:
socket是否存在- 連接是否仍然有效
然後才把字符串編碼成字節並送出。這仍然是同步 Send,後面還會繼續處理更完整的收發問題。
主線程 Update 的角色
真正的消息解析與分發,是在主線程的 Update 裡做的。
public static void Update()
{
if (msgList.Count <= 0) { return; }
string msgStr = msgList[0];
msgList.RemoveAt(0);
string[] split = msgStr.Split('|');
string msgName = split[0];
string msgArgs = split[1];
if (listeners.ContainsKey(msgName))
{
listeners[msgName](msgArgs);
}
}
流程可以概括成:
- 如果隊列沒消息,就直接返回
- 取出最前面的一條消息
- 用分隔符
|拆成消息名與消息參數 - 查找是否有對應監聽器
- 如果有,就把參數交給它處理
這樣就把「網絡接收」和「遊戲邏輯處理」分開了。
這個方案的價值
這種設計雖然簡單,但很實用,因為它同時解決了兩個問題:
第一,消息有了明確的語義入口
透過 消息名 -> 處理函數 的映射,程序不再只是收一堆原始字節,而是能把不同類型的消息導到不同業務邏輯。
第二,線程邊界被隔開
異步接收線程只負責收包入隊,不碰 Unity UI 或場景物件;主線程再統一做解析與處理,風險會小很多。
這一篇先記住的重點
如果只收斂成最重要的幾點,可以記成:
- Socket 傳的是字節流,不是語義化消息
- 通信協議的作用,是讓雙方知道怎麼解析資料
- Unity 的網絡回調不適合直接操作 UI
- 消息隊列是一個非常常見也很實用的主線程橋接方案
- 異步接收線程負責入隊,主線程負責消費與分發
下一步就會進一步碰到更底層但更麻煩的問題:TCP 是流,不是天然分包的消息,這會引出粘包、半包與發送不完整等問題。