Loading [MathJax]/extensions/tex2jax.js

深入淺出SagaECO RPC篇(三) “解耦封包”

以下討論基於SagaECO svn 900版本
源代碼可以從這裡下載(約svn 400版本)

注意: 被分發SagaECO源代碼並不代表你被授權使用、修改、發布衍生的程式
換言之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))
// 邏輯一樣, 為免影響觀感, 去掉~
// ...
}
}
}
}
view raw 1.cs hosted with ❤ by GitHub

一路追到最盡頭,發現使用了的是工廠模式。寫代碼的時候很可能還沒有出現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
public virtual Packet New() 被lambda工廠搞到失業
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();
}
}
遊戲客戶端把03E8對話訊息發送給伺服器
伺服器發送03E9對話訊息時消息泡泡才會成立

日服於2017年8月31日終止營運,自此 ECO只剩私服
取自断ジニ@fc2

public virtual bool SizeIsOk
public virtual Packet New()
public virtual void Parse(Client client) 解耦完成,NetIO跟MapClient之間的事本來就沒有插手的餘地

解除束縛之後接收方的封包可以與上一篇的進度接軌了。

// 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之所以面對嚴重技術負債問題,除了受早期語法等客觀限制之外,無論是早已離開團隊的核心開發者還是接手的維護者也好,未曾把代碼解讀過予他人。如果有這樣做的話,根基不會在不知不覺間歪了一個十年。
透過這裡寫的每一篇文章,解釋給他人的過程順道梳理了代碼,讓每一句代碼安詳地躺在它應該在的地方,詳和地微笑。多年後的我現在再次掀開這份古老的書本, 記憶昇華

1

One Response to “深入淺出SagaECO RPC篇(三) “解耦封包”

  • 請問up主,留有ecore和se-a的封包紀錄嗎,看完你的文章後很想要自己動手試試看,但是網站se-a進不去了 也找不到ecore的2013版本,要是能提供的話真的非常感謝

Leave a Reply

Your email address will not be published. Required fields are marked *