protobufプロトコル互換処理

5625 ワード

ターゲット:
protobufがプロトコル互換性をどのようにしているかを理解します.
質問:
protobufでは、どのようにして新旧プロトコル伝送の互換性を実現しますか.例えば、(問題1)新しいプロトコルにフィールドを追加し、他方に伝送するにはどのように復号しますか.(問題2)例えば、新しいプロトコルでフィールドを削除し、他方に転送すると復号化されますか?(以下、説明の便宜上、新しいプロトコルを使用する側がサービス側、古いプロトコル側がクライアント)
問題1:新しいプロトコルにフィールドを追加し、古いプロトコルを使用するクライアントはどのように復号されますか?
テスト方法:まず、サービス側でprotoファイルを作成し、protoファイルをクライアントにコピーします.次に、サービス側のprotoファイルを変更してフィールドを追加し、コードを再生成します.クライアントは元のprotoファイルとコードを使用します.
ステップ1:protoファイルの作成
この問題について、私たちのprotoファイルPersonMsgを定義する例を書きます.proto
syntax ="proto3";

package com.simple;

option java_package="com.simple";
option java_outer_classname="PersonMsg";

message Person{
    string name=1;
    int32 age=2;
}

ステップ2:protoファイルをクライアントにコピーします.
クライアントとサービス側にprotoファイルがあります
ステップ3:protocコマンドを呼び出してjavaコードを生成する
私のサービス側とクライアントはjava言語を使用しているので、javaコードとして生成されます.(ここではコードが貼られていません)
ステップ4:サービス側のprotoファイルにフィールドを追加する
ここではemailのフィールドを追加します
syntax ="proto3";

package com.simple;

option java_package="com.simple";
option java_outer_classname="PersonMsg";

message Person{
    string name=1;
    int32 age=2;
    string email=3;//     
}

次にprotocコマンドを呼び出して、サービス側のプロトコルコードを再生成します.
手順5:テストコードを作成し、新しいプロトコルを使用したPersonオブジェクトを作成し、ファイルにオブジェクトをシーケンス化します.
@Test
    public void testSerilize() throws IOException {
        Person.Builder builder = Person.newBuilder();
        
        builder.setName("xiaoshan");
        builder.setAge(20);
        builder.setEmail("[email protected]");
        
        Person person =builder.build();
        
        byte[] byteArray = person.toByteArray();

        FileOutputStream outstream = new FileOutputStream(new File("Person.txt"));
        
        outstream.write(byteArray);
        outstream.close();
    }

ステップ6:クライアントでPersonを読み出す.txtファイルは、Personオブジェクトに逆シーケンス化されます.
    @Test
    public void testDeserialize() throws Exception{
        FileInputStream inputStream = new FileInputStream(new File("Person.txt"));
        
        Person person = Person.parseFrom(inputStream);
        inputStream.close();
        assertEquals(person.getName(),"xiaoshan");
        assertEquals(person.getAge(),20);
        // proto         email  ,     java       
        
    }

junitを実行し、テストはokです.次の問題はprotobufがどのようにしてやったのかということです.タイプと長さを知っていれば、これらのフィールドをスキップする方法を知ることができると思います.protobufのやり方を紹介します.
private Person(
        com.google.protobuf.CodedInputStream input,
        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
        throws com.google.protobuf.InvalidProtocolBufferException {
      this();
     ...........    
      try {
        boolean done = false;
        while (!done) {
          int tag = input.readTag();
          switch (tag) {
            case 0:
              done = true;
              break;
            default: {
              if (!parseUnknownFieldProto3(
                  input, unknownFields, extensionRegistry, tag)) {//          
                done = true;
              }
              break;
            }
            case 10: {
              java.lang.String s = input.readStringRequireUtf8();//   name  

              name_ = s;
              break;
            }
            case 16: {

              age_ = input.readInt32();//   age  
              break;
            }
          }
        }
........


ここで重要なのはparseUnknownFieldProto 3メソッドで、streamとunknownFieldのbuilderを伝えています.
  protected boolean parseUnknownFieldProto3(
      CodedInputStream input,
      UnknownFieldSet.Builder unknownFields,
      ExtensionRegistryLite extensionRegistry,
      int tag)
      throws IOException {
    if (input.shouldDiscardUnknownFieldsProto3()) {//           ,       unknownFieldSet 
      return input.skipField(tag); //  key           
    }
    return unknownFields.mergeFieldFrom(tag, input);
  }

私たちはinputを見続けます.skipFieldの内容
@Override
    public boolean skipField(final int tag) throws IOException {
      switch (WireFormat.getTagWireType(tag)) {
        case WireFormat.WIRETYPE_VARINT:
          skipRawVarint();
          return true;
        case WireFormat.WIRETYPE_FIXED64:
          skipRawBytes(FIXED64_SIZE);
          return true;
        case WireFormat.WIRETYPE_LENGTH_DELIMITED:  //        string   email,       
          skipRawBytes(readRawVarint32());
          return true;
        case WireFormat.WIRETYPE_START_GROUP:
          skipMessage();
          checkLastTagWas(
              WireFormat.makeTag(WireFormat.getTagFieldNumber(tag), WireFormat.WIRETYPE_END_GROUP));
          return true;
        case WireFormat.WIRETYPE_END_GROUP:
          return false;
        case WireFormat.WIRETYPE_FIXED32:
          skipRawBytes(FIXED32_SIZE);
          return true;
        default:
          throw InvalidProtocolBufferException.invalidWireType();
      }
    }

予想通りkeyのタイプに応じて対応する処理を行います.私たちの例で新しく追加されたemailはstringタイプなので、WireFormatを歩いています.WIRETYPE_LENGTH_DELIMITEDは、stringタイプのシーケンス化フォーマットは、key+length+valueであるため、readRawVarint 32を呼び出してstringの長さを読み取るには、後でskipRawBytesを呼び出してスキップすればよいと判断した.
注意:
proto 2では、未知にシーケンス化されたフィールドはunknowFieldに格納されることに同意しますが、proto 3はこれを保証しません.上記の例では未知のフィールドが放棄された場合、ソースコードを確認すると、proto 3のCodedInputStreamにexplictDiscardUnknownFieldsフィールド制御が追加されたか、unknowFieldに格納されたかがわかります.だからproto 3ではunknownFieldにあまり頼らないでください.
残りの問題2は読者に自分で考えさせましょう.(答えは削除されたフィールドはシーケンス化されないので、そちらも読めません.PersonMsgのgerSerializedSizeメソッドを具体的に見てください)