web-dev-qa-db-ja.com

バイナリシリアル化ストリームの内容を分析する方法は?

比較的複雑な(ゲーム)オブジェクト構造のファイルに状態情報を格納するための一時的なメカニズムとして、バイナリシリアル化(BinaryFormatter)を使用しています。ファイルは予想よりもはるかに大きく大きく出ており、データ構造には再帰参照が含まれています-したがって、BinaryFormatterが実際に複数のコピーを格納しているかどうか疑問に思っています同じオブジェクトの、または私の基本的な「必要なオブジェクトと値の数」の算術演算がベースから大きく外れているかどうか、または過剰なサイズがどこから来ているか。

スタックオーバーフローを検索すると、Microsoftのバイナリリモート形式の仕様を見つけることができました: http://msdn.Microsoft.com/en-us/library/cc236844(PROT.10).aspx

私が見つけられないのは、binaryformatter出力ファイルの内容を「覗き見」できる既存のビューアです。ファイル内のさまざまなオブジェクトタイプのオブジェクト数と合計バイト数などを取得します。

これは私の「google-fu」が私を失敗させているに違いないと思います(私が持っているものはほとんどありません)-誰か助けてもらえますか?これは必須以前に行われたことがありますか?


[〜#〜] update [〜#〜]:見つからず、回答も得られなかったので、比較的迅速にまとめました(ダウンロード可能なリンク以下のプロジェクト); BinaryFormatterが同じオブジェクトの複数のコピーを格納していないことは確認できますが、ストリームにかなりの量のメタデータが出力されます。効率的なストレージが必要な場合は、独自のカスタムシリアル化メソッドを構築してください。

35
Tao

誰かが興味を持っているかもしれないので、この投稿を行うことにしましたシリアル化された.NETオブジェクトのバイナリ形式はどのように見え、どのように正しく解釈できますか?

私はすべての調査を 。NET Remoting:Binary Format Data Structure 仕様に基づいています。



クラスの例:

実用的な例として、Aという単純なクラスを作成しました。このクラスには、1つの文字列と1つの整数値の2つのプロパティが含まれ、SomeStringおよびSomeValueと呼ばれます。

クラスAは次のようになります:

[Serializable()]
public class A
{
    public string SomeString
    {
        get;
        set;
    }

    public int SomeValue
    {
        get;
        set;
    }
}

シリアル化には、もちろんBinaryFormatterを使用しました。

BinaryFormatter bf = new BinaryFormatter();
StreamWriter sw = new StreamWriter("test.txt");
bf.Serialize(sw.BaseStream, new A() { SomeString = "abc", SomeValue = 123 });
sw.Close();

ご覧のとおり、A123を値として含むクラスabcの新しいインスタンスを渡しました。



結果データの例:

シリアル化された結果を16進エディターで見ると、次のようになります。

Example result data



結果データの例を解釈しましょう:

上記の仕様(PDFへの直接リンクは次のとおりです: [MS-NRBF] .pdf )によると、ストリーム内のすべてのレコードはRecordTypeEnumerationで識別されます。セクション2.1.2.1 RecordTypeNumerationは次のように述べています:

この列挙は、レコードのタイプを識別します。各レコード(MemberPrimitiveUnTypedを除く)は、レコードタイプの列挙で始まります。列挙のサイズは1バイトです。



SerializationHeaderRecord:

したがって、取得したデータを振り返ると、最初のバイトの解釈を開始できます。

SerializationHeaderRecord_RecordTypeEnumeration

2.1.2.1 RecordTypeEnumerationに記載されているように、0の値は、2.6.1 SerializationHeaderRecordで指定されているSerializationHeaderRecordを識別します。

SerializationHeaderRecordレコードは、バイナリシリアル化の最初のレコードである必要があります。このレコードには、フォーマットのメジャーバージョンとマイナーバージョン、および最上位オブジェクトとヘッダーのIDが含まれています。

構成:

  • RecordTypeEnum(1バイト)
  • RootId(4バイト)
  • HeaderId(4バイト)
  • MajorVersion(4バイト)
  • MinorVersion(4バイト)



その知識があれば、17バイトを含むレコードを解釈できます。

SerializationHeaderRecord_Complete

00RecordTypeEnumerationを表し、この場合はSerializationHeaderRecordです。

01 00 00 00RootIdを表します

BinaryMethodCallレコードもBinaryMethodReturnレコードもシリアル化ストリームに存在しない場合、このフィールドの値には、シリアル化ストリームに含まれるClass、Array、またはBinaryObjectStringレコードのObjectIdが含まれている必要があります。

したがって、この場合、これは値1ObjectIdである必要があります(データはリトルエンディアンを使用してシリアル化されているため)。

FF FF FF FFHeaderIdを表します

01 00 00 00MajorVersionを表します

00 00 00 00MinorVersionを表します



BinaryLibrary:

指定されているように、各レコードはRecordTypeEnumerationで始まる必要があります。最後のレコードが完成したので、新しいレコードが始まると想定する必要があります。

次のバイトを解釈しましょう:

BinaryLibraryRecord_RecordTypeEnumeration

ご覧のとおり、この例ではSerializationHeaderRecordの後にBinaryLibraryレコードが続きます。

BinaryLibraryレコードは、INT32 ID([MS-DTYP]セクション2.2.22で指定されている)をライブラリ名に関連付けます。これにより、他のレコードがIDを使用してライブラリ名を参照できるようになります。このアプローチは、同じライブラリ名を参照する複数のレコードがある場合にワイヤサイズを削減します。

構成:

  • RecordTypeEnum(1バイト)
  • LibraryId(4バイト)
  • LibraryName(可変バイト数(LengthPrefixedString))



2.1.1.6 LengthPrefixedString ..に記載されているとおり.

LengthPrefixedStringは、文字列値を表します。文字列の前には、UTF-8でエンコードされた文字列の長さがバイト単位で付けられます。長さは、最小1バイトから最大5バイトの可変長フィールドにエンコードされます。ワイヤサイズを最小化するために、長さは可変長フィールドとしてエンコードされます。

この簡単な例では、長さは常に1 byteを使用してエンコードされます。その知識があれば、ストリーム内のバイトの解釈を続けることができます。

BinaryLibraryRecord_RecordTypeEnumeration_LibraryId

0Cは、RecordTypeEnumerationレコードを識別するBinaryLibraryを表します。

02 00 00 00LibraryIdを表し、この場合は2です。



これで、LengthPrefixedStringは次のようになります。

BinaryLibraryRecord_RecordTypeEnumeration_LibraryId_LibraryName

42は、LengthPrefixedStringを含むLibraryNameの長さ情報を表します。

この場合、42(10進数の66)の長さ情報は、次の66バイトを読み取り、それらをLibraryNameとして解釈する必要があることを示しています。

すでに述べたように、文字列はUTF-8でエンコードされているため、上記のバイトの結果は次のようになります。_WorkSpace_, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null



ClassWithMembersAndTypes:

ここでも、レコードが完成しているので、次のレコードのRecordTypeEnumerationを解釈します。

ClassWithMembersAndTypesRecord_RecordTypeEnumeration

05は、ClassWithMembersAndTypesレコードを識別します。セクション2.3.2.1 ClassWithMembersAndTypesは次のように述べています:

ClassWithMembersAndTypesレコードは、Classレコードの中で最も冗長です。これには、メンバーの名前やリモートタイプなど、メンバーに関するメタデータが含まれています。また、クラスのライブラリ名を参照するライブラリIDも含まれています。

構成:

  • RecordTypeEnum(1バイト)
  • ClassInfo(可変バイト数)
  • MemberTypeInfo(可変バイト数)
  • LibraryId(4バイト)



ClassInfo:

2.3.1.1 ClassInfoに記載されているように、レコードは次のもので構成されます。

  • ObjectId(4バイト)
  • 名前(可変バイト数(これもLengthPrefixedString))
  • MemberCount(4バイト)
  • MemberNames(これはLengthPrefixedStringのシーケンスであり、アイテムの数はMemberCountフィールドで指定された値と等しくなければなりません。)



生データに戻って、ステップバイステップで:

ClassWithMembersAndTypesRecord_RecordTypeEnumeration_ClassInfo_ObjectId

01 00 00 00ObjectIdを表します。これはすでに見てきましたが、RootIdSerializationHeaderRecordとして指定されました。

ClassWithMembersAndTypesRecord_RecordTypeEnumeration_ClassInfo_ObjectId_Name

0F 53 74 61 63 6B 4F 76 65 72 46 6C 6F 77 2E 41は、Nameを使用して表されるクラスのLengthPrefixedStringを表します。前述のように、この例では、文字列の長さは1バイトで定義されているため、最初のバイト0Fは、UTF-8を使用して15バイトを読み取ってデコードする必要があることを指定します。結果は次のようになります。StackOverFlow.A-明らかに、名前空間の名前としてStackOverFlowを使用しました。

ClassWithMembersAndTypesRecord_RecordTypeEnumeration_ClassInfo_ObjectId_Name_MemberCount

02 00 00 00MemberCountを表し、両方ともLengthPrefixedStringで表される2つのメンバーが続くことを示しています。

最初のメンバーの名前: ClassWithMembersAndTypesRecord_MemberNameOne

1B 3C 53 6F 6D 65 53 74 72 69 6E 67 3E 6B 5F 5F 42 61 63 6B 69 6E 67 46 69 65 6C 64は最初のMemberNameを表し、1Bは文字列の長さで27バイトの長さであり、結果は次のようになります:<SomeString>k__BackingField

2番目のメンバーの名前: ClassWithMembersAndTypesRecord_MemberNameTwo

1A 3C 53 6F 6D 65 56 61 6C 75 65 3E 6B 5F 5F 42 61 63 6B 69 6E 67 46 69 65 6C 64は2番目のMemberNameを表し、1Aは文字列の長さが26バイトであることを指定します。結果は次のようになります:<SomeValue>k__BackingField



MemberTypeInfo:

ClassInfoの後に、MemberTypeInfoが続きます。

セクション2.3.1.2 - MemberTypeInfoは、構造に次のものが含まれていると述べています。

  • BinaryTypeEnums(長さが可変)

転送されるメンバータイプを表す一連のBinaryTypeEnumeration値。アレイは次のことを行う必要があります。

  • ClassInfo構造のMemberNamesフィールドと同じ数のアイテムがあります。

  • BinaryTypeEnumerationがClassInfo構造のMemberNamesフィールドのメンバー名に対応するように順序付けられます。

  • BinaryTpeEnumの追加情報に応じて、AdditionalInfos(長さは可変)が存在する場合と存在しない場合があります。

| BinaryTypeEnum | AdditionalInfos |
|----------------+--------------------------|
| Primitive | PrimitiveTypeEnumeration |
| String | None |

それを考慮に入れると、ほぼそこにあります... 2つのBinaryTypeEnumeration値が期待されます(MemberNamesに2つのメンバーがあったため)。



ここでも、完全なMemberTypeInfoレコードの生データに戻ります。

ClassWithMembersAndTypesRecord_MemberTypeInfo

01は最初のメンバーのBinaryTypeEnumerationを表し、2.1.2.2 BinaryTypeEnumerationによれば、Stringが期待でき、LengthPrefixedStringを使用して表されます。

00は、2番目のメンバーのBinaryTypeEnumerationを表します。また、仕様によれば、これはPrimitiveです。上記のように、Primitiveの後に追加情報(この場合はPrimitiveTypeEnumeration)が続きます。そのため、次のバイトである08を読み取り、それを2.1.2.3 PrimitiveTypeEnumerationに記載されているテーブルと照合し、4で表されるInt32を期待できることに驚いています。基本的なデータ型に関する他のドキュメントに記載されているバイト。



LibraryId:

MemerTypeInfoLibraryIdが続くと、4バイトで表されます。

ClassWithMembersAndTypesRecord_LibraryId

02 00 00 00は、2であるLibraryIdを表します。



値:

2.3 Class Recordsで指定されているとおり:

クラスのメンバーの値は、セクション2.7で指定されているように、このレコードに続くレコードとしてシリアル化する必要があります。レコードの順序は、ClassInfo(セクション2.3.1.1)構造で指定されているMemberNamesの順序と一致する必要があります。

だからこそ、メンバーの価値観が期待できるようになりました。

最後の数バイトを見てみましょう:

BinaryObjectStringRecord_RecordTypeEnumeration

06BinaryObjectStringを識別します。これは、SomeStringプロパティ(正確には<SomeString>k__BackingField)の値を表します。

2.5.7 BinaryObjectStringによると、次のものが含まれています。

  • RecordTypeEnum(1バイト)
  • ObjectId(4バイト)
  • 値(可変長、LengthPrefixedStringとして表される)



それを知っていると、それを明確に特定できます

BinaryObjectStringRecord_RecordTypeEnumeration_ObjectId_MemberOneValue

03 00 00 00ObjectIdを表します。

03 61 62 63Valueを表します。ここで、03は文字列自体の長さであり、61 62 63abcに変換されるコンテンツバイトです。

2番目のメンバーであるInt32があったことを思い出していただければ幸いです。 Int32が4バイトを使用して表されていることを知っていると、次のように結論付けることができます。

BinaryObjectStringRecord_RecordTypeEnumeration_ObjectId_MemberOneValue_MemberTwoValue

2番目のメンバーのValueである必要があります。 7B16進数は123 10進数に等しく、サンプルコードに適合しているようです。

したがって、ここに完全なClassWithMembersAndTypesレコードがあります。 ClassWithMembersAndTypesRecord_Complete



MessageEnd:

MessageEnd_RecordTypeEnumeration

最後に、最後のバイト0BMessageEndレコードを表します。

99
Markus Safar

Vasiliyは、バージョン管理をより適切に処理し、(圧縮前に)はるかにコンパクトなストリームを出力するために、最終的には独自のフォーマッター/シリアル化プロセスを実装する必要があるという点で正しいです。

しかし、ストリームで何が起こっているのかを理解したかったので、私が望んでいたことを実行する(比較的)クイッククラスを作成しました。

  • ストリームを解析し、オブジェクト名、カウント、サイズのコレクションを構築します
  • 完了すると、検出された内容の簡単な要約が出力されます-ストリーム内のクラス、カウント、合計サイズ

Codeprojectのように見える場所に置くのはあまり役に立たないので、プロジェクトを自分のWebサイトのZipファイルにダンプしました: http://www.architectshack.com/BinarySerializationAnalysis.ashx

私の特定のケースでは、問題は2つあることがわかりました。

  • BinaryFormatterは非常に冗長です(これは既知ですが、範囲を認識していませんでした)
  • クラスで問題が発生しましたが、不要なオブジェクトを保存していたことが判明しました

これがいつか誰かに役立つことを願っています!


更新:Ian Wrightから、元のコードの問題について連絡がありました。ソースオブジェクトに「10進数」の値が含まれているとクラッシュしました。これは修正され、コードをGitHubに移動して、(パーミッシブ、BSD)ライセンスを付与する機会を利用しました。

7
Tao

私たちのアプリケーションは大量のデータを操作します。ゲームと同様に、最大1〜2GBのRAMが必要になる場合があります。同じ「同じオブジェクトの複数のコピーを保存する」という問題が発生しました。また、バイナリシリアル化では、保存するメタデータが多すぎます。最初に実装されたとき、シリアル化されたファイルは約1〜2GBかかりました。今日、私はなんとか値を減らすことができました-50-100MB。私たちは何をしましたか。

簡単な答え-.Netバイナリシリアル化を使用せず、独自のバイナリシリアル化メカニズムを作成します。独自のBinaryFormatterクラスとISerializableインターフェイス(Serialize、Deserializeの2つのメソッド)があります。

同じオブジェクトを複数回シリアル化しないでください。一意のIDを保存し、キャッシュからオブジェクトを復元します。

よろしければ、コードを共有できます。

編集:あなたは正しいようです。次のコードを参照してください-それは私が間違っていたことを証明しています。

[Serializable]
public class Item
{
    public string Data { get; set; }
}

[Serializable]
public class ItemHolder
{
    public Item Item1 { get; set; }

    public Item Item2 { get; set; }
}

public class Program
{
    public static void Main(params string[] args)
    {
        {
            Item item0 = new Item() { Data = "0000000000" };
            ItemHolder holderOneInstance = new ItemHolder() { Item1 = item0, Item2 = item0 };

            var fs0 = File.Create("temp-file0.txt");
            var formatter0 = new BinaryFormatter();
            formatter0.Serialize(fs0, holderOneInstance);
            fs0.Close();
            Console.WriteLine("One instance: " + new FileInfo(fs0.Name).Length); // 335
            //File.Delete(fs0.Name);
        }

        {
            Item item1 = new Item() { Data = "1111111111" };
            Item item2 = new Item() { Data = "2222222222" };
            ItemHolder holderTwoInstances = new ItemHolder() { Item1 = item1, Item2 = item2 };

            var fs1 = File.Create("temp-file1.txt");
            var formatter1 = new BinaryFormatter();
            formatter1.Serialize(fs1, holderTwoInstances);
            fs1.Close();
            Console.WriteLine("Two instances: " + new FileInfo(fs1.Name).Length); // 360
            //File.Delete(fs1.Name);
        }
    }
}

BinaryFormatterはobject.Equalsを使用して同じオブジェクトを検索しているようです。

生成されたファイルの内部を見たことがありますか?コード例から「temp-file0.txt」と「temp-file1.txt」を開くと、メタデータがたくさんあることがわかります。そのため、独自のシリアル化メカニズムを作成することをお勧めしました。

混乱してすみません。

5
Vasyl Boroviak

プログラムをデバッグモードで実行して、コントロールポイントを追加してみてください。

ゲームのサイズやその他の依存関係のためにそれが不可能な場合は、デシリアライズコードを含むシンプルで小さなアプリをいつでもコーディングして、そこのデバッグモードから覗くことができます。

0
Juan Nunez