[开源]KJFramework.Message 高性能二进制消息框架 - 多元素数组的高性能优化

框架的介绍: 


1. 已经完成了对于消息内部类型的支持(int, short, long, uint, ushort, ulong, bool ,float, double, byte, sbyte, byte[], decimal, DateTime, IntPtr, Guid)
2. 支持内部序列化元数据(.NET可序列化的对象)
3. 对于智能对象内部的“智能对象”提供支持。 支持迭代元数据转换。


[说明:]
KJFramework框架中内置了一套,可以将网络传输实体自动转换为二进制元数据的能力框架。
使用此框架,将使得用户不在关心底层传输时对于二进制编码/解码的繁琐过程,解放程序员的脑力劳动。
目前此内置框架被初步定义在命名空间:KJFramework.Messages内,拥有独立Assembly.
对于智能二进制消息框架, 内部提供了对于诸多类型的支持,甚至可以用于在不编写序列化代码的情况下,
将一个网络传输实体转换为二进制操作。
这一切的操作,都全部由内部框架完成,通过简单的属性标签,就可以实现很强大的转换功能。
目前智能二进制消息框架中,仍保留了内部扩展能力,日后扩展后,将会通过配置文件来增强配置能力。

 

新优化

1. 我们不难发现,在数据的传输中,如果在特定的场景下,对于数组的元素数量需求很高,那这将是一个很大的性能瓶颈。
   经过测试,我们定义:数组元素个数10,为性能瓶颈的边界值,如果一个数组元素超过此数量则会产生非常大的性能瓶颈。
   所以,就在现在,我们特地为常用的数组类型增加了对于超大量数组元素个数的高性能支持,这是一个令人兴奋的优化点。
   此次优化所涉及的数组元素类型有: int[], short[], float[], double[], byte[]
2. 除此之外,我们还发现,原来框架中对于byte[]类型的序列化/反序列化并没有提供很好的高性能支持,本次更新中,我们特地
   提供了对于这个类型的高性能序列化/反序列化实现。
3. 为本次的更新提供了相关的单元测试

 

新能力

1. 新增轻量级序列化/反序列化帮助器  

   (1). 增加了DataHelper, 这是一个轻量级的序列化/反序列化帮助器
         哦,是的,框架中除了需要为智能对象提供序列化/反序列化之外,我们的程序中
         还会经常涉及到一些其余数据类型的二进制数据操作,在以往,我们不得不单独为
         这些类型做数据处理, 现在,这一切都会变得容易起来,那就是我们提供了轻量级的
         DataHelper! 好吧,当你的工作中涉及到int, long, array,等等小数据类型的二进制操作时,
         就来尝试一下吧。

2. 新增对于.NET可空值类型的支持:


     (1). 这是一个令人振奋人心的消息,不是吗? 当我们定制协议的时候,再也不会为了某些值类型的协议字段是否需要下发而苦恼了!现在,一切都变得更加美好,如果我们在定制自定义实体的时候,期望某些值类型字段在某些场景下不需要参与序列化,以省去
消息序列化的大小,那么,尝试把这些字段设置为可空类型吧(比如 int?,double?,short?......等等).
     (2). 通过对于可空类型序列化/反序列化成本的评估,我们发现,为当前框架加入此能力,并不会明显的拖慢原有框架性能,
只会在原有速度上平均慢去 60~100ms+(量级序列化/反序列化的效果), 所以,我们认为这种成本是可以接受的。
2. 增加了对于可空类型序列化/反序列化的多个单元测试

 

3. 可自定义的固定字节长度类型:

在我们的系统中,通常会存在一种可预知长度的小对象,而这种对象有的时候还是不可缺少的,比如如下这个例子:

 

//这个UID类型只是一个例子,实际中可能还会包含多个能力字段
public class UID : IntellectObject
{
  [IntellectProperty(0)]
  public int UserId { get; set; }
  [IntellectProperty(1)]
  public bool IsDirty { get; set; }
}

 

从上述类定义我们不难看出,一个UID类型标示了一个唯一用户,这种对象,在很多业务场景下都是非常常见的。
然而,在服务间的通讯协议上,这种类型的传递也是很频繁的。那么,如果通讯协议中频繁的包含此对象,其实是很消耗性能的,为什么呢?
因为,按照此框架的规定,用户自定义的类型,在序列化/反序列化的时候是需要遵循TLV模式的。这样的话,每次每一个字段都会带有4个字节的长度(L).
对于很多系统中的对象,TLV是没有问题的,但是总有类似于上述的自定义对象,在序列化的时候完全可以把这4个字节的长度给省去。


那么,该怎么办呢?让我们来考虑以下消息场景,好吧,我们定义了一个消息,来传递该对象:

 

public class DemoMessage : IntellectObject
{
    [IntellectProperty(0)]
    public UID Uid { get; set; }

    /*Other properties.*/
}

 

每一次这种消息的传递,我们都不得不使用TLV的形式,将UID对象进行序列化,每次都有字节数量上的冗余。 那么,我们何不进行以下优化呢?

 

#新能力:

我们特地的考虑到了以上需求,为KJFramework.Message消息框架内部增加了对于此场景的支持。对于新版本的更新中,我们外露了一个类型
"FixedTypeManager", 通过该类型,使得用户可以在程序初始化的时候打入自定义的固定字节数类型需求。
当然,我们需要为这个上述的自定义类型提供自身的处理器,并将处理器加入到框架内部的处理器集合中。 这一切看起来就像是这样:

 

internal class UIDIntellectTypeProcessor : IntellectTypeProcessor
{
    #region Constructor

    public UIDIntellectTypeProcessor()
    {
        _supportedType = typeof (UID);
    }

    #endregion

    #region Overrides of IntellectTypeProcessor

    /// <summary>
    ///     从第三方客户数据转换为元数据
    /// </summary>
    /// <param name="attribute">当前字段标注的属性</param>
    /// <param name="value">第三方客户数据</param>
    /// <returns>返回转换后的元数据</returns>
    /// <exception cref="Exception">转换失败</exception>
    public override byte[] Process(IntellectPropertyAttribute attribute, object value)
    {
        UID uid = (UID) value;
        //fixed data length.
        byte[] data = new byte[5];
        Buffer.BlockCopy(BitConverter.GetBytes(uid.UserId), 0, data, 0, 4);
        data[4] = (byte) (uid.IsDirty ? 1 : 0);
        return data;
    }

    /// <summary>
    ///     从元数据转换为第三方客户数据
    /// </summary>
    /// <param name="attribute">当前字段标注的属性</param>
    /// <param name="data">元数据</param>
    /// <returns>返回转换后的第三方客户数据</returns>
    /// <exception cref="Exception">转换失败</exception>
    public override object Process(IntellectPropertyAttribute attribute, byte[] data)
    {
        return Process(attribute, data, 0, 5);
    }

    /// <summary>
    ///     从元数据转换为第三方客户数据
    /// </summary>
    /// <param name="attribute">当前字段标注的属性</param>
    /// <param name="data">元数据</param>
    /// <param name="offset">元数据所在的偏移量</param>
    /// <param name="length">元数据长度</param>
    /// <returns>返回转换后的第三方客户数据</returns>
    /// <exception cref="Exception">转换失败</exception>
    public override object Process(IntellectPropertyAttribute attribute, byte[] data, int offset, int length = 0)
    {
        int userId = BitConverter.ToInt32(data, offset);
        bool isDirty = BitConverter.ToBoolean(data, offset + 4);
        return new UID {UserId = userId, IsDirty = isDirty};
    }

    #endregion
}

 

当然,在程序初始化的时候还需要执行一句话,这句话看起来就像这样:

 

//add UID type to fixed binary data length type.
FixedTypeManager.Add(typeof (UID), 5);

 

这句话的意思是说,我的系统中,我需要为UID类型 设定为支持固定字节数的类型。以后,再遇到UID类型序列化/反序列化的时候,
就会使用给出的字节长度来解析,整个的序列化/反序列化过程中,就不会存在L(4个字节的长度)了。 怎么样,这一切都变得更简洁了,不是吗?
一个完整的Test Case,就像下面这样~

 

[TestMethod]
[Description("对于自定义可固定字节数类型的测试")]
public void FixedTypeManagaerTest()
{
    //regist customer type processor.
    IntellectTypeProcessorMapping.Instance.Regist(new UIDIntellectTypeProcessor());
    //add UID type to fixed binary data length type.
    FixedTypeManager.Add(typeof (UID), 5);
    FixedFieldMessage fieldMessage = new FixedFieldMessage{Uid = new UID {UserId = 1, IsDirty = true}};
    fieldMessage.Bind();
    Assert.IsTrue(fieldMessage.IsBind);
    //head total length(4) + property 0 id(1) + property 0 value(5)
    Assert.IsTrue(fieldMessage.Body.Length == 10);
    PrintBytes(fieldMessage.Body);

    FixedFieldMessage newObj = IntellectObjectEngine.GetObject<FixedFieldMessage>(fieldMessage.Body);
    Assert.IsNotNull(newObj);
    Assert.IsTrue(newObj.Uid.UserId == 1);
    Assert.IsTrue(newObj.Uid.IsDirty);
}

怎么样,还不错吧?  快快来体验吧!

 

与此框架类似的通用组件:

ProtoBuffer - Google.

 

此框架的应用:

可以将此框架应用到网络对象的传输上,也就是说,当我们做一个分布式系统的时候,

只需要使用此框架,我们将无需再关心底层消息对象的序列化和反序列化细节,这一切的

工作将会由框架内部来完成。

 

性能指标:

此框架的基于.NETFRAMEWORK 4.0开发

测试平台:

CPU: Intel(R)Xeon(R)CPU X5670 @2.93GHz @2.93GHz (2处理器) 

System: Windows Server 2008 R2 Enterprise 

定义: 复杂对象,内部包含了多个数组类型的成员,比如string[], int[],

          内部还嵌套了其余类。

*想看看在这里测试的复杂对象到底有多复杂吗?  附上测试类的代码

*在我们的测试中,使用的是下列代码中的TestObject. 从下列代码中可以看到,此类型拥有很多的数组,而且还包含了其他的子类型 

 

 

public class TestObject : IntellectObject
    {
        private TestObject1 _obj;
        [IntellectProperty(7)]
        public TestObject1 Obj
        {
            get { return _obj; }
            set { _obj = value; }
        }

        private int[] _mm;
        [IntellectProperty(0)]
        public int[] Mm
        {
            get { return _mm; }
            set { _mm = value; }
        }

        private TestObject1[] _pp;
        [IntellectProperty(27)]
        public TestObject1[] Pp
        {
            get { return _pp; }
            set { _pp = value; }
        }


        private String[] _uu;
        [IntellectProperty(28)]
        public String[] Uu
        {
            get { return _uu; }
            set { _uu = value; }
        }


        private TestObject2[] _nn;
        [IntellectProperty(30)]
        public TestObject2[] Nn
        {
            get { return _nn; }
            set { _nn = value; }
        }

        private String[] _jj;
        [IntellectProperty(26)]
        public String[] Jj
        {
            get { return _jj; }
            set { _jj = value; }
        }

        private int _wokao;
        [IntellectProperty(4)]
        public int Wokao
        {
            get { return _wokao; }
            set { _wokao = value; }
        }

        private int _wocao;
        [IntellectProperty(2)]
        public int Wocao
        {
            get { return _wocao; }
            set { _wocao = value; }
        }

        private string _woqunimade;
        [IntellectProperty(3)]
        public string Woqunimade
        {
            get { return _woqunimade; }
            set { _woqunimade = value; }
        }

        private byte[] _metadata;
        [IntellectProperty(13)]
        public byte[] Metadata
        {
            get { return _metadata; }
            set { _metadata = value; }
        }


        private byte _metadata1;
        [IntellectProperty(15)]
        public byte Metadata1
        {
            get { return _metadata1; }
            set { _metadata1 = value; }
        }

        private TestObject2 _obj2;
        [IntellectProperty(16)]
        public TestObject2 Obj2
        {
            get { return _obj2; }
            set { _obj2 = value; }
        }

        private DateTime _time;
        [IntellectProperty(100)]
        public DateTime Time
        {
            get { return _time; }
            set { _time = value; }
        }

    }



    public class TestObject1 : IntellectObject
    {
        private string _haha;
        [IntellectProperty(0)]
        public string Haha
        {
            get { return _haha; }
            set { _haha = value; }
        }

        private Colors _colors;
        [IntellectProperty(1)]
        public Colors Colors
        {
            get { return _colors; }
            set { _colors = value; }
        }
    }



    [Serializable]
    public class TestObject2 : IClassSerializeObject
    {
        private int _nice;
        public int Nice
        {
            get { return _nice; }
            set { _nice = value; }
        }
    }

 

 

 

 

 *请注意: 由于性能的提升, 我们在2012年02月28日更新了性能指标!

序列化复杂对象(DEBUG):

     .次数 100000: 1100(ms) *此值根据测试机器的配置不同而不同,仅供参考
     .Gen0回收次数: 30
     .Gen1回收次数: 12
     .Gen2回收次数: 1

反序列化复杂对象(DEBUG):

     .次数 100000: 863(ms) *此值根据测试机器的配置不同而不同,仅供参考
     .Gen0回收次数: 22
     .Gen1回收次数: 1
     .Gen2回收次数: 0



序列化复杂对象(RELEASE):

     .次数 100000: 950(ms) *此值根据测试机器的配置不同而不同,仅供参考
     .Gen0回收次数: 30
     .Gen1回收次数: 12
     .Gen2回收次数: 1

反序列化复杂对象(RELEASE):

     .次数 100000: 610(ms) *此值根据测试机器的配置不同而不同,仅供参考
     .Gen0回收次数: 22
     .Gen1回收次数: 1
     .Gen2回收次数: 0



*具体的测试截图, 请查看源代码Pictures目录下的图片.  

 

 

 

 

数据段格式图:

 

 

 

 

 

 

更高的自定义需求:  

  在此框架中,对于每一个可以序列化的类型(int, string .....等等),都会为其配备一个智能类型处理器(IIntellectTypeProcessor),在框架的使用中,这些处理器都是默认的,如果,您感觉还有更好的实现能够加速

当前的序列化或者反序列化流程,那么,您可以在您的系统初始化的时候,使用自己的智能类型处理器来替换系统

现有的。 这样,就达到了可自定义类型序列化和反序列化的标准和算法 :)  当然,您也可以选择添加一个新的处理器。

  其次,在KJFramework.Message中,我们能看到,每一个可序列化的字段,都需要一个智能属性标记[IntellectPropery]

而每个这种标记都需要为序列化的字段提供一个唯一的数字序号,就比如:[IntellectProperty(0)] ,如果当您的自定义类型,需要为一个特殊的字段做特殊处理的时候,可以选择为一个特殊的编号字段来定制一个特殊的智能类型处理器。

比如,我们的登录消息,如果有一个字段用来存储密码,而我们恰巧需要为此字段进行MD5加密。

  那么,该怎么办呢?  当然,办法有很多种,我们可以选择先进行MD5的加密,然后再赋值,也可以选择如下的方式:

 

/*以下为代码示例*/
public class LogonMessage : IntellectObject
{
  [IntellectProperty(0)]
  public string UserName{get;set;}
  
   //可以看到,Password字段的序号为1. 
   //我们就可以单独添加一个字段处理器 来处理每一个消息实体内包含有序号1的字段
  [IntellectProperty(1)]
  public string Password{get;set;}
}

 

  *请不用担心,KJFramework.Message内部会对此操作做特殊的支持,好来完成您的特殊需求 :)

 

 

更专业的需求:

   现在我们已经为一个智能对象(IntellectObject)加入了描述自身的能力(New)。 也就是说,使用者随时随地

都可以通过如下调用 来得到当前对象的字符串形式描述信息。

   IntellectObject.ToString(); 

  当得到了一个对象的描述信息后,我们就能够很轻松的将其记录到日志文件中,这是不是很方便呢?

在日后,我们将会不停地更新这套框架,这也就意味着,会有越来越多的功能在以后会被加入,

如果您感兴趣,我们希望您能够持续关注。

 

附:

   此框架目前已经能够达到初步的商用层次,完全可以当做网络消息协议的序列化和反序列化转换层来使用,

  相信使用过GOOGLE的ProtoBuffer的朋友都应该明白了。我会一直更新此框架,如果您的项目中有此类需求,

  那么您可以选择尝试一下这套框架, 不会让您失望的 :)

 

   项目地址:http://message.codeplex.com/ 

   目前此项目已经发布了Release版本,您可以选择直接下载二进制文件使用,或者下载源代码 :)

   如果在使用此框架的途中,有任何问题,您也可以选择与我联系,我将提供最优的技术支持。

    QQ:250623008

    Email: Kevin.Jee@live.cn

 

 

 

谢谢. 

 

 

posted @ 2012-06-07 13:39  Kevin.Jee  阅读(870)  评论(2编辑  收藏  举报