二进制 XML 如何简化数据库和协议设计
一种处理二进制XML的轻量级方法。作为一种通用的数据表示技术,它方便地简化了数据库和协议的设计。
引言
在我习惯了本文介绍的方法论之前,我经常问自己
- 所有底层数据都以二进制形式保存和传输,但为什么处理类型(尤其是面向对象)如此复杂?
- 像XML这样的标记语言在处理结构化文本(以无上下文文法形式)方面非常优雅,易于词法分析器/语法分析器处理,但当你想要超越文本类型时,事情就变得如此糟糕? 比如
<![CDATA[]]>
.... 在结构良好的分层结构中嵌入文本编码的数据? 一定有问题。
我们知道二进制至关重要,XML非常强大,但主流技术并不使用二进制XML进行数据封送。 在行业标准方面有一些进展,但开发人员对此并不太关注。 为什么? 我认为可能需要一些方法论上的考虑。 我想谈谈几个你们一定非常熟悉的真实世界场景。
1. 数据库还是XML?
经过多个项目,我发现对于小型项目来说,设计和维护一个关系型数据库很不方便。 通常,一个小项目包含不到20个表,并存储一些图片和文件。 如果只有文本信息需要保存,XML是一个不错的选择。 但是,图片和文件怎么办? 使用文件系统并在XML节点/属性中记录文件/路径会更烦人。
数据库提供了各种类型。 然而,它是关系型的,特别是对于具有复杂业务数据的应用程序。 关系模型强大、笨重且不易维护,但适合持久化。 编程完全不同。 高效编程基于内存模型,通常是各种数据类型与基于这些类型的数组/集合。 XML在编程场景中操作起来更方便。 然而,我们知道XML本质上是一种基于文本的“语言”。
如果我们需要在应用程序中保存/加载各种类型的数据,如果数据可以以标记语言的方式组织成层次结构,那么应用程序可以非常轻量级。 稍后我们将讨论如何使用简单的二进制XML处理技术来实现这一点。
2. RFC风格协议 vs 基于XML的WebAPI
你是否参与过通信或分布式系统的开发? 传统上,你需要为每个字段指定字节偏移量和含义,就像那些大型RFC文档一样。 让我们看一段 RFC 973 (TCP) 规范
当我阅读和实现协议时,日子过得很艰难。 我们不想解释如图所示的含义。 我在这里展示它的唯一原因是为了让你直观地了解在典型协议设计过程中,数据序列化的实现有多么糟糕。 当出现与无效字节偏移相关的错误时,调试可能是一场噩梦。
相比之下,让我们看看Web服务如何实现协议,这是一段 Google Maps API 的片段
非常简单,没有字节对齐,没有偏移量计算。 一眼就能看到分层结构。 这是对程序员友好的实现。 但是,对于传输包含多媒体而不是纯文本XML的消息怎么办?
使用二进制XML工具,我们不需要任何字节偏移量规范。 它就像设计基于XML的协议一样简单,而这通常出现在Web服务API中。 区别很大,一方面,我们的程序员可以像XML一样方便地进行通信,另一方面,任何二进制数据都可以包含在二进制XML消息中,而无需额外的代码。
一种轻量级的二进制XML解决方案
现在是我们的解决方案。 二进制XML由一个简单的树状结构表示,带有子节点和属性。 节点内容和属性值可以存储二进制对象。 我实现了许多预定义类型,例如 int
、double
、time
、bitmap
...它们以二进制形式保存,但开发人员使用常见的已知类型进行访问。 使用这个工具,我们加速了具有更简单数据结构的小项目的设计和编码。
Using the Code
1. IDump
在深入研究二进制XML之前,让我们看看 {IDump.cs}。 这里,我们定义了一个简单的接口。 任何继承 IDump
的类都可以被保存到或加载自一个字节列表中。 二进制XML经常使用 IDump
来实现功能。
{IDump.cs}
public interface IDump
{
int AppendToBytes(ref List<byte> byte_segments, ref int index);
void LoadFromBytes(byte[] bytes, ref int index);
}
2. Content
每个元素都是内容。 内容是几个字节,存储实际数据,以及额外的字节来指定内容的类型。 我们有以下几种预定义类型。 你可以在特定项目中定义自己的类型。
{Content.cs}
public enum EnumType
{
Null,
RawBin,
IDump,
String,
Int32,
Int64,
Double,
DateTime,
TimeSpan,
Bmp,
Boolean
}
当有类型规范时,我们几乎可以用二进制形式处理任何内容。 内容可以简单地通过以下方式创建:
{Content.cs}
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<byte> ls = new List<byte>();
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是如何做的
{Program.cs}
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
后缀指定长整型,即长整型的二进制表示形式。 然后,由于我们手头没有莎士比亚的图标,我们绘制了一个空白的位图,背景颜色为 Aqua
。 图标位图直接保存在 Icon
属性中。 如果你愿意,也可以将其保存在单独子节点的內容中,就像我们在XML中通常做的那样。
在上面的示例代码中,我们还看到了二进制XML如何填充子节点以及如何操作集合。 我们可以看到处理保存、传输和显示等杂项问题有多么简单。
{Program.cs}
//Convert to bytes, then to be saved in binary files or transfered remotely
List<byte> bin = new List<byte>();
int index = 0;
(bt as IDump).AppendToBytes(ref bin, ref index);
//Build an XML tree to facilitate the developers
StringBuilder sb = new StringBuilder();
bt.PopulateXml(sb, 0, System.Environment.NewLine, " ");
Console.WriteLine(sb.ToString());
这里是从二进制XML派生出的文本XML。 由于 Icon
是一个二进制位图,我们在这里看到 Icon = "{100 x 100}"
。
关注点
好了。 我们几乎完成了。 剩下的就是简化你的应用程序。 如果你正在使用轻量级的关系型数据库,请考虑将其迁移到一个简洁的二进制XML文件。 如果你正在设计一个二进制通信协议,请考虑以标记语言格式指定细节,并传输整个二进制XML数据,而无需过多规范。
另一个问题是性能。 使用关系型数据库或RFC风格协议通常涉及大量的性能考虑。 我在本文中没有讨论二进制XML内部是如何处理的。 你可以在源代码中找到。 如果你有兴趣,我提供一些提示:
- XML是基于词法分析器/语法分析器的。 从计算复杂度的角度来看,由于源代码中实现的线性索引机制,二进制XML更先进。
- 当二进制XML扩展到一定规模时,我们可以使用多个二进制偏移量(指针)来访问文件内不同的偏移地址,以分块的形式。 在处理I/O时,我们也可以分离文件或使用其他类似的并行化方法。 事实上,高性能数据库正是这样访问物理文件的。
历史
- 2015年9月21日:初稿