跳至主内容

在此,我们列出了 0-9-1 规范中的错误和歧义,并提供了我们的解读和更正。

1 “消费者”(consumer) 的使用

“消费者”一词的使用相当笼统。通常情况下,我们将其解释为“客户端”,除非在某些明确指向三元组 {连接, 通道, 消费者标签} 的特定情境下。

第一个通用含义的例子出现在 channel.flow 中:“此方法要求对端暂停或重新开始由消费者发送的内容数据流。”

第二个具体用法的例子出现在 basic.consume 中:“请求独占消费者访问权限,意味着只有该消费者可以访问队列。”

(在规范的后续修订中,这确实应该得到整理。)

2 basic.recover 与 requeue=false

basic.recover 的节内容写道:

“此方法要求服务器在指定通道上重新发送所有未确认的消息。可以重新发送零条或多条消息。”

“如果 [requeue] 为 0,消息将重新发送给原始接收者。如果该位为 1,服务器将尝试将消息重新入队,随后可能会将其发送给其他订阅者。”

这些内容未考虑:

  • 作为 basic.get 结果而移交的消息;或者,
  • 此后已取消的消费者。

RabbitMQ 不会尝试重新发送作为 basic.get 响应而发送的消息,也不会尝试探查消费者是否已被取消。

(针对这种规范不足,最好的解决方案可能是移除 requeue 标志,并始终执行重新入队操作。)

3 字段类型

Tony 致 AMQ 开发列表

The changes from 0-9's table field type definitions (S,I,D,T,F,V) to 0-9-1's type definitions (t,b,B,U,u,I,i,L,l,f,d,D,s,S,A,T,F,V) are not, as far as I can tell, the changes that were discussed in the 0-9-1 SIG.

The types mentioned in the final spec do not line up with any of the variants discussed in the 0-9-1 SIG. In particular, they are gratuitously INCOMPATIBLE with the Qpid-java 0-9 field type extensions, which are also implemented by the RabbitMQ client libraries and the RabbitMQ broker.

The final word I can find on the subject by trawling my archive is a message from John O'Hara (attached). Unless there was subsequent discussion that I've missed, I would have expected the final version of the 0-9-1 spec to at least be based on the list John provided.

Here's a tabular summary of the state of things:

0-9 0-9-1 Qpid/Rabbit Type Remarks

t t Boolean b b Signed 8-bit B B Unsigned 8-bit U s Signed 16-bit (A1) u u Unsigned 16-bit I I I Signed 32-bit i i Unsigned 32-bit L l Signed 64-bit (B) l Unsigned 64-bit f f 32-bit float d d 64-bit float D D D Decimal s Short string (A2) S S S Long string A A Array (C) T T T Timestamp (u64) F F F Nested Table V V V Void x Byte array (D)

Remarks:

A1, A2: Notice how the types CONFLICT here. In Qpid and Rabbit, 's' means a signed 16-bit integer; in 0-9-1, it means a short string.

B: Notice how the signednesses CONFLICT here. In Qpid and Rabbit, 'l' means a signed 64-bit integer; in 0-9-1, it means an unsigned 64-bit integer.

C: I cannot find any discussion about the addition of this. Is my archive missing a few messages?

D: John [O'Hara]objected to this when John proposed a list himself. I believe it to be vital: byte arrays are not strings. Furthermore, Qpid and Rabbit already have code deployed that uses this type specifier.

RabbitMQ 继续使用第三列中的标签。

其他说明

  • 小数编码:“它们被编码为一个八位字节(代表小数位数),后跟一个长符号整数”,但语法与此矛盾,称:“decimal-value = scale long-uint”。我们将十进制值视为符号整数。

4 field-array

协议语法中有一个针对 field-array 的产生式,它出现在 field-value 产生式的右侧。

field-array = long-int *field-value

field-table 外,别处没有对此进行解释。特别是,没有任何解释说明 long-int 值代表什么。RabbitMQ 使用已编码字段值的总字节长度,这与 field-table 类似;它也将该值视为 long-uint,因为 field-table 使用的就是这个类型(假设“long-int”是一个拼写错误)。

5 独占和自动删除队列的删除

AMQP 0-8 和 0-9-1 都有关于删除独占和自动删除队列的规则。然而,它们并没有指定这是否同步发生;即在代理响应其对端之前。

(实际上 0-8 曾说服务器应该等待“一段礼貌的时间”。)

RabbitMQ 现在将 auto-delete 队列的删除与 basic.cancel、通道关闭和连接关闭同步,并将独占队列的删除与后两者同步。

如果满足以下条件,自动删除队列将不会被删除:

  • 它没有消费者——使用 basic.get 进行消费不计作拥有消费者
  • 消费者未能发送 basic.cancel(例如消费者崩溃)

6 队列和交换机的“等效性”和“预存在”规则

“如果未设置 [passive] 且队列已存在,服务器必须检查现有队列的 durable、exclusive、auto-delete 和 arguments 字段值是否相同。如果请求的队列与这些字段匹配,服务器必须以 Declare-Ok 响应,否则必须引发通道异常。”

交换机声明也是如此。

规则未能指定确切的错误代码。RabbitMQ 使用 'precondition-failed'。

此外,exchange.declare 规范包含一条关于使用不同类型重新声明的单独“类型化”规则。该规则是多余的,因为它已被等效规则所涵盖,并指定了一个 'not-allowed' 的连接错误。该规则应该被删除。

7 独占规则 (amqp0-9-1.xml:1503)

“一个客户端声明了一个独占队列。另一个连接上的第二个客户端尝试声明、绑定、消费、清除、删除或声明同名的队列。”

“declare”出现了两次,而 basic.get 和 queue.unbind 完全没有提及(basic.cancel 本身就是通道局部的)。RabbitMQ 将独占性扩展到了 queue.declare(包括被动声明)、queue.bind、queue.unbind、queue.purge、queue.delete、basic.consume 和 basic.get。

8 独占和其他错误

通常,RabbitMQ 会优先处理独占性;即它会先检查这一点,即使存在其他问题(如不等效性),也会发送 resource-locked 错误。

9 Passive 和 no-wait (amqp0-9-1.xml:1214,1423)

“...同时带有 passive 和 no-wait 的声明不起作用。”

然而,根据独占规则 (amqp0-9-1.xml:1486),明确禁止被动声明一个已存在且对另一个连接独占的队列;且被动声明不存在的队列或交换机应发送通道异常。RabbitMQ 忽略了上述引用句;即如果被动声明了一个对另一个连接独占的队列,或者不存在的队列或交换机,即使带有 no-wait,它也会发送通道异常。

10 channel-max 和 frame-max 的特殊情况值

connection.tune 中 channel-max 字段的规范称“零表示没有指定限制”,frame-max 也有类似的特殊情况;但在 tune-ok 中,“上限”规则称:“如果客户端指定的 channel max [/frame-max] 高于服务器提供的值,服务器必须关闭连接,而不尝试协商关闭。”

如果服务器在 tune 中发送了零,这就产生了歧义:如果字面理解该规则,客户端在 tune-ok 中不允许发送除零以外的任何值,因此无法协商降低限制。

RabbitMQ 不对 channel-max 设置限制,并将 tune-ok 中的任何数字视为有效。它确实对 frame-max 设置了限制,并检查 tune-ok 中发送的值是否小于或等于该限制。

11 frame-max,方法帧和内容头帧

规范指出 frame-max(在 connection.tune 和 connection.tune-ok 中协商)给出了总帧大小的上限;也就是说,包括帧头和帧结束八位字节。然而,没有办法将方法或内容头拆分到多个帧中。RabbitMQ 目前忽略了针对方法和内容头的 frame-max。

12 心跳格式

语法中有以下产生式:

heartbeat = %d8 %d0 %d0 frame-end

这不符合帧格式。RabbitMQ、Qpid 和 OpenAMQ 都发送八个字节,遵循帧格式:

frame-type (octet) = %d8 channel (short) = %d0 %d0 payload (long) = %d0 %d0 %d0 %d0 frame-end (octet) = %xCE

13 心跳监控

规范说:

The client should start sending heartbeats after receiving a Connection.Tune method, and start monitoring heartbeats after receiving Connection.Open.

但当然,客户端会发送 Connection.Open。

14 默认交换机

规范对默认交换机描述得有点模糊:它说:

The server MUST NOT allow clients to access the default exchange except by specifying an empty exchange name in the Queue.Bind and content Publish methods.

然而,0-10 规范使情况变得稍微严格了一些:

The default exchange MUST NOT be accessible to the client except by specifying an empty exchange name in a content publish command (such as message.transfer). That is, the server must not let clients explicitly bind, unbind, delete, or make any other reference to this exchange.

RabbitMQ 阻止除发布外的所有对默认交换机的访问。这包括交换机重新声明和到队列的 queue.bind 重新声明。对于交换机到交换机的绑定,源和目标都不允许是默认交换机。

15 EBNF 语法

EBNF 语法(第 32 页)是错误的:8*OCTET应该为:8 OCTET根据语法描述,8*OCTET意味着至少 8 个八位字节(参见要点“运算符 '*'...”)。

16 字符串与字节

规范对 utf-8 字符串与原始字节感到困惑。例如,什么是“字符”?有效的码点?一个八位字节?相等意味着什么?相同的字节?以某种范式相同?如何处理格式错误的 utf-8?第 31 页的语法建议字符串只是原始字节(即可以包含无效的 utf-8),但规范的其他部分似乎与此矛盾(例如 4.2.5.3, 第 35 页)。

17 缺失 312 ("No route") 通道异常

第 1.2 节应该定义一个异常 312 "No route",它曾存在于 0-9 中,也是 RabbitMQ 在 'mandatory' 消息无法发送到任何队列时通过 'basic.return' 返回的内容。

18 502 通道异常

关于队列名称的第 1.1 节指出“如果客户端没有声明队列,并且该方法需要队列名称,这将导致 502(语法错误)通道异常”。这实际上是一个连接异常。

19 自动删除 vs 独占

第 4.5 节说“当服务器关闭连接时,它会删除该连接拥有的任何自动删除队列。” 这不是自动删除的意思——这大概应该写成“...删除该连接拥有的任何独占队列。”

20 amq.* 交换机的持久性

规范没有说标准交换机是否应该是持久的。RabbitMQ 将它们声明为持久的。不幸的是,Qpid Java 客户端强制将它们声明为非持久的(通过 N 层抽象),从而几乎完全破坏了互操作性。

21 拒绝具有未知投递标签的消息

规范没有说明当客户端发送带有未知投递标签的 basic.reject 时会发生什么。服务器可能应该以与 basic.ack 相同的方式处理此问题,即引发 precondition_failed 错误。

22 心跳错误

在第 4.2.7 节中,规范说“接收到无效心跳帧的对端必须引发回复代码为 501(帧错误)的连接异常。” 目前尚不清楚心跳帧如何被识别为心跳帧的同时又被判定为无效。

23 必需的 server-properties

connection.start 的 xml 规范指出 server-properties 字段“应该至少包含以下字段:'host',指定服务器主机名或地址...”。 'host' 字段是在 0-9 中添加到此列表的;它在 0-8 中不存在。它是唯一一个值不由实现自由选择的字段。此外,这里应该包含什么信息以及客户端能从中学到什么,完全不清楚。主机名可能无法唯一标识代理,也可能无法通过 DNS 解析。防火墙和代理的存在也使得 IP 地址同样不可靠。此外,包含此信息可能会向客户端泄露本应隐藏的代理网络基础设施细节。

因此,该字段不应该是必需的。RabbitMQ 不提供此字段。

24 所有同步命令上的 'nowait'

所有同步命令都应该有一个 'nowait' 标志使其变为异步,除非在特定情况下这没有意义。目前违反此规则的有:'queue.unbind', 'basic.qos', 'basic.recover'(它反而有同步和异步变体), 'tx.select', 'tx.commit', 'tx.rollback'。其中,'queue.unbind' 是与既定模式最明显的背离。

25 弃用自动删除交换机

'exchange.declare' 上的 'auto-delete' 标志在 0-9-1 中被弃用。自动删除交换机实际上非常有用,因此应该恢复此标志。

另请注意,规范仍然引用了自动删除交换机:“交换机可以是持久的、临时的或自动删除的。”(第 26 页)。

RabbitMQ 支持自动删除交换机。

26 弃用内部交换机

'exchange.declare' 上的 'internal' 标志在 0-9-1 中被弃用。内部交换机(0-8 定义为用于内部布线且不允许客户端直接发布的交换机)实际上非常有用,特别是在与诸如交换机到交换机绑定等扩展结合使用时。因此,应该恢复此标志。

另请注意,基本发布规则 (02) 仍然引用了内部交换机:“如果交换机被声明为内部交换机...”

27 基本内容字段解释引用队列

基本内容字段上的 XML 注释指出“delivery-mode”是“用于实现持久性的队列”。这具有误导性(它提到了队列)并且是循环论证。delivery-mode 控制消息在代理重启后是否保留。

同样,注释指出“priority”是“用于实现优先级的队列”。代理在消息处理的所有阶段都可以考虑优先级,而不仅仅是在入队时。

顺便提一下,基本内容定义的垂直对齐略有偏差。

28 basic.recover 同步性

尽管 basic.recover 被引入作为 basic.recover-async 的同步版本,但在 XML 规范中它未被标记为同步。

29 心跳帧类型不一致

PDF 指定心跳帧类型为 4。XML 指定其为 8。

30 持久性 vs 独占

规范没有定义如果客户端尝试声明一个既持久又独占的队列会发生什么。RabbitMQ 将声明此类队列的请求视为对独占瞬态队列的请求。

获取帮助和提供反馈

如果您对本指南的内容或与 RabbitMQ 相关的任何其他主题有疑问,请随时通过 GitHub Discussions 或我们的社区 Discord 服务器 提问。

帮助我们改进文档 <3

如果您想为本网站贡献改进,其源代码可在 GitHub 上找到。 签署贡献者 CLA,然后只需 fork 存储库并提交一个拉取请求。谢谢!

© . This site is unofficial and not affiliated with VMware.