《Unity3D網絡遊戲實踐》(第2版)要點摘錄 - 「異步和多路復用」
書名:****《Unity3D網絡遊戲實踐》(第2版)
作者:羅培羽
所讀版本:機械工業出版社
異步客戶端
-
連接
-
IAsyncResult BeginConnect(string host, int port, AsyncCallback requestCallback, object state) void EndConnect(IAsyncResult asyncResult) -
AsyncCallback requestCallback:回調函數,函數返回值必須為空,接受一個IAsyncResult類型的參數
- object state:該對象會回傳給回調函數
- 在BeginConnect的回調函數中調用EndConnect可以完成連接
-
public void Connect() { socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socket.BeginConnect("127.0.0.1", 8888, ConnectCallback, socket); } public void ConnectCallback(IAsyncResult result) { try { Socket socket = (Socket)result.AsyncState; socket.EndConnect(result); } catch (SocketException ex) { Debug.Log("Socket Connect Failed: " + ex.ToString()); } }
-
-
接收
-
IAsyncResult BeginReceive(byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, object state) int EndReceive(IAsyncResult asyncResult)- byte[] readBuff:接收緩沖區
- int offset, int size:從第offset位開始接收數據,最多接收size個字節的數據
-
public void ConnectCallback(IAsyncResult result) { try { //... //確認連接後開始接收數據 socket.BeginReceive(readBuff, 0, 1024, 0, ReceiveCallback, socket); } catch(SocketException ex) { // ... } } public void ReceiveCallback(IAsyncResult result) { try { Socket socket = (Socket)result.AsyncState; int count = socket.EndReceive(result); recvStr = System.Text.Encoding.UTF8.GetString(readBuff, 0, count); Debug.Log("Receive Success"); //結束接受一次後,再開始準備接收下一串的數據 socket.BeginReceive(readBuff, 0, 1024, 0, ReceiveCallback, socket); } catch (SocketException ex) { Debug.Log("Socket Receive Failed: " + ex.ToString()); } }
-
-
發送
- Send方法是一個阻塞方法
- 由於TCP是可靠連接,當接收方沒有收到數據時,發送方會重新發送數據,直到確認接收方接收到數據為止
- 在OS內部,每個Socket都有一個發送緩沖區,用來保存那些接收方未確認的數據
- Send方法是一個阻塞方法

-
-
- 調用Send時,程序將要發送的字節寫入緩沖區,再由OS完成數據的發送和確認
- 緩沖區長度為8KB,滿了之後Send方法就會阻塞
- Send只是把數據寫入發送緩沖區,由OS負責重傳,確認
- Send方法返回只代表成功將數據寫入緩沖區
- 異步Send:當數據成功寫入緩沖區時會調用回調函數
-
IAsyncResult BeginSend(byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, object state) int EndSend(IAsyncResult asyncResult) -
public void Send() { //Send Req 異步 string sendStr = reqField.text; byte[] sendBytes = System.Text.Encoding.UTF8.GetBytes(sendStr); socket.BeginSend(sendBytes, 0, sendBytes.Length, 0, SendCallback, socket); } public void SendCallback(IAsyncResult result) { try { Socket socket = (Socket)result.AsyncState; int count = socket.EndSend(result); Debug.Log("Socket Send Success: " + count); } catch(SocketException ex) { Debug.Log("Socket Send Failed: " + ex.ToString()); } }
-
- 調用Send時,程序將要發送的字節寫入緩沖區,再由OS完成數據的發送和確認
-
異步服務端
-
接收連接
-
IAsyncResult BeginAccept(AsyncCallback callback, object state) Socket EndAccept(IAsyncResult asyncResult) -
static void Main(string[] args) { //Create Socket listenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //Bind IPAddress ipAdr = IPAddress.Parse("192.168.1.5"); IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888); //綁定IP和端口 listenSocket.Bind(ipEp); //Listen listenSocket.Listen(0); Console.WriteLine("Server Activated"); //Async Accept listenSocket.BeginAccept(AcceptCallback, listenSocket); Console.ReadLine(); } public static void AcceptCallback(IAsyncResult result) { try { Console.WriteLine("Server Accepted"); Socket listenfd = (Socket)result.AsyncState; //為連接分配一個ClientState Socket clientfd = listenfd.EndAccept(result); //返回連接到服務端的客戶端Socket ClientState state = new ClientState(); state.socket = clientfd; clients.Add(clientfd, state); //異步接收客戶端數據 clientfd.BeginReceive(state.readBuff, 0, 1024, 0, ReceiveCallback, state); //重新開始接收連接 listenfd.BeginAccept(AcceptCallback, listenfd); } catch(SocketException ex) { Console.WriteLine("Socket Accept Failed: " + ex.ToString()); } }
-
-
接收數據
- 使用與客戶端相同的BeginReceive/EndReceive
-
public static void AcceptCallback(IAsyncResult result) { try { //... //異步接收客戶端數據 clientfd.BeginReceive(state.readBuff, 0, 1024, 0, ReceiveCallback, state); //... } //... } public static void ReceiveCallback(IAsyncResult result) { try { ClientState state = (ClientState)result.AsyncState; Socket clientfd = state.socket; int count = clientfd.EndReceive(result); //數據接收完畢,返回值<=0代表Socket連接斷開 if(count == 0) //客戶端關閉 { clientfd.Close(); clients.Remove(clientfd); Console.WriteLine("Socket Closed"); return; } //處理數據 string recvStr = System.Text.Encoding.UTF8.GetString(state.readBuff, 0, count); byte[] sendBytes = System.Text.Encoding.UTF8.GetBytes(recvStr); clientfd.Send(sendBytes); //重新開始接收數據 clientfd.BeginReceive(state.readBuff, 0, 1024, 0, ReceiveCallback, state); } catch(SocketException ex) { Console.WriteLine("Socket Receive Failed: " + ex.ToString()); } }
狀態檢測(Poll)
-
Poll
-
public bool Pool(int microSeconds, SelectMode mode)- microSeconds:等待回應的時間(微秒),如果參數為-1,表示無限期等待響應(阻塞);如果參數為0,表示不阻塞
- mode:
- SelectMode.SelectRead:如果Socket可讀(緩沖區有數據可以讀取),返回true
- SelectMode.SelectWrite:如果Socket可寫(緩沖區有數據可以發送),返回true
- SelectMode.SelectError:連接失敗返回true
-
原理:
- 通過SelectMode檢查Socket的狀態是否可讀/可寫/連接失敗
- 在指定時段內阻止執行
- 目的:解決同步程序的阻塞問題,在阻塞方法前做判斷,有數據讀時才Receive,有數據寫時才Send
-
-
客戶端Poll
-
private void Update() { if (socket == null) { return; } //有可讀數據 if(socket.Poll(0, SelectMode.SelectRead)) { byte[] readBuff = new byte[1024]; int count = socket.Receive(readBuff); string recvStr = System.Text.Encoding.UTF8.GetString(readBuff, 0, count); responseText.text = recvStr; } }
-
-
服務端Poll
-
static void Main(string[] args) { //Create Socket ... //Bind ... //Listen ... while (true) { //檢查有沒有客戶端連接 if(listenSocket.Poll(0, SelectMode.SelectRead)) { ReadListenfd(listenSocket); } foreach (ClientState state in clients.Values) { Socket clientfd = state.socket; //檢查客戶端Socket有沒有可讀的信息 if(clientfd.Poll(0, SelectMode.SelectRead)) { if (!ReadClientfd(clientfd)) { break; } } } System.Threading.Thread.Sleep(1); } } public static void ReadListenfd(Socket listenSocket) { Console.WriteLine("Accept"); Socket clientfd = listenSocket.Accept(); ClientState cs = new ClientState(); cs.socket = clientfd; clients.Add(clientfd, cs); } public static bool ReadClientfd(Socket clientSocket) { ClientState cs = clients[clientSocket]; //接收數據 int count = 0; try { count = clientSocket.Receive(cs.readBuff); } catch(SocketException ex) { clientSocket.Close(); clients.Remove(clientSocket); Console.WriteLine("Receive Socket Exception: " + ex.ToString()); return false; } //客戶端關閉 if(count == 0) { clientSocket.Close(); clients.Remove(clientSocket); Console.WriteLine("Socket Closed"); return false; } //廣播信息至所有客戶端 string recvStr = System.Text.Encoding.UTF8.GetString(cs.readBuff, 0, count); Console.WriteLine("Receive: " + recvStr); string sendStr = clientSocket.RemoteEndPoint.ToString() + ":" + recvStr; byte[] sendBytes = System.Text.Encoding.UTF8.GetBytes(sendStr); foreach (ClientState state in clients.Values) { state.socket.Send(sendBytes); } return true; }
-
-
弊端
- 如果沒有收到數據,Poll客戶端/服務端的循環也會一直在做檢測,造成浪費
多路複用(Select)
-
多路複用:同時處理多路信號(如多個Socket的狀態)
- 設置要監聽的Socket列表,如果有Socket可讀/可寫/報錯,就返回這些Socket列表,如果沒有就阻塞程序,防止CPU資源的消耗
-
public static void Select(IList checkRead, IList checkWrite, IList, checkError, int microSeconds)- 調用Select後,Select會根據是checkRead(檢查可讀性)還是checkWrite(檢查可寫性)或者checkError(檢查錯誤條件),對每個列表進行檢查和修改
- 當有某個/多個Socket滿足條件時,會修改這些列表
-
Select服務端
-
//Select模式 List<Socket> checkRead = new List<Socket>(); while (true) { checkRead.Clear(); checkRead.Add(listenSocket); foreach (ClientState clientState in clients.Values) { checkRead.Add(clientState.socket); } Socket.Select(checkRead, null, null, 1000); foreach (Socket socket in checkRead) { if(socket == listenSocket) { ReadListenfd(socket); } else { ReadClientfd(socket); } } }
-
-
Select客戶端
-
private void Update() { if (socket == null) { return; } //Select checkRead.Clear(); checkRead.Add(socket); Socket.Select(checkRead, null, null, 0); foreach (Socket s in checkRead) { byte[] readBuff = new byte[1024]; int count = socket.Receive(readBuff); string recvStr = System.Text.Encoding.UTF8.GetString(readBuff, 0, count); responseText.text = recvStr; } }
-