在编程的世界中,经常会遇到一个问题:尽管底层数据都是以二进制形式保存和传输的,但处理数据类型(尤其是面向对象编程中的类型)却异常复杂。XML这样的标记语言在处理结构化文本方面非常优雅,它基于上下文无关文法,非常适合词法分析器/解析器处理。然而,当想要超越文本类型,将编码数据嵌入到结构良好的层次结构中时,事情就变得复杂起来。
知道二进制是至关重要的,XML也非常强大,但主流技术并不使用二进制XML来编组数据。尽管在工业标准方面有所进展,但开发者对此并不关心。这可能是因为存在一些方法论上的考虑。想谈谈一些可能会非常熟悉的真实世界场景。
在许多项目中,发现设计和维护关系数据库管理系统(RDBMS)在一些小项目中非常不方便。通常,一个小项目包含不到20个表,并且存储一些图片和文件。如果只有基于文本的信息需要保存,XML是一个很好的选择。但是,图片和文件呢?使用文件系统并在XML节点/属性中记录文件路径更加令人烦恼。
数据库提供了各种类型,但它是关系型的,特别是对于具有复杂业务数据的应用程序。关系模式强大、重量级且不易维护,但适合持久化。编程则完全不同。高效的编程通常基于内存模型,通常是基于各种数据类型的数组/集合。XML在编程场景中更容易操作。然而,知道XML是一种本质上基于文本的“语言”。
如果应用程序需要保存/加载各种类型的数据,如果数据可以以层次结构的方式组织,就像标记语言一样,那么应用程序可能会非常轻量级。将在后面讨论如何使用简单的二进制XML处理技术使其成为可能。
有没有参与过通信或分布式系统的开发?传统上,需要指定每个字段的字节偏移量和含义,就像那些大型RFC文档一样。让看看一段RFC 973(TCP)规范:
当阅读并实现协议时,那真是艰难的日子。不想解释图中所示的含义。在这里展示它的原因只是为了给一个直觉,说明在典型的协议设计过程中,数据序列化实现是多么可怕。当出现与无效字节偏移量相关的bug时,调试可能是一场噩梦。
相比之下,让看看Web服务是如何实现协议的,这里是Google Maps API的一段代码:
如此简单,没有字节对齐,没有偏移量计算。一眼就能看到层次结构。这是一个对程序员友好的实现。但是,如何处理传输多媒体消息而不仅仅是文本XML呢?
使用二进制XML工具,不需要任何字节偏移量规范。它就像通常出现在Web服务API中的基于XML的协议设计一样简单。差异是显著的,一方面,程序员享受与XML一样方便的通信,另一方面,任何二进制数据都可以在二进制XML消息中处理,无需额外的代码。
现在是解决方案的时候了。二进制XML由一个简单的树结构表示,具有后代节点和属性。节点内容和属性值可以保存二进制对象。实现了许多预定义类型,如int、double、time、bitmap等,它们以二进制形式保存,但开发者可以使用常见的已知类型进行访问。使用这个工具,加速了小项目的设计和编码,简化了数据结构。
1. IDump
在进入二进制XML之前,让看看IDump.cs。在这里,定义了一个简单的接口。任何继承IDump的类都可以保存到字节列表中或从字节列表中加载。二进制XML经常使用IDump来实现功能。
public interface IDump
{
int AppendToBytes(ref List byte_segments, ref int index);
void LoadFromBytes(byte[] bytes, ref int index);
}
2. 内容
每个元素都是内容。内容是一些字节,保存真实数据,并有额外的字节指定内容的类型。有几种预定义类型如下。可以在特定项目中定义自己的。
public enum EnumType
{
Null,
RawBin,
IDump,
String,
Int32,
Int64,
Double,
DateTime,
TimeSpan,
Bmp,
Boolean
}
当有类型规范时,可以虚拟地以二进制形式处理任何内容。内容可以通过以下方式简单地创建:
public Content()
{
}
public Content(string val)
{
this.type = EnumType.String;
this.content = System.Text.Encoding.UTF8.GetBytes(val);
}
public Content(int val)
{
this.type = EnumType.Int32;
this.content = System.BitConverter.GetBytes(val);
}
public Content(long val)
{
this.type = EnumType.Int64;
this.content = System.BitConverter.GetBytes(val);
}
public Content(double val)
{
this.type = EnumType.Double;
this.content = System.BitConverter.GetBytes(val);
}
public Content(DateTime val)
{
this.type = EnumType.DateTime;
this.content = System.BitConverter.GetBytes(val.Ticks);
}
public Content(TimeSpan val)
{
this.type = EnumType.TimeSpan;
this.content = System.BitConverter.GetBytes(val.Ticks);
}
public Content(IDump val)
{
this.type = EnumType.IDump;
List ls = new List();
int index = 0;
byte[] encoded = System.Text.Encoding.UTF8.GetBytes(val.GetType().FullName);
ls.AddRange(System.BitConverter.GetBytes(encoded.Length)); index += 4;
ls.AddRange(encoded); index += encoded.Length;
val.AppendToBytes(ref ls, ref index);
}
public Content(System.Drawing.Bitmap bmp)
{
this.type = EnumType.Bmp;
System.IO.MemoryStream ms = new System.IO.MemoryStream();
bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Bmp);
this.content = ms.ToArray();
}
public Content(byte[] val)
{
this.type = EnumType.RawBin;
this.content = val;
}
3. 二进制XML
现在有了Content类来保存任何类型的二进制数据。接下来,将使用Content作为元素构建层次结构。
在XML中,有:
<Master ID="1" Name="William Shakespeare" Icon="image-src">
<Works>
<Work>Hamlet</Work>
<Work>Otello</Work>
<Work>King Lear</Work>
</Works>
</Master>
首先,需要指定一个更强大的ID类型。让ID="1"成为一个更强的64位整数形式。其次,需要Icon属性的二进制内容直接嵌入到XML中,而不是外部链接。让看看二进制XML是如何做的:
BinTree bt = new BinTree("Master");
bt["ID"] = new Content(1L);
bt["Name"] = new Content("William Shakespeare");
Bitmap bmp = new Bitmap(100, 100);
using (Graphics g = Graphics.FromImage(bmp))
g.Clear(Color.Aqua);
bt["Icon"] = new Content(bmp);
BinTree bt_tags = bt.FindChildOrAppend("Works");
bt_tags.children.Add(new BinTree("Work") { content = new Content("Hamlet") });
bt_tags.children.Add(new BinTree("Work") { content = new Content("Otello") });
bt_tags.children.Add(new BinTree("Work") { content = new Content("King Lear") });
bt["ID"] = new Content(1L); 这行代码使用-L后缀指定了长类型,即长类型二进制表示形式。然后,由于手头没有William Shakespeare的图标,用背景颜色为Aqua的空白位图绘制了一个图标。图标位图直接保存在Icon属性中。如果愿意,也可以将其保存在一个单独的子节点的内容中,就像通常在XML中所做的那样。
在上面的示例代码中,也看到了二进制XML如何填充后代节点并操作集合。可以看到处理保存、传输和显示等杂项问题是多么简单:
// 转换为字节,然后保存在二进制文件中或远程传输
List bin = new List();
int index = 0;
(bt as IDump).AppendToBytes(ref bin, ref index);
// 构建一个XML树以方便开发人员
StringBuilder sb = new StringBuilder();
bt.PopulateXml(sb, 0, System.Environment.NewLine, "\n");
Console.WriteLine(sb.ToString());
这里是从二进制XML派生的基于文本的XML。由于Icon是一个二进制位图,这里看到Icon = "{100 x 100}"。
好吧。几乎完成了。剩下的就是简化应用程序。如果使用的是轻量级的关系数据库,请考虑将其迁移到一个简洁的二进制XML文件。如果正在设计一个二进制通信协议,请考虑在标记语言格式中指定详细信息,并传输整个二进制XML数据,而不需要太多规范。
另一个问题是性能。使用关系数据库或RFC风格的协议通常涉及很多性能考虑。本文没有讨论二进制XML是如何内部处理的。可以在源代码中找到。如果有兴趣,给一些提示:
XML是基于词法分析器/解析器的。从计算复杂性的角度看,二进制XML更先进,因为它在源代码中实现了线性索引机制。