Unity网络通信(part5.编写服务端与客户端)
目录
前言
基础写法
服务端基础写法
客户端基础写法
效果实现
服务端效果
客户端效果
服务端与多个客户端进行通信
封装服务端
客户端的Socket类
服务端Socket类
启动类
封装客户端
网络管理类
启动类
最终实现
前言
在前面的部分中,我们已经探讨了Unity网络通信的基础知识和相关概念。现在,我们将进入实际编程阶段,通过编写服务端与客户端的代码,来实现两者之间的通信。
网络通信是现代游戏开发中不可或缺的一部分,它允许游戏内的玩家进行实时互动和数据传输。在Unity中,我们可以利用Socket编程来实现这一功能。Socket是一种网络通信的端点,它提供了在不同计算机或进程之间进行数据传输的能力。
在本次教程中,我们将分别编写服务端和客户端的代码。服务端将负责监听客户端的连接请求,并处理客户端发送的数据。客户端则负责连接到服务端,并发送和接收数据。
基础写法
服务端的 Accept、Receive是会阻塞主线程的,要等到执行完毕才会继续执行后面的内容。
服务端基础写法
using System.Net;
using System.Net.Sockets;
using System.Text;namespace TcpServer
{class Server{static void Main(string[] args){#region 服务端需要做的事情//1.创建套接字socket//2.用Bind方法将套接字与本地地址绑定//3.用Listen方法监听//4.用Accept方法等待客户端连接//5.建立连接,Accept返回新套接字//6.用send和Receive相关方法收发数据//7.用shutdown方法释放连接//8.关闭套接字#endregion#region 实现服务端的基本逻辑//1.创建套接字socket(TCP)Socket socketTcp = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);//2.用Bind方法将套接字与本地地址绑定try{IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);socketTcp.Bind(ipPoint);}catch (Exception e){Console.WriteLine("绑定IP地址与端口失败:"+e.Message);return;}//3.用Listen方法监听socketTcp.Listen(1024);//监听最大数量Console.WriteLine("服务端监听结束,等待客户端连入");//4.用Accept方法等待客户端连入//5.建立连接,Accept返回新套接字Socket socketClient = socketTcp.Accept();//阻塞方法,后面的方法会等待该方法执行完后才会执行Console.WriteLine("有客户端连入");//6.用Send和Receive相关的方法收发数据//发送socketClient.Send(Encoding.UTF8.GetBytes("服务端连接成功"));//接收byte[] result = new byte[1024];int receiveNumber = socketClient.Receive(result);//记录接收到了多少个字节数Console.WriteLine("接收到了{0}发来的消息:{1}",socketClient.RemoteEndPoint.ToString(),Encoding.UTF8.GetString(result,0,receiveNumber));//7.用Shutdown方法释放连接socketClient.Shutdown(SocketShutdown.Both);//接受和发送消息都停止Console.WriteLine("停止收发消息");//8.关闭套接字socketClient.Close();Console.WriteLine("连接已关闭");#endregionConsole.WriteLine("按任意键退出");Console.ReadKey();}}
}
客户端基础写法
using System.Net;
using System.Net.Sockets;
using System.Text;
using UnityEngine;namespace TcpClient
{class Client: MonoBehaviour{void Start (){#region 客户端需要做的事情//1.创建套接字Socket//2.用Connect方法与服务端相连//3.用Send和Receive相关方法收发数据//4.用Shutdown方法释放连接//5.关闭套接字#endregion#region 实现客户端基本逻辑//1.创建套接字类型Socket socket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);//用Connect方法与服务端相连//确定服务端的IP和端口IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"),8080);//此处应填写远端服务器的地址try{socket.Connect(ipPoint);}catch (SocketException e){if (e.ErrorCode == 10061){print("服务器拒绝连接");}elseprint("连接服务器失败"+e.ErrorCode);}//3.用Send和Recive相关方法收发数据//接收数据byte[] receiveBytes = new byte[1024];int receiveNum = socket.Receive(receiveBytes);print("收到服务端发来的消息:"+Encoding.UTF8.GetString(receiveBytes,0,receiveNum));//发送数据socket.Send(Encoding.UTF8.GetBytes("我是客户端,我来了。"));//4.用Shutdown方法释放连接socket.Shutdown(SocketShutdown.Both);//5.关闭套接字socket.Close();#endregion}
效果实现
先运行服务端代码(服务端代码为控制台工程),再将客户端脚本挂载到Unity场景中,运行Unity项目。
服务端效果
客户端效果
服务端与多个客户端进行通信
using System.Net.Sockets;
using System.Net;
using System.Text;namespace TcpServer
{class MultipleConnectionsServer{static Socket socketTcp;static List<Socket> clientSockets = new List<Socket>();//记录客户端的Socketstatic bool isClose = false;static void Main(string[] args){ //创建套接字socket(TCP)socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);//用Bind方法将套接字与本地地址绑定try{IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);socketTcp.Bind(ipPoint);}catch (Exception e){Console.WriteLine("绑定IP地址与端口失败:" + e.Message);return;}//用Listen方法监听socketTcp.Listen(1024);//监听最大数量Console.WriteLine("服务端监听结束,等待客户端连入");//建立客户端连接Thread acceptThread = new Thread(AcceptClientConnect);acceptThread.Start();//收发数据Thread receiveThread = new Thread(ReceiveMsg);receiveThread.Start();//关闭相关Console.WriteLine("输入Quit退出");while (true){string input = Console.ReadLine();//定义一个规则,关闭服务器,断开所有连接if (input =="Quit"){for(int i = 0;i<clientSockets.Count;i++){clientSockets[i].Shutdown(SocketShutdown.Both);clientSockets[i].Close();}break;}//定义一个规则 广播消息 就是让所有客户端和服务端发送的消息else if(input.Substring(0,2)=="B:"){for(int i=0;i<clientSockets.Count;i++){//给所有客户端广播消息clientSockets[i].Send(Encoding.UTF8.GetBytes(input.Substring(2)));}}}}static void AcceptClientConnect(){while (!isClose)//用来不停接收客户端的连入{Socket clientSocket = socketTcp.Accept();clientSockets.Add(clientSocket);clientSocket.Send(Encoding.UTF8.GetBytes("服务端连接成功"));}}static void ReceiveMsg(){//将临时变量声明到while循环外面降低对设备的开销Socket clientSocket;byte[] result = new byte[1024*1024];//1兆的最大容量int receiveNum;int i;while(!isClose){for(i = 0;i<clientSockets.Count;i++){clientSocket = clientSockets[i];//判断Socket是否有可以接收的消息,返回值就是字节数if(clientSocket.Available>0){receiveNum = clientSocket.Receive(result);//如果直接在这里收到消息就是处理,可能照成问题//不能够即使的处理别人的消息//为了不影响别人消息的处理,我们把消息处理交给新的线程,为了节约线程的开销,我们使用线程池//用过的线程放入池中,要用时将他拿出来//(clientSocket,Encoding.UTF8.GetString(result,0,receiveNum))第一个参数为通信通道ThreadPool.QueueUserWorkItem(HandleMsg,(clientSocket,Encoding.UTF8.GetString(result,0,receiveNum)));}}}}static void HandleMsg(Object obj){(Socket s, string str) info = ((Socket s, string str))obj;Console.WriteLine("收到客户端{0}f发来的信息:{1}",info.s.RemoteEndPoint,info.str);}}
}
封装服务端
客户端的Socket类
客户端的Socket类,服务端会连接多个客户端,因此对每个客户端对象的操作方法封装在这里。
using System.Net.Sockets;
using System.Text;namespace TcpServer
{class ClientSocket{private static int CLIENT_BEGIN_ID = 1;public int clientID;public Socket socket;public ClientSocket(Socket socket){this.clientID = CLIENT_BEGIN_ID;this.socket = socket;++CLIENT_BEGIN_ID;}//是否是连接状态public bool Conected => this.socket.Connected;//封装方法//关闭public void Close(){if(socket!=null){socket.Shutdown(SocketShutdown.Both);socket.Close();}}//发送public void Send(string info){if(socket != null){try{socket.Send(Encoding.UTF8.GetBytes(info));}catch(Exception e){Console.WriteLine("发送消息出错:"+e.Message);}}}//接收public void Receive(){if(socket != null){try{if(socket.Available >0){byte[] result = new byte[1024 * 5];//5KBint recevieNum = socket.Receive(result);ThreadPool.QueueUserWorkItem(MsgHandle,Encoding.UTF8.GetString(result,0,recevieNum));}}catch (Exception e){Console.WriteLine("接收消息出错:"+e.Message);Close();}}}private void MsgHandle(object obj){string? str = obj as string;Console.WriteLine("收到客户端{0}发来的消息:{1}",this.socket,str);}}
}
服务端Socket类
服务端ServerSocket类,封装了服务端开启、关闭和收发消息的方法。
using System.Net;
using System.Net.Sockets;namespace TcpServer
{class ServerSocket{//服务端Socketpublic Socket socket;//客户端链接的所有Socketpublic Dictionary<int, ClientSocket> clientDic = new Dictionary<int, ClientSocket>();private bool isClose;//开启服务器端public void Start(string ip,int port,int num){isClose = false;socket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip),port);socket.Bind(ipPoint);socket.Listen(num);ThreadPool.QueueUserWorkItem(Accpet);ThreadPool.QueueUserWorkItem(Receive);}//关闭服务器端public void Close(){foreach (ClientSocket client in clientDic.Values){client.Close();}clientDic.Clear();socket.Shutdown(SocketShutdown.Both);socket.Close();socket = null;}//接收客户端连入private void Accpet(object obj){while(!isClose){try{//连入了一个客户端Socket clientSockket = socket.Accept();ClientSocket client = new ClientSocket(clientSockket);client.Send("欢迎连入服务器");clientDic.Add(client.clientID,client);}catch (Exception e){Console.WriteLine("客户端连入报错"+e.Message);throw;}}}//接收客户端消息private void Receive(object obj){while(!isClose){if(clientDic.Count>0){foreach(ClientSocket client in clientDic.Values ){client.Receive();}}}}public void Broadcast(string info){foreach (ClientSocket client in clientDic.Values){client.Send(info);}}}
}
启动类
namespace TcpServer
{class Program{static void Main(string[] args){ServerSocket socket =new ServerSocket();socket.Start("127.0.0.1",8080,1024);Console.WriteLine("服务开启成功");while(true){string input = Console.ReadLine();if(input == "Quit"){socket.Close();}else if(input.Substring(0,2)=="B:"){socket.Broadcast(input.Substring(2));}}}}
}
封装客户端
网络管理类
使用单例模式,将与客户端有关的方法都封装在这里
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using UnityEngine;public class NetMgr : MonoBehaviour
{private static NetMgr instance;public static NetMgr Instance => instance;//客户端Socketprivate Socket socket;//用于发送消息的队列 公共容器 主线程往里面放 发送线程往里面取private Queue<string> sendMsgQueue = new Queue<string>();//用于接收消息的队列 公共容器 子线程往里面放 主线程往里面取private Queue<string> receiveQueue = new Queue<string>();//用于收消息的容器private byte[] receiveBytes = new byte[1024*1024];//返回收到的字节数private int receiveNum;//是否连接private bool isConnect=false;private void Awake(){instance = this; DontDestroyOnLoad(this.gameObject);}private void Update(){if(receiveQueue.Count>0){print(receiveQueue.Dequeue());}}//连接服务端public void Connect(string ip,int port){//如果是连接状态 直接返回if(isConnect){return;}if(socket==null){socket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);}//连接服务端IPEndPoint iPPoint = new IPEndPoint(IPAddress.Parse(ip),port);try{socket.Connect(iPPoint);isConnect=true;//开启发送线程ThreadPool.QueueUserWorkItem(SendMsg);//开启接收线程ThreadPool.QueueUserWorkItem(ReceiveMsg);}catch(SocketException e){if(e.ErrorCode == 10061){print("服务器拒绝连接");}else{print("连接失败"+e.ErrorCode+e.Message);}}}//发送消息public void Send(string info){sendMsgQueue.Enqueue(info);}private void SendMsg(object obj){while(isConnect){if(sendMsgQueue.Count>0){socket.Send(Encoding.UTF8.GetBytes(sendMsgQueue.Dequeue()));}}}//接收消息public void ReceiveMsg(object obj){while(isConnect){if(socket.Available >0){receiveNum = socket.Receive(receiveBytes);//收到消息 解析消息为字符串 并放入公共容器receiveQueue.Enqueue(Encoding.UTF8.GetString(receiveBytes,0,receiveNum));}}}//关闭连接public void Close(){if(socket!=null){socket.Shutdown(SocketShutdown.Both);socket.Close();isConnect = false;}}private void OnDestroy(){Close();}
}
启动类
将脚本挂载到场景里,绑定按钮和文本输入框。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;public class Main : MonoBehaviour
{public Button btn;public InputField input;void Start(){if(NetMgr.Instance==null){GameObject obj = new GameObject("Net");obj.AddComponent<NetMgr>();}NetMgr.Instance.Connect("127.0.0.1",8080);btn.onClick.AddListener(()=>{if(input.text!=""){NetMgr.Instance.Send(input.text);}});}
}
最终实现
先启动服务端,再启动客户端,两端会不停的接收对方发送的消息。