書名:《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時,程序將要發送的字節寫入緩沖區,再由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());
}
}
異步服務端
接收連接
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;
}
}