深入淺出SagaECO RPC篇(三) “解耦封包”
以下討論基於SagaECO svn 900版本
注意: 被分發SagaECO源代碼並不代表你被授權使用、修改、發布衍生的程式
源代碼可以從這裡下載(約svn 400版本)
換言之SagaECO團隊仍然保留一切權利(雖然SagaECO團隊早就消失了…
上文我們已經處理好輸出數據的格式和流程了,我們還有接收封包(Packet)的部分未解說呢。時間關係,讓我們一次過掀開完整的流程。
上一篇說的從Packet類的虛擬方法三巨頭
近乎沒有用到、忽略public virtual bool SizeIsOk
public virtual Packet New()
public virtual void Parse(Client client)
// SagaECO/SagaLib/ClientMananger.cs | |
// 部分省略 | |
namespace SagaLib | |
{ | |
public class ClientManager | |
{ | |
// ... | |
/// <summary> | |
/// Command table contains the commands that need to be called when a | |
/// packet is received. Key will be the packet type | |
/// </summary> | |
public Dictionary<ushort, Packet> commandTable; | |
} | |
} | |
// SagaECO/SagaMap/Manager/MapClientManager.cs | |
// 部分省略 | |
namespace SagaMap.Manager | |
{ | |
public sealed class MapClientManager : ClientManager | |
{ | |
List<MapClient> clients; | |
MapClientManager() | |
{ | |
clients = new List<MapClient>(); | |
commandTable = new Dictionary<ushort, Packet>(); | |
commandTable.Add(0x000A, new CSMG_SEND_VERSION()); | |
commandTable.Add(0x0010, new CSMG_LOGIN()); | |
commandTable.Add(0x001F, new CSMG_LOGOUT()); | |
commandTable.Add(0x0032, new CSMG_PING()); | |
// ... | |
} | |
} | |
} | |
namespace SagaMap.Network.Client | |
{ | |
public partial class MapClient : SagaLib.Client | |
{ | |
// ... | |
public MapClient(Socket mSock, Dictionary<ushort, Packet> mCommandTable) | |
{ | |
this.netIO = new NetIO(mSock, mCommandTable, this, MapClientManager.Instance); | |
// ... | |
} | |
} | |
} | |
namespace SagaLib | |
{ | |
public class NetIO | |
{ | |
/// <summary> | |
/// Command table contains the commands that need to be called when a | |
/// packet is received. Key will be the packet type | |
/// </summary> | |
private Dictionary<ushort, Packet> commandTable; | |
/// <summary> | |
/// Create a new netIO class using a given socket. | |
/// </summary> | |
/// <param name="sock">The socket for this netIO class.</param> | |
public NetIO(Socket sock, Dictionary<ushort, Packet> commandTable, Client client ,ClientManager manager) | |
{ | |
this.commandTable = commandTable; | |
// ... | |
} | |
// 旅程的終點 | |
private void ProcessPacket(Packet p) | |
{ | |
// 如果有這個ID的封包結構就嘗試產生相應類別的封包 | |
if (commandTable.ContainsKey(p.ID)) | |
{ | |
// 工廠模式 | |
Packet p1 = commandTable[p.ID].New(); | |
p1.data = p.data; | |
p1.size = (ushort)(p.data.Length); | |
ClientManager.EnterCriticalArea(); | |
try | |
{ | |
// 注意此行 | |
p1.Parse(this.client); | |
} | |
catch (Exception ex) | |
{ | |
Logger.ShowError(ex); | |
} | |
ClientManager.LeaveCriticalArea(); | |
} | |
// 沒有的話則嘗試創建"萬能"封包處理 | |
else | |
{ | |
if (commandTable.ContainsKey(0xFFFF)) | |
// 邏輯一樣, 為免影響觀感, 去掉~ | |
// ... | |
} | |
} | |
} | |
} |
一路追到最盡頭,發現使用了的是工廠模式。寫代碼的時候很可能還沒有出現lambda,加上工廠模式角色不明朗,先進行抽離不然很多人直接翻桌子(包括筆者)。
public interface IPacketFactory | |
{ | |
public Packet CreatePacket(Packet p); | |
// Less is More 我們不用關心工廠內部是怎樣的 | |
} | |
public class MapPacketFactory: IPacketFactory | |
{ | |
// 私有化, 改為儲存各種封包類型的工廠 | |
private Dictionary<ushort, Func<Packet>> = new Dictionary<ushort, Func<Packet>>(); | |
PacketFactory() | |
{ | |
// 使用Lambda | |
commandTable.Add(0x000A, () => return new CSMG_SEND_VERSION()); | |
commandTable.Add(0x0010, () => return new CSMG_LOGIN()); | |
commandTable.Add(0x001F, () => return new CSMG_LOGOUT()); | |
commandTable.Add(0x0032, () => return new CSMG_PING()); | |
// 如此類推 | |
} | |
public Packet CreatePacket(Packet p) | |
{ | |
// 如果有這個ID的封包結構就嘗試產生相應類別的封包 | |
if (commandTable.ContainsKey(p.ID)) | |
{ | |
// 直接調用工廠 | |
Packet p1 = commandTable[p.ID](); | |
p1.data = p.data; | |
p1.size = (ushort)(p.data.Length); | |
return p1; | |
} | |
else if (commandTable.ContainsKey(0xFFFF)) | |
{ | |
// 邏輯一樣, 為免影響觀感, 去掉~ | |
// ... | |
} | |
else | |
// 也只能報錯或者回傳null了 | |
} | |
} | |
namespace SagaLib | |
{ | |
public class NetIO | |
{ | |
// ... | |
private IPacketFactory packetFactory; | |
public NetIO(Socket sock, IPacketFactory packetFactory, Client client ,ClientManager manager) | |
{ | |
this.packetFactory = packetFactory; | |
// ... | |
} | |
private void ProcessPacket(Packet p) | |
{ | |
// 無情地玩壞血汗工廠就好了 | |
Packet p1 = packetFactory.CreatePacket(p); | |
ClientManager.EnterCriticalArea(); | |
try | |
{ | |
// 注意此行 | |
p1.Parse(this.client); | |
} | |
catch (Exception ex) | |
{ | |
Logger.ShowError(ex); | |
} | |
ClientManager.LeaveCriticalArea(); | |
} | |
} | |
} |
這下總算是釐清了抽象工廠的角色。
public virtual bool SizeIsOk
被lambda工廠搞到失業public virtual Packet New()
public virtual void Parse(Client client)
最後剩神秘的Parse。先回到一個來自客戶端的封包類別看看。
// SagaECO/SagaMap/Packets/Client/Chat/CSMG_CHAT_PUBLIC.cs | |
namespace SagaMap.Packets.Client | |
{ | |
public class CSMG_CHAT_PUBLIC : Packet | |
{ | |
public CSMG_CHAT_PUBLIC() | |
{ | |
this.offset = 2; | |
} | |
public string Content | |
{ | |
get | |
{ | |
byte size = this.GetByte(2); | |
// 順帶一提這樣做是因為最後一個byte是null terminator | |
size--; | |
return Global.Unicode.GetString(this.GetBytes(size, 3)); | |
} | |
} | |
// Deprecated 已經不需要了 | |
public override SagaLib.Packet New() | |
{ | |
return (SagaLib.Packet)new SagaMap.Packets.Client.CSMG_CHAT_PUBLIC(); | |
} | |
// 重點 | |
public override void Parse(SagaLib.Client client) | |
{ | |
((MapClient)(client)).OnChat(this); | |
} | |
} | |
} |
作為區區一個DTO,居然還扮演著橋樑角色,作為底層物件被傳入高層物件這樣的的思想實在是太超前了XD。重點是,明明MapClient也有剛才的commandTable啊。最起碼,最起碼,該是這樣:
// SagaECO/SagaMap/Network/Client/MapClient.Chat.cs | |
public partial class MapClient | |
{ | |
public void OnChat(CSMG_CHAT_PUBLIC p) | |
{ | |
// ... | |
ChatArg arg = new ChatArg(); | |
arg.content = p.Content; | |
// 不得不吐糟一下這方法名有夠長 34個英文字也太verbose了 | |
// 但是不得不說 我想了好久也沒有想到更好的名字 | |
// 例如 SendEventToVisibleActors 就不符合原意 | |
// 原意是指向"所有感知到此人存在的人"發送事件而不是向"所有可以被感知到存在的人"發送事件 | |
// 有機會的話會詳細解釋這個東西 | |
Map.SendEventToAllActorsWhoCanSeeActor(Map.EVENT_TYPE.CHAT, arg, this.Character, true); | |
} | |
// 看出來了嗎? OnPacket全是void並且只接受一個Packet參數 | |
public void OnMotion(CSMG_CHAT_MOTION p) | |
{ | |
// 以後有機會再詳述這為所欲為的東東 | |
ChatArg arg = new ChatArg(); | |
arg.motion = p.Motion; | |
arg.loop = p.Loop; | |
Map.SendEventToAllActorsWhoCanSeeActor(Map.EVENT_TYPE.MOTION, arg, this.Character, true); | |
} | |
} | |
public partial class MapClient : SagaLib.Client | |
{ | |
public Dictionary<ushort, Action<Packet>> packetHandlers; | |
public MapClient(Socket mSock, IPacketFactory packetFactory) | |
{ | |
this.netIO = new NetIO(mSock, packetFactory, this, MapClientManager.Instance); | |
// ... | |
packetHandlers.Add(0x03E8, (Packet p) => OnChat((CSMG_CHAT_PUBLIC)p)); | |
// 省略... | |
} | |
public override void HandlePacket(Packet p) | |
{ | |
if (packetHandlers.ContainsKey(p.ID)) | |
{ | |
packetHandlers[p.ID](p); | |
} | |
} | |
} | |
// 使用abstract關鍵字 | |
public abstract class Client | |
{ | |
public NetIO netIO; | |
public uint SessionID; | |
// 新增 | |
public abstract void HandlePacket(Packet p); | |
} | |
public class NetIO | |
{ | |
// ... | |
private IPacketFactory packetFactory; | |
public NetIO(Socket sock, IPacketFactory packetFactory, Client client ,ClientManager manager) | |
{ | |
this.packetFactory = packetFactory; | |
// ... | |
} | |
private void ProcessPacket(Packet p) | |
{ | |
// 無情地玩壞血汗工廠就好了 | |
Packet p1 = packetFactory.CreatePacket(p); | |
ClientManager.EnterCriticalArea(); | |
try | |
{ | |
// 交由client處理packet | |
this.client.HandlePacket(p1); | |
} | |
catch (Exception ex) | |
{ | |
Logger.ShowError(ex); | |
} | |
ClientManager.LeaveCriticalArea(); | |
} | |
} |

伺服器發送03E9對話訊息時消息泡泡才會成立
日服於2017年8月31日終止營運,自此 ECO只剩私服
取自断ジニ@fc2
public virtual bool SizeIsOk
public virtual Packet New()
解耦完成,NetIO跟MapClient之間的事本來就沒有插手的餘地public virtual void Parse(Client client)
解除束縛之後接收方的封包可以與上一篇的進度接軌了。
// IReadablePacket.cs | |
public interface IReadablePacket | |
{ | |
public void Deserialize(PacketStream stream); | |
} | |
// CSMG_CHAT_PUBLIC.cs | |
namespace SagaMap.Packets.Client | |
{ | |
public class CSMG_CHAT_PUBLIC : IReadablePacket | |
{ | |
public string Content { get; private set; } | |
public void Deserialize(PacketStream stream) | |
{ | |
Content = stream.ReadString(); | |
} | |
} | |
} |
接收方和發送方不對稱乃正常現象,必然地我們要先取得ID才有之後的故事,所以 接收進來的封包天然地不用包括ID。至於應否在發送方封包那邊寫ID,其實也是沒有必要的(才不是偷懶稍後再動它),最少IWritablePacket完全可以去掉ID這項。
這篇在止結束。下篇再見嚕~
後記: 竊以為SagaECO之所以面對嚴重技術負債問題,除了受早期語法等客觀限制之外,無論是早已離開團隊的核心開發者還是接手的維護者也好,未曾把代碼解讀過予他人。如果有這樣做的話,根基不會在不知不覺間歪了一個十年。
透過這裡寫的每一篇文章,解釋給他人的過程順道梳理了代碼,讓每一句代碼安詳地躺在它應該在的地方,詳和地微笑。多年後的我現在再次掀開這份古老的書本, 記憶昇華
請問up主,留有ecore和se-a的封包紀錄嗎,看完你的文章後很想要自己動手試試看,但是網站se-a進不去了 也找不到ecore的2013版本,要是能提供的話真的非常感謝