翼度科技»论坛 编程开发 .net 查看内容

C#对象二进制序列化优化:位域技术实现极限压缩

5

主题

5

帖子

15

积分

新手上路

Rank: 1

积分
15
目录

1. 引言

在操作系统中,进程信息对于系统监控和性能分析至关重要。假设我们需要开发一个监控程序,该程序能够捕获当前操作系统的进程信息,并将其高效地传输到其他端(如服务端或监控端)。在这个过程中,如何将捕获到的进程对象转换为二进制数据,并进行优化,以减小数据包的大小,成为了一个关键问题。本文将通过逐步分析,探讨如何使用位域技术对C#对象进行二进制序列化优化。

首先,我们给出了一个进程对象的字段定义示例。为了通过网络(TCP/UDP)传输该对象,我们需要将其转换为二进制格式。在这个过程中,如何做到最小的数据包大小是一个挑战。
字段名说明示例PID进程ID10565Name进程名称码界工坊Publisher发布者沙漠尽头的狼CommandLine命令行dotnet CodeWF.Tools.dllCPUCPU(所有内核的总处理利用率)2.3%Memory内存(进程占用的物理内存)0.1%Disk磁盘(所有物理驱动器的总利用率)0.1 MB/秒Network网络(当前主要网络上的网络利用率0 MbpsGPUGPU(所有GPU引擎的最高利用率)2.2%GPUEngineGPU引擎GPU 0 - 3DPowerUsage电源使用情况(CPU、磁盘和GPU对功耗的影响)低PowerUsageTrend电源使用情况趋势(一段时间内CPU、磁盘和GPU对功耗的影响)非常低Type进程类型应用Status进程状态效率模式2. 优化过程

2.1. 进程对象定义与初步分析

我们根据字段的示例值确定了每个字段的数据类型。
字段名数据类型说明示例PIDint进程ID10565Namestring?进程名称码界工坊Publisherstring?发布者沙漠尽头的狼CommandLinestring?命令行dotnet CodeWF.Tools.dllCPUstring?CPU(所有内核的总处理利用率)2.3%Memorystring?内存(进程占用的物理内存)0.1%Diskstring?磁盘(所有物理驱动器的总利用率)0.1 MB/秒Networkstring?网络(当前主要网络上的网络利用率0 MbpsGPUstring?GPU(所有GPU引擎的最高利用率)2.2%GPUEnginestring?GPU引擎GPU 0 - 3DPowerUsagestring?电源使用情况(CPU、磁盘和GPU对功耗的影响)低PowerUsageTrendstring?电源使用情况趋势(一段时间内CPU、磁盘和GPU对功耗的影响)非常低Typestring?进程类型应用Statusstring?进程状态效率模式创建一个C#类SystemProcess表示进程信息:
  1. public class SystemProcess
  2. {
  3.     public int PID { get; set; }
  4.     public string? Name { get; set; }
  5.     public string? Publisher { get; set; }
  6.     public string? CommandLine { get; set; }
  7.     public string? CPU { get; set; }
  8.     public string? Memory { get; set; }
  9.     public string? Disk { get; set; }
  10.     public string? Network { get; set; }
  11.     public string? GPU { get; set; }
  12.     public string? GPUEngine { get; set; }
  13.     public string? PowerUsage { get; set; }
  14.     public string? PowerUsageTrend { get; set; }
  15.     public string? Type { get; set; }
  16.     public string? Status { get; set; }
  17. }
复制代码
定义测试数据
  1. private SystemProcess _codeWFObject = new SystemProcess()
  2. {
  3.     PID = 10565,
  4.     Name = "码界工坊",
  5.     Publisher = "沙漠尽头的狼",
  6.     CommandLine = "dotnet CodeWF.Tools.dll",
  7.     CPU = "2.3%",
  8.     Memory = "0.1%",
  9.     Disk = "0.1 MB/秒",
  10.     Network = "0 Mbps",
  11.     GPU = "2.2%",
  12.     GPUEngine = "GPU 0 - 3D",
  13.     PowerUsage = "低",
  14.     PowerUsageTrend = "非常低",
  15.     Type = "应用",
  16.     Status = "效率模式"
  17. };
复制代码
2.2. 排除Json序列化

将对象转为Json字段串,这在Web开发是最常见的,因为简洁,前后端都方便处理:
  1. public class SysteProcessUnitTest
  2. {
  3.     private readonly ITestOutputHelper _testOutputHelper;
  4.     private SystemProcess _codeWFObject // 前面已给出定义,这里省
  5.     public SysteProcessUnitTest(ITestOutputHelper testOutputHelper)
  6.     {
  7.         _testOutputHelper = testOutputHelper;
  8.     }
  9.     /// <summary>
  10.     /// Json序列化大小测试
  11.     /// </summary>
  12.     [Fact]
  13.     public void Test_SerializeJsonData_Success()
  14.     {
  15.         var jsonData = JsonSerializer.Serialize(_codeWFObject);
  16.         _testOutputHelper.WriteLine($"Json长度:{jsonData.Length}");
  17.         var jsonDataBytes = Encoding.UTF8.GetBytes(jsonData);
  18.         _testOutputHelper.WriteLine($"json二进制长度:{jsonDataBytes.Length}");
  19.     }
  20. }
复制代码
  1. 标准输出: 
  2. Json长度:366
  3. json二进制长度:366
复制代码
尽管Json序列化在Web开发中非常流行,因为它简洁且易于处理,但在TCP/UDP网络传输中,Json序列化可能导致不必要的数据包大小增加。因此,我们排除了Json序列化,并寻找其他更高效的二进制序列化方法。
  1. {"PID":10565,"Name":"\u7801\u754C\u5DE5\u574A","Publisher":"\u6C99\u6F20\u5C3D\u5934\u7684\u72FC","CommandLine":"dotnet CodeWF.Tools.dll","CPU":"2.3%","Memory":"0.1%","Disk":"0.1 MB/\u79D2","Network":"0 Mbps","GPU":"2.2%","GPUEngine":"GPU 0 - 3D","PowerUsage":"\u4F4E","PowerUsageTrend":"\u975E\u5E38\u4F4E","Type":"\u5E94\u7528","Status":"\u6548\u7387\u6A21\u5F0F"}
复制代码
2.3. 使用BinaryWriter进行二进制序列化

使用站长前面一篇文章写的二进制序列化帮助类SerializeHelper转换,该类使用BinaryWriter将对象转换为二进制数据。
首先,我们使SystemProcess类实现了一个空接口INetObject,并在类上添加了NetHeadAttribute特性。
  1. /// <summary>
  2. /// 网络对象序列化接口
  3. /// </summary>
  4. public interface INetObject
  5. {
  6. }
复制代码
  1. [NetHead(1, 1)]
  2. public class SystemProcess : INetObject
  3. {
  4.         // 省略字段定义   
  5. }
复制代码
然后,我们编写了一个测试方法来验证序列化和反序列化的正确性,并打印了序列化后的二进制数据长度。
  1. /// <summary>
  2. /// 二进制序列化测试
  3. /// </summary>
  4. [Fact]
  5. public void Test_SerializeToBytes_Success()
  6. {
  7.     var buffer = SerializeHelper.SerializeByNative(_codeWFObject, 1);
  8.     _testOutputHelper.WriteLine($"序列化后二进制长度:{buffer.Length}");
  9.     var deserializeObj = SerializeHelper.DeserializeByNative<SystemProcess>(buffer);
  10.     Assert.Equal("码界工坊", deserializeObj.Name);
  11. }
复制代码
  1. 标准输出: 
  2. 序列化后二进制长度:152
复制代码
比Json体积小了一半多(366到152),上面单元测试也测试了数据反序列化后验证数据是否正确,我们就以这个基础继续优化。
2.4. 数据类型调整

为了进一步优化二进制数据的大小,我们对数据类型进行了调整。通过对进程数据示例的分析,我们发现一些字段的数据类型可以更加紧凑地表示。例如,CPU利用率可以只传递数字部分(如2.3),而不需要传递百分号。这种调整可以减小数据包的大小。
字段名数据类型说明示例PIDint进程ID10565Namestring?进程名称码界工坊Publisherstring?发布者沙漠尽头的狼CommandLinestring?命令行dotnet CodeWF.Tools.dllCPUfloatCPU(所有内核的总处理利用率)2.3Memoryfloat内存(进程占用的物理内存)0.1Diskfloat磁盘(所有物理驱动器的总利用率)0.1Networkfloat网络(当前主要网络上的网络利用率0GPUfloatGPU(所有GPU引擎的最高利用率)2.2GPUEnginebyteGPU引擎,0:无,1:GPU 0 - 3D1PowerUsagebyte电源使用情况(CPU、磁盘和GPU对功耗的影响),0:非常低,1:低,2:中,3:高,4:非常高1PowerUsageTrendbyte电源使用情况趋势(一段时间内CPU、磁盘和GPU对功耗的影响),0:非常低,1:低,2:中,3:高,4:非常高0Typebyte进程类型,0:应用,1:后台进程0Statusbyte进程状态,0:正常运行,1:效率模式,2:挂起1修改测试数据定义:
  1. [NetHead(1, 2)]
  2. public class SystemProcess2 : INetObject
  3. {
  4.     public int PID { get; set; }
  5.     public string? Name { get; set; }
  6.     public string? Publisher { get; set; }
  7.     public string? CommandLine { get; set; }
  8.     public float CPU { get; set; }
  9.     public float Memory { get; set; }
  10.     public float Disk { get; set; }
  11.     public float Network { get; set; }
  12.     public float GPU { get; set; }
  13.     public byte GPUEngine { get; set; }
  14.     public byte PowerUsage { get; set; }
  15.     public byte PowerUsageTrend { get; set; }
  16.     public byte Type { get; set; }
  17.     public byte Status { get; set; }
  18. }
复制代码
  1. /// <summary>
  2. /// 普通优化字段数据类型
  3. /// </summary>
  4. private SystemProcess2 _codeWFObject2 = new SystemProcess2()
  5. {
  6.     PID = 10565,
  7.     Name = "码界工坊",
  8.     Publisher = "沙漠尽头的狼",
  9.     CommandLine = "dotnet CodeWF.Tools.dll",
  10.     CPU = 2.3f,
  11.     Memory = 0.1f,
  12.     Disk = 0.1f,
  13.     Network = 0,
  14.     GPU = 2.2f,
  15.     GPUEngine = 1,
  16.     PowerUsage = 1,
  17.     PowerUsageTrend = 0,
  18.     Type = 0,
  19.     Status = 1
  20. };
复制代码
添加单元测试如下:
  1. /// <summary>
  2. /// 二进制序列化测试
  3. /// </summary>
  4. [Fact]
  5. public void Test_SerializeToBytes2_Success()
  6. {
  7.     var buffer = SerializeHelper.SerializeByNative(_codeWFObject2, 1);
  8.     _testOutputHelper.WriteLine($"序列化后二进制长度:{buffer.Length}");
  9.     var deserializeObj = SerializeHelper.DeserializeByNative<SystemProcess2>(buffer);
  10.     Assert.Equal("码界工坊", deserializeObj.Name);
  11.     Assert.Equal(2.2f, deserializeObj.GPU);
  12. }
复制代码
测试结果:
  1. 标准输出: 
  2. 序列化后二进制长度:99
复制代码
又优化了50%左右(152到99),爽不爽?继续,还有更爽的。
2.5. 再次数据类型调整与位域优化

更进一步地,我们引入了位域技术。位域允许我们更加精细地控制字段在内存中的布局,从而进一步减小二进制数据的大小。我们重新定义了字段规则,并使用位域来表示一些枚举值字段。通过这种方式,我们能够显著地减小数据包的大小。
看前面一张表,部分字段只是一些枚举值,使用的byte表示,即8位(bit),其中比如进程类型只有2个状态(0:应用,1:后台进程),正好可以用1位即表示;像电源使用情况,无非就是5个状态,用3位可表示全,按这个规则我们重新定义字段规则如下:
字段名数据类型说明示例PIDint进程ID10565Namestring?进程名称码界工坊Publisherstring?发布者沙漠尽头的狼CommandLinestring?命令行dotnet CodeWF.Tools.dllDatabyte[8]固定大小的几个字段,见下表定义固定字段(Data)的详细说明如下:
字段名OffsetSize说明示例CPU010CPU(所有内核的总处理利用率),最后一位表示小数位,比如23表示2.3%23Memory1010内存(进程占用的物理内存),最后一位表示小数位,比如1表示0.1%,值可根据基本信息计算1Disk2010磁盘(所有物理驱动器的总利用率),最后一位表示小数位,比如1表示0.1%,值可根据基本信息计算1Network3010网络(当前主要网络上的网络利用率),最后一位表示小数位,比如253表示25.3%,值可根据基本信息计算0GPU4010GPU(所有GPU引擎的最高利用率),最后一位表示小数位,比如253表示25.322GPUEngine501GPU引擎,0:无,1:GPU 0 - 3D1PowerUsage513电源使用情况(CPU、磁盘和GPU对功耗的影响),0:非常低,1:低,2:中,3:高,4:非常高1PowerUsageTrend543电源使用情况趋势(一段时间内CPU、磁盘和GPU对功耗的影响),0:非常低,1:低,2:中,3:高,4:非常高0Type571进程类型,0:应用,1:后台进程0Status582进程状态,0:正常运行,1:效率模式,2:挂起1上面这张表是位域规则表,Offset表示字段在Data字节数组中的位置(以bit为单位计算),Size表示字段在Data中占有的大小(同样以bit单位计算),如Memory字段,在Data字节数组中,占据10到20位的空间。
修改类定义如下,注意看代码中的注释:
  1. [NetHead(1, 3)]
  2. public class SystemProcess3 : INetObject
  3. {
  4.     public int PID { get; set; }
  5.     public string? Name { get; set; }
  6.     public string? Publisher { get; set; }
  7.     public string? CommandLine { get; set; }
  8.     private byte[]? _data;
  9.     /// <summary>
  10.     /// 序列化,这是实际需要序列化的数据
  11.     /// </summary>
  12.     public byte[]? Data
  13.     {
  14.         get => _data;
  15.         set
  16.         {
  17.             _data = value;
  18.             // 这是关键:在反序列化将byte转换为对象,方便程序中使用
  19.             _processData = _data?.ToFieldObject<SystemProcessData>();
  20.         }
  21.     }
  22.     private SystemProcessData? _processData;
  23.     /// <summary>
  24.     /// 进程数据,添加NetIgnoreMember在序列化会忽略
  25.     /// </summary>
  26.     [NetIgnoreMember]
  27.     public SystemProcessData? ProcessData
  28.     {
  29.         get => _processData;
  30.         set
  31.         {
  32.             _processData = value;
  33.             // 这里关键:将对象转换为位域
  34.             _data = _processData?.FieldObjectBuffer();
  35.         }
  36.     }
  37. }
  38. public record SystemProcessData
  39. {
  40.     [NetFieldOffset(0, 10)] public short CPU { get; set; }
  41.     [NetFieldOffset(10, 10)] public short Memory { get; set; }
  42.     [NetFieldOffset(20, 10)] public short Disk { get; set; }
  43.     [NetFieldOffset(30, 10)] public short Network { get; set; }
  44.     [NetFieldOffset(40, 10)] public short GPU { get; set; }
  45.     [NetFieldOffset(50, 1)] public byte GPUEngine { get; set; }
  46.     [NetFieldOffset(51, 3)] public byte PowerUsage { get; set; }
  47.     [NetFieldOffset(54, 3)] public byte PowerUsageTrend { get; set; }
  48.     [NetFieldOffset(57, 1)] public byte Type { get; set; }
  49.     [NetFieldOffset(58, 2)] public byte Status { get; set; }
  50. }
复制代码
添加单元测试如下:
  1. /// <summary>
  2. /// 极限优化字段数据类型
  3. /// </summary>
  4. private SystemProcess3 _codeWFObject3 = new SystemProcess3()
  5. {
  6.     PID = 10565,
  7.     Name = "码界工坊",
  8.     Publisher = "沙漠尽头的狼",
  9.     CommandLine = "dotnet CodeWF.Tools.dll",
  10.     ProcessData = new SystemProcessData()
  11.     {
  12.         CPU = 23,
  13.         Memory = 1,
  14.         Disk = 1,
  15.         Network = 0,
  16.         GPU = 22,
  17.         GPUEngine = 1,
  18.         PowerUsage = 1,
  19.         PowerUsageTrend = 0,
  20.         Type = 0,
  21.         Status = 1
  22.     }
  23. };
  24. /// <summary>
  25. /// 二进制极限序列化测试
  26. /// </summary>
  27. [Fact]
  28. public void Test_SerializeToBytes3_Success()
  29. {
  30.     var buffer = SerializeHelper.SerializeByNative(_codeWFObject3, 1);
  31.     _testOutputHelper.WriteLine($"序列化后二进制长度:{buffer.Length}");
  32.     var deserializeObj = SerializeHelper.DeserializeByNative<SystemProcess3>(buffer);
  33.     Assert.Equal("码界工坊", deserializeObj.Name);
  34.     Assert.Equal(23, deserializeObj.ProcessData.CPU);
  35.     Assert.Equal(1, deserializeObj.ProcessData.PowerUsage);
  36. }
复制代码
测试输出:
  1. 标准输出: 
  2. 序列化后二进制长度:86
复制代码
99又优化到86个字节,13个字节哦,有极限网络环境下非常可观,比如100万数据,那不就是12.4MB了?关于位域序列化和反序列的代码这里不细说了,很枯燥,站长可能也说不清楚,代码长这样:
[code]public partial class SerializeHelper{    public static byte[] FieldObjectBuffer(this T obj) where T : class    {        var properties = typeof(T).GetProperties();        var totalSize = 0;        // 计算总的bit长度        foreach (var property in properties)        {            if (!Attribute.IsDefined(property, typeof(NetFieldOffsetAttribute)))            {                continue;            }            var offsetAttribute =                (NetFieldOffsetAttribute)property.GetCustomAttribute(typeof(NetFieldOffsetAttribute))!;            totalSize = Math.Max(totalSize, offsetAttribute.Offset + offsetAttribute.Size);        }        var bufferLength = (int)Math.Ceiling((double)totalSize / 8);        var buffer = new byte[bufferLength];        foreach (var property in properties)        {            if (!Attribute.IsDefined(property, typeof(NetFieldOffsetAttribute)))            {                continue;            }            var offsetAttribute =                (NetFieldOffsetAttribute)property.GetCustomAttribute(typeof(NetFieldOffsetAttribute))!;            dynamic value = property.GetValue(obj)!; // 使用dynamic类型动态获取属性值            SetBitValue(ref buffer, value, offsetAttribute.Offset, offsetAttribute.Size);        }        return buffer;    }    public static T ToFieldObject(this byte[] buffer) where T : class, new()    {        var obj = new T();        var properties = typeof(T).GetProperties();        foreach (var property in properties)        {            if (!Attribute.IsDefined(property, typeof(NetFieldOffsetAttribute)))            {                continue;            }            var offsetAttribute =                (NetFieldOffsetAttribute)property.GetCustomAttribute(typeof(NetFieldOffsetAttribute))!;            dynamic value = GetValueFromBit(buffer, offsetAttribute.Offset, offsetAttribute.Size,                property.PropertyType);            property.SetValue(obj, value);        }        return obj;    }    ///     /// 将值按位写入buffer    ///     ///     ///     ///     ///     private static void SetBitValue(ref byte[] buffer, int value, int offset, int size)    {        var mask = (1 > (8 - offset % 8));        }    }    ///     /// 从buffer中按位读取值    ///     ///     ///     ///     ///     ///     private static dynamic GetValueFromBit(byte[] buffer, int offset, int size, Type propertyType)    {        var mask = (1 > (offset % 8)) & mask;        if (offset % 8 + size > 8)        {            bitValue |= (buffer[offset / 8 + 1]

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具