我们先将一些服务器上面的代码转移到之前的ResLoadPrg项目里。服务器和客户端不同,不要让客户端依赖服务器的dll库。反过来可以。在开发阶段可以将一些公用的代码放到客户端里,然后使用Unity的Assembly Definition生成一个独立的dll库来让服务端的工程依赖上。
首先在Script文件夹里新建Net文件夹,将服务器工程的ByteArray.cs粘贴过来,在Net文件夹内新建Proto文件夹,将服务器工程的MsgBase.cs、MsgSecret.cs、ProtocolEnum.cs粘贴过来。
将MsgBase
内添加上一些命名空间,并把剩下的报错注释掉
1 2 3 4 using ProtoBuf;using System;using UnityEngine;using System.IO;
给ByteArray
添加命名空间
如果想要消除一些CS8632警告,将相关可空类型的声明的上下文添加#nullable enable
和#nullable disable
如果是新工程,参考热更教程里面接入protobuf的方法,并且将服务器工程的SingletonPattern.cs
和AES.cs
转移到Unity里。
NetManager 在Net文件夹内新建NetManager
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 using SimpleServer.Net;using System;using System.Collections;using System.Collections.Generic;using System.Net.Sockets;using UnityEngine;public class NetManager : SingletonPattern <NetManager >{ public enum NetEvent { ConnectSucc, ConnectFail, Close } public string PublicKey { get ; private set ; } = "ATAOServer" ; public string SecretKey { get ; private set ; } private Socket m_Socket; private ByteArray m_ReadBuff; private string m_IP; private int m_Port; private bool m_IsConnecting; private bool m_IsClosing; public delegate void NetEventListener (string str ) ; private Dictionary<NetEvent,NetEventListener> m_ListenerDic = new Dictionary<NetEvent,NetEventListener>(); public void AddNetEventListener (NetEvent netEvent, NetEventListener listener ) { if (m_ListenerDic.ContainsKey(netEvent)) { m_ListenerDic[netEvent] += listener; } else { m_ListenerDic.Add(netEvent, listener); } } public void RemoveNetEventListener (NetEvent netEvent,NetEventListener listener ) { if (m_ListenerDic.ContainsKey(netEvent)) { m_ListenerDic[netEvent] -= listener; if (m_ListenerDic[netEvent] == null ) { m_ListenerDic.Remove(netEvent); } } } void FirstEvent (NetEvent netEvent,string str ) { if (m_ListenerDic.ContainsKey(netEvent)) { m_ListenerDic[netEvent].Invoke(str); } } public void SetKey (string key ) { SecretKey = key; } public void Connect (string ip, int port ) { if (m_Socket != null && m_Socket.Connected) { Debug.LogError("链接失败,和服务器已经链接了" ); return ; } if (m_IsConnecting) { Debug.LogError("链接失败,正在和服务器链接中" ); return ; } InitState(); m_Socket.NoDelay = true ; m_IsConnecting = true ; m_Socket.BeginConnect(ip, port, ConnectCallback, m_Socket); m_IP = ip; m_Port = port; } void InitState () { m_Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); m_ReadBuff = new ByteArray(); m_IsConnecting = false ; m_IsClosing = false ; } void ConnectCallback (IAsyncResult ar ) { try { Socket socket = (Socket)ar.AsyncState; socket.EndConnect(ar); FirstEvent(NetEvent.ConnectSucc, "" ); m_IsConnecting = false ; Debug.Log("Socket Connect Success" ); m_Socket.BeginReceive(m_ReadBuff.Bytes, m_ReadBuff.WriteIdx, m_ReadBuff.Remain, SocketFlags.None, ReceiveCallback, socket); } catch (SocketException se) { Debug.LogError("Socket Connect fail: " + se); m_IsConnecting = false ; } } void ReceiveCallback (IAsyncResult ar ) { try { Socket socket = (Socket)ar.AsyncState; int count = socket.EndReceive(ar); if (count <= 0 ) { return ; } m_ReadBuff.WriteIdx += count; OnReceiveData(); if (m_ReadBuff.Remain < 8 ) { m_ReadBuff.MoveBytes(); m_ReadBuff.ReSize(m_ReadBuff.Length * 2 ); } socket.BeginReceive(m_ReadBuff.Bytes,m_ReadBuff.WriteIdx,m_ReadBuff.Remain,SocketFlags.None, ReceiveCallback, socket); } catch (SocketException ex) { Debug.LogError("Socket ReceiveCallback fail: " + ex); CloseConnection(); } } void OnReceiveData () { } }
在正式的游戏中,分为游戏服和目录服,客户端需要先链接一次目录服,从目录服中可以查看到游戏所有服务器的在线情况并且可以自主选服,客户端选择后需要再链接一次游戏服。选择游戏服后,还需要根据当前服务器的拥挤状况进行排队。
客户端使用异步链接,所以NetManager
内需要声明IsConnecting
和IsClosing
状态。
使用客户端连接时,打开m_Socket.NoDelay = true;
这代表着关闭C#TCP的Nagle算法,这意味着减少TCP延迟,但是增加TCP流量。
使用m_Socket.BeginConnect(ip, port, ConnectCallback, m_Socket);
开始连接服务器,其中ConnectCallback
是一个回调方法,具体是ConnectCallback(IAsyncResult ar)
,IAsyncResult
存储了异步操作的状态信息和用户所有的保存数据,我们最后传入的m_Socket
也会被保存在里面
在ConnectCallback(IAsyncResult ar)
回调方法内,使用(Socket)ar.AsyncState;
来将BeginConnect
最后传入的m_Socket
转换出来,并且调用socket.EndConnect(ar);
结束异步链接请求。注意,每次调用Socket.BeginConnect
一定要有对应的socket.EndConnect
,注意这里的EndConnet
和Socket.Close
不一样,后者会关闭连接并释放所有资源。
在ConnectCallback
回调内使用Socket.BeginReceive(byte[] buffer, int offset, int size, SocketFlags.None, ReceiveCallback, object o);
这个异步方法来获取传输的内容,和服务器不同的是,服务器用的都是同步方法Socket.Recerive
,这里需要有一个获取内容的回调方法,也就是ReceiveCallback
,它和ConnectCallback
相同。
在ReceiveCallback(IAsyncResult ar)
回调方法内,使用(Socket)ar.AsyncState;
来将BeginReceive
最后传入的m_Socket
转换出来,并且调用socket.EndReceive(ar)
结束异步接收内容的请求。注意,每次调用Socket.BeginReceive
一定要有对应的socket.EndReceive
。
链接有链接的回调,获取传输内容有获取传输内容的回调,因为这两步都是异步操作。
接着在ReceiveCallback(IAsyncResult ar)
回调方法内,调用OnReceiveData
函数来处理接收来的数据。这里就像服务器ServerSocket.ReadClient
和ServerSocket.OnReceiveData
的关系一样。但是要注意,服务器用的都是同步方法多路复用,而客户端这里用的都是异步方法,而且服务器使用ByteArray.CheckAndMoveBytes
来重置readBuff,客户端这里直接使用ByteArray.MoveBytes
,服务器使用while(true)循环来扩容,而客户端不需要此循环,因为在ReceiveCallback
里面又调用了Socket.BeginReceive
,自己本身就是递归无限循环的,当需要扩容时,每次递归扩容一下,早晚会达到要求的。