《Unity3D网络游戏实战》学习与实践
纸上得来终觉浅,绝知此事要躬行~
Echo
网络上的两个程序通过一个双向的通信连接实现数据交换,这个连接的一端称为一个Socket
“端口”是英文port的意译,是设备与外界通信交流的出口。每台计算机可以分配0到65535共65536个端口
每一条Socket连接代表着本地Socket→本地端口→网络介质→远程端口→远程Socket的链路
Socket通信的基本流程
- 开启一个连接之前,需要创建一个Socket对象(使用API Socket),然后绑定本地使用的端口(使用API Bind)。对客户端而言,连接时(使用API Connect)会由系统分配端口,可以省去绑定步骤。
- 对客户端而言,连接时(使用API Connect)会由系统分配端口,可以省去绑定步骤。
- 客户端连接服务器(使用API Connect)
- 服务器接受连接(使用API Accept)
- 客户端和服务端通过Send和Receive等API收发数据,操作系统会自动完成数据的确认、重传等步骤,确保传输的数据准确无误。
- 某一方关闭连接(使用API Close),操作系统会执行“四次挥手”的步骤,关闭双方连接
using System.Collections;using System.Collections.Generic;using UnityEngine;using System.Net.Sockets;using UnityEngine.UI;public class Echo : MonoBehaviour {//定义套接字Socket socket;//UGUIpublic InputField InputFeld;public Text text;//点击连接按钮//客户端通过socket.Connect(远程IP地址,远程端口)连接服务端。Connect是一个阻塞方法,程 //序会卡住直到服务端回应(接收、拒绝或超时)。public void Connection(){//Socketsocket = new Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp);//这一行用于创建一个Socket对象,它的三个参数分别代表地址族、套接字类型和协议。//Connectsocket.Connect("127.0.0.1", 8888);}//点击发送按钮//客户端通过socket.Send发送数据,这也是一个阻塞方法。该方法接受一个byte[]类型的参数指明 //要发送的内容。Send的返回值指明发送数据的长度(例子中没有使用)。程序用 //System.Text.Encoding.Default.GetBytes(字符串)把字符串转换成byte[]数组,然后发送给服 //务端。public void Send(){//Sendstring sendStr = InputFeld.text;byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);socket.Send(sendBytes);//Recvbyte[] readBuff = new byte[1024];//客户端通过socket.Receive接收服务端数据。Receive也是阻塞方法,没有收到服务端数据 //时,程序将卡在Receive不会往下执行。Receive带有一个byte[]类型的参数,它存储接收到 //的数据。Receive的返回值指明接收到数据的长度。之后使用System.Text.Encoding. //Default.GetString(readBuff,0, count)将byte[]数组转换成字符串显示在屏幕上。int count = socket.Receive(readBuff);string recvStr = System.Text.Encoding.Default.GetString(readBuff, 0, count);text.text = recvStr;//Closesocket.Close();}}
此时运行游戏点击连接会出现
因为我们还没有启动服务器,所以属于正常现象
创建服务端程序
using System.Net;
using System.Net.Sockets;internal class Program
{private static void Main(string[] args){Console.WriteLine("Hello, World!");//SocketSocket listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);IPAddress ipAdr = IPAddress.Parse("127.0.0.1");IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);listenfd.Bind(ipEp);//Listenlistenfd.Listen(0);Console.WriteLine("[服务器]启动成功");while (true){//AcceptSocket connfd = listenfd.Accept();Console.WriteLine("[服务器]Accept");//Receivebyte[] readBuff = new byte[1024];int count = connfd.Receive(readBuff);string readStr = System.Text.Encoding.Default.GetString(readBuff, 0, count);Console.WriteLine("[服务器接受]" + readStr);//Sendbyte[] sendBytes = System.Text.Encoding.Default.GetBytes(readStr);connfd.Send(sendBytes);}}
}
绑定 Bind
listenfd.Bind(ipEp)将给listenfd套接字绑定IP和端口。程序中绑定本地地址“127.0.0.1”和8888号端口。127.0.0.1是回送地址,指本地机,一般用于测试。读者也可以设置成真实的IP地址,然后在两台计算机上分别运行客户端和服务端程序
监听 Listen
服务端通过listenfd.Listen(backlog)开启监听,等待客户端连接。参数backlog指定队列中最多可容纳等待接受的连接数,0表示不限制。
应答 Accept
开启监听后,服务器调用listenfd.Accept()接收客户端连接。本例使用的所有Socket方法都是阻塞方法,也就是说当没有客户端连接时,服务器程序卡在listenfd.Accept()不会往下执行,直到接收了客户端的连接。Accept返回一个新客户端的Socket对象,对于服务器来说,它有一个监听Socket(例子中的listenfd)用来监听(Listen)和应答(Accept)客户端的连接,对每个客户端还有一个专门的Socket(例子中的connfd)用来处理该客户端的数据。
IPAddress 和 IPEndPoint
使用IPAddress指定IP地址,使用IPEndPoint指定IP和端口。
System.Text.Encoding.Default.GetString
Receive方法将接收到的字节流保存到readBuff上,readBuff是byte型数组。GetString方法可以将byte型数组转换成字符串。同理,System.Text.Encoding.Default.GetBytes可以将字符串转换成byte型数组。
测试 :
Socket类的一些常用方法
公网和局域网
把宽带连接到家里的路由器,再由路由器分发到多台计算机(校园网、公司局域网同理),在这种情况下,路由器会有公网和局域网两个IP
比如:路由器的公网IP是123.207.111.220,局域网IP为192.168.0.1,连接路由器的计算机只有内网IP,它们分别是192.168.0.10和192.168.0.12。。如果将服务端放到连接路由器的某台计算机上,因为它只有局域网IP,所以只有局域网内的计算机可以连接上。如果拥有路由器的控制权,可以使用一种叫“端口映射”的技术,即设置路由器,将路由器IP地址的一个端口映射到内网中的一台计算机,提供相应的服务。当用户访问该IP的这个端口时,路由器自动将请求映射到对应局域网内部的计算机上
异步和多路复用
上面的程序全部使用阻塞API(Connect、Send、Receive等),可称为同步Socket程序
一个简单的异步程序示例:
using System.Collections;using System.Collections.Generic;using UnityEngine;using System.Threading;public class Async : MonoBehaviour {// Use this for initializationvoid Start () {//创建定时器Timer timer = new Timer(TimeOut, null, 5000, 0);//其他程序代码//……}//回调函数private void TimeOut(System.Object state){Debug.Log("铃铃铃");}}
异步Connect
每一个同步API(如Connect)对应着两个异步API,分别是在原名称前面加上Begin和End(如BeginConnect和EndConnect)
BeginConnect的函数原型如下:
public IAsyncResult BeginConnect( string host, int port, AsyncCallback requestCallback, object state )
修改代码:
using System;//点击连接按钮public void Connection(){//Socketsocket = new Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp);//Connectsocket.BeginConnect("127.0.0.1", 8888, ConnectCallback, socket);}//Connect回调public void ConnectCallback(IAsyncResult ar){try{Socket socket = (Socket) ar.AsyncState;socket.EndConnect(ar);Debug.Log("Socket Connect Succ");}catch (SocketException ex){Debug.Log("Socket Connect fail" + ex.ToString());}}
说明:
- 由BeginConnect最后一个参数传入的socket,可由ar.AsyncState获取到。
异步Receive
public IAsyncResult BeginReceive ( byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, object state )
public int EndReceive( IAsyncResult asyncResult ) 它的返回值代表接收到的字节数
修改客户端代码:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net.Sockets;
using UnityEngine;
using UnityEngine.UI;public class Echo : MonoBehaviour
{//UGUIpublic InputField inputField;public Text text;//定义套接字Socket socket;//接收缓冲区byte[] readBuff = new byte[1024];string recvStr = "";public void Connection(){socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);socket.BeginConnect("127.0.0.1", 8888,ConnectCallback,socket);}//Connect回调public void ConnectCallback(IAsyncResult ar){try{Socket socket = (Socket)ar.AsyncState;socket.EndConnect(ar);Debug.Log("Socket Connect Succ");socket.BeginReceive(readBuff,0,1024,0,ReceiveCallback,socket);}catch (SocketException ex){Debug.Log("Socket Connect fail" + ex.ToString());}}public void ReceiveCallback(IAsyncResult ar) {try{Socket socket = (Socket) ar.AsyncState;int count = socket.EndReceive(ar);recvStr = System.Text.Encoding.Default.GetString(readBuff,0,count);socket.BeginReceive(readBuff,0,1024,0,ReceiveCallback,socket);Debug.Log("ReceiveCallback" + recvStr);}catch (SocketException ex){Debug.Log("Socket Receive fail" + ex.ToString());}}public void Send(){string sendStr = inputField.text;byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);socket.Send(sendBytes);}// Start is called before the first frame updatevoid Start(){}// Update is called once per framevoid Update(){text.text = recvStr;}
}
说明:
BeginReceive的参数
上述程序中,BeginReceive的参数为(readBuff, 0, 1024, 0, ReceiveCallback,socket)。第一个参数readBuff表示接收缓冲区;第二个参数0表示从readBuff第0位开始接收数据,这个参数和TCP粘包问题有关,第三个参数1024代表每次最多接收1024个字节的数据
BeginReceive的调用位置
程序在两个地方调用了BeginReceive:一个是ConnectCallback,在连接成功后,就开始接收数据,接收到数据后,回调函数ReceiveCallback被调用。另一个是BeginReceive内部,接收完一串数据后,等待下一串数据的到来
Update和recvStr
在Unity中,只有主线程可以操作UI组件。由于异步回调是在其他线程执行的,如果在BeginReceive给text.text赋值,Unity会弹出“get_isActiveAndEnabled can onlybe called from the main thread”的异常信息,所以程序只给变量recvStr赋值,在主线程执行的Update中再给text.text赋值
异步Send
Socket使用的协议、IP、端口属于用户层面的属性,可以直接修改;操作系统层面拥有“发送”和“接收”两个缓冲区,当调用Send方法时,程序将要发送的字节流写入到发送缓冲区中,再由操作系统完成数据的发送和确认
如果缓冲区满,那么Send就会阻塞,直到缓冲区的数据被确认腾出空间
值得注意的是,Send过程只是把数据写入到发送缓冲区,然后由操作系统负责重传、确认等步骤。Send方法返回只代表成功将数据放到发送缓存区中,对方可能还没有收到数据。
异步Send不会卡住程序,当数据成功写入输入缓冲区(或发生错误)时会调用回调函数。异步Send方法BeginSend的原型如下。
public IAsyncResult BeginSend( byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, object state )
public int EndSend ( IAsyncResult asyncResult )
修改客户端代码,使用异步发送:
public void Send(){string sendStr = inputField.text;byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);for(int i = 0;i < 10000;i++) {socket.BeginSend(sendBytes,0,sendBytes.Length,0,SendCallback,socket);}}public void SendCallback(IAsyncResult ar) {try{Socket socket = (Socket) ar.AsyncState;int count = socket.EndSend(ar);Debug.Log("Socket Send succ" + count);}catch (SocketException ex){Debug.Log("Socket Send fail" + ex.ToString());}}
异步服务端
上面的同步服务端程序同一时间只能处理一个客户端的请求,因为它会一直阻塞,等待某一个客户端的数据,无暇接应其他客户端。使用异步方法,可以让服务端同时处理多个客户端的数据,及时响应
管理客户端
定义一个名为ClientState的类,用于保存一个客户端信息。ClientState包含TCP连接所需Socket,以及用于填充BeginReceive参数的读缓冲区readBuff
//数据结构class ClientState {public Socket socket;public byte[] readBuff = new byte[1024];}static Dictionary<Socket, ClientState> clients =new Dictionary<Socket, ClientState>();
异步Accept
public IAsyncResult BeginAccept( AsyncCallback callback, object state )
public Socket EndAccept( IAsyncResult asyncResult )
程序结构:
修改代码:
using System.Net;
using System.Net.Sockets;class ClientState {public Socket socket;public byte[] readBuff = new byte[1024];
}internal class Program
{//监听Socketstatic Socket listenfd;//客户端Socket及状态信息static Dictionary<Socket,ClientState> clients = new Dictionary<Socket, ClientState>();private static void Main(string[] args){Console.WriteLine("Hello, World!");//Socketlistenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);IPAddress ipAdr = IPAddress.Parse("127.0.0.1");IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);listenfd.Bind(ipEp);//Listenlistenfd.Listen(0);Console.WriteLine("[服务器]启动成功");listenfd.BeginAccept(AcceptCallback,listenfd);//等待Console.ReadLine();// while (true)// {// //Accept// Socket connfd = listenfd.Accept();// Console.WriteLine("[服务器]Accept");// //Receive// byte[] readBuff = new byte[1024];// int count = connfd.Receive(readBuff);// string readStr = System.Text.Encoding.Default.GetString(readBuff, 0, count);// Console.WriteLine("[服务器接受]" + readStr);// //Send// string sendStr = System.DateTime.Today.ToString();// byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);// connfd.Send(sendBytes);// }}//Accept回调public static void AcceptCallback(IAsyncResult ar) {try{Console.WriteLine("[服务器]Accept");Socket listenfd = (Socket) ar.AsyncState;Socket clientfd = listenfd.EndAccept(ar);//clients列表ClientState state = new ClientState();state.socket = clientfd;clients.Add(clientfd,state);//接收数据的BeginReceiveclientfd.BeginReceive(state.readBuff,0,1024,0,ReceiveCallback,state);//继续Acceptlistenfd.BeginAccept(AcceptCallback,listenfd);}catch (SocketException ex){Console.WriteLine("Socket Accept fail" + ex.ToString());}}//Receive回调public static void ReceiveCallback(IAsyncResult ar) {try{ClientState state = (ClientState) ar.AsyncState;Socket clientfd = state.socket;int count = clientfd.EndReceive(ar);//客户端关闭if(count == 0) {clientfd.Close();clients.Remove(clientfd);Console.WriteLine("Socket Close");return;}string recvStr = System.Text.Encoding.Default.GetString(state.readBuff,0,count);byte[] sendBytes = System.Text.Encoding.Default.GetBytes("echo" + recvStr);clientfd.Send(sendBytes); //减少代码量,不用异步clientfd.BeginReceive(state.readBuff,0,1024,0,ReceiveCallback,state);//注意BeginReceive的最后一个参数,这里以ClientState代替了原来的Socket。}catch (SocketException ex){Console.WriteLine("Socket Receive fail" + ex.ToString());throw;}}
}
AcceptCallback是BeginAccept的回调函数,它处理了三件事情:
- 给新的连接分配ClientState,并把它添加到clients列表中;
- 异步接收客户端数据;
- 再次调用BeginAccept实现循环。
ReceiveCallback是BeginReceive的回调函数,它也处理了三件事情:
- 服务端收到消息后,回应客户端;
- 如果收到客户端关闭连接的信号“if(count == 0)”,断开连接;
- 继续调用BeginReceive接收下一个数据。
当Receive返回值小于等于0时,表示Socket连接断开,可以关闭Socket。
聊天室
在聊天室中,某个客户端发送聊天消息,所有在线的客户端都会收到这条消息。也就是会遍历在线的客户端,然后推送消息
修改服务端代码:
//Receive回调public static void ReceiveCallback(IAsyncResult ar){try {……string recvStr = System.Text.Encoding.Default.GetString(state.readBuff,0, count);string sendStr = clientfd.RemoteEndPoint.ToString() + ":" + recvStr;byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);foreach (ClientState s in clients.Values){s.socket.Send(sendBytes);}clientfd.BeginReceive( state.readBuff, 0, 1024, 0,ReceiveCallback, state);}catch (SocketException ex){……}}
服务端整体代码:
using System.Net;
using System.Net.Sockets;class ClientState {public Socket socket;public byte[] readBuff = new byte[1024];
}internal class Program
{//监听Socketstatic Socket listenfd;//客户端Socket及状态信息static Dictionary<Socket,ClientState> clients = new Dictionary<Socket, ClientState>();private static void Main(string[] args){Console.WriteLine("Hello, World!");//Socketlistenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);IPAddress ipAdr = IPAddress.Parse("127.0.0.1");IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);listenfd.Bind(ipEp);//Listenlistenfd.Listen(0);Console.WriteLine("[服务器]启动成功");listenfd.BeginAccept(AcceptCallback,listenfd);//等待Console.ReadLine();}//Accept回调public static void AcceptCallback(IAsyncResult ar) {try{Console.WriteLine("[服务器]Accept");Socket listenfd = (Socket) ar.AsyncState;Socket clientfd = listenfd.EndAccept(ar);//clients列表ClientState state = new ClientState();state.socket = clientfd;clients.Add(clientfd,state);//接收数据的BeginReceiveclientfd.BeginReceive(state.readBuff,0,1024,0,ReceiveCallback,state);//继续Acceptlistenfd.BeginAccept(AcceptCallback,listenfd);}catch (SocketException ex){Console.WriteLine("Socket Accept fail" + ex.ToString());}}//Receive回调public static void ReceiveCallback(IAsyncResult ar) {try{ClientState state = (ClientState) ar.AsyncState;Socket clientfd = state.socket;int count = clientfd.EndReceive(ar);//客户端关闭if(count == 0) {clientfd.Close();clients.Remove(clientfd);Console.WriteLine("Socket Close");return;}string recvStr = System.Text.Encoding.Default.GetString(state.readBuff,0,count);string sendStr = clientfd.RemoteEndPoint.ToString() + ":" + recvStr;byte[] sendBytes = System.Text.Encoding.Default.GetBytes("echo" + sendStr);foreach(ClientState s in clients.Values) {s.socket.Send(sendBytes);}clientfd.BeginReceive(state.readBuff,0,1024,0,ReceiveCallback,state);}catch (SocketException ex){Console.WriteLine("Socket Receive fail" + ex.ToString());throw;}}
}
修改客户端代码,显示历史聊天:
//Receive回调public void ReceiveCallback(IAsyncResult ar){try {Socket socket = (Socket) ar.AsyncState;int count = socket.EndReceive(ar);string s = System.Text.Encoding.Default.GetString(readBuff, 0, count);recvStr = s + "\n" + recvStr;socket.BeginReceive( readBuff, 0, 1024, 0,ReceiveCallback, socket);}catch (SocketException ex){Debug.Log("Socket Receive fail" + ex.ToString());}}
客户端整体代码:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net.Sockets;
using UnityEngine;
using UnityEngine.UI;public class Echo : MonoBehaviour
{//UGUIpublic InputField inputField;public Text text;//定义套接字Socket socket;//接收缓冲区byte[] readBuff = new byte[1024];string recvStr = "";public void Connection(){socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);socket.BeginConnect("127.0.0.1", 8888,ConnectCallback,socket);}//Connect回调public void ConnectCallback(IAsyncResult ar){try{Socket socket = (Socket)ar.AsyncState;socket.EndConnect(ar);Debug.Log("Socket Connect Succ");socket.BeginReceive(readBuff,0,1024,0,ReceiveCallback,socket);}catch (SocketException ex){Debug.Log("Socket Connect fail" + ex.ToString());}}public void ReceiveCallback(IAsyncResult ar) {try{Socket socket = (Socket) ar.AsyncState;int count = socket.EndReceive(ar);string s = System.Text.Encoding.Default.GetString(readBuff,0,count);recvStr = s + "\n" + recvStr;socket.BeginReceive(readBuff,0,1024,0,ReceiveCallback,socket);Debug.Log("ReceiveCallback" + recvStr);}catch (SocketException ex){Debug.Log("Socket Receive fail" + ex.ToString());}}public void Send(){string sendStr = inputField.text;byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);socket.BeginSend(sendBytes,0,sendBytes.Length,0,SendCallback,socket);}public void SendCallback(IAsyncResult ar) {try{Socket socket = (Socket) ar.AsyncState;int count = socket.EndSend(ar);Debug.Log("Socket Send succ" + count);}catch (SocketException ex){Debug.Log("Socket Send fail" + ex.ToString());}}// Start is called before the first frame updatevoid Start(){}// Update is called once per framevoid Update(){text.text = recvStr;}
}
效果:
状态检测Poll
什么是Poll
处理阻塞的代码:
if(socket有可读数据){socket.Receive()}if(socket缓冲区可写){socket.Send()}if(socket发生程序){错误处理}
public bool Poll ( int microSeconds, SelectMode mode )
防止单线程卡住程序的Poll方法
Poll方法将会检查Socket的状态。如果指定mode参数为SelectMode.SelectRead,则可确定Socket是否为可读;指定参数为SelectMode.SelectWrite,可确定Socket是否为可写;指定参数为SelectMode.SelectError,可以检测错误条件。Poll将在指定的时段(以微秒为单位)内阻止执行,如果希望无限期地等待响应,可将microSeconds设置为一个负整数;如果希望不阻塞,可将microSeconds设置为0。
//省略各种usingpublic class Echo : MonoBehaviour {//定义套接字Socket socket;//UGUIpublic InputField InputFeld;public Text text;//点击连接按钮public void Connection(){//Socketsocket = new Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp);//Connectsocket.Connect("127.0.0.1", 8888);}//点击发送按钮public void Send(){……//略}public 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.Default.GetString(readBuff, 0, count);text.text = recvStr;}}}
服务端代码:
class MainClass{//监听Socketstatic Socket listenfd;//客户端Socket及状态信息static Dictionary<Socket, ClientState> clients =new Dictionary<Socket, ClientState>();public static void Main (string[] args){//Socketlistenfd = new Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp);//BindIPAddress ipAdr = IPAddress.Parse("127.0.0.1");IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);listenfd.Bind(ipEp);//Listenlistenfd.Listen(0);Console.WriteLine("[服务器]启动成功");//主循环while(true){//检查listenfdif(listenfd.Poll(0, SelectMode.SelectRead)){ReadListenfd(listenfd);}//检查clientfdforeach (ClientState s in clients.Values){Socket clientfd = s.socket;if(clientfd.Poll(0, SelectMode.SelectRead)){if(! ReadClientfd(clientfd)){break;}}}//防止CPU占用过高System.Threading.Thread.Sleep(1);}}}//读取Listenfdpublic static void ReadListenfd(Socket listenfd){Console.WriteLine("Accept");Socket clientfd = listenfd.Accept();ClientState state = new ClientState();state.socket = clientfd;clients.Add(clientfd, state);}//读取Clientfdpublic static bool ReadClientfd(Socket clientfd){ClientState state = clients[clientfd];//接收int count = 0;try{count = clientfd.Receive(state.readBuff);}catch(SocketException ex){clientfd.Close();clients.Remove(clientfd);Console.WriteLine("Receive SocketException " + ex.ToString());return false;}//客户端关闭if(count == 0){clientfd.Close();clients.Remove(clientfd);Console.WriteLine("Socket Close");return false;}//广播string recvStr =System.Text.Encoding.Default.GetString(state.readBuff, 0, count);Console.WriteLine("Receive" + recvStr);string sendStr = clientfd.RemoteEndPoint.ToString() + ":" + recvStr;byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);foreach (ClientState cs in clients.Values){cs.socket.Send(sendBytes);}return true;}
多路复用Select(重点)
多路复用,就是同时处理多路信号,比如同时检测多个Socket的状态。
解决Poll服务端中CPU占用率过高的方法,那就是:同时检测多个Socket的状态。在设置要监听的Socket列表后,如果有一个(或多个)Socket可读(或可写,或发生错误信息),那就返回这些可读的Socket,如果没有可读的,那就阻塞。
public static void Select( IList checkRead, IList checkWrite, IList checkError, int microSeconds )
如图所示:把包含6个Socket的列表传给Select, Select方法将会阻塞,等到超时或某个(或多个)Socket可读时返回,并且修改checkRead列表,仅保存可读的socket A和socket C。当没有任何可读Socket时,程序将会阻塞,不占用CPU资源。
Select 服务端
using System;using System.Net;using System.Net.Sockets;using System.Collections.Generic;class ClientState{public Socket socket;public byte[] readBuff = new byte[1024];}class MainClass{//监听Socketstatic Socket listenfd;//客户端Socket及状态信息static Dictionary<Socket, ClientState> clients =new Dictionary<Socket, ClientState>();public static void Main (string[] args){//Socketlistenfd = new Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp);//BindIPAddress ipAdr = IPAddress.Parse("127.0.0.1");IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);listenfd.Bind(ipEp);//Listenlistenfd.Listen(0);Console.WriteLine("[服务器]启动成功");//checkReadList<Socket> checkRead = new List<Socket>();//主循环while(true){//填充checkRead列表checkRead.Clear();checkRead.Add(listenfd);foreach (ClientState s in clients.Values){checkRead.Add(s.socket);}//selectSocket.Select(checkRead, null, null, 1000);//检查可读对象foreach (Socket s in checkRead){if(s == listenfd){ReadListenfd(s);}else{ReadClientfd(s);}}}}}
- 将监听Socket(listenfd)和客户端Socket(遍历clients列表)添加到待检测Socket可读状态的列表checkList中。
- 调用Select,程序中设置超时时间为1秒,若1秒内没有任何可读信息,Select方法将checkList列表变成空列表,然后返回。
- 对Select处理后的每个Socket做处理,如果监听Socket(listenfd)可读,说明有客户端连接,需调用Accept。如果客户端Socket可读,说明客户端发送了消息(或关闭),将消息广播给所有客户端。
Select客户端
public void Update(){if(socket == null) {return;}//填充checkRead列表checkRead.Clear();checkRead.Add(socket);//selectSocket.Select(checkRead, null, null, 0);//checkforeach (Socket s in checkRead){byte[] readBuff = new byte[1024];int count = socket.Receive(readBuff);string recvStr =System.Text.Encoding.Default.GetString(readBuff, 0, count);text.text = recvStr;}}}
为了不卡住客户端,Select的超时时间设置为0,永不阻塞
参考书籍:《Unity3D网络游戏实战(第2版)》 (豆瓣) (douban.com)
相关文章:

《Unity3D网络游戏实战》学习与实践
纸上得来终觉浅,绝知此事要躬行~ Echo 网络上的两个程序通过一个双向的通信连接实现数据交换,这个连接的一端称为一个Socket “端口”是英文port的意译,是设备与外界通信交流的出口。每台计算机可以分配0到65535共65536个端口 每一条Sock…...

Machine_Matrix打靶渗透【附代码】(权限提升)
靶机下载地址: https%3A%2F%2Fdownload.vulnhub.com%2Fmatrix%2FMachine_Matrix.zip 1. 主机发现端口扫描目录扫描敏感信息获取 1.1. 主机发现 nmap -sn 192.168.7.0/24|grep -B 2 08:00:27:D9:36:81 1.2. 端口扫描 nmap -p- 192.168.7.155 1.3. 目录扫描 dir…...

代码随想录算法训练营Day22 | Leetcode 77 组合 Leetcode 216 组合总和Ⅲ Leetcode17 电话号码的字母组合
前言 回溯算法中递归的逻辑不重要,只要掌握回溯的模板以及将问题转化为树形图,整个问题就很好解决了,比二叉树简单。 Leetcode 77 组合 题目链接:77. 组合 - 力扣(LeetCode) 代码随想录题解:…...

【微信小程序实战教程】之微信小程序中的 JavaScript
微信小程序中的 JavaScript 微信小程序的业务逻辑都是通过JavaScript语言来实现的,本章我们将详细的讲解JavaScript的基本概念,以及在小程序中如何使用JavaScript语言。JavaScript是一种轻量的、解释型的、面向对象的头等函数语言,是一种动态…...
K-近邻算法(一)
一、 K- 近邻算法 (KNN) 概念 1.1 K- 近邻算法 (KNN) 概念 K Nearest Neighbor 算法⼜叫 KNN 算法,这个算法是机器学习⾥⾯⼀个⽐较经典的算法, 总体来说 KNN 算法是相对⽐ 较容易理解的算法 定义 : 如果⼀个样本在特征空间中的k 个最相似 ( 即特征空间…...

从零开始之AI视频制作篇
从零开始之AI视频制作篇 文章目录 从零开始之AI视频制作篇前言一、工具列表二、成片展示三、制作流程1、获取图片素材2、图片生成视频2.1 Runway操作流程 3、文本生成语音3.1 Fish Audio操作流程 4、视频剪辑4.1 音频素材4.2 字幕生成 四、Runway提示词参考:参考 前…...
Java之TCP编程综合案例
1.反转案例 搭建一个TCP客户端,从键盘录入整行数据(遇到quit结束录入)然后发送给服务器,再接收服务器返回的数据并输出。 package com.briup.chap12;public class Test064_ReversalClient {public static void main(String[] ar…...

【数据分析---Pandas实战指南:精通数据查询、增删改操作与高效索引和列名操作管理】
前言: 💞💞大家好,我是书生♡,本阶段和大家一起分享和探索数据分析,本篇文章主要讲述了:数据查询操作,数据增删改操作,索引和列名操作等等。欢迎大家一起探索讨论&#x…...
Spring Cloud全解析:注册中心之Eureka服务获取和服务续约
服务获取和服务续约 eureka客户端通过定时任务的方式进行服务获取和服务续约,在com.netflix.discovery.DiscoveryClient类中,启动了两个定时任务来进行处理 private void initScheduledTasks() {// 是否需要拉取if (clientConfig.shouldFetchRegistry(…...

三相整流电路交流侧谐波仿真分析及计算
一、三相桥式全控整流电路和功率因数测量电路SIMULINK 模型 如图4-1,根据高频焊机的主电路机构和工作原理,可将高频焊机三相整流部分等效为阻感负载的三相桥式全控整流电路模型,其由三相交流电压源、三相晶闸管整流桥、同步六脉冲触发器和阻感…...
了解Java中的反射,带你如何使用反射
反射的定义 反射(Reflection)是Java的一种强大机制,它允许程序在运行时动态地查询和操作类的属性和方法。通过反射,Java程序可以获取类的信息,比如类的名称、方法、字段,以及可以动态地创建对象、调用方法…...

【c++】基础知识——快速入门c++
🌟🌟作者主页:ephemerals__ 🌟🌟所属专栏:C 目录 前言 一、手搓一个Hello World 二、命名空间namespace 1.命名空间的定义 2.命名空间的使用 3.命名空间补充知识 三、c中的输入和输出 四、缺省参…...

AI学习记录 - 自注意力机制的计算流程图
过段时间解释一下,为啥这样子计算,研究这个自注意力花了不少时间,网上很多讲概念,但是没有具体的流程图和计算方式总结…...
JavaScript快速入门,满满干货总结,快速掌握JS语法,DOM,BOM,事件
目录 一. JavaScript、HTML、CSS简介 1.1 HTML简介和举例说明 1.2 CSS简介和举例说明 1.3 JavaScript 简介和举例说明 二. JavaScript 基本语法 2.1 变量类型和定义方式 2.2 逻辑运算符,比较运算符 2.3 流程控制,if,if...else...&…...

【C++】C++入门基础【类与对象】
目录 1.类 1.1类的定义 1.2struct 与 class对比 2.访问限定符 3. 类域 4.实例化 5.存储大小----内存对齐 6.this指针 1.类 1.1类的定义 class作为类的关键字,后面跟的是类的名字,如Stack,{}中的为类的主体,类定义结束时…...
Qt | QScatterSeries 散点图
点击上方"蓝字"关注我们 01、QScatterSeries QScatterSeries 的类,它将代表散点图中的一个系列。这个类将包含数据点、颜色和样式等属性,以及用于绘制散点图的方法。 02、main.cpp #include <QtWidgets/QApplication>#include <QtWidgets/QMainWindow…...

无缝协作的艺术:Codigger 视频会议(Meeting)的用户体验
在当今数字化的时代,远程协作已经成为工作和学习中不可或缺的一部分。然而,远程协作也面临着诸多挑战,如沟通不畅、信息同步不及时、协作工具的复杂性等。而 Codigger 视频会议(Meeting)作为一款创新的工具,…...

C基础练习(学生管理系统)
1.系统运行,打开如下界面。列出系统帮助菜单(即命令菜单),提示输入命令 2.开始时还没有录入成绩,所以输入命令 L 也无法列出成绩。应提示“成绩表为空!请先使用命令 T 录入学生成绩。” 同理,当…...
网络安全抓包封包WEB
目录 1.抓包 1. 网络故障排除 应用 意义 2. 网络安全监控 应用 意义 3. 性能优化 应用 意义 4. 协议分析与开发 应用 意义 5. 数据分析与合规性审计 应用 意义 抓包工具 总结 2.抓包的应用对象 1. 网络设备 路由器和交换机 防火墙和入侵检测系统ÿ…...

Spring Boot - 在Spring Boot中实现灵活的API版本控制(上)
文章目录 为什么需要多版本管理?在Spring Boot中实现多版本API的常用方法1. URL路径中包含版本号2. 请求头中包含版本号3. 自定义注解和拦截器 注意事项 为什么需要多版本管理? API接口的多版本管理在我们日常的开发中很重要,特别是当API需要…...

华为云AI开发平台ModelArts
华为云ModelArts:重塑AI开发流程的“智能引擎”与“创新加速器”! 在人工智能浪潮席卷全球的2025年,企业拥抱AI的意愿空前高涨,但技术门槛高、流程复杂、资源投入巨大的现实,却让许多创新构想止步于实验室。数据科学家…...
【杂谈】-递归进化:人工智能的自我改进与监管挑战
递归进化:人工智能的自我改进与监管挑战 文章目录 递归进化:人工智能的自我改进与监管挑战1、自我改进型人工智能的崛起2、人工智能如何挑战人类监管?3、确保人工智能受控的策略4、人类在人工智能发展中的角色5、平衡自主性与控制力6、总结与…...

23-Oracle 23 ai 区块链表(Blockchain Table)
小伙伴有没有在金融强合规的领域中遇见,必须要保持数据不可变,管理员都无法修改和留痕的要求。比如医疗的电子病历中,影像检查检验结果不可篡改行的,药品追溯过程中数据只可插入无法删除的特性需求;登录日志、修改日志…...
pam_env.so模块配置解析
在PAM(Pluggable Authentication Modules)配置中, /etc/pam.d/su 文件相关配置含义如下: 配置解析 auth required pam_env.so1. 字段分解 字段值说明模块类型auth认证类模块,负责验证用户身份&am…...
在 Nginx Stream 层“改写”MQTT ngx_stream_mqtt_filter_module
1、为什么要修改 CONNECT 报文? 多租户隔离:自动为接入设备追加租户前缀,后端按 ClientID 拆分队列。零代码鉴权:将入站用户名替换为 OAuth Access-Token,后端 Broker 统一校验。灰度发布:根据 IP/地理位写…...

华为OD机试-食堂供餐-二分法
import java.util.Arrays; import java.util.Scanner;public class DemoTest3 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseint a in.nextIn…...
Git常用命令完全指南:从入门到精通
Git常用命令完全指南:从入门到精通 一、基础配置命令 1. 用户信息配置 # 设置全局用户名 git config --global user.name "你的名字"# 设置全局邮箱 git config --global user.email "你的邮箱example.com"# 查看所有配置 git config --list…...

Unity UGUI Button事件流程
场景结构 测试代码 public class TestBtn : MonoBehaviour {void Start(){var btn GetComponent<Button>();btn.onClick.AddListener(OnClick);}private void OnClick(){Debug.Log("666");}}当添加事件时 // 实例化一个ButtonClickedEvent的事件 [Formerl…...
WEB3全栈开发——面试专业技能点P7前端与链上集成
一、Next.js技术栈 ✅ 概念介绍 Next.js 是一个基于 React 的 服务端渲染(SSR)与静态网站生成(SSG) 框架,由 Vercel 开发。它简化了构建生产级 React 应用的过程,并内置了很多特性: ✅ 文件系…...
32单片机——基本定时器
STM32F103有众多的定时器,其中包括2个基本定时器(TIM6和TIM7)、4个通用定时器(TIM2~TIM5)、2个高级控制定时器(TIM1和TIM8),这些定时器彼此完全独立,不共享任何资源 1、定…...