《攻城Online》快速原型:服務端設計

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

相關文章
相關標籤/搜索