命名二进制标签序列化






4.93/5 (23投票s)
本文描述了 NBT 文件格式,并展示了如何在实际应用程序中实现它以存储数据。
引言
命名二进制标签(也称为 NBT)是一种非常简单的文件格式,它使用二进制标签来存储数据。这种文件格式由 Markus Persson 创建,用于存储游戏数据《我的世界》。
不幸的是,NBT 格式存在多种压缩格式
- 未压缩。
- 使用 GZIP 压缩。
- 使用 ZLIB 压缩。
标准标签类型
标签ID | 名称 | 有效载荷大小(字节) | 描述 |
0 | TagEnd(标签结束) | 0 | 此标签的目的是指示已打开的 TagCompound 的结束。 |
1 | TagByte(字节标签) | 1 | 一个无符号字节。 |
2 | TagShort(短整型标签) | 2 | 一个有符号短整型。 |
3 | TagInt(整型标签) | 4 | 一个有符号整型。 |
4 | TagLong(长整型标签) | 8 | 一个有符号长整型。 |
5 | TagFloat(浮点型标签) | 4 | 一个有符号浮点型。 |
6 | TagDouble(双精度浮点型标签) | 8 | 一个有符号双精度浮点型。 |
7 | TagByteArray(字节数组标签) | 变量 | 字节数组。此标签以一个有符号整型作为前缀,表示数组的大小。 |
8 | TagString(字符串标签) | 变量 | 一个 UTF-8 字符串。字符串以一个无符号短整型作为前缀,表示字符串的大小。 |
9 | TagList(列表标签) | 变量 | 无名标签列表,所有标签必须是相同标签类型。列表以其包含的项的 TagID(仅一个字节)作为前缀,以及一个有符号整型表示列表的长度。此列表可排序。 |
10 | TagCompound(复合标签) | 变量 | 命名标签列表。此列表不可排序,且其大小可变(无前缀长度)。 |
11 | TagIntArray(整型数组标签) | 变量 | 整型数组。此标签以一个整型作为前缀,表示数组的大小。 |
我的自定义标签类型
为了给 NBT 文件格式提供更多选项,我创建了新的标签类型。
252 | TagImage(图片标签) | 变量 | 用于存储图片。(System.Drawing.Image) |
253 | TagIP(IP 地址标签) | 变量 | 用于存储 IPAddress。 |
254 | TagMAC(MAC 地址标签) | 变量 | 用于存储物理地址。 |
251 | TagSByte(有符号字节标签) | 1 | 用于存储有符号字节。 |
250 | TagUShort(无符号短整型标签) | 2 | 用于存储无符号短整型。 |
249 | TagUINT(无符号整型标签) | 4 | 用于存储无符号整型。 |
248 | TagULong(无符号长整型标签) | 8 | 用于存储无符号长整型。 |
247 | TagShortArray(短整型数组标签) | 变量 | 短整型数组。此标签以一个整型作为前缀,表示数组的大小。 |
246 | TagDateTime(日期时间标签) | 8 | 用于存储日期时间值。 |
245 | TagTimeSpan(时间间隔标签) | 8 | 用于存储时间间隔值。 |
244 | TagLongArray(长整型数组标签) | 变量 | 长整型数组。此标签以一个整型作为前缀,表示数组的大小。 |
243 | TagFloatArray(浮点型数组标签) | 变量 | 浮点型数组。此标签以一个整型作为前缀,表示数组的大小。 |
242 | TagDoubleArray(双精度浮点型数组标签) | 变量 | 双精度浮点型数组。此标签以一个整型作为前缀,表示数组的大小。 |
241 | TagSByteArray(有符号字节数组标签) | 变量 | 有符号字节数组。此标签以一个整型作为前缀,表示数组的大小。 |
240 | TagUShortArray(无符号短整型数组标签) | 变量 | 无符号短整型数组。此标签以一个整型作为前缀,表示数组的大小。 |
239 | TagUIntArray(无符号整型数组标签) | 变量 | 无符号整型数组。此标签以一个整型作为前缀,表示数组的大小。 |
238 | TagULongArray(无符号长整型数组标签) | 变量 | 无符号长整型数组。此标签以一个整型作为前缀,表示数组的大小。 |
237 | TagImageArray(图片数组标签) | 变量 | 图片数组。此标签以一个整型作为前缀,表示数组的大小。 |
文件格式规则
- 所有数据均采用大端序。
- 所有 NBT 文件必须以 TagCompound 开头。
- 所有标签都以一个字节开头,表示其标签类型。
- 所有标签(TagEnd 和 TagList 中的项除外)都以 TagString 开头。
- TagCompound 的所有标签必须由 TagEnd 关闭。
格式规范和示例
标签包含以下格式
标签类型 (TagID) | 标签字符串 (名称) | Payload(载荷) |
以下示例展示了此格式如何在 TagCompound 中存储 TagShort
理论
TagCompound: ('Test')
{
TagShort: ('sample') : 123
}
磁盘上的数据(十六进制格式)
(1) 10 |
(2) 00 04 |
(3) 54 65 73 74 |
(4) 02 |
(5) 00 06 |
(6) 73 61 6D 70 6C 65 |
(7) 00 7B | (8) 00 |
(1) TagCompound 的 ID。
(2) TagCompound 名称的长度。
(3) UTF-8 字符串 ("Test")。
(4) 标签 ID,在此例中为 2,因为我们的标签是 TagShort。
(5) 其名称的长度。
(6) UTF-8 字符串 ("sample")。
(7) 有效载荷。
(8) TagEnd
(表示 TagCompound
的结束)。
使用代码
要使用我的库,需要导入以下命名空间
- NBT.IO(此命名空间包含与文件及其压缩相关的所有内容)
- NBT.Tags(包含所有支持的标签类型)
有两个命名空间(NBT.Exceptions
和 NBT.Compression
)。
NBT.Exceptions
包含库可能抛出的所有异常,NBT.Compression
包含所有与压缩相关的内容。
库内部 - 第 1 部分 (NBT.IO)
NBT.IO 命名空间负责处理文件、读取、写入、异常等相关事宜。
要读取文件,需要创建 NBTFile
类的实例。NBTFile
类提供文件管理的主要方法。它还会自动检测压缩格式。
//
// Part of the code from NBTCompression Headers
//
public static NBTCompressionTypes.enumNBTCompressionTypes CompressionType(string filePath)
{
NBTCompressionTypes.enumNBTCompressionTypes result =
NBTCompressionTypes.enumNBTCompressionTypes.Uncompressed;
//We open the file and check if file have the header of GZIP
using (Stream fStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
result = NBTCompressionHeaders.CompressionType(fStream);
}
return result;
}
public static bool IsGzipStream(Stream stream)
{
bool result = false;
if (stream == null)
{
throw new NBT_InvalidArgumentNullException();
}
if (stream.CanSeek == false)
{
throw new NBT_IOException("Can't seek in the stream");
}
//we keep the current position within the stream.
long initialOffset = stream.Seek(0, SeekOrigin.Current);
stream.Seek(0, SeekOrigin.Begin);
//Check if the stream is a gzip stream.
if ((stream.ReadByte() == GZIP_Header[0]) && (stream.ReadByte() == GZIP_Header[1]))
{
result = true;
}
//Set the position to the initial position
stream.Seek(initialOffset, SeekOrigin.Begin);
return result;
}
//
// Part of the code from NBTFile
//
public void Load(Stream stream)
{
try
{
//Indicates if the stream will be closed after this function.
bool closeAuxStream = false;
if (stream == null)
{
throw new NBT_IOException();
}
//Determine the compression
NBTCompressionType fileCompression = NBTCompressionHeaders.CompressionType(stream);
Stream auxStream = null;
switch (fileCompression)
{
case NBTCompressionType.Uncompressed:
{
auxStream = stream;
closeAuxStream = false;
break;
}
case NBTCompressionType.GZipCompression:
{
auxStream = new GZipStream(stream, CompressionMode.Decompress, true);
closeAuxStream = true;
break;
}
}
if (auxStream == null)
{
throw new NBT_IOException();
}
byte firstTag = (byte)auxStream.ReadByte();
if (firstTag != TagTypes.TagCompound)
{
throw new NBT_IOException("The first tag must be a TagCompound");
}
this.fileType = fileCompression;
this.rootTagName = TagString.ReadString(auxStream);
this.rootTagValue.readTag(auxStream);
if (closeAuxStream == true)
{
//Close the auxStream, but the original stream still opened
auxStream.Close();
}
}
catch (Exception ex)
{
throw new NBT_IOException("Load exception", ex);
}
}
public void Load(string filePath)
{
try
{
if (File.Exists(filePath) != true)
{
throw new NBT_IOException("File not found");
}
using (Stream stream = File.OpenRead(filePath))
{
this.Load(stream);
this.filePath = filePath;
}
}
catch (Exception ex)
{
throw new NBT_IOException("Load exception", ex);
}
}
库内部 - 第 2 部分 (NBT.Tags)
此命名空间包含所有可用的标签类型。主要思想是所有标签都继承自抽象类 Tag。
这是因为 Tag 类提供了所有标签必须具备的最小功能。
因为 TagCompound
是 NBT 文件的第一个标签,所以 NBTFile.Load()
会调用 readTag
(此函数在 TagCompound
类中)。
这是解释和代码
internal override void readTag(Stream stream)
{
if (stream == null)
{
throw new NBT_InvalidArgumentNullException();
}
//Clear the current content in the dictionary
this.Clear();
bool exit = false;
while (exit != true)
{
//Read the tag ID
byte id = TagByte.ReadByte(stream);
//If tagID = 0 (TagEnd), is the end of the list and we close the list.
if (id == TagTypes.TagEnd)
{
exit = true;
}
if (exit != true)
{
//Read the Key (unique name of the tag in this TagCompound list)
string tagEntry_Key = TagString.ReadString(stream);
//Read the value (the tag)
//See bellow to see the ReadTag code
Tag tagEntry_Value = Tag.ReadTag(stream, id);
//Add the tag with its key to the dictionary inside the TagCompound
this.value.Add(tagEntry_Key, tagEntry_Value);
}
}
}
此函数在抽象类 Tag 中,其功能很简单,就是创建一个与 id 参数匹配的标签实例。
internal static Tag ReadTag(Stream stream, byte id)
{
switch (id)
{
case TagTypes.TagEnd:
return new TagEnd();
case TagTypes.TagByte:
return new TagByte(stream);
case TagTypes.TagShort:
return new TagShort(stream);
.
.
.
.
}
}
思路很简单,每个标签负责从输入流加载数据,并保存数据。
示例代码
frmMain.cs
//
// the following sample show how you can store
// a undefined number of TagStrings into the main TagCompound.
//
//We need import the library.
using NBT.Tags;
using NBT.IO;
public partial class frmMain : Form
{
//We create a instance of NBTFile to manage the data file.
NBTFile nbtFile = new NBTFile();
//Path where we found the nbt file.
string filePath = Application.StartupPath + @"\test.nbt";
public frmMain()
{
InitializeComponent();
}
private void frmMain_Load(object sender, EventArgs e)
{
if (File.Exists(this.filePath) == true)
{
//We open the file using the function LoadFromFile, if you don't use
//a file, because you use a stream, you can use the function LoadFromStream.
this.nbtFile.Load(this.filePath);
//Reload the list
this.ReloadList();
}
}
private void btnSave_Click(object sender, EventArgs e)
{
//Save the current data into the specified file.
this.nbtFile.Save(this.filePath);
}
private void btnAdd_Click(object sender, EventArgs e)
{
//Save a new TagString into the main TagCompound. (The key must be unique)
this.nbtFile.RootTag.Add(this.txtKey.Text, new TagString(this.txtValue.Text));
this.ReloadList();
}
private void ReloadList()
{
this.lstItems.Items.Clear();
//Retrieve all items stored in the main TagCompound
foreach (KeyValuePair<string,> item in this.nbtFile.RootTag)
{
//Check if the tag is a TagString to retrieve its value,
//but it isn't necessary if you use ToString()
if (item.Value.GetType() == typeof(TagString))
{
ListBoxItem lstItem = new ListBoxItem();
lstItem.Text = ((TagString)item.Value).value;
lstItem.Tag = item.Key;
this.lstItems.Items.Add(lstItem);
}
}
}
private void btnDelete_Click(object sender, EventArgs e)
{
if (this.lstItems.SelectedItems.Count > 0)
{
ListBoxItem selectedItem = (ListBoxItem)this.lstItems.SelectedItem;
//Delete the selected key
this.nbtFile.RootTag.Remove((string)selectedItem.Tag);
this.ReloadList();
}
}
}
public class ListBoxItem
{
private string visibleText = "";
private object itemTag = null;
public string Text
{
get
{
return this.visibleText;
}
set
{
this.visibleText = value;
}
}
public object Tag
{
get
{
return this.itemTag;
}
set
{
this.itemTag = value;
}
}
public ListBoxItem()
{
}
public override string ToString()
{
return this.visibleText;
}
}
我的免费图形工具,用于编辑任何 NBT 文件。
您可以直接通过此链接下载,以测试您自己的 NBT 文件。从我的 Skydrive 下载 NBT Maker(免费软件)
可能的用法
我最近编写了一些程序,它们使用此格式存储数据。其中包括一个局域网唤醒程序,它使用 TagCompound
将计算机存储在目录中。
结论
我认为这种格式虽然非常简单,但功能强大,因为它允许存储任何数据。此外,它按名称和子目录组织的事实是一个很棒的特性,应该考虑到分层存储数据。
历史
- 2012年7月27日
- 首次发布
- 2012年8月11日
- 新增 4 种标签类型(TagSByte | TagUShort | TagUInt | TagULong)
- 2012年10月13日
- 新增 3 种标签类型(TagShortArray | TagDateTime | TagTimeSpan)
- Load/Save 函数已重载
- 2012年12月26日
- 新增 8 种标签类型:(TagLongArray | TagFloatArray | TagDoubleArray | TagSByteArray | TagUShortArray | TagUIntArray | TagULongArray | TagImageArray)
- 修复了小 bug
- 2013年1月26日
- 新增示例(NBT 电话簿 - 支持联系人图片)
- 2013年3月31日
- 修复了 TagImageArray 错误
- 2013年5月25日
- 添加了相等函数
- 在写入 nbt 文件时,每个标签数组中都添加了空保护
- 2014年10月18日
- 修复了 TagCompound 克隆
- 修复了 TagEnd 相等
- 更新至 Microsoft .Net 4.5.1
- 添加了 ZLib 压缩
- 2015年2月16日
- 修复了使用 ZLib 压缩包含空 TagString 的数据时出现的问题
- 一些小改动