65.9K
CodeProject 正在变化。 阅读更多。
Home

命名二进制标签序列化

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (23投票s)

2012年7月26日

CC (ASA 3U)

6分钟阅读

viewsIcon

70287

downloadIcon

2182

本文描述了 NBT 文件格式,并展示了如何在实际应用程序中实现它以存储数据。

 

引言

命名二进制标签(也称为 NBT)是一种非常简单的文件格式,它使用二进制标签来存储数据。这种文件格式由 Markus Persson 创建,用于存储游戏数据《我的世界》。

不幸的是,NBT 格式存在多种压缩格式

  1. 未压缩。
  2. 使用 GZIP 压缩。
  3. 使用 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(图片数组标签) 变量 图片数组。此标签以一个整型作为前缀,表示数组的大小。

文件格式规则

  1. 所有数据均采用大端序。
  2. 所有 NBT 文件必须以 TagCompound 开头。
  3. 所有标签都以一个字节开头,表示其标签类型。
  4. 所有标签(TagEnd 和 TagList 中的项除外)都以 TagString 开头。
  5. 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 的结束)。

使用代码

要使用我的库,需要导入以下命名空间

  1. NBT.IO(此命名空间包含与文件及其压缩相关的所有内容)
  2. NBT.Tags(包含所有支持的标签类型)

有两个命名空间(NBT.ExceptionsNBT.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();            
        }
    }
}
ListBoxItem.cs
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 的数据时出现的问题
    • 一些小改动

相关链接

© . All rights reserved.