021-直接利用Socket/TCP开发网络游戏四
今天的部分依旧是很详细。为什么详细呢?是因为这个部分我还是有点蒙蔽的,所以就要再一次将思路整理一下,看看到底讲了什么,也许在接下来的部分中我都会说的很详细,因为我很懵逼。
我们在前面的部分是将个个部分都说了一遍,接下来就开始真正的项目开发。我们在unity中创建新的项目,导入资源。
也和昨天的图一样,开局一张图,其他全靠编。
这个图是我们项目的整个框架,这个Gamefacde是一个中介者,是所有脚本的中转站。我们下面要讲的也是按照这个图片上的。
首先创建GameFacade脚本。接着创建GameManager AudioManager PlayerManager RequestMangager ClinetMananger脚本。接着创建一个BaseMangaer,这个是所有manager的基类
protected GameFacade gameFacade; public virtual void OnInit() { }//初始化的方法 public virtual void OnDestroy() { }//当项目删除的时候调用 public BaseManager(GameFacade gameFacade) { this.gameFacade = gameFacade; }
接着让所有的manager都继承于BaseManager中并完成构造函数。下面说一个例子:
public AudioManager(GameFacade gameFacade) : base(gameFacade) { }
接着在GameFacade中将初始化方法,生命结束的方法都写好:
private static GameFacade _instance; public static GameFacade Instance { get { return _instance; } } //将所有的manager在这里声明,方便统一管理 private UIManager uiMng; private CameraManager cameraMng; private AudioManager audioMng; private PlayerManager playerMng; private RequestManager requestMng; private ClientManager clientMng; void Awake() { _instance = this; } //初始化方法 private void InitManager() { uiMng = new UIManager(this); cameraMng = new CameraManager(this); audioMng = new AudioManager(this); playerMng = new PlayerManager(this); requestMng = new RequestManager(this); clientMng = new ClientManager(this); uiMng.OnInit(); cameraMng.OnInit(); audioMng.OnInit(); playerMng.OnInit(); requestMng.OnInit(); clientMng.OnInit(); } //删除方法 private void OnDertroy() { uiMng.OnDestroy(); cameraMng.OnDestroy(); audioMng.OnDestroy(); playerMng.OnDestroy(); requestMng.OnDestroy(); clientMng.OnDestroy(); } // Use this for initialization void Start () { //在开始中调用初始化的方法 InitManager(); }
接着我们开始确定一下网络的部分:就是ClientManager
private const string IP = "127.0.0.1"; private const int PORT = 6608;
完成一下连接的基本操作:
public ClientManager(GameFacade gameFacade) : base(gameFacade) { } //这个类是用作处理服务器对客户端的类的并进行管理 //设立两个常量,IP地址与端口号 private const string IP = "127.0.0.1"; private const int PORT = 6608; private Socket clientSocket;//服务器端为客户端单独创建的一个socket private Message msg = new Message();//得到message的成员变量 //初始化方法 public override void OnInit() { base.OnInit(); try { clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //再次开始接收数据 Start(); } catch (Exception e) { Console.WriteLine("无法连接到服务器,请重试:" + e); } } private void Start() { //开始接收来自客户端的数据 clientSocket.BeginReceive(msg.Data, msg.StartIndex, msg.RemainSize, SocketFlags.None, ReceiveCallBack, null); } //回调函数 private void ReceiveCallBack(IAsyncResult ar) { try { //得到从客户端传来的数据的个数 int count = clientSocket.EndReceive(ar); //然后进行解析 msg.ReadMessage(count, OnProcessDataCallBack); Start(); } catch (Exception e) { Console.WriteLine(e); } } private void OnProcessDataCallBack(ActionCode actionCode ,string data) { //接收来自message的方法完成一些操作 //接着就是一些处理响应的方法 gameFacade.HandleOnReqone(actionCode, data); } //删除的方法 public override void OnDestroy() { base.OnDestroy(); try { //关闭连接 clientSocket.Close(); OnInit();//再次接收下一个客服端的要求 } catch (Exception e) { Console.WriteLine("无法关闭与服务器的连接,请重试" + e); } }
上面这个说是前面已经讲过了。
接下来我们换个说法,就是数据从客户端开始发送,然后到服务器端是怎样的过程。
客服向服务器请求连接,服务器同意客户端的请求,并将创建一个socket负责专门与这个服务器进行数据接收,如下:
//初始化方法 public override void OnInit() { base.OnInit(); try { clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //再次开始接收数据 Start(); } catch (Exception e) { Console.WriteLine("无法连接到服务器,请重试:" + e); } }
这个就是客户端的一些基本设置,因为这个继承与basemanager所以这个会被执行的。图中的start是下面的代码:
private void Start() { //开始接收来自客户端的数据 clientSocket.BeginReceive(msg.Data, msg.StartIndex, msg.RemainSize, SocketFlags.None, ReceiveCallBack, null); }
这个服务器端专门用来与特定客户端连接的socket开始接收来自客户端的数据,就是上面的代码。上面的msg就是message,等下面再说。上面的ReceiveCallBack就是一个委托AsyncCallback ,接下来我们说一说委托和回调函数,这两个比较重要的函数。先说回调
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
好了接下来就是委托,委托就是指针。在上面代码中调用这个ReceiveCallBack变量,我们在前面写一个相同的方法:
private void ReceiveCallBack(IAsyncResult ar) { try { //得到从客户端传来的数据的个数 int count = clientSocket.EndReceive(ar); //然后进行解析 msg.ReadMessage(count, OnProcessDataCallBack); Start(); } catch (Exception e) { Console.WriteLine(e); } }
这个方法就是委托,我们用这个变量就是调用这个方法,与C语言中的指针是相同的作用。当clientsocket完成对来自客户端的数据额接收的时候我们首先得到传来的字节数组的个数,看看它到底有多少个。然后就是解析,因为不解析的话我们是不能用的,传来的都是二进制。就通过msg.read message方法调用,就是message。如下图:clietmanager
private byte[] data = new byte[1024]; private int startIndex = 0;//开始索引 public byte[] Data { get { return data; } } public int StartIndex { get { return startIndex; } } //还剩余什么 public int RemainSize { get { return data.Length - startIndex; } } //读数据讲过这个方法的解析,然后通过processDataCallBack,因为用的是委托,所以就会传给clientmanager中的OnProcessDataCallBack public void ReadMessage(int newDataAmount, Action<ActionCode, string> processDataCallBack) { startIndex += newDataAmount; while (true) { //如果数据长度不足4的话,会返回 if (startIndex <= 4) return; //得到传来数据的长度,因为toint32一次只会解析前四个字节,这样就知道了数组中还有几个数据 int count = BitConverter.ToInt32(data, 0); if ((startIndex - 4) >= count) { ActionCode actionCode = (ActionCode)BitConverter.ToInt32(data, 4); //索引减4就是真正的数据长度,就接受数据 string s = Encoding.UTF8.GetString(data, 8, count); Console.WriteLine("解析出来的数据为:" + s); //将数据进行更新 Array.Copy(data, count + 4, data, 0, startIndex - 4 - count); startIndex -= (count + 4); processDataCallBack(actionCode, s); } else { break; } } }
上面的代码就是解析数据的过程,它的方法中也是有一个委托的,当我们将数据处理好了,就要返回这个委托processDataCallBack,而这个方法指向的就是下面的:
clientManager的:
private void OnProcessDataCallBack(ActionCode actionCode ,string data) { //接收来自message的方法完成一些操作 //接着就是一些处理响应的方法 gameFacade.HandleOnReqone(actionCode, data); }
会调用GameFacade的:
public void HandleOnReqone(ActionCode actionCode, string data) { requestMng.HandleOnReqone(actionCode, data); }
接着调用reqestmanagar,因为这些都是公共方法:
//处理request的方法 //处理响应的方法 public void HandleOnReqone(ActionCode actionCode, string data) { BaseRequest request = requestDict.TryGet<ActionCode, BaseRequest>(actionCode); if (request == null) { Debug.LogWarning("无法处理" + actionCode); return; } request.OnRespone(data);//进行响应的方法 }
接着我们要写一些方法在requestmanager,因为这些都是基本方法:
public RequestManager(GameFacade gameFacade) : base(gameFacade) { } //在这里创建一个字典,用来管理所有的request 状态---基础类 private Dictionary<ActionCode, BaseRequest> requestDict = new Dictionary<ActionCode, BaseRequest>(); //添加request的方法 public void AddRequest(ActionCode actionCode, BaseRequest baseRequest) { requestDict.Add(actionCode, baseRequest); } //删除的方法 public void RemoveRequest(ActionCode actionCode) { requestDict.Remove(actionCode); }
因为gamefacade是中介者,其他的都是通过它来访问manager的所以:
//GameFrcade作为中介者 //在这里调用所有manager的方法 //添加request的方法 //在这里说明一下,因为处理之后就会返回,那么格式为数据长度:actioncode:数据 public void AddRequest(ActionCode actionCode, BaseRequest baseRequest) { requestMng.AddRequest(actionCode, baseRequest); } //删除request的方法 public void RemoveRequest(ActionCode actionCode) { requestMng.RemoveRequest(actionCode); }
接着我们还要写一下requestmanager这个管理类:
//处理request的方法 //处理响应的方法 public void HandleOnReqone(ActionCode actionCode, string data) { BaseRequest request = requestDict.TryGet<ActionCode, BaseRequest>(actionCode); if (request == null) { Debug.LogWarning("无法处理" + actionCode); return; } request.OnRespone(data);//进行响应的方法 }
接着request.Onrespone进行处理,就是下面的图,但是这个是一个基类,在以后我们会创建很多request的类,会调用他们的方法进行使用。
//得到状态 private RequestCode requestCode = RequestCode.None; private ActionCode actionCode = ActionCode.None; public virtual void Awake() { //将自己加入到requestmanager的中requestDcit中 GameFacade.Instance.AddRequest(actionCode, this); } public virtual void SendRequest() { }//发起请求 public virtual void OnRespone(string data) { }//进行响应 //当整个生命周期结束时自动调用 private void OnDestroy() { //将自己从requestmanager中的requestDict中删除 GameFacade.Instance.RemoveRequest(actionCode); }
最后忘记了我们还有在client manager,写一个向服务器端发送数据的方法,数据先要变成二进制
//客户端向服务器端发送要求的函数 //在一些request类中进行调用 public void SendRequest(RequestCode requestCode, ActionCode actionCode, string data) { //得到字节数组 byte[] bytes = Message.PackData(requestCode, actionCode, data); //向socket发送字节数据 clientSocket.Send(bytes); }
接着是message中的方法:
//重载方法,这个是客户端向服务器端发送数据需要requestcode //格式:数据长度:requestcode:actioncode:数据 public static byte[] PackData(RequestCode requestData,ActionCode ActionData, string data) { byte[] requestCodeBytes = BitConverter.GetBytes((int)requestData); byte[] actionCodeBytes = BitConverter.GetBytes((int)ActionData); byte[] dataBtyes = Encoding.UTF8.GetBytes(data); int dataAmount = requestCodeBytes.Length + actionCodeBytes.Length + dataBtyes.Length; byte[] dataAmountBytes = BitConverter.GetBytes(dataAmount); return dataAmountBytes.Concat(requestCodeBytes).Concat(actionCodeBytes).Concat(dataBtyes).ToArray(); }
这个是一个重载方法,系统会匹配的,就是转换成二进制的方法。好了我们今天的就结束了。