“攻城”服务端采用Photon引擎的框架,其主要逻辑如如下UML所示。html
服务端的启动入口为ServerApplication,该类包含着相关的Collection数据集合,而Collection内又有与数据库文件夹Database关联的文件。两个文件夹的内容如图。数据库
简单来讲,ServerApplication内缓存着各种数据,并完成与数据库等的关联。而本篇的重点是ServerPeer这个类。下面介绍什么是Peer。api
每当一个客户端链接到服务端时,服务端会自动生成一个客户端链接实例,称其为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会调用特定类的一个静态方法,这样即可不须要实例化这些类型的对象后再使用。例如:ide
1 // 帐号登录 2 case (byte) OperationCode.Login: 3 Login.OnRequest(operationRequest, sendParameters, this); 4 break;
当识别为Login操做后,则调用Login里的OnRequest方法,并把参数原封不动传过去,在另外的文件里进行处理,这样子方便操做。同时OnRequest方法还要求第三个参数,为ServerPeer类型的对象,这样经过把this传过去,便能在其余地方引用到该Peer,而Peer又存放着ServerApplication的引用,方法调用及数据传输便畅通无阻。须要注意的是,无论操做码是什么,都是直接调用相应的OnRequest方法,如上面的代码所示。测试
接下来是对请求的处理逻辑。ui
当前实现了对三个不一样请求的处理,在此用Login操做讲解。每一个逻辑处理文件都包含OnRequest方法,除此以外,还有一个以“Try”开头命名的方法,该方法参数与OnRequest相同,即OnRequest再次将数据传给Try方法,而Try方法则真正进行处理,完成后将回应发生请求的客户端,或者向特定客户端发送广播。this
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。spa
客户端发送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便可。这样一旦某个角色进入了游戏主场景,则全部在线玩家都会接收到提示。
下图是服务端原型的组织结构。
最下端的Protocol项目为协议内容,由客户端和服务端共用,会在后面文章详细介绍。
服务端主体框架就这些,其他内容还待详细设计。
Photon Server有个英文的在线文档,更多的用法能够参照如下网址:
http://doc-api.exitgames.com/en/onpremise/current/server/doc/index.html