5

小さなマルチプレイヤー ゲーム用のプロトコルを実装しました。これはバイトに基づいていたので、受信したメッセージをデシリアライズするには、バイト ストリームを繰り返し処理し、ビットごとに解析する必要がありました。すべてのバイトを取得し、メッセージの種類を把握した後、未加工のバイトからプロトコル データ ユニットを構築するリバース コンストラクターにバイトを投入しました。

このプロセス全体は非常に醜く、実際には OO ではなく、判読できないif/elseコードがありました。reverseConstructor(byte[] bytes)追加したすべてのプロトコル データ ユニット (pdu) を実装する必要がありました。ある種のスキーマが pdu ごとに定義されているアプローチ (たとえば、スキーマ = [1 バイトの int (id = x)、x バイトの ascii 文字列、4 バイトの double])、およびバイトの処理がそのスキーマで行われるアプローチは、次のようになります。よりエレガントに。

ここで、Googleのprotobufを使用するためのヒントを得ました (protobuf標準に準拠するにはプロトコルを変更する必要があるため、明らかにそれらは私のニーズに合わないようです)。

情報

プロトコルを変更できません。2 つの異なるシナリオがあります (同時に、または同じプログラム内でさえもサポートしたくありません)。

  • プロトコル データ ユニットには、ヘッダーにエンコードされた長さフィールドがあります。
  • プロトコル データ ユニットには長さフィールドはありませんが、メッセージがいつどこで終了するかをメッセージ タイプから導き出すことができます。

私は個人的に長さフィールドのファンです。しかし、他の誰かが設計したプロトコルに従わなければならない場合もあります。したがって、プロトコルは修正されています。それらはすべて、プロトコル ID、一意のメッセージ ID、および最初のシナリオでは長さフィールドを含むヘッダーを持っています。

質問

効率的で一般的な受信メソッドによって解析される 2 つの単純なプロトコル データ ユニットを使用した非常に小さな例を誰か教えてもらえますか? protobuf チュートリアルで見つけた唯一の例は、ユーザー a がメッセージ x を送信し、ユーザー b がメッセージ X を予期し、問題なく逆シリアル化できるタイプのものだけでした。

しかし、ユーザー b がメッセージ x、y、z に対して準備をしなければならない場合はどうなるでしょうか。コードをあまり重複させずに、インテリジェントな方法でこの状況を処理するにはどうすればよいでしょうか。

また、extern ライブラリを使用せずに、ここでより優れたコードを実現できるようにする設計原則へのヒントもいただければ幸いです。


編集

私はそのような sth が行く方法だと思います。コードの詳細については、こちらを参照してください。オブジェクトが見つかるまでバイトが動的に読み取られ、その後バッファの位置がリセットされます。

                while (true) {
                        if (buffer.remaining() < frameLength) {
                                buffer.reset();
                                break;
                        }
                        if (frameLength > 0) {
                                Object resultObj = prototype.newBuilderForType().mergeFrom(buffer.array(), buffer.arrayOffset() + buffer.position(), frameLength).build();
                                client.fireMessageReceived(resultObj);
                                buffer.position(buffer.position() + frameLength);
                                buffer.mark();
                        }
                        if (buffer.remaining() > fieldSize) {
                                frameLength = getFrameLength(buffer);
                        } else {
                                break;
                        }
                }

JavaDoc - mergeFrom

データをこのタイプのメッセージとして解析し、作成中のメッセージとマージします。これは、MessageLite.Builder.mergeFrom(CodedInputStream) の小さなラッパーです。 https://developers.google.com/protocol-buffers/docs/reference/java/com/google/protobuf/Message.Builder#mergeFrom(byte[])

問題はこのタイプの部分メッセージですが、一般的なアプローチでこの問題に対処できるはずです。


サンプル

サンプルのプロトコル データ ユニットを次に示します。長さフィールドがあります。pdus に長さフィールドがない別のシナリオがあります。この PDU は可変サイズです。固定サイズの pdu もあります。

サンプル PDU

完全を期すために。ここでは、プロトコル データ ユニットでの文字列の表現を示します。

pdu の文字列

4

7 に答える 7

7

1.プロトコル設計

率直に言って、それ以上の変更を念頭に置いていない最初のプロトコル実装を作成するのはよくある間違いです。演習として、柔軟なプロトコルを設計してみましょう。

ここに画像の説明を入力

基本的には、いくつかのフレームを互いにカプセル化するという考え方です。ペイロード IDが利用可能であるため、シーケンス内の次のフレームを簡単に識別できることに注意してください。

Wiresharkを使用して、実際のプロトコルが通常同じ原則に従っていることを確認できます。

ここに画像の説明を入力

このようなアプローチにより、パケットの分析が大幅に簡素化されますが、他のプロトコルを処理することも可能です。

2. プロトコル解読(解剖)

私は、前の会社で次世代ネットワーク アナライザの開発にかなりの時間を費やしました。

ここに画像の説明を入力

すべての詳細を公開することはできませんが、重要な機能の 1 つは、プロトコル フレームを識別できる柔軟なプロトコル スタックでした。下位層 (通常は UDP) の次のフレームには RTP フレームがないため、RTPは良い例です。特別な VM は、ディセクタを実行し、プロセスを制御するために開発されました。

幸いなことに、私は Java ベースのディセクタを使用した小規模な個人プロジェクトを持っています (いくつかの行を節約するために、いくつかの javadoc を省略します)。

/**
 * High-level dissector contract definition. Dissector is meant to be a simple
 * protocol decoder, which analyzes protocol binary image and produces number
 * of fields.
 *
 * @author Renat.Gilmanov
 */
public interface Dissector {

    /**
     * Returns dissector type.
     */
    DissectorType getType();

    /**
     * Verifies packet data belongs to the protocol represented by this dissector.
     */
    boolean isProtocol(DataInput input, Dissection dissection);

    /**
     * Performs the dissection.
     */
    Dissection dissect(DataInput input, Dissection dissection);

    /**
     * Returns a protocol which corresponds to the current dissector.
     *
     * @return a protocol instance
     */
    Protocol getProtocol();
}

プロトコル自体は上位層のプロトコルを認識しているため、利用可能な直接的なヒントがない場合は、既知のプロトコルを反復処理し、isProtocolメソッドを使用して次のフレームを識別することができます。

public interface Protocol {

    // ...

    List<Protocol> getUpperProtocols(); }

私が言ったように、RTP プロトコルは扱いが少し難しいです:

ここに画像の説明を入力

それでは、実装の詳細を確認しましょう。検証は、プロトコルに関するいくつかの既知の事実に基づいています。

/**
 * Verifies current frame belongs to RTP protocol.
 *
 * @param input data input
 * @param dissection initial dissection
 * @return true if protocol frame is RTP
 */
@Override
public final boolean isProtocol(final DataInput input, final Dissection dissection) {
    int available = input.available();
    byte octet    = input.getByte();
    byte version  = getVersion(octet);
    byte octet2   = input.getByte(1);
    byte pt       = (byte) (octet2 & 0x7F);

    return ((pt < 0x47) & (RTP_VERSION == version));
}

解剖は基本的な操作のセットです。

public final Dissection dissect(DataInput 入力, 解剖 d) {

    // --- protocol header --------------------------------
    final byte octet1 = input.getByte(0);
    final byte version = getVersion(octet1);
    final byte p = (byte) ((octet1 & 0x20) >> 5);
    final byte x = (byte) ((octet1 & 0x10) >> 4);
    final byte cc = (byte) ((octet1 & 0x0F));

    //...

    // --- seq --------------------------------------------
    final int seq = (input.getInt() & 0x0000FFFF);
    final int timestamp = input.getInt();
    final int ssrc = input.getInt();

最後に、プロトコル スタックを定義できます。

public interface ProtocolStack {

    String getName();

    Protocol getRootProtocol();

    Dissection dissect(DataInput input, Dissection dissection, DissectOptions options);
}

内部では、すべての複雑さを処理し、フレームごとにパケットをデコードします。最大の課題は、解剖プロセスを防弾で安定させることです。そのようなまたは同様のアプローチを使用すると、プロトコルデコードコードを整理できます。isProtocolの適切な実装により、異なるバージョンなどを処理できる可能性があります。いずれにせよ、このアプローチは単純とは言えませんが、多くの柔軟性と制御を提供します。

3. 普遍的な解決策はありますか?

はい、ASN.1があります:

Abstract Syntax Notation One (ASN.1) は、電気通信およびコンピューター ネットワーキングでデータを表現、エンコード、送信、およびデコードするためのルールと構造を記述する標準および表記法です。正式なルールにより、マシン固有のエンコーディング手法に依存しないオブジェクトの表現が可能になります。正式な表記法を使用すると、データ表現の特定のインスタンスが仕様に従っているかどうかを検証するタスクを自動化できます。つまり、検証にはソフトウェア ツールを使用できます。

以下は、ASN.1 を使用して定義されたプロトコルの例です。

FooProtocol DEFINITIONS ::= BEGIN

    FooQuestion ::= SEQUENCE {
        trackingNumber INTEGER,
        question       IA5String
    }

    FooAnswer ::= SEQUENCE {
        questionNumber INTEGER,
        answer         BOOLEAN
    }

END

ところで、利用可能なJava Asn.1 コンパイラがあります。

JAC (Java Asn1 Compiler) は、(1) asn1 ファイルを解析し、(2) .java クラスを作成し、(3) クラスのインスタンスをエンコード/デコードする場合のツールです。すべての asn1 バイト ストリームを忘れて、OOP を活用してください。BER、CER、DER はすべてサポートされています。

ついに

私は通常、可能な限り最善の解決策を見つけるために、いくつかの簡単な PoC を行うことをお勧めします。複雑さを軽減し、最適化の余地を残すために、ASN.1 を使用しないことにしましたが、役に立つかもしれません。

とにかく、できる限りのことを試して、結果をお知らせください:)

次のトピックも確認できます:バイナリおよびテキスト構造 (パケット) の効率的なデコード

4. アップデート: 双方向アプローチ

かなり長い回答で申し訳ありません。可能な限り最善の解決策を見つけるために十分なオプションを用意してほしいだけです。双方向アプローチに関する質問への回答:

  • オプション 1 : 対称シリアライゼーション アプローチを使用できます。DataOutput を定義し、シリアライゼーション ロジックを記述します。これで完了です。BerkeleyDB API とTupleBindingを調べることをお勧めします。ストア/復元手順を完全に制御することで、同じ問題を解決します。

このクラスは、エントリを TupleInput および TupleOutput オブジェクトとの間で変換します。その 2 つの抽象メソッドは、タプルとキー オブジェクトまたはデータ オブジェクトとの間で変換するために、具体的なサブクラスによって実装する必要があります。

entryToObject(TupleInput)
objectToEntry(Object,TupleOutput)
  • オプション 2 : 最も一般的な方法は、一連のフィールドを含む構造を定義することです。すべてのフィールドには、次の情報が必要です。
    • 名前
    • タイプ
    • サイズ (ビット)

たとえば、RTP の場合は次のようになります。

Version:          byte (2 bits)
Padding:          bool (1 bit)
Extension:        bool (1 bit)
CSRC Count:       byte (4 bits) 
Marker:           bool (1 bit)
Payload Type:     byte (7 bits)
Sequence Number:  int  (16 bits)

そのような構造を読み書きする一般的な方法を定義できます。私が知っている最も近い実例はJavolution Structです。目を通してください、彼らは本当に良い例を持っています:

class Clock extends Struct { // Hardware clock mapped to memory.
     Unsigned16 seconds  = new Unsigned16(5); // unsigned short seconds:5 bits
     Unsigned16 minutes  = new Unsigned16(5); // unsigned short minutes:5 bits
     Unsigned16 hours    = new Unsigned16(4); // unsigned short hours:4 bits
     ...
 }
于 2013-08-22T21:10:11.883 に答える
2

(注: Java を使用してからしばらく経っているので、これは C# で記述しましたが、一般的な考え方は理解できるはずです)

一般的な考え方は次のとおりです。

  1. 各パーサーは、基本的に、次のようなシグネチャを持つインターフェイス、またはデリゲート (またはメソッド、または関数ポインター) として表す必要があります。

    interface IParser<T>
    {   
         IParserResult<T> Parse(IIndexable<byte> input);
    }
    
  2. 解析操作の結果はIParserResult<T>インターフェイスのインスタンスであり、次のことがわかります。

    • 解析が成功したかどうか、

    • 失敗した場合、失敗した理由 (解析を完了するのに十分なデータがない、適切なパーサーではない、CRC エラー、または解析中の例外)、

    • 成功した場合、実際に解析されたメッセージ値、

    • 成功した場合は、次のパーサー オフセット。

    つまり、次のようなものです。

    interface IParserResult<T>
    {
         boot Success { get; } 
         ErrorType Error { get; } // in case it failed
         T Result { get; } // null if failed
         int BytesToSkip { get; } // if success, number of bytes to advance 
    }
    
  3. パーサー スレッドは、パーサーのリストを繰り返し処理し、結果を確認する必要があります。多かれ少なかれ次のようになります。

    // presuming inputFifo is a Queue<byte> 
    while (inputFifo.ContainsData) 
    {
         foreach (IParser parser in ListOfParsers) 
         {
             var result = parser.Parse(inputFifo);
    
             if (result.Success) 
             {
                 FireMessageReceived(result.Value);
                 inputFifo.Skip(result.BytesToSkip);
                 break;
             }
    
             // wrong parser? try the next one
             if (result.ErrorType == ErrorType.UnsupportedData)
             {
                 continue;
             }
    
             // otherwise handle errors
             switch (result.ErrorType) 
             {
                 ...
             }
         }
    }
    

このIIndexable<byte>インターフェイスは .NET の一部ではありませんが、多数の配列割り当てを回避するために重要です (これはCodeProject の記事です)。

このアプローチの良い点は、Parseメソッドが特定のメッセージを「サポート」しているかどうかを判断するために多くのチェックを実行できることです (Cookie、長さ、crc などをチェックします)。信頼性の低い接続から別のスレッドで常に受信されるデータを解析するときにこのアプローチを使用するため、長さが短すぎてメッセージが有効かどうかを判断できない場合、各パーサーは「NotEnoughData」エラーも返します (この場合、ループが中断され、次のデータを待ちます)。

[編集]

さらに (これも役立つ場合)、厳密に型指定され、特定のパーサー/メッセージ タイプに結び付けられた「メッセージ コンシューマー」のリスト (正確には辞書) を使用します。このようにして、特定のメッセージが解析されたときに、関係者だけに通知されます。これは基本的に、パーサーのリストとマッピングの辞書 (メッセージ タイプ -> メッセージ コンシューマー) を作成する必要がある単純なメッセージング システムです。

于 2013-08-19T12:12:22.073 に答える
0

x、y、z のオプションのメッセージを含むメッセージ (myMessage など) を作成します。これについては、こちらで説明しています。つまり、テクニック ドキュメントの Foo、Barr、Baz の例です。

message OneMessage {
    // One of the following will be filled in.
    optional Foo foo = 1;
    optional Bar bar = 2;
    optional Baz baz = 3;
}
于 2013-08-16T13:25:04.847 に答える
0

プロトコル バッファは、バイナリ タグと値のペアに基づいて独自のワイヤ プロトコルを定義します。変更できない既存のプロトコルがある場合、Protocol Buffers を使用してそれを解析することはできません。

于 2013-08-16T21:10:16.147 に答える
0

これは役に立たないかもしれませんが、非常によく似たプロトコルを使用する quake では、アルゴリズムは次のようになります (レシーバー/サーバーはプレイヤー ID を既に認識しています)。

ByteBuffer frame; 

int first = frame.getInt(), magic = 0x4C444732;

if(  first != magic  )
   if( !player_list.containsKey(first) )  /* must be a "string" pdu (a chat)*/
      x = new StringPDU( frame );
   else                                  /* not a chat? must be a player_id */
      x = new PlayerNamePDU( frame ); 

else                                     /* starts with magic... a game startup pdu + playername
   x = new GamePDU( frame );             /*  maybe that's the player host, or must have at least
                                             one player */ 

各 PDU には、ByteBuffer からバイトを読み取る readFrame メソッドまたはコンストラクターがあります。見栄えは悪いですが、リフレクションを使用する必要はありません。

class GamePDU extends PDU {

    byte  command; 
    short length; 
    byte  min_players;

    short time_to_start; 
    byte  num_players;  /// after this same as a player_name packet


    GamePDU( ByteBuffer b ) {
        command = b.readByte();
        length = b.readShort();
        min_players = b.readByte();
        time_to_start = b.readShort();
        num_players = b.readByte();
        // the rest of the frame is player name
        /// players_for_game.add( new PlayerPDU(b) );
        ///  this player is in the game_start pdu to ensure that the 
        //// player_list[ num_players ] has been allocated. and has a list head. ;) Whips!!
    }
    /** if the same code is reading/writing both ends, you don't have to worry about
        endianess or signededness. ;)
        In C, in parallel, some of the game code just whips!!!
       */
}

class PDU {}
class GamePDU  extends PDU {}
class PlayerNamePDU  extends PDU {}
class StringPDU  extends PDU {}
于 2013-08-24T08:20:54.613 に答える