跳到主要内容

2. 分块传输

如引言所述, 在受限网络中限制数据报大小有充分理由:

  • 最大数据报大小限制, UDP 约为 64 KiB.

  • 避免 IP 分片的需求, IPv6 的 MTU 为 1280.

  • 避免适配层分片的需求, 6LoWPAN [RFC4919] 中约为 60-80 字节.

当资源表示大于单个 CoAP 数据报负载中可舒适传输的大小时, 可以使用 Block 选项指示分块传输. 由于请求和响应都可以携带负载, 本规范为每个负载传输方向提供两个独立选项. 在命名这些选项时, 包括第 4 节中的选项, 数字 1 ("Block1", "Size1") 指与请求相关的资源表示传输, 数字 2 ("Block2", "Size2") 指响应资源表示的传输.

下文中, "payload" 指单个 CoAP 消息中的实际内容, 即正在传输的单个块; "body" 指以分块方式传输的完整资源表示. Content-Format Option 适用于 body, 而不是 payload; 特别是, 块边界可能位于并不按 Content-Format 使用的结构、编码或内容编码来分隔完整单元的位置. 类似地, [RFC7252] 第 5.10.6 节定义的 ETag Option 适用于资源的完整表示, 因而适用于响应 body.

在多数情况下, 为一个 body 传输的所有块除最后一个外大小相同. 如果第一个请求使用的块大小大于接收方偏好的块大小, 后续请求会使用偏好块大小. 块大小不是协议固定值. 为尽可能保持实现简单, Block 选项只支持一小段 2 的幂块大小, 从 24 (16) 到 210 (1024) 字节. 由于 body 往往不能被所选 2 的幂块大小整除, 最后一个块不需要达到该大小; 但即使对最后一个块, Block 选项的块大小字段仍会指示所选 2 的幂大小.

2.1. Block2 和 Block1 选项

+-----+---+---+---+---+--------+--------+--------+---------+
| No. | C | U | N | R | Name | Format | Length | Default |
+-----+---+---+---+---+--------+--------+--------+---------+
| 23 | C | U | - | - | Block2 | uint | 0-3 | (none) |
| | | | | | | | | |
| 27 | C | U | - | - | Block1 | uint | 0-3 | (none) |
+-----+---+---+---+---+--------+--------+--------+---------+

Table 1: Block Option Numbers

Block1 和 Block2 Option 都可以同时出现在请求和响应消息中. 无论哪种情况, Block1 Option 都与请求负载相关, Block2 Option 都与响应负载相关.

因此, 对 [RFC7252] 定义的方法而言, Block1 适用于带负载的 POST 和 PUT 请求及其响应. Block2 适用于 GET、POST 和 PUT 请求及其带负载响应, 即 2.01、2.02、2.04 和 2.05, 参见 [RFC7252] 第 5.5 节.

当 Block1 出现在请求中或 Block2 出现在响应中, 即出现在其所指负载所在的消息中时, 它指示分块传输, 并描述该特定分块负载如何构成正在传输的完整 body 的一部分, 这称为 "描述性用法". 当它出现在相反方向时, 它对该负载将如何形成或已如何处理提供额外控制, 这称为 "控制用法".

任一 Block 选项的实现都是可选的. 然而, 当它出现在 CoAP 消息中时, 必须处理它, 否则拒绝该消息; 因此它被标识为 Critical 选项. 任一 Block 选项在单个消息中禁止出现超过一次.

2.2. Block 选项结构

Block (Block1 或 Block2) 选项可能需要传输三项信息:

  • 块大小 (SZX).

  • 是否还有更多块跟随 (M).

  • 在给定大小的一系列块中, 该块的相对编号 (NUM).

Block 选项的值是可变大小 (0 到 3 字节) 的无符号整数 (uint, 参见 [RFC7252] 第 3.2 节). 该整数值编码这三个字段, 如图 1 所示. 根据 CoAP uint 编码规则, 当 NUM、M 和 SZX 恰好都为零时, 会发送零字节整数.

     0
0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
| NUM |M| SZX |
+-+-+-+-+-+-+-+-+

0 1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| NUM |M| SZX |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

0 1 2
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| NUM |M| SZX |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure 1: Block Option Value

块大小使用三位无符号整数编码, 从 0 表示 24 字节到 6 表示 210 字节. 该字段称为 "SZX" ("size exponent"). 实际块大小为 2**(SZX + 4). SZX 在选项值的最低三位中传输, 即 val & 7, 其中 val 是选项值.

第四个最低有效位 M 或 "more" 位 (val & 8) 指示是否还有更多块跟随, 或当前分块传输是否是正在传输的最后一个块.

选项值除以 16 后得到 NUM 字段, 即当前传输块的序列号, 从零开始. 因此, 当前传输涉及从字节 NUM << (SZX + 4) 开始的 "size" 个字节.

实现说明: 为实现方便, (val & ~0xF) << (val & 7), 即把选项值最后 4 位屏蔽后再左移 SZX 的值, 可得到正在传输块的第一个字节位置.

更具体地说, 在 Block1 或 Block2 Option 的选项值中, 各字段含义定义如下:

NUM: : Block Number, 指示被请求或提供的块编号. 块编号 0 表示 body 的第一个块, 即从 body 的第一个字节开始.

M: : More Flag ("非最后一个块"). 在描述性用法中, 如果该标志未设置, 表示此消息中的负载是 body 中的最后一个块; 如果设置, 表示还有一个或多个额外块可用. 当 Block2 Option 在请求中用于取得特定块编号 (控制用法) 时, M 位必须作为零发送, 接收时忽略. 在响应中的 Block1 Option 中, M 标志用于指示原子性, 见下文.

SZX: : Block Size. 块大小表示为三位无符号整数, 指示以二的幂表示的块大小. 因此, block size = 2**(SZX + 4). SZX 允许值为 0 到 6, 即最小块大小为 2**(0+4) = 16, 最大为 2**(6+4) = 1024. SZX 值 7 表示块大小 2048, 为保留值, 禁止发送; 在请求中收到时必须导致 4.00 Bad Request 响应码.

Block1 和 Block2 Option 没有默认值. 缺少这些选项之一, 就 NUM 和 M 可在选项中给出的值而言, 等价于选项值 0, 即表示当前块是传输的第一个且唯一的块, 块编号为 0 且 M 位未设置. 但是, 与显式值 0 不同, 显式值 0 会指示 SZX 为 0 且大小值为 16 字节; 缺少该选项并不暗示任何具体显式大小, 大小未指定. 与任何 uint 一样, 显式值 0 通过零长度选项高效表示; 因而其语义不同于缺少该选项.

2.3. 请求和响应中的 Block 选项

Block 选项用于三种角色之一:

  • 描述性用法, 即响应中的 Block2 Option, 例如 GET 的 2.05 响应, 或请求中的 Block1 Option, 即 PUT 或 POST:

    • 选项值中的 NUM 字段描述该消息负载中包含的块编号.

    • M 位指示是否还需要传输更多块才能完成该 body 的传输.

    • 如果设置了 M 位, SZX 隐含的块大小必须与负载字节大小匹配. 如果 M 未设置, SZX 不控制负载大小. 对 Block2 而言, 如果请求建议了更大的 SZX 值, 下一请求必须把 SZX 降到响应给出的大小. 其效果是, 如果服务器使用其偏好块大小和请求块大小中的较小者, 则一个 body 的所有块都使用相同块大小.

  • 请求中的 Block2 Option 控制用法, 例如 GET:

    • Block2 Option 中的 NUM 字段给出请求在响应中返回的负载块编号.

    • 在这种情况下, M 位没有功能且必须设置为零.

    • 给出的块大小 (SZX) 建议一个块大小, 在块编号为 0 时; 或重复先前已接收块的块大小, 在块编号非零时.

  • 响应中的 Block1 Option 控制用法, 例如 PUT 或 POST 请求的 2.xx 响应:

    • Block1 Option 的 NUM 字段指示正在确认的块编号.

    • 如果请求中设置了 M 位, 服务器可以选择无状态地分别处理每个块, 也可以原子地处理整个 body 的请求, 或采用二者任意混合.

      • 如果响应中也设置了 M 位, 表示该响应不携带请求的最终响应码, 即服务器正在从同一端点收集更多块, 并计划原子地实现该请求, 例如只在收到最后一个负载块时行动. 在这种情况下, 响应禁止携带 Block2 Option.

      • 相反, 如果响应中的 M 位未设置, 即使请求中设置了它, 也表示该分块请求现在专门针对此块执行, 响应携带该请求的最终响应, 也包括此前在此分块传输序列中响应的 Block1 Option 里 M 位被设置的任何请求; 客户端仍预期继续发送更多块, 后续块的请求方法可以按块执行, 也可以不按块执行. 注意, 资源现在处于部分更新状态; 只有在暴露这种中间状态可接受时, 该方法才适用. 客户端可通过快速继续更新资源来缩短窗口, 或在失败时重新开始更新.

    • 最后, 控制型 Block1 Option 中给出的 SZX 块大小表示服务器针对指向该资源的传输所偏好的最大块大小, 它等于或小于初始交换所用大小; 客户端在该传输序列的所有后续请求中应当使用该块大小或更小块大小, 即使这意味着从此改变块大小, 并可能相应缩放块编号.

使用一个或两个 Block 选项, 单个 REST 操作可以拆分为多个 CoAP 消息交换. 如 [RFC7252] 所规定, 每个消息交换都使用自己的 CoAP Message ID.

随请求或响应发送的 Content-Format Option 必须反映整个 body 的 Content-Format. 如果响应 body 的各块携带不同 Content-Format Option, 如何处理该错误由客户端决定, 通常会中止任何正在进行的分块传输. 如果请求块到达服务器时 Content-Format Option 不匹配, 服务器禁止将它们组装成单个请求; 这通常会在不匹配块上导致 4.08 (Request Entity Incomplete, 第 2.9.2 节) 错误响应.

2.4. 使用 Block2 Option

当请求的响应携带设置了 M 位的 Block2 Option 时, 请求方可以发送带有与初始请求相同选项并包含期望块编号和块大小的 Block2 Option 的后续请求, 以取得资源表示的额外块. 在请求中, 客户端必须把 Block2 Option 的 M 位设置为零, 服务器接收时必须忽略该位.

为影响响应中使用的块大小, 请求方也可以在初始请求中使用 Block2 Option, 给出期望大小、块编号零以及 M 位零. 服务器必须使用所指示块大小或更小大小. 对第一个块之后的任何进一步分块请求, 必须指示服务器在响应第一个使用 Block2 Option 给出期望大小的请求时实际使用的相同块大小.

一旦请求方使用 Block2 Option 并收到带有可能调整过块大小的第一个响应, 单个分块传输中的所有后续请求最终都会收敛到使用相同大小, 但最后一个块可能没有足够内容填满, 即 M 位未设置的块. 注意, 客户端可以在未带 Block2 Option 的第一个请求产生带 Block2 Option 响应后, 从第二个请求开始使用 Block2 Option. 服务器使用请求选项中指示的块大小或更小大小, 但请求方必须记录其初始请求所收到响应中实际使用的块大小, 并在后续请求中继续使用该大小. 服务器行为必须确保这种客户端行为会使序列中的所有响应使用相同块大小, 但 M 位未设置的最后一个响应除外, 如果初始请求未包含 Block2 Option, 第一个响应也可能例外.

分块传输可用于 GET 资源表示完全静态的资源, 例如描述设备的 schema, 也可用于动态变化资源. 在后一种情况下, Block2 Option 应当与 ETag Option ([RFC7252] 第 5.10.6 节) 一起使用, 以确保重新组装的块来自同一版本表示: 服务器应在每个响应中包含 ETag Option. 如果 ETag Option 可用, 客户端在从交换的块重新组装表示时必须比较 ETag Option. 如果 GET 传输中的 ETag Option 不匹配, 请求方可以尝试为其最先取得的块获取新值. 为减少由此产生的低效, 服务器可以为正在进行的一系列请求缓存表示的当前值. 服务器可以通过请求端点与每个分块请求中的 URI 相同这一组合来识别序列. 需要特别注意, 本规范不要求服务器建立任何状态; 但是, 提供快速变化资源的服务器可能因此使客户端永远无法取得一组一致的块. 希望取得资源所有块的客户端应力求不无故延迟. 服务器完全可以预期在最后一次访问状态后经过 EXCHANGE_LIFETIME ([RFC7252] 第 4.8.2 节) 后自由丢弃任何缓存状态, 但不要求始终保留状态这么久.

Block2 Option 没有为单个端点提供一种方式, 让其对同一资源执行多个并发进行的分块响应负载传输操作, 例如 GET. 这很少是需求, 但作为变通方法, 客户端可以改变缓存键, 例如使用访问语义相同资源的多个 URI 之一, 或改变代理安全的可选选项.

2.5. 使用 Block1 Option

在带有请求负载的请求中, 例如 PUT 或 POST, Block1 Option 指请求中的负载, 即描述性用法.

在对带负载请求的响应中, 例如 PUT 或 POST 传输, Block1 Option 中给出的块大小表示服务器对该资源的块大小偏好, 即控制用法. 显然, 此时客户端已经在不知道该偏好的情况下传输了第一个块. 尽管如此, 客户端仍应遵循所指示偏好, 并在所有后续块中使用服务器偏好的块大小或更小大小. 注意, 任何块大小降低都可能意味着第二个请求从大于一的块编号开始, 因为第一个请求按较小大小计算时已经传输了多个块.

为抵消适配层分片对数据包交付概率的影响, 客户端可能希望在达到 MAX_RETRANSMIT 前就放弃重传相对较大负载的请求, 并尝试把请求重新表述为使用更小负载的分块传输. 注意, 这个新尝试是新的消息层事务, 需要新的 Message ID. 由于不确定是请求丢失还是确认丢失, 该策略主要对幂等请求有用.

在请求负载的分块传输中, 例如 PUT 或 POST, 如果目标是在服务器端以原子方式实现, 实际创建/替换会在收到最终块时发生, 即 Block1 Option 中 M 位未设置的块. 在这种情况下, 对非最终块的所有成功响应都携带响应码 2.31 (Continue, 第 2.9.1 节). 如果在处理最终块时服务器没有所有先前块, 传输失败并且必须返回错误码 4.08 (Request Entity Incomplete, 第 2.9.2 节). 服务器也可以对任何非按顺序的 Block1 传输返回 4.08 错误码, 无论该块是否最终块; 因此, 没有特定机制处理这种情况的客户端应始终从块零开始, 并按顺序发送后续块.

客户端遇到 4.08 错误码的一个原因是服务器已经超时并丢弃了正在组装的部分请求 body. 客户端应力求不无故延迟地发送请求所有块. 服务器完全可以预期在最近一个块传输后经过 EXCHANGE_LIFETIME ([RFC7252] 第 4.8.2 节) 后自由丢弃任何部分请求 body; 但不要求服务器始终保留部分请求 body 这么久.

服务器在任何时候如果当前没有资源存储它打算以原子方式实现的分块请求负载传输的块, 都可以返回错误码 4.13 (Request Entity Too Large). 注意, 对未使用 Block1 的请求返回 4.13 是提示客户端尝试发送 Block1; 对带 Block1 Option 的请求返回 4.13 且响应中的 Block1 Option 携带比请求更小的 SZX, 是提示客户端尝试更小 SZX.

服务器以无状态方式实现的请求负载分块传输, 很可能在传输仍在进行时或客户端未完成传输时, 使被操作资源处于不一致状态. 该特征更接近远程文件系统而非 HTTP, 后者在传输期间始终在服务器上保持状态. 可以使用共享文件访问中熟知的技术, 例如客户端专用临时资源, 来缓解与 HTTP 的这种差异.

Block1 Option 没有为单个端点提供一种方式, 让其对同一资源执行多个并发进行的分块请求负载传输操作, 例如 PUT 或 POST. 对同一资源启动新的分块请求序列, 且来自同一端点的旧序列尚未完成, 只会覆盖服务器可能仍保留的上下文. 这在此情况下可能正是期望行为, 因为客户端可能只是已经重启并丢失了前一序列的知识.

2.6. 将分块传输与 Observe Option 结合

Observe option 提供一种方式, 让客户端收到资源随时间变化的通知 [RFC7641]. 客户端观察的资源可能大于单个 CoAP 消息能舒适处理或传输的大小. 以下规则适用于分块传输与通知的组合.

观察关系总是适用于整个资源; Block2 Option 不提供观察资源单个块的方式.

与基本 GET 传输一样, 客户端可以在建立或续订观察关系的 GET 请求中的 Block2 Option 中指示期望块大小. 如果服务器支持分块传输, 它应当记录该块大小, 并把它作为该 GET 请求所产生的所有通知/响应的最大大小, 直到客户端从观察者列表移除, 或服务器收到该客户端对该资源的新 GET 请求而更新该列表中的条目.

发送 2.05 (Content) 通知时, 服务器只发送表示的第一个块. 客户端像该第一个响应由 GET 请求引起一样取得表示的其余部分, 即使用包含 NUM 值大于零的 Block2 Option 的额外 GET 请求. 这会导致传输整个表示, 即使相对于先前通知只有某些块发生变化.

与其他动态变化资源一样, 为确保重新组装的块来自同一版本表示, 服务器应当在每个响应中包含 ETag Option, 重新组装客户端必须比较 ETag Option (第 2.4 节). 与一般 Block2 情况相比, 客户端在收到第一个块通知后如果希望取得资源所有块, 更应力求不无故延迟.

示例见第 3.4 节.

2.7. 组合 Block1 和 Block2

在 PUT, 尤其是 POST 交换中, 请求 body 和响应 body 都可能大到需要使用分块传输. 首先, 请求 body 的 Block1 传输照常进行. 在该分块传输最后一个片段的交换中, 响应携带 Block2 传输的第一个片段 (NUM 为零). 为继续该 Block2 传输, 客户端继续发送类似 Block1 阶段请求的请求, 但省略 Block1 Option, 并包含 NUM 非零的 Block2 请求选项.

为使用了 Block1 的请求取得响应 body 的 Block2 传输必须按顺序执行.

2.8. 组合 Block2 与组播

客户端可以在组播 GET 请求中使用 NUM = 0 的 Block2 Option, 以帮助限制响应大小.

类似地, 对组播 GET 请求的响应如果表示较大, 或希望进一步限制响应大小, 可以使用 NUM = 0 的 Block2 Option.

在两种情况下, 客户端都使用单播交换取得任何后续块; 在单播请求中, 客户端应遵循服务器在组播请求响应中指示的任何块大小偏好.

Block 选项与组播消息结合的其他用法留待进一步研究.

2.9. 响应码

除 [RFC7252] 定义的响应码外, 本规范定义两个响应码并扩展一个响应码的含义.

2.9.1. 2.31 Continue

这个新的成功状态码表示请求 body 此块的传输成功, 服务器鼓励发送更多块, 但整个分块请求的最终结果尚无法确定. 此响应码不返回负载.

2.9.2. 4.08 Request Entity Incomplete

这个新的客户端错误状态码表示服务器尚未收到继续处理所需的请求 body 块. 客户端可能未发送所有块, 未按服务器要求的顺序发送, 或发送时间过早以致服务器已经丢弃这些块.

注意, 手头没有必要块的一个原因可能是 Content-Format 不匹配, 参见第 2.3 节. 实现说明: 当 NUM != 0 且指示的 Content-Format 与资源当前状态预期不同, 服务器可以用该代码拒绝 Block1 传输. 如果它以无状态方式实现传输, 可把该块的 Content-Format 与现有资源的 Content-Format 进行匹配. 如果它以原子方式实现传输, 可把该块与将要替换资源状态的部分重新组装表示进行匹配.

2.9.3. 4.13 Request Entity Too Large

[RFC7252] 第 5.9.2.9 节定义响应码 4.13 (Request Entity Too Large), 含义类似 HTTP 413 "Request Entity Too Large". [RFC7252] 还建议此响应应包含 Size1 Option (第 4 节), 用以指示服务器能够并愿意处理的请求实体最大大小, 除非服务器无法提供该信息.

本规范允许服务器在 Block1 传输期间任何时候返回该响应码, 表示其当前没有资源存储它打算以原子方式实现的传输块. 它还允许服务器对未使用 Block1 的请求返回 4.13 响应, 作为提示客户端尝试发送 Block1. 最后, 对带 Block1 Option 的请求, 如果 4.13 响应携带的 Block1 Option 中 SZX 小于请求值, 这提示客户端尝试该更小 SZX, 参见第 2.3 节中的控制用法.

2.10. 缓存考虑

本规范试图为缓存, 尤其是缓存代理中的缓存, 保留多种实现策略. 例如, 缓存可以自由地分别缓存块, 也可以等待取得完整表示后再提供其中部分内容. 部分缓存对于跨代理可能更高效, 等价于流式 HTTP 代理. 缓存块, 即部分缓存响应, 可代替完整响应来满足提交给缓存的分块请求. 注意, 不同块可以有不同 Max-Age 值, 因为它们在不同时间传输. 带块的响应会更新完整表示的新鲜度. 单个块可以被验证, 验证单个块即验证完整表示. 控制用法中带 Block1 Option 且 M 位设置的响应会使目标 URI 的缓存响应失效.

组合响应的缓存或代理, 例如在请求中拆分块、在响应中增加块大小或跨代理, 可能需要组合 2.31 与 2.01/2.04 响应; 无状态服务器可能只在传输的第一个 Block1 块上响应 2.01, 它会支配后续块的任何 2.04 响应.

If-None-Match 只能在 NUM=0 的 Block1 请求上正确工作, 禁止用于 NUM != 0 的 Block1 请求.