「攻城」服務端採用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