3.2、強大的MongoBson庫(ETBook)

後端開發,統計了一下大概有這些場景需要用到序列化:

  1. 對象通過序列化反序列化clone
  2. 服務端數據庫存儲數據,二進制
  3. 分佈式服務端,多進程間的消息,二進制
  4. 後端日誌,文本格式
  5. 服務端的各種配置文件,文本格式

C#序列化庫有非常非常多了,protobuf,json等等。但是這些序列化庫都無法應當所有場景,既要可讀又要小。protobuf不支持複雜的對象結構(無法使用繼承),做消息合適,做數據庫存儲和日誌格式並不好用。json做日誌格式合適,但是做網絡消息和數據存儲就太大。我們當然希望一個庫能滿足上面所有場景,理由如下:

  1. 你想想某天你的配置文件需要放到數據庫中保存,你不需要進行格式轉換,後端直接把前端發過來的配置消息保存到數據庫中,這是不是能減少非常多錯誤呢?
  2. 某天有些服務端的配置文件不用文件格式了,需要放在數據庫中,同樣,只需要幾行代碼就可以完成遷移。
  3. 某天后端服務器crash,你需要掃描日誌進行數據恢復,把日誌進行反序列化成C#對象,一條條進行處理,再轉成對象保存到數據庫就完成了。
  4. 對象保存在數據庫,直接就可以看到文本內容,可以做各種類sql的操作
  5. 想像一個場景,一個配置文本對象,反序列化到內存,通過網絡消息發送,存儲到數據庫中。整個過程一氣呵成。

簡單來說就是減少各種數據轉換,減少代碼,提高開發效率,提高可維護性。當然,Mongo Bson就能夠滿足。MongoDB庫既可以序列化成文本也可以序列化成BSON的二進制格式,並且MongoDB本身就是一個遊戲中使用非常多的數據庫。Mongo Bson非常完善,是我見過功能最全使用最強大的序列化庫,有些功能十分貼心。其支持功能如下:

  1. 支持複雜的繼承結構
  2. 支持忽略某些字段序列化
  3. 支持字段默認值
  4. 結構多出多餘的字段照樣可以反序列化,這對多版本協議非常有用
  5. 支持ISupportInitialize接口使用,這個在反序列化的時候簡直就是神器
  6. 支持文本json和二進制bson序列化
  7. MongoDB數據庫支持

簡單的介紹下mongo bson庫

1.支持序列化反序列化成json或者bson

    public sealed class Player
    {
        public long Id;

        public string Account { get; private set; }

        public long UnitId { get; set; }
    }

    Player player1 = new Player() { Id = 1 };
    string json = player1.ToJson();
    Console.WriteLine($"player1 to json: {json}");
    Console.WriteLine($"player to bson: {player.ToBson().ToHex()}");
    // output:
    // player to json: { "_id" : NumberLong(1), "C" : [], "Account" : null, "UnitId" : NumberLong(0) }
    // player to bson: B000000125F69640001000000000000000A4163636F756E740012556E6974496400000000000000000000

注意mongo的json跟標準的json有點區別,如果想用標準的json,可以傳入一個JsonWriterSettings對象,限制使用JsonOutputMode.Strict模式

    // 使用標準json
    Player player2 = new Player() { Id = 1 };
    Console.WriteLine($"player to json: {player2.ToJson(new JsonWriterSettings() {OutputMode = JsonOutputMode.Strict})}");
    // player to json: { "_id" : 1, "C" : [], "Account" : null, "UnitId" : 0 }

反序列化json:

            // 反序列化json
        Player player11 = BsonSerializer.Deserialize<Player>(json);
        Console.WriteLine($"player11 to json: {player11.ToJson()}");

反序列化bson:

    // 反序列化bson
    using (MemoryStream memoryStream = new MemoryStream(bson))
    {
        Player player12 = (Player) BsonSerializer.Deserialize(memoryStream, typeof (Player));
        Console.WriteLine($"player12 to json: {player12.ToJson()}");
    }

2.可以忽略某些字段

[BsonIgnore]該標籤用來禁止字段序列化。

	public sealed class Player
	{
        public long Id;

		[BsonIgnore]
		public string Account { get; private set; }
		
		public long UnitId { get; set; }
    }

    Player player = new Player() { Id = 2, UnitId = 3, Account = "panda"};
	Console.WriteLine($"player to json: {player.ToJson()}");
    // player to json: { "_id" : 2, "UnitId" : 3 }

3.支持默認值以及取別名

[BsonElement] 字段加上該標籤,即使是private字段也會序列化(默認只序列化public字段),該標籤還可以帶一個string參數,給字段序列化指定別名。

	public sealed class Player
	{
        public long Id;

		public string Account { get; private set; }

		[BsonElement("UId")]
		public long UnitId { get; set; }
    }
    Player player = new Player() { Id = 2, UnitId = 3, Account = "panda"};
	Console.WriteLine($"player to json: {player.ToJson()}");
    // player to json: { "_id" : 2, "Account" : "panda", "UId" : 3 }

4.升級版本支持

[BsonIgnoreExtraElements] 該標籤用在class上面,反序列化時用來忽略多餘的字段,一般版本兼容需要考慮,低版本的協議需要能夠反 序列化高版本的內容,否則新版本加了字段,舊版本結構反序列化會出錯

	[BsonIgnoreExtraElements]
	public sealed class Player
	{
        public long Id;

		public string Account { get; private set; }

		[BsonElement("UId")]
		public long UnitId { get; set; }
    }

5.支持複雜的繼承結構

mongo bson庫強大的地方在於完全支持序列化反序列化繼承結構。需要注意的是,繼承反序列化需要註冊所有的父類,有兩種方法: a. 你可以在父類上面使用[BsonKnownTypes]標籤聲明繼承的子類,這樣mongo會自動註冊,例如:

    [BsonKnownTypes(typeof(Entity))]
    public class Component
    {
    }
    [BsonKnownTypes(typeof(Player))]
    public class Entity: Component
    {
    }
    public sealed class Player: Entity
    {
        public long Id;
        
        public string Account { get; set; }
		
        public long UnitId { get; set; }
    }

這樣有缺陷,因爲框架並不知道一個類會有哪些子類,這樣做對框架代碼有侵入性,我們希望能解除這個耦合 。可以掃描程序集中所有子類父類的類型,將他們註冊到mongo驅動中

			Type[] types = typeof(Game).Assembly.GetTypes();
			foreach (Type type in types)
			{
				if (!type.IsSubclassOf(typeof(Component)))
				{
					continue;
				}

				BsonClassMap.LookupClassMap(type);
			}

			BsonSerializer.RegisterSerializer(new EnumSerializer<NumericType>(BsonType.String));

這樣完全的自動化註冊,使用者也不需要關係類是否註冊。

6.ISupportInitialize接口

mongo bson反序列化時支持一個ISupportInitialize接口,ISupportInitialize有兩個方法

    public interface ISupportInitialize
    {
        void BeginInit();
        void EndInit();
    }

BeginInit在反序列化前調用,EndInit在反序列化後調用。這個接口非常有用了,可以在反序列化後執行一些操作。例如

	[BsonIgnoreExtraElements]
	public class InnerConfig: AConfigComponent
	{
		[BsonIgnore]
		public IPEndPoint IPEndPoint { get; private set; }
		
		public string Address { get; set; }

		public override void EndInit()
		{
			this.IPEndPoint = NetworkHelper.ToIPEndPoint(this.Address);
		}
	}

InnerConfig是ET中進程內網地址的配置,由於IPEndPoint不太好配置,我們可以配置成string形式,然後反序列化的時候在EndInit中把string轉換成IPEndPoint。 同樣我給protobuf反序列化方法也加上了這個調用,參考ProtobufHelper.cs,ET的protobuf因爲要支持ilruntime,所以去掉了map的支持,假如我們想要一個map怎麼辦呢?這裏我給生成的代碼都做了手腳,把proto消息都改成了partial class,這樣我們可以自己擴展這個class,比如:

message UnitInfo
{
	int64 UnitId  = 1;

	float X = 2;
	float Y = 3;
	float Z = 4;
}

// protobuf
message G2C_EnterMap // IResponse
{
	int32 RpcId = 90;
	int32 Error = 91;
	string Message = 92;
	// 自己的unit id
	int64 UnitId = 1;
	// 所有的unit
	repeated UnitInfo Units = 2;
}

這個網絡消息有個repeated UnitInfo字段,在protobuf中其實是個數組,使用起來不是很方便,我希望轉成一個Dictionary<Int64, UnitInfo>的字段,我們可以做這樣的操作:

    public partial class G2C_EnterMap: ISupportInitialize
    {
        public Dictionary<Int64, UnitInfo> unitsDict = new Dictionary<long, UnitInfo>();
        
        public void BeginInit()
        {
        }

        public void EndInit()
        {
            foreach (var unit in this.Units)
            {
                this.unitsDict.Add(unit.UnitId, unit);
            }
        }
    }

通過這樣一段代碼把消息進行擴展一下,反序列化出來之後,自動轉成了一個Dictionary。

ET原址:https://github.com/egametang/ET

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章