在此,我们列出了 0-9-1 规范中的错误和歧义,并提供我们的解释和修正。
1 “consumer” 的使用
“consumer” 这个词使用得相当随意。总的来说,我们将其解释为“client”,除非在明确指代 {connection, channel, consumer-tag} 三元组的情况下。
第一种通用含义的例子是 channel.flow:“此方法要求对端暂停或重新启动来自 consumer 的内容数据流。”
第二种特定用法的例子是 basic.consume:“请求独占 consumer 访问,这意味着只有此 consumer 可以访问队列。”
(这在规范的后续修订中应该得到真正的清理。)
2 basic.recover with requeue=false
basic.recover 的条文说明
“此方法要求服务器重新传递指定通道上的所有未确认消息。可能会重新传递零条或多条消息。”
和
“如果 [requeue] 为零,消息将被重新传递给原始接收者。如果此位为 1,服务器将尝试重新排队消息,然后可能将其传递给另一个订阅者。”
这些并未考虑到
- 通过 basic.get 传递的消息;或
- 后来已取消的 consumer。
RabbitMQ 不会尝试重新传递作为 basic.get 响应的消息,也不会尝试找出 consumer 是否已被取消。
(这个规范不足的最佳解决方案可能是移除 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 编码:“它们被编码为一个表示小数位数的字节,后跟一个长有符号整数”,但语法与之矛盾,并说明:“decimal-value = scale long-uint”。我们将 decimal value 视为 **有符号** 整数。
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”是typo的假设)。
5 Exclusive 和 auto-delete 删除
AMQP 0-8 和 0-9-1 都规定了有关 exclusive 和 auto-delete 队列删除的规则。但是,它们没有说明这是否是同步发生的;即,在 broker 响应其对端之前。
(实际上 0-8 说服务器应该等待“合理的时间”。)
RabbitMQ 现在与 basic.cancel、channel close 和 connection close 同步删除 auto-delete 队列,并与后两者同步删除 exclusive 队列。
当满足以下条件时,auto-delete 队列不会被删除:
- 它没有 consumer - 使用 basic.get 进行消费不计为有 consumer
- consumer 未能发送 basic.cancel(例如,consumer 崩溃)
6 队列和交换机“等效”和“预先存在”规则
“如果 [passive] 未设置且队列存在,服务器必须检查现有队列的 durable、exclusive、auto-delete 和 arguments 字段是否具有相同的值。如果请求的队列与这些字段匹配,服务器必须响应 Declare-Ok,否则必须引发通道异常。”
交换机声明也类似。
规则未能指定确切的错误代码。RabbitMQ 使用“precondition-failed”。
此外,exchange.declare 规范包含一个关于使用不同类型重新声明的单独“类型化”规则。该规则是多余的,因为它被等效规则所涵盖,并且指定了一个“not-allowed”的 **连接** 错误。该规则应该被删除。
7 Exclusive 规则 (amqp0-9-1.xml:1503)
“一个客户端声明了一个 exclusive 队列。第二个客户端在不同的连接上尝试声明、绑定、消费、清除、删除或声明同名队列。”
“declare”出现了两次,而 basic.get 和 queue.unbind 完全没有(basic.cancel 本地到通道)。RabbitMQ 将独占性扩展到 queue.declare(包括 passive declare)、queue.bind、queue.unbind、queue.purge、queue.delete、basic.consume 和 basic.get。
8 Exclusive 和其他错误
总的来说,RabbitMQ 优先处理独占性;即,它首先检查这一点,即使存在其他问题(如不匹配)也会发送 resource-locked 错误。
9 Passive 和 no-wait (amqp0-9-1.xml:1214,1423)
“.. 带有 passive 和 no-wait 的 declare 命令无效。”
然而,在 exclusive 规则 (amqp0-9-1.xml:1486) 中明确禁止被动声明一个存在且独占于另一个连接的队列,并且被动声明一个不存在的队列或交换机应该引发通道异常。RabbitMQ 忽略了上面引用的句子;即,即使带有 no-wait,如果被动声明了一个独占于另一个连接的队列,或者一个不存在的队列或交换机,它仍然会引发通道异常。
10 channel-max 和 frame-max 的特殊值
connection.tune 中 channel-max 字段的规范说明“零表示没有指定限制”,并且 frame-max 也有类似的特殊情况;但是在 tune-ok 中,“upper-limit”规则说:“如果客户端指定的 channel max [/frame-max] 高于服务器提供的值,服务器必须在不尝试协商关闭的情况下关闭连接”。
如果服务器在 tune 中发送零,这一点就很模糊:如果严格按照规则,客户端不允许在 tune-ok 中发送除零以外的任何值,因此它无法协商降低限制。
RabbitMQ 不限制 channel-max,并将 tune-ok 中的任何数字视为有效。但它确实限制了 frame-max,并检查 tune-ok 中发送的值是否小于或等于。
11 frame-max、method 和 content header 帧
规范指出 frame-max(在 connection.tune 和 connection.tune-ok 中协商)为帧的总大小提供了上限;也就是说,包括帧头和帧尾字节。然而,没有办法将方法或内容头拆分到多个帧中。RabbitMQ 目前忽略了方法和内容头的 frame-max。
12 Heartbeat 格式
语法有产生式
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 Heartbeat 监控
规范说
The client should start sending heartbeats after receiving a Connection.Tune method, and start monitoring heartbeats after receiving Connection.Open.
但当然,客户端 **发送** Connection.Open。
14 Default exchange
规范对默认交换机有些模糊;它说
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 禁止除发布之外对默认交换机的所有访问。这包括交换机重新声明和队列绑定重新声明到队列。对于交换机到交换机的绑定,源或目标都不允许是默认交换机。
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(“无路由”)通道异常
第 1.2 节应定义异常 312“No route”,它曾存在于 0-9 中,并且是 RabbitMQ 在 'basic.return' 时返回的,当一个 'mandatory' 消息无法传递给任何队列时。
18 502 通道异常
第 1.1 节 queue-name 说“如果客户端未声明队列,并且方法需要一个队列名称,这将导致一个 502(语法错误)通道异常”。这实际上是一个连接异常。
19 auto-delete 与 exclusive
第 4.5 节说“当服务器关闭连接时,它会删除该连接拥有的任何 auto-delete 队列。”这并非 auto-delete 的含义 - 这里应该说“...删除该连接拥有的任何 exclusive 队列...”。
20 amq.* 交换机的持久性
规范没有说明标准交换机应该是持久的还是非持久的。RabbitMQ 将它们声明为持久的。不幸的是,Qpid Java Client 强制将它们声明为非持久的(通过 N 层抽象),从而几乎完全破坏了互操作性。
21 拒绝带有未知 delivery tag 的消息
规范没有说明客户端发送带有未知 delivery tag 的 basic.reject 时应该发生什么。服务器可能应该像对待 basic.ack 一样处理这种情况,即引发 precondition_failed 错误。
22 Heartbeat 错误
在第 4.2.7 节中,规范说“收到无效 heartbeat 帧的对端必须引发一个具有 reply code 501(frame error)的连接异常。”目前尚不清楚 heartbeat 帧如何被识别为无效帧。
23 必需的 server-properties
connection.start 的 xml 规范说明 server-properties 字段“应包含至少这些字段:'host',指定服务器主机名或地址...”。'host' 字段在 0-9 中被添加到此列表中;它在 0-8 中不存在。它是唯一一个其值不由实现自由选择的字段。此外,不清楚在这里应该包含什么信息,以及客户端可以从中了解什么。主机名可能无法唯一标识一个 broker。并且它可能无法通过 DNS 解析。并且防火墙和代理的存在使得 IP 地址也存在同样的问题。此外,包含此信息可能会向客户端泄露 broker 网络基础设施的详细信息,而这些信息本应隐藏起来。
因此,此字段不应该是必需的。RabbitMQ 不提供此字段。
24 所有同步命令上的 'nowait'
所有同步命令都应该有一个 'nowait' 标志,使其变为异步,除非有特定情况不适用。目前违反此规则的是:'queue.unbind'、'basic.qos'、'basic.recover'(它有一个同步和异步版本)、'tx.select'、'tx.commit'、'tx.rollback'。其中,'queue.unbind' 是最明显偏离既定模式的地方。
'exchange.declare' 上的 'auto-delete' 标志在 0-9-1 中被弃用。Auto-delete 交换机实际上非常有用,因此应该恢复此标志。
另外请注意,规范仍然引用 auto-delete 交换机:“交换机可以是持久的、临时的或 auto-deleted 的。”(第 26 页)。
RabbitMQ 支持 auto-delete 交换机。
26 弃用 internal 交换机
'exchange.declare' 上的 'internal' 标志在 0-9-1 中被弃用。Internal 交换机(0-8 将其定义为用于内部连接且客户端不允许直接发布的交换机)实际上非常有用,特别是与 exchange-to-exchange 绑定等扩展结合使用。因此,应该恢复此标志。
另外请注意,basic publish 规则(02)仍然引用 internal 交换机:“如果交换机被声明为 internal 交换机...”
27 Basic content 字段说明引用了队列
basic content 字段上的 XML 注释说明“delivery-mode”用于“实现持久性的队列”。这是误导性的(因为它提到了队列)且循环的。delivery-mode 控制消息是否在 broker 重启后仍然存在。
类似地,注释说明“priority”用于“实现优先级的队列”。优先级可以在消息处理的所有阶段被 broker 考虑,而不仅仅是在入队时。
另外,在美学方面,basic content 定义的垂直对齐略有偏差。
28 basic.recover 同步性
虽然 basic.recover 被引入为 basic.recover-async 的同步版本,但在 XML 规范中并未将其标记为同步。
29 Heartbeat 帧类型不一致
PDF 指定 heartbeat 帧类型为 4。XML 指定其为 8。
30 durable vs exclusive
规范未定义当客户端尝试声明一个既 durable 又 exclusive 的队列时会发生什么。RabbitMQ 将此类队列的声明请求视为请求一个 exclusive 的瞬态队列。
获取帮助和提供反馈
如果您对本指南的内容或与 RabbitMQ 相关的任何其他主题有疑问,请随时通过 GitHub Discussions 或我们的社区 Discord 服务器 提问。