标签:
“攻城”服务端采用Photon引擎的框架,其主要逻辑如以下UML所示。
服务端的启动入口为ServerApplication,该类包含着相关的Collection数据集合,而Collection内又有与数据库文件夹Database关联的文件。两个文件夹的内容如图。
简单来说,ServerApplication内缓存着各类数据,并完成与数据库等的关联。而本篇的重点是ServerPeer这个类。下面介绍什么是Peer。
每当一个客户端连接到服务端时,服务端会自动生成一个客户端连接实例,称其为Peer。通过Peer,便能完成服务端与客户端之间的数据交互,ServerPeer类便是完成这个任务的类。在这里通过简单代码介绍这个类的内容。
1 //----------------------------------------------------------------------------------------------------------- 2 // Copyright (C) 2015-2016 SiegeOnline 3 // 版权所有 4 // 5 // 文件名:ServerPeer.cs 6 // 7 // 文件功能描述: 8 // 9 // 服务端与客户端的连线实例 10 // 11 // 创建标识:taixihuase 20150712 12 // 13 // 修改标识: 14 // 修改描述: 15 // 16 // 17 // 修改标识: 18 // 修改描述: 19 // 20 //----------------------------------------------------------------------------------------------------------- 21 22 using System; 23 using ExitGames.Logging; 24 using Photon.SocketServer; 25 using PhotonHostRuntimeInterfaces; 26 using SiegeOnlineServer.Protocol; 27 using SiegeOnlineServer.ServerLogic; 28 29 namespace SiegeOnlineServer 30 { 31 /// <summary> 32 /// 类型:类 33 /// 名称:ServerPeer 34 /// 作者:taixihuase 35 /// 作用:用于服务端与客户端之间的数据传输 36 /// 编写日期:2015/7/12 37 /// </summary> 38 public class ServerPeer : PeerBase 39 { 40 // 日志 41 public static readonly ILogger Log = LogManager.GetCurrentClassLogger(); 42 43 // 索引 44 public Guid PeerGuid { get; protected set; } 45 46 // 服务端 47 public readonly ServerApplication Server; 48 49 /// <summary> 50 /// 类型:方法 51 /// 名称:ServerPeer 52 /// 作者:taixihuase 53 /// 作用:构造 ServerPeer 对象 54 /// 编写日期:2015/7/12 55 /// </summary> 56 /// <param name="protocol"></param> 57 /// <param name="unmanagedPeer"></param> 58 /// <param name="server"></param> 59 public ServerPeer(IRpcProtocol protocol, IPhotonPeer unmanagedPeer, ServerApplication server) : base(protocol, unmanagedPeer) 60 { 61 PeerGuid = Guid.NewGuid(); 62 Server = server; 63 64 // 将当前 peer 加入连线列表 65 Server.Users.AddConnectedPeer(PeerGuid, this); 66 } 67 68 /// <summary> 69 /// 类型:方法 70 /// 名称:OnOperationRequest 71 /// 作者:taixihuase 72 /// 作用:响应并处理客户端发来的请求 73 /// 编写日期:2015/7/14 74 /// </summary> 75 /// <param name="operationRequest"></param> 76 /// <param name="sendParameters"></param> 77 protected override void OnOperationRequest(OperationRequest operationRequest, SendParameters sendParameters) 78 { 79 switch (operationRequest.OperationCode) 80 { 81 // 账号登陆 82 case (byte) OperationCode.Login: 83 Login.OnRequest(operationRequest, sendParameters, this); 84 break; 85 86 // 创建新角色 87 case (byte) OperationCode.CreateCharacter: 88 CreateCharacter.OnRequest(operationRequest, sendParameters, this); 89 break; 90 91 // 角色进入场景 92 case (byte) OperationCode.WorldEnter: 93 WorldEnter.OnRequest(operationRequest, sendParameters, this); 94 break; 95 96 97 } 98 } 99 100 /// <summary> 101 /// 类型:方法 102 /// 名称:OnDisconnect 103 /// 作者:taixihuase 104 /// 作用:当与客户端失去连接时进行处理 105 /// 编写日期:2015/7/12 106 /// </summary> 107 /// <param name="reasonCode"></param> 108 /// <param name="reasonDetail"></param> 109 protected override void OnDisconnect(DisconnectReason reasonCode, string reasonDetail) 110 { 111 Server.Players.RemoveCharacter(PeerGuid); 112 Server.Users.UserOffline(PeerGuid); 113 Server.Users.RemovePeer(PeerGuid); 114 } 115 } 116 }
可以看到,ServerPeer的重点在于OnOperationRequest方法,该方法其中一个参数为OperationRequest类型的对象,这个对象中包含着一个byte型的OperationCode对象和一个Dictionary<byte, object>的对象Parameters。其中OperationCode即为客户端的操作请求码,服务端需要通过识别这个操作码才能对特定数据执行正确的处理。Parameters包含着等待处理的数据,这是个字典类型的对象,其键为参数类型码,对应的值即为该参数类型码所说明的数据对象。由于Photon的这个字典中object无法直接对自定义类进行序列化,因此需要通过手动序列化为二进制数据后再传入字典,取出时也要根据操作码或参数类型码手动反序列化为特定实例。这里封装好这两个操作为静态方法,直接调用即可。
1 //----------------------------------------------------------------------------------------------------------- 2 // Copyright (C) 2015-2016 SiegeOnline 3 // 版权所有 4 // 5 // 文件名:Serialization.cs 6 // 7 // 文件功能描述: 8 // 9 // 数据对象二进制序列化及反序列化 10 // 11 // 创建标识:taixihuase 20150714 12 // 13 // 修改标识: 14 // 修改描述: 15 // 16 // 17 // 修改标识: 18 // 修改描述: 19 // 20 //---------------------------------------------------------------------------------------------------------- 21 22 using System.IO; 23 using System.Runtime.Serialization; 24 using System.Runtime.Serialization.Formatters.Binary; 25 26 namespace SiegeOnlineServer.Protocol 27 { 28 /// <summary> 29 /// 类型:类 30 /// 名称:Serialization 31 /// 作者:taixihuase 32 /// 作用:对数据进行二进制序列化与反序列化 33 /// 编写日期:2015/7/14 34 /// </summary> 35 public class Serialization 36 { 37 /// <summary> 38 /// 类型:方法 39 /// 名称:Serialize 40 /// 作者:taixihuase 41 /// 作用:将一个对象二进制序列化 42 /// 编写日期:2015/7/14 43 /// </summary> 44 /// <param name="unSerializedObj"></param> 45 /// <returns></returns> 46 public static byte[] Serialize(object unSerializedObj) 47 { 48 MemoryStream stream = new MemoryStream(); 49 IFormatter formatter = new BinaryFormatter(); 50 formatter.Serialize(stream, unSerializedObj); 51 return stream.ToArray(); 52 } 53 54 /// <summary> 55 /// 类型:方法 56 /// 名称:Deserialize 57 /// 作者:taixihuase 58 /// 作用:将一个二进制序列化数据流反序列化为一个对象 59 /// 编写日期:2015/7/14 60 /// </summary> 61 /// <param name="serializedArray"></param> 62 /// <returns></returns> 63 public static object Deserialize(object serializedArray) 64 { 65 MemoryStream stream = new MemoryStream((byte[])serializedArray); 66 IFormatter formatter = new BinaryFormatter(); 67 stream.Seek(0, SeekOrigin.Begin); 68 object unSerializedObj = formatter.Deserialize(stream); 69 return unSerializedObj; 70 } 71 } 72 }
重新回到OnOperationRequest方法,当判别了操作码类型后,则Peer会调用特定类的一个静态方法,这样便可不需要实例化这些类型的对象后再使用。例如:
1 // 账号登陆 2 case (byte) OperationCode.Login: 3 Login.OnRequest(operationRequest, sendParameters, this); 4 break;
当识别为Login操作后,则调用Login里的OnRequest方法,并把参数原封不动传过去,在另外的文件里进行处理,这样子方便操作。同时OnRequest方法还要求第三个参数,为ServerPeer类型的对象,这样通过把this传过去,便能在其他地方引用到该Peer,而Peer又存放着ServerApplication的引用,方法调用及数据传输便畅通无阻。需要注意的是,不管操作码是什么,都是直接调用相应的OnRequest方法,如上面的代码所示。
接下来是对请求的处理逻辑。
当前实现了对三个不同请求的处理,在此用Login操作讲解。每个逻辑处理文件都包含OnRequest方法,除此之外,还有一个以“Try”开头命名的方法,该方法参数与OnRequest相同,即OnRequest再次将数据传给Try方法,而Try方法则真正进行处理,完成后将回应发生请求的客户端,或者向特定客户端发送广播。
1 //----------------------------------------------------------------------------------------------------------- 2 // Copyright (C) 2015-2016 SiegeOnline 3 // 版权所有 4 // 5 // 文件名:Login.cs 6 // 7 // 文件功能描述: 8 // 9 // 登录用户账号,响应客户端登录账号请求 10 // 11 // 创建标识:taixihuase 20150714 12 // 13 // 修改标识: 14 // 修改描述: 15 // 16 // 17 // 修改标识: 18 // 修改描述: 19 // 20 //----------------------------------------------------------------------------------------------------------- 21 22 using System; 23 using System.Collections.Generic; 24 using Photon.SocketServer; 25 using SiegeOnlineServer.Collection; 26 using SiegeOnlineServer.Protocol; 27 using SiegeOnlineServer.Protocol.Common.Character; 28 using SiegeOnlineServer.Protocol.Common.User; 29 30 namespace SiegeOnlineServer.ServerLogic 31 { 32 /// <summary> 33 /// 类型:类 34 /// 名称:Login 35 /// 作者:taixihuase 36 /// 作用:响应登录请求 37 /// 编写日期:2015/7/14 38 /// </summary> 39 public class Login 40 { 41 /// <summary> 42 /// 类型:方法 43 /// 名称:OnRequest 44 /// 作者:taixihuase 45 /// 作用:当收到请求时,进行处理 46 /// 编写日期:2015/7/14 47 /// </summary> 48 /// <param name="operationRequest"></param> 49 /// <param name="sendParameters"></param> 50 /// <param name="peer"></param> 51 public static void OnRequest(OperationRequest operationRequest, SendParameters sendParameters, ServerPeer peer) 52 { 53 TryLogin(operationRequest, sendParameters, peer); 54 } 55 56 /// <summary> 57 /// 类型:方法 58 /// 名称:TryLogin 59 /// 作者:taixihuase 60 /// 作用:通过登录数据尝试登录 61 /// 编写日期:2015/7/14 62 /// </summary> 63 /// <param name="operationRequest"></param> 64 /// <param name="sendParameters"></param> 65 /// <param name="peer"></param> 66 private static void TryLogin(OperationRequest operationRequest, SendParameters sendParameters, ServerPeer peer) 67 { 68 ServerPeer.Log.Debug("Logining..."); 69 70 LoginInfo login = (LoginInfo) 71 Serialization.Deserialize(operationRequest.Parameters[(byte) ParameterCode.Login]); 72 73 #region 对账号密码进行判断 74 75 ServerPeer.Log.Debug(DateTime.Now + " : Loginning..."); 76 ServerPeer.Log.Debug(login.Account); 77 ServerPeer.Log.Debug(login.Password); 78 79 // 获取用户资料 80 UserBase user = new UserBase(peer.PeerGuid, login.Account); 81 UserCollection.UserReturn userReturn = peer.Server.Users.UserOnline(ref user, login.Password); 82 83 // 若成功取得用户资料 84 if (userReturn.ReturnCode == (byte) UserCollection.UserReturn.ReturnCodeTypes.Success) 85 { 86 ServerPeer.Log.Debug(user.LoginTime + " :User " + user.Nickname + " loginning..."); 87 88 // 用于选择的数据返回参数 89 var parameter = new Dictionary<byte, object>(); 90 91 // 用于选择的字符串信息 92 string message = ""; 93 94 // 用于选择的返回值 95 short returnCode = -1; 96 97 #region 获取角色资料 98 99 Character character = new Character(user); 100 PlayerCollection.CharacterReturn characterReturn = 101 peer.Server.Players.SearchCharacter(ref character); 102 103 // 若取得角色资料 104 if (characterReturn.ReturnCode == (byte) PlayerCollection.CharacterReturn.ReturnCodeTypes.Success) 105 { 106 byte[] playerBytes = Serialization.Serialize(character); 107 parameter.Add((byte) ParameterCode.Login, playerBytes); 108 returnCode = (short) ErrorCode.Ok; 109 message = ""; 110 111 ServerPeer.Log.Debug(character.Occupation.Name); 112 } 113 else if (characterReturn.ReturnCode == 114 (byte) PlayerCollection.CharacterReturn.ReturnCodeTypes.CharacterNotFound) 115 { 116 byte[] userBytes = Serialization.Serialize(user); 117 parameter.Add((byte) ParameterCode.Login, userBytes); 118 returnCode = (short) ErrorCode.CharacterNotFound; 119 message = characterReturn.DebugMessage.ToString(); 120 } 121 122 #endregion 123 124 OperationResponse response = new OperationResponse((byte) OperationCode.Login, parameter) 125 { 126 ReturnCode = returnCode, 127 DebugMessage = message 128 }; 129 peer.SendOperationResponse(response, sendParameters); 130 ServerPeer.Log.Debug(user.LoginTime + " : User " + user.Account + " logins successfully"); 131 } 132 // 若重复登录 133 else if (userReturn.ReturnCode == (byte) UserCollection.UserReturn.ReturnCodeTypes.RepeatedLogin) 134 { 135 OperationResponse response = new OperationResponse((byte) OperationCode.Login) 136 { 137 ReturnCode = (short) ErrorCode.RepeatedOperation, 138 DebugMessage = "账号已登录!" 139 }; 140 peer.SendOperationResponse(response, sendParameters); 141 ServerPeer.Log.Debug(DateTime.Now + " : Failed to login " + user.Account + " Because of " + 142 Enum.GetName(typeof (UserCollection.UserReturn.ReturnCodeTypes), 143 userReturn.ReturnCode)); 144 } 145 else 146 { 147 // 返回非法登录错误 148 OperationResponse response = new OperationResponse((byte) OperationCode.Login) 149 { 150 ReturnCode = (short) ErrorCode.InvalidOperation, 151 DebugMessage = userReturn.DebugMessage.ToString() 152 }; 153 peer.SendOperationResponse(response, sendParameters); 154 ServerPeer.Log.Debug(DateTime.Now + " : Failed to login " + user.Account + " Because of " + 155 Enum.GetName(typeof (UserCollection.UserReturn.ReturnCodeTypes), 156 userReturn.ReturnCode)); 157 } 158 } 159 160 #endregion 161 } 162 }
TryLogin将OperationRequest中的数据反序列化后取出,然后通过ServerPeer对象作为中介,与ServerApplication关联,从而可以通过Application里的数据库关联来获取账号、角色信息等,并将结果和数据返回到Login中。之后需要将结果发送回给客户端接收,ServerPeer通过继承后,包含有一个SendOperationResponse方法,这是为什么需要传给OnRequest和TryLogin方法第三个参数的原因,在这里便能直接调用了。SendOperationResponse方法需要一个OperationResponse类型的对象和一个SendParameters类型的对象,后者一般填入层层调用传过来的那个sendParameters或者自己new一个即可。此处的重点是OperationResponse。
客户端发送Request请求,服务端接收请求并处理后,就要给客户端答应,这就是Response。OperationResponse跟OperationRequest长得相似,同样带一个操作码参数和一个字典,操作码跟Request一般填一样,这样客户端接收后便能知道是发送了什么请求后得到的答应。字典类型的Parameters也是以一个参数类型码为键,以object对象为值,这里的值同样用二进制数据,调用Serialization的Serialize方法即可。此外,还有一个short型的ReturnCode字段,该字段填入请求处理的情况码,即操作正确或者某些不正确操作的错误码,这里封进枚举里表示。最后需要填的是一个DebugMessage字符串,我们可以填入操作信息,支持中文,这样在客户端测试时便能打印出来,对整个操作的执行情况一目了然。如果Response不需要回给客户端数据,则可以省略掉Parameters,但其他的还是要填。
服务端除了给发送请求的客户端进行回应外,还能对其他客户端进行广播。这里用角色进入场景时,服务端给当前正在进行游戏的所有客户端连接发送某个玩家上线的提示信息为例。
1 //----------------------------------------------------------------------------------------------------------- 2 // Copyright (C) 2015-2016 SiegeOnline 3 // 版权所有 4 // 5 // 文件名:WorldEnter.cs 6 // 7 // 文件功能描述: 8 // 9 // 进入游戏场景,响应客户端进入场景请求 10 // 11 // 创建标识:taixihuase 20150722 12 // 13 // 修改标识: 14 // 修改描述: 15 // 16 // 17 // 修改标识: 18 // 修改描述: 19 // 20 //----------------------------------------------------------------------------------------------------------- 21 22 using System.Collections.Generic; 23 using Photon.SocketServer; 24 using SiegeOnlineServer.Protocol; 25 using SiegeOnlineServer.Protocol.Common.Character; 26 27 namespace SiegeOnlineServer.ServerLogic 28 { 29 /// <summary> 30 /// 类型:类 31 /// 名称:WorldEnter 32 /// 作者:taixihuase 33 /// 作用:响应进入场景请求 34 /// 编写日期:2015/7/22 35 /// </summary> 36 public class WorldEnter 37 { 38 /// <summary> 39 /// 类型:方法 40 /// 名称:OnRequest 41 /// 作者:taixihuase 42 /// 作用:当收到请求时,进行处理 43 /// 编写日期:2015/7/22 44 /// </summary> 45 /// <param name="operationRequest"></param> 46 /// <param name="sendParameters"></param> 47 /// <param name="peer"></param> 48 public static void OnRequest(OperationRequest operationRequest, SendParameters sendParameters, ServerPeer peer) 49 { 50 TryEnter(operationRequest, sendParameters, peer); 51 } 52 53 /// <summary> 54 /// 类型:方法 55 /// 名称:TryEnter 56 /// 作者:taixihuase 57 /// 作用:通过角色数据尝试进入场景 58 /// 编写日期:2015/7/22 59 /// </summary> 60 /// <param name="operationRequest"></param> 61 /// <param name="sendParameters"></param> 62 /// <param name="peer"></param> 63 private static void TryEnter(OperationRequest operationRequest, SendParameters sendParameters, ServerPeer peer) 64 { 65 ServerPeer.Log.Debug("Entering"); 66 67 Character character = (Character) 68 Serialization.Deserialize(operationRequest.Parameters[(byte) ParameterCode.WorldEnter]); 69 70 peer.Server.Players.CharacterEnter(ref character); 71 peer.Server.Data.CharacterData.GetCharacterPositionFromDatabase(ref character); 72 73 // 返回数据给客户端 74 75 byte[] data = Serialization.Serialize(character); 76 77 var reponseData = new OperationResponse((byte) OperationCode.WorldEnter, new Dictionary<byte, object> 78 { 79 {(byte) ParameterCode.WorldEnter, data} 80 }); 81 peer.SendOperationResponse(reponseData, sendParameters); 82 83 var eventData = new EventData((byte)EventCode.WorldEnter, new Dictionary<byte, object> 84 { 85 {(byte) ParameterCode.WorldEnter, data} 86 }); 87 eventData.SendTo(peer.Server.Players.GamingClients, sendParameters); 88 } 89 } 90 }
WorldEnter文件对这一操作进行处理,主要逻辑在于TryEnter中。服务端试图获取角色数据,然后通过SendOperationResponse返回给客户端,并且实例化一个EventData对象,该类型需要填入一个byte类型的事件代码,其实跟操作码相似,然后是一个字典类型的对象,传入给接收广播的客户端的所需数据。EventData有一个SendTo方法,第一个参数代表着要广播的客户端集合,此处可以用一个List<ServerPeer>类型的对象表示,该方法会自动遍历每一个Peer,第二个参数没特殊要求的话,照填sendParameters即可。这样一旦某个角色进入了游戏主场景,则所有在线玩家都会接收到提示。
服务端主体框架就这些,其余内容还待详细设计。
Photon Server有个英文的在线文档,更多的用法可以参照以下网址:
http://doc-api.exitgames.com/en/onpremise/current/server/doc/index.html
标签:
原文地址:http://www.cnblogs.com/SiegeOL/p/4690954.html