如何在使用 writeDelimitedTo() 写入的字节流中的格式错误块后挽救数据

How to rescue data after malformed chunk in a byte stream which is written with writeDelimitedTo()

我使用 protobuf-java-util:3.0.0-beta-2.

我创建了一个文件,其中包含许多用 Message#writeDelimitedTo() 编写的 Protobuf 消息。代码是这样的:

Iterable<SomeMessage> messages = getHugeDataSet();
OutputStream os = new FileOutputStream("a_lot_of_messages.protobuf");
for (SomeMessage msg : messages) msg.writeDelimitedTo(os);

我用 Builder#mergeDelimitedFrom() 阅读了这些文件。像这样:

InputStream is = new FileInputStream("a_lot_of_messages.protobuf");
SomeMessage msg1 = SomeMessage.newBuilder().mergeDelimitedFrom(is).build();
SomeMessage msg2 = SomeMessage.newBuilder().mergeDelimitedFrom(is).build();
... // This is simplified - it's implemented as an Iterator in the real code

通过这种方式,我可以毫无问题地读取绝大多数文件,但有时我会遇到这样的异常:

com.google.protobuf.InvalidProtocolBufferException: Protocol message had invalid UTF-8. ...

在有时会断电的移动设备上写入这些文件的代码运行。在这种情况下,我的应用程序会一直写入同一个文件,并且出现异常的可能性很高。因此,显然我的代码在这种情况下会创建一些格式错误的文件。我的代码可以读取文件的某些部分,但由于错误无法读取格式错误的块之后的部分。

现在我需要拯救和读取畸形块之后的数据,但我找不到任何方法来做到这一点。所以,我想知道以下内容:

  1. 有什么方法可以挽救和读取上面代码写的畸形块之后的部分吗?
  2. 如果没有办法做到这一点,我该如何改进我的代码以便我的应用程序能够应对此类问题?有什么最佳做法可以容忍电源故障吗?

完整的异常堆栈跟踪是这样的:

com.google.protobuf.InvalidProtocolBufferException: Protocol message had invalid UTF-8.
    at com.google.protobuf.InvalidProtocolBufferException.invalidUtf8(InvalidProtocolBufferException.java:120) ~[protobuf-java-3.0.0-beta-2.jar:na]
    at com.google.protobuf.CodedInputStream.readStringRequireUtf8(CodedInputStream.java:410) ~[protobuf-java-3.0.0-beta-2.jar:na]
    at com.example.Model$SomeData.<init>(Model.java:14775) ~[my-app-1.0-SNAPSHOT.jar:na]
    at com.example.Model$SomeData.<init>(Model.java:14717) ~[my-app-1.0-SNAPSHOT.jar:na]
    at com.example.Model$SomeData.parsePartialFrom(Model.java:18240) ~[my-app-1.0-SNAPSHOT.jar:na]
    at com.example.Model$SomeData.parsePartialFrom(Model.java:18234) ~[my-app-1.0-SNAPSHOT.jar:na]
    at com.google.protobuf.CodedInputStream.readMessage(CodedInputStream.java:495) ~[protobuf-java-3.0.0-beta-2.jar:na]
    at com.example.Model$SomeMessage.<init>(Model.java:27250) ~[my-app-1.0-SNAPSHOT.jar:na]
    at com.example.Model$SomeMessage.<init>(Model.java:27197) ~[my-app-1.0-SNAPSHOT.jar:na]
    at com.example.Model$SomeMessage.parsePartialFrom(Model.java:28678) ~[my-app-1.0-SNAPSHOT.jar:na]
    at com.example.Model$SomeMessage.parsePartialFrom(Model.java:28672) ~[my-app-1.0-SNAPSHOT.jar:na]
    at com.example.Model$SomeMessage$Builder.mergeFrom(Model.java:27802) ~[my-app-1.0-SNAPSHOT.jar:na]
    at com.example.Model$SomeMessage$Builder.mergeFrom(Model.java:27653) ~[my-app-1.0-SNAPSHOT.jar:na]
    at com.google.protobuf.AbstractMessageLite$Builder.mergeFrom(AbstractMessageLite.java:235) ~[protobuf-java-3.0.0-beta-2.jar:na]
    at com.google.protobuf.AbstractMessage$Builder.mergeFrom(AbstractMessage.java:516) ~[protobuf-java-3.0.0-beta-2.jar:na]
    at com.google.protobuf.AbstractMessage$Builder.mergeFrom(AbstractMessage.java:290) ~[protobuf-java-3.0.0-beta-2.jar:na]
    at com.google.protobuf.AbstractMessageLite$Builder.mergeDelimitedFrom(AbstractMessageLite.java:305) ~[protobuf-java-3.0.0-beta-2.jar:na]
    at com.google.protobuf.AbstractMessage$Builder.mergeDelimitedFrom(AbstractMessage.java:530) ~[protobuf-java-3.0.0-beta-2.jar:na]
    at com.google.protobuf.AbstractMessageLite$Builder.mergeDelimitedFrom(AbstractMessageLite.java:311) ~[protobuf-java-3.0.0-beta-2.jar:na]
    at com.google.protobuf.AbstractMessage$Builder.mergeDelimitedFrom(AbstractMessage.java:522) ~[protobuf-java-3.0.0-beta-2.jar:na]
    at com.example.MyUtils.read(MyUtils.java:54) [my-app-1.0-SNAPSHOT.jar:na]

从原始 protobuf 数据中恢复消息边界是相当有问题的。这通常适用于数据格式,它越小越压缩,它对传输错误的弹性就越小。

最好的方法是在消息的开头总是出现一些字节序列。例如,如果您碰巧有任何像 required string software_version = 1; 这样的几乎恒定的字段,您可以在二进制数据中搜索它,从而找到下一条消息的起点。

如果不是这样,您可以在编写消息时引入这样的标记。例如,选择一些 64 位随机数作为标记。您可以将它单独写入 protobuf 之外的文件,或者您可以在消息中有一个索引为 1 的 fixed64 字段以使其接近开头。

即使标记可能随机出现在您的数据中的其他地方,这也不会导致太大的问题,因为您可以跳过不解析的消息。