【问题标题】:Forward compatibility in storage size constrained protocol存储大小受限协议的前向兼容性
【发布时间】:2023-11-14 17:52:02
【问题描述】:

我有一个简单的协议,由 4 个字段组成:

Field-1 (4-bits)
Field-2 (6-bits)
Field-3 (4-bits)
Field-4 (2-bits)

目前,我将它们组织起来,使它们按字节对齐:

Field-1,Field-3,Field-2,Field-4

消息总共占用 2 个字节,开销为 0 个字节。

为了使其向后兼容,以便我可以理解来自以前版本的消息,我在开头添加了一个 1 字节的版本字段,它变为:

Version-Field,Field-1,Field-3,Field-2,Field-4

总共 3 个字节,开销为 1 个字节。

如何添加前向兼容性,以便我可以在新版本的协议中添加新字段,同时确保旧版本的软件仍然可以理解消息,并且开销尽可能低?

【问题讨论】:

  • 收件人可以在没有版本字段的情况下计算出消息的长度吗?
  • @Rei:不,很遗憾,这不能假设。当然,可以添加一个 size 字段。
  • 不需要,版本字段就足够了。看我的回答。

标签: protocols backwards-compatibility forward-compatibility


【解决方案1】:

通常,您的协议会指定每条消息具有:

  • 适用于所有未来版本的消息长度指示器。这通常是保证足够大的固定大小整数,或者使用扩展位的可变长度编码整数,就像您在 VLQ 或 UTF-8 中看到的那样。
  • 您需要了解以解析消息的最低协议版本的指标。这很重要,因为新版本可能 介绍必须了解的内容。

然后,每个新版本的协议都允许您将新数据添加到符合先前版本协议的前缀,并且每个版本的协议都必须指定如何识别它定义的数据的结尾(在你的例子是固定长度的,所以很容易),以及未来版本中定义的数据的开始。

为了处理消息,消费者检查以确保它是足够高的版本,处理它理解的前缀,并使用长度字段跳过其余部分。

对于像你的协议这样空间受限的东西,我可能会这样做:

  • 第一个字节是 4 位最小版本和 4 位长度字段。

  • 如果长度字段 L 在 0-11 之间,则消息的剩余部分为 L+1 个字节长。

  • 否则第一个字节之后的 L-11 个字节是包含长度的整数。
  • 当您必须了解的最低版本 > 15 时,则版本 15 之前的某些协议版本将在消息中定义附加版本信息。

【讨论】:

    【解决方案2】:

    通过使用此规则确保严格的 BC,您将拥有 FC:

    新版本必须保持以前版本已知的字段布局。

    如果您能遵守规则,您将自动拥有 BC 和 FC。 因此,使用该规则,您只能通过将新字段附加到现有布局来添加新字段。

    让我用一个例子来解释。 假设您需要为版本 2 添加这些字段:

    Field-5 (1-bit)
    Field-6 (7-bits)
    

    记住规则,新字段只能附加到现有布局。 所以,这是版本 2 的消息布局:

    Version-Field,Field-1,Field-3,Field-2,Field-4,Field-5,Field-6
    

    由于版本 1 已知的布局是完整的,因此您的版本 1 代码可以读取任何版本的消息(伪代码):

    function readMessageVersion1(byte[] input) {
        var msg = {};
        msg.version = input[0];
    
        msg.field1 = input[1] & 0x0f;
        msg.field3 = input[1] >> 4 & 0x0f;
    
        msg.field2 = input[2] & 0x3f;
        msg.field4 = input[2] >> 6 & 0x03;
    
        return msg;
    }
    

    版本 1 不需要检查版本字段,因为已知布局是无条件的。 但是,版本 2 和所有其他版本都需要检查版本字段。 假设我们使用值 2 来表示版本 2,这会做(伪代码):

    function readMessageVersion2(byte[] input) {
        var msg = readMessageVersion1(input);
    
        //check version field
        if (msg.version < 2) return msg;
    
        msg.field5 = input[3] & 0x01;
        msg.field6 = input[3] >> 1 & 0x7f;
    
        return msg;
    }
    

    代码中最重要的部分是它重用了以前版本的代码和这个检查:

    if (msg.version < 2) return msg;
    

    版本 3 的代码可以像这样简单地遵循版本 2:

    function readMessageVersion3(byte[] input) {
        var msg = readMessageVersion2(input);
    
        //check version field
        if (msg.version < 3) return msg;
    
        // read the input bytes here
    
        return msg;
    }
    

    将其视为未来版本的模板。 通过遵循规则和示例,任何版本的协议都可以读取来自任何版本的消息,只需 1 字节开销。

    【讨论】: