消息中间件 一 之 AMQP译文(上)

RabbitMQ是AMQP的实现成果, 所以在研究RabbitMQ之前, 不如先看看协议本身.

1 Overview 概述

1.1 Goals of This Document 文档目标

本文档定义了网络协议AMQP, 客户端可以使用该协议与消息中间件服务器进行通信. 我们面向的是在本领域有一定经验的技术人员, 我们会提供充分的文档, 工程师可以通过这些文档使用任何高级语言或者硬件平台构建解决方案.

1.2 Summary 概述

1.2.1 Why AMQP 为什么用AMQP

AMQP提供了完整的方法支持客户端和消息中间件服务器之间通信的操作. 我们希望开发和使用都使用标准化的消息中间件技术, 这样会降低企业的花销和系统集成的成本, 而且会为大众提供一个企业级的集成服务. 我们的目标是通过AMQP消息中间件可以驱动网络本身, 而且通过消息中间件可以开发出新的有用的应用.

1.2.2 Scope of AMQP

为了完成一个完整功能的消息中间件需要两方面的支持, 详细的网络协议和服务端服务.
因此AMQP在这两方面做了如下定义:
1 AMQ模型是定义的一套消息处理操作, 它包含了一套在服务端处理消息路由和消息存储的组件, 以及关联这些组件的规则.
2 AMQP是一个wire-level(?)的网络协议, 它允许客户端应用和服务端进行通信, 而且可以通过自己实现的AMQ模型进行交互.

虽然通过AMQP可以大概推断出服务端的消息操作, 但是我们认为对服务端有个详细的讲解有助于理解协议.

1.2.3 The Advanced Message Queuing Model (AMQ model)

我们定义了一些服务端的名词, 因为这些操作在任何服务端实现中都应该表现的一样, 因此在AMQ模型中定义了一些列组件以及这些组件如何关联的标准规则. 下面是三个重要的组件, 他们贯穿服务端整个执行过程.
1 exchange, 它接收来自生产者应用的信息, 而且通过一些特定的条件路由信息到Message queues, 这些条件通常是信息的属性或者信息的内容.
2 message queue, 它会一直存储信息, 直到消息被一个或多个消费者应用安全的使用后才会删除该信息.
3 binding, 它定义了exchangemessage queue之间的关系, 也就是提供上面提到的路由规则

使用AMQ模型我们可以简单模拟传统的面向消息的中间件概念, 比如存储转发队列和主题订阅. 当然我们也可以描述一些更难的概念, 比如基于内容的路由, 工作负载分发, 根据需求改变的消息队列. 笼统的说, AMQP服务器就像是一个邮件服务器, exchange就像是邮件发送代理, message queue就像是邮箱. binding就是发送代理中的路由表. 发布者发送信息给一个特定的代理, 代理会路由该信息到邮箱. 与AMQP相比, 之前的中间件都是发送者之间发送信息到邮箱(存储转发队列), 或者是邮件列表(主题订阅).

这个区别就在于绑定exchangemessage queue的规则是由工程师规定的而不是集成在代码中的, 这样的话我们就有可能做一些有趣的事, 比如定义这样的规则: 复制所有包含某某消息头的消息到某个message queue. AMQ模型的设计依据以下几个需求:
1 支持主流消息产品中的一些名词概念
2 提供和主流信息产品相当的性能
3 允许应用程序通过协议对服务端的特定组件进行编程
4 灵活, 扩展性, 简单

1.2.4 The Advanced Message Queuing Protocol (AMQP)

AMQP协议是一个有现代特性的二进制协议: 多通道, negotiated, 异步, 安全, 可移植, neutral, 高效. AMQP被有效的分为两层:


image.png

功能层定义了一套命令(通过功能逻辑分类), 这些命令可以表示客户端应用的操作.
传输层负责客户端到服务端的方法执行, 信道的多路复用, framing(?), 内容编码, 心跳操作, 数据展示, 错误处理. 我们可以在不改变协议的应用层(就是应用可见的部分)的情况下替换任意的传输层. 我们也可以为不同的高级协议使用相同的传输层.
所以AMQ模型需要满足下面的需求:
1 保证一致性实现下的操作性
2 提供明确的服务质量控制
3 命名显式而且一致
4 允许通过协议完成服务器连接的配置
5 使用容易映射应用级api的命令
6 为了清晰, 每个操作只做一件事.

AMQP传输层的设计依赖下面的需求, 不分先后:
1 为了信息紧凑, 使用二进制编码, 这样打包拆包很快.
2 不限制信息长度
3 在一个连接上实现多个通道
4 长连接, 不需要特殊内部限制
5 允许在管道内使用异步命令
6 扩展方便
7 向前兼容
8 可补偿的, 通过使用强断言模型(?)
9 对编程语言采取中立态度
10 适应代码发展过程(?)

1.2.5 Scales of Deployment 部署规模

AMQP使用场景包含了不同的规模, 大体如下:
1 开发者使用: 1个服务端, 1个用户, 10个消息队列, 每秒一条信息.
2 产品应用: 2个服务端, 10-100用户, 10-50个消息队列, 每秒十条信息(每小时36K条信息)
3 部门关键应用: 4个服务端, 100-500用户, 50-100个消息队列, 每秒100条信息(每小时360K条信息)
4 地区关键应用: 略
5 全球关键应用: 略
6 市场数据(交易): 略
与容量一样, 信息的延迟也是非常重要的. 比如市场数据很快就会没有用. 所以我们可以在遵从规范的前提下提供不同的服务质量.

1.2.6 Functional Scope

我们希望提供一种多样化的信息架构
1 多对一的存储转发
2 多对多的工作负载分发
3 多对多的发布订阅
4 多对多的基于内容路由
5 多对多的文件传输
6 两个终端的点对点连接
7 大数据分布式下的多对多

1.3 Organisation of This Document 文档的组织结构

文档分为五章, 大部分是比较独立的, 你可以阅读自己感兴趣的部分:
1 overview(本章). 就是个介绍
2 General Architecture, 这章我们是描述了整体架构和AMQP的整体设计. 我们希望通过这章可以让工程师知道AMQP是如何工作的.
3 Functional Specifications, 这章我们定义了应用是如何使用AMQP的. 这章在每一个协议命令后会有一个小讨论来作为一种实现例子. 阅读这章之前需要阅读前一章.
4 Technical Specifications, 这章我们讲了AMQP的传输层是如何工作的.

1.4 Conventions 约定

2 General Architecture 整体架构

2.1 AMQ Model Architecture AMQ模型架构

This section explains the server semantics that must be standardised in order to guarantee interoperability between AMQP implementations.

2.1.1 Main Entities 主要组件

下面这张图展示了AMQ模型的整体结构


image.png

我们可以总结下一个中间件服务器是什么: 中间件服务器是一个数据服务器, 它接收信息而且只对信息主要做两件事, 一是通过特定的条件将信息路由给不同的消费者, 二是当消费者没有办法马上接收信息的时候中间件缓存信息到内存或者硬盘.
之前的中间件服务器通常是通过一个完整的引擎实现各种特定的路由和存储. AMQ模型将实现划分为更小的模块化的, 然后以更多样化更强健的方式实现. 首先会将以上任务划分为两个截然不同的角色:
1 exchange, 从生产者接收信息, 并路由信息到message queue
2 message queue, 存储信息并转发信息给消费者.

后面我们会介绍. AMQP提供了运行期的编程能力, 主要通过以下两个发面实现:
1 在运行期通过协议创建任意的exchangemessage queue
2 在运行期通过协议绑定exchangemessage queue以此创建任意的消息处理系统.

2.1.1.1 The Message Queue 信息队列

消息队列可以存储信息到内存或者硬盘, 然后按一定顺序分发信息给一个或多个消费者. 消息队列就是消息的存储和分发组件. 每个消息队列是完全独立的, 而且相当智能. 消息队列有很多属性: 私有的或者共享的, 持久的或者临时的, 客户端命名的或者服务端命名的等等. 通过选择适当的属性, 我们可以使用消息队列实现传统中间件功能, 比如:
1 一个共享的存储转发队列, 用来保存信息和按照轮询的方式分发信息给消费者. 存储转发队列通常是持久化队列, 而且是在多个消费者间共享的.
2 一个私有应答(?)队列, 只存储和发送信息给一个消费者. 应答队列通常是临时的, 由服务端命名的, 而且是消费者私有的队列.
3 一个私有的订阅队列, 会通过订阅信息收集信息, 然后分发这些信息给唯一的消费者. 订阅队列农场是临时的, 由服务端命名的, 而且是消费者私有的.
这些条目没有在AMQP中正式定义: 它们只是告诉我们如何使用消息队列的例子. 所以像创建一个持久的共享的订阅队列这种操作是非常容易的.

2.1.1.2 The Exchange 交换机(太难听, 后面就直接用exchange)

exchange会接收生产者发送的信息, 然后根据事先排列好的条件路由信息到不同信息队列. 这些条件叫做bindings. exchange是绑定和路由引擎. 也就是说exchange会检查信息, 然后使用自己的绑定表来决定怎么发送信息给消息队列或者其他的exchange. exchange绝对不会存储信息. exchange这个词既是指一类算法也是指这类算法的实例. 为了更恰当, 我们使用exchange类型和exchange实例.
AMQP定义了一系列标准exchange类型, 这些类型覆盖了基本的路由分发信息所需要的类型. AMQP服务器会默认提供这些类型对应的exchange实例. 应用端在使用AMQP中可以额外创建自己的exchange实例. exchange类型需要命名以便应用可以使用他们自己创建的exchange类型. 同样exchange实例也需要命名, 这样应用才知道怎么去绑定队列和发布者消息.

2.1.1.3 The Routing Key

通常情况下, exchange会检查信息属性, 它的头信息和内容信息, 使用这些信息和其他合理信息来决定如何路由这条信息.
在大多数简单情况下, exchange只检查信息的一个被叫做routing key的字段. routing key是exchange可以用来决定如何路由信息的虚拟地址.
对于点对点路由, routing key通常是信息队列的名称. 对于发布订阅路由, routing key通常是一个有层级结构的主题值.
在更复杂的情况下, routing key可能要结合信息的头信息或者内容信息.

2.1.1.4 Analogy to Email 类比邮箱系统

类比邮箱系统来看AMQP中的概念, 我们会发现这些概念并不是很新奇.
1 AMQP信息与邮件信息类似
2 消息队列就像是一个邮箱
3 消费者就像一个可以获取和删除邮件的邮件客户端
4 exchange就像一个MTA邮件转发代理, MTA会检查邮件而且根据routing key和路由表决定如何发送信息给一个或多个邮箱.
5 a routing key corresponds to an email To: or Cc: or Bcc: address, without the server information (routing is entirely internal to an AMQP server);(?)
6 exchange实例就像是独立的MTA进程, 它们处理一些邮件子域, 或者特殊的邮件通信类型.
7 binding就像MTA中路由表中的条目

AMQP的优势是我们可以在运行时创建消息队列, exchange, bindings, 而且我们可以编排这些组件的关系, 使得它远比只是映射到邮箱名字这样的方式复杂.
我们不应该太深入的将AMQP和邮箱类比, 它们还是有本质的区别的. 对于AMQP来说挑战是在服务器中路由和转发信息, 这种服务器按照SMTP的说法是"自主系统". 相较而言, 邮箱系统的挑战是如何在自主系统间路由信息. 在系统间和在系统中路由信息是完全不同的问题而且有不同的解决方法, 即使只是为了维护性能这样的普通问题.(这句话应该是说这么简单的问题的解决也是不一样的, 突出两个问题的不同?)
为了在不同的AMQP服务器之间路由信息, 需要构建沟通的桥梁, 也就是为了传输信息其中一个服务器需要充当其他服务器的客户端. 这种工作方式一般适合预期使用AMQP的业务类型,因为这些桥梁可能由业务流程、合同义务和安全考虑来支撑的.

2.1.2 Message Flow 信息流转

This diagram shows the flow of messages through the AMQ model server:
下面这幅图展示了AMQ模型中信息是如何流转的


image.png

2.1.2.1 Message Life-cycle 信息声明周期

AMQP信息包括一系列属性外加可见的内容.
一个新的信息是由生产者应用通过AMQP客户端api创建的. 生产者将信息内容放到信息中, 可能还会加入一些属性. 生产者使用路由信息标记信息, 这个路由信息特别像是一个地址, 但是结构更多样复杂. 然后生产者会发送信息给服务器上的exchange. 当信息到达服务器, exchange通常会路由这些信息给服务器上已经存在的消息队列. 如果信息没有被路由, 那么exchange可能会默默的删除这条信息, 或者把信息返回给生产者. 这里就需要生产者自己选择信息没有被路由时的执行策略了.

一条信息可以存在于多个信息队列. 服务器可以通过多种方法实现, 比如复制信息, 使用引用计数等(?). 这样做并不会影响功能. 但是, 当一个信息被路由到多个队列, 那么这些队列中该条信息是一模一样的, 没有唯一的信息可以区分这些副本.
当一条信息到达信息队列, 信息队列会马上尝试通过AMQP传输信息给消费者. 如果没有办法传输, 消息队列会保存信息(至于是保存到内存还是硬盘需要生产者指定)直到消费者可以接受信息. 如果没有消费者, 消息队列可能会通过AMQP返回这个信息给生产者(同样, 如果生产者这样指定的话).

当信息队列可以发送信息给消费者时, 消息队列会从它的内部缓存中删除这条信息. 可能会立即删除, 也可能等接收到消费者确认成功消费信息之后. 由消费者决定什么时候什么方式告知信息队列.消费者也可以拒绝信息(消极确认).
生产者发送信息和消费者确认信息属于事务范畴. 现实经常出现一个应用既是生产者也是消费者的情况, 这时候它既要发送信息也要发送确认信息, 然后提交或者回滚事务. 信息从服务端发送给消费者是没有事务的, 只对确认信息执行事务已经足够了.

2.1.2.2 What The Producer Sees 生产者视角

通过前面类比邮件系统, 我们知道生产者不会直接发送信息给消费者. 否则会打乱AMQ模型. 就好像允许邮件绕开MTA的路由表直接到达邮箱一样. 这样的话就没有办法增加一些中间过滤器和操作, 比如垃圾邮件检测. AMQ模型使用和邮件系统一样的原则: 所有的信息都会发送到一个地方, exchange或者MTA, 这个地方可以通过一些发送者不知道的规则和信息检测信息, 然后路由信息到下一个节点, 这个操作对于发送者也是透明的.

2.1.2.3 What The Consumer Sees 消费者视角

从消费者的视角来看会打破我们之前的邮件系统类比. 邮件客户端完全是被动的, 它们可以查看它们的邮箱, 但是它们不会知道它们的邮箱是如何被填满的. AMQP消费者也可以像邮件系统客户端那样. 我们创建一个应用, 该应用只接收来自特定消息队列的信息并处理.
但是我们也可以让AMQP客户端应用有下面的功能:
1 创建或者销毁消息队列
2 通过创建binding, 客户端可以定义消息队列接收什么样的信息
3 选择不同的exchange, 这样就完全改变了路由规则
对于邮件系统这些功能就像是:
1 创建一个邮箱
2 告诉MTA把包含特殊头信息的信息都复制到这个邮箱
3 完全改变邮件系统解析地址和其他消息头的方式
我们发现AMQP与其说是系统其实更像一个可以编写不同组件到一起的语言. 这也是我们一个目标: 通过协议可以编写服务端功能.

2.1.2.4 Automatic Mode 自动模式(默认模式?)

大部分集成架构不需要上面说到的这种复杂模式. 就像是一个摄影爱好者, 大部分AMQP用户需要一种"傻瓜"模式. AMQP通过以下两个简单原则提供这种模式:
1 为生产者提供默认exchange
2 为消息队列提供一个默认binding, 基于消息队列的名称.
实际上, 默认binding允许生产者直接发送信息给消息队列, 并授予适当权限. 这种模式模拟了人们期望的传统中间件发送信息到目的地的最简单的方式.
默认binding并不会限制消息队列在复杂场景的应用. 但是它确实可以让用户在使用AMQP的时候不用关心exchange和binding的工作.

2.1.3 Exchanges

2.1.3.1 Types of Exchange

每一种exchange类型都实现了一种特殊的路由算法. 有一些标准exchange类型会在后面的章节介绍, 但是其中有两个特别重要:
1 直连exchange, 它通过一个routing key路由信息, 默认的exchange就是一个直连exchange.
2 主题exchange, 它通过一个路由模板路由信息.
服务端会在启动的时候创建一些周知的exchange, 它们有直连的和主题的, 客户端可以使用它们.

2.1.3.2 Exchange Life-cycle exchange声明周期

每一个AMQP服务器都会预先创建一系列exchange实例. 这些exchange会在服务器启动的时候创建而且不会被销毁. AMQP应用也可以自己创建自己的exchange. AMQP在创建exchange的时候不会使用像是"创建"这样的方法, 而是类似"声明"方法, 它的意思是: 如果当前没有则创建, 否则继续. 通常我们觉得应用创建exchange私用, 然后当处理完工作后销毁这样是合理的. 而且AMQP也提供了销毁方法, 但是通常应用是不需要这样做的. 在我们本章的例子中, 我们假设exchange都是由服务器创建的, 我们不会展示应用声明exchange.

2.1.4 Message Queues

2.1.4.1 Message Queue Properties 消息队列属性

当客户端应用创建消息队列时, 有一些重要属性可以选择:
1 名称, 如果没有设置, 服务端会自动给客户端命名. 通常情况下, 当应用间共享消息队列时, 一般会先对消息队列的名称达成一致, 当应用自己使用消息队列是, 它可以让服务端自己命名队列.
2 专一的, 如果设置, 该队列只属于当前连接, 当连接断开时队列也被销毁
3 持久化, 如果设置, 服务器重启消息队列还会存在, 但是队列会丢失临时信息.

2.1.4.2 Queue Life-cycles 队列的生命周期

有两个主要的消息队列生命周期:
1 持久化消息队列, 它会被多个消费者共享, 而且可以独立存在.比如, 无论是否有消费者接收信息, 该队列都会存在而且接收信息.
2 临时消息队列, 它是某个消费者私有的, 而且绑定到该消费者的. 当消费者下线, 消息队列也会被删除.
其中还有一些变体, 比如共享消息队列, 它可以被多个消费者共享, 当最后一个消费者下线后会删除该消息队列.下图展示了一个临时消息队列的创建和销毁


image.png

2.1.5 Bindings

binding就是exchange和消息队列之间的关系, 它可以告诉exchange如何路由信息到消息队列. binding是拥有和使用队列的客户端应用通过命令给exchange创建的. 使用伪代码表示绑定命令大概如下:
Queue.Bind TO WHERE
下面我们看看三个典型应用: 共享队列, 私有应答队列, 发布订阅队列.

2.1.5.1 Constructing a Shared Queue 构造一个共享队列

共享队列是经典中间件的点对点队列. 在AMQP中我们可以使用默认的exchange和binding. 定义我们的队列名称为"app.svc01". 下面是创建队列的伪代码:
Queue.Declare queue=app.svc01
共享队列可能有多个消费者. 为了从队列消费信息, 每个消费者需要这样:
Basic.Consume queue=app.svc01
对于发送信息, 每个生产者会发送信息给默认exchange:
Basic.Publish routing-key=app.svc01

2.1.5.2 Constructing a Reply Queue 构造一个应答队列

应答队列通常是由服务端命名的临时队列. 这些队列通常是私有的, 比如只有一个消费者. 除了这些特殊的部分, 应答队列使用和标准队列一个样的匹配条件, 所以我们也可以使用默认exchange.
下面是创建应答队列的伪代码, 其中S:表示一个服务端的响应:

Queue.Declare 
    queue=
    exclusive=TRUE 
S:Queue.Declare-Ok
    queue=tmp.1

生产者发送信息到默认exchange:

Basic.Publish 
    exchange=
    routing-key=tmp.1

标准信息属性中有一个是"Reply-To", 它用来表示应答队列的名称.

2.1.5.3 Constructing a Pub-Sub Subscription Queue 构造发布订阅队列

在传统中间件中, "订阅"的定义不明确, 它至少表示两种完全不一样的概念: 匹配消息的一系列条件和临时队列持有的匹配到的信息. AMQP把这部分工作分给了binding和消息队列. 在AMQP中没有组件叫做"订阅".
我们统一下什么是发布订阅:
1 保存一个或多个消费者信息
2 通过各种不同的方式从多个数据源收集信息, 这些方式可能是匹配主题, 消息内容等.
订阅队列和应答队列最主要的区别是, 订阅队列的名称是和如何路由无关的, 而且路由方式是通过抽象的匹配条件而不是1对1的routing key. 我们来看一个通用的发布订阅模型然后实现它. 我们需要一个可以匹配主题树的exchange类型. 在AMQP中这就是主题exchange. 主题exchange可以匹配通配符, 比如"STOCK.USD.*" 可以匹配到routing key "STOCK.USD.NYSE". 我们不能使用默认的exchange和binding, 因为这些不能进行发布订阅路由. 所以我们必须创建一个binding. 下面是创建binding和发布订阅队列的伪代码:

Queue.Declare 
    queue=
    exclusive=TRUE 
S:Queue.Declare-Ok
    queue=tmp.2 
Queue.Bind
    queue=tmp.2
    TO exchange=amq.topic
    WHERE routing-key=STOCK.USD.*

消费者消费代码如下:

Basic.Consume 
    queue=tmp.2

生产者发送信息代码如下:

Basic.Publish
    exchange=amq.topic 
    routing-key=STOCK.USD.ACME

主题exchange执行时会收到routing key ("STOCK.USD.ACME"), 然后根据绑定表, 找到一个匹配队列tmp.2, 然后就会路由这些信息给这个队列.

2.2 AMQP Command Architecture AMQP命令架构

这章介绍应用如何和服务端进行通信.

2.2.1 Protocol Commands (Classes & Methods) 协议命令(类和方法)

中间件非常复杂, 我们的挑战是在设计协议架构时克服这种复杂性. 我们的方法是构建一个传统的api模型, 该模型会有一些类, 每个类会包含一些方法, 每个方法只做好一件事. 这会导致有大量的命令, 但是另一方面也会相对容易理解.
AMQP命令以类进行分组. 每个类都会覆盖一个特殊的功能领域. 有些类是可选的, 每一个需要支持的终端可以去实现这些类.
有两个完全不一样的方法如下:
1 同步请求响应, 一端发送请求另一端需要给出响应. 同步请求响应用在对性能要求不严格的场景.
2 异步通知, 一端发送方法不会等待响应. 异步方法用在对性能要求严格的场景.
为了保证方法执行简单, 我们为每一个同步请求定义了不同的响应. 也就是说没有一个方法是两个不同请求的响应. 也就是说一个端点在发送一个同步请求后, 还可以接收和处理传入的方法直到接收到同步响应. 这也是AMQP和大多数传统RPC协议的区别.
一个方法会被定义为同步请求同步响应或者异步. 最后, 每个方法都会被定会为客户端侧的还是服务端侧的.

2.2.2 Mapping AMQP to a middleware API

我们讲AMQP设计为可以映射到中间件API. 这个映射是自动的(不是所有方法和参数对应用是由意义的), 但是也需要手动(需要给定一些规则, 后面的所有方法的映射都不需要手动干涉).
这样的好处是当我们学习了AMQP的语法后, 开发者会发现在他们使用的任何环境都会发现相同的语法提供.
比如, 下面是队列声明方法例子:

Queue.Declare 
    queue=my.queue
    auto-delete=TRUE 
    exclusive=FALSE

这个可以转换为线级结构(?)


image.png

或者一个更高级的API
queue_declare (session, "my.queue", TRUE, FALSE);
映射异步方法的伪代码如下:
send method to server
同步代码如下

send request method to server 
repeat
    wait for response from server
    if response is an asynchronous method
        process method (usually, delivered or returned content) 
    else
        assert that method is a valid response for request
        exit repeat 
    end-if
end-repeat

值得注意的是, 对于大多数应用, 中间件可以完全隐藏在技术层, 而且实际使用的API要比中间件的健壮性和性能重要.

2.2.3 No Confirmations 没有确认

一个拖沓的协议是缓慢的. 我们使用大量的异步来解决性能问题. 通常我们发送信息从一端到另一端, 然后我们尽快结束信息发送, 不等待消息确认. 必要的情况下我们会在更高层面, 比如消费者层实现节流.
因为我们采用了断言模型, 所以我们可以忽略确认信息. 我们要么成功, 要么失败然后关闭连接或者通道.
在AMQP中没有确认. 成功是没事, 失败就比较麻烦. 当应用需要跟踪成功和失败时, 他们需要使用事务.

2.2.4 The Connection Class 连接类

AMQP是一个连接的协议. 连接被设计为持久的, 而且可以承载多个通道. 连接的生命周期如下:
1 客户端打开一个和服务端的TCP/IP连接, 然后发送协议头. 这是唯一一个客户端需要发送但是没有被构造为方法的信息.
2 客户端选择安全策略(Start-Ok, 括号里的就是一个指令, 表示当前行为)
3 服务端开始权限验证过程, 该过程会使用SASL质疑响应模型. 它会发送给客户端一个质疑(Secure)
4 客户端发送一个权限响应(Secure-OK). 比如用最简单的机制, 响应包括用户名和密码.
5 如果没有通过服务端会重发发送质疑信息, 否则就顺利通过, 发送一系列参数给客户端, 比如信息结构的最大长度(Tune)
6 客户端接收参数, 或者可以降低参数(Tune-Ok)
7 客户端正式打开连接然后选择一个虚拟主机(Open)
8 服务端确认选择的虚拟主机是有效的(Open-Ok)
9 现在客户端就可以使用自己选中的连接了
10 客户端或者服务端选择关闭连接(Close)
11 另一端握手策略关闭连接(Close-Ok)
12 服务端和客户端关闭它们自己的socket连接
对于连接没有完全打开的错误是不可以握手的. 在检测协议头成功之后(后面会详细介绍细节), 在发送Open或者Open-Ok指令之前, 任何一端如果检测到错误则必须关闭自己的socket而不用发送任何信息.

2.2.5 The Channel Class 通道

AMQP是一个多通道协议. 多通道提供了一种方式, 将重量级的TCP/IP连接分解为多个轻量级连接. 这样会使得协议是"防火墙友好的", 因为这样对端口的使用就是可预测的. 这也意味着流量整形(traffic shaping?)和其他网络QoS特性可以很方便的使用.
通道之间是相互独立的, 而且可以同时执行不同的方法, 有效带宽会在当前活动的通道间共享.
为了编程方便, 多线程客户端应用可能会经常使用"每个线程一个通道"模型, 这样是被期望和鼓励的. 然而, 一个客户端也可以打开多个连接和一个或者多个AMQP服务器, 这样也是完全可以的. 通道的生命周期如下:
1 客户端打开一个新的通道(Open)
2 服务端确认新通道已经准备就绪(Open-Ok)
3 客户端和服务端开始使用通道
4 任何一端关闭通道(Close)
5 另一端握手关闭通道(Close-Ok)

2.2.6 The Exchange Class

exchange类允许客户端应用管理服务端的exchange. 该类允许应用通过脚本修改自己的链路(应该指的是动态修改路由链路?)而不是依赖一些配置接口. 注意: 大部分的应用不需要这种复杂程度, 现存的中间件(legacy)也不太可能支持这种语法. exchange声明周期如下:
1 客户端请求服务端确保exchange是否存在(Declare). 客户端可以完善这个功能, 比如"如果不存在则创建一个exchange", 或者"没有的话警告我, 但是不要创建"
2 客户端发送信息给exchange
3 客户端可以选择删除exchange(Delete)

2.2.7 The Queue Class

队列类允许客户端应用管理在服务器上的队列. 对于几乎所有的应用, 在消费信息的时候最基础的一步就是至少检查下要消费信息的队列是否存在.
持久化信息队列的生命周期相当简单:
1 客户端判断信息队列是否存在(Declare, 通过使用"passive"参数)
2 服务端确认信息队列存在(Declare-Ok)
3 客户端从消息队列获取信息
临时信息队列的生命周期更有趣些:
1 客户端创建一个信息队列(Declare, 通常是没有名字的信息队列, 这样服务端就要为这个队列命名). 服务端确认队列(Declare-Ok)
2 客户端创建一个该队列的消费者. 消费者的严格定义在Basic类中.
3 客户端取消消费者, 可以是显式的也可以通过关闭通道或者链接.
4 当消息队列的最后一个消费者也消失后, 在一个合适的延迟后, 服务端会删除该队列.
AMQP实现了可以发布订阅发送消息的消息队列. 它支持一种有趣的结构, 该结构允许订阅在订阅者之间实现负载均衡.
订阅消息队列的生命周期包含一个额外的绑定步骤:
1 客户端创建消息队列, 服务端确认
2 客户端绑定信息队列和主题exchange, 然后服务端确认
3 客户端使用信息队列像之前的例子那样

2.2.8 The Basic Class

Basic类实现了这篇文章描述的信息收发能力. 它支持下面这些重要语法:
1 从客户端发送信息给服务端, 异步执行(Publish)
2 创建和删除消费者(Consume, Cancel)
3 从服务端发送信息给客户端, 异步(Deliver, Return)
4 确认信息(Ack, Reject)
5 同步从信息队列中获取信息(Get)

2.2.9 The Transaction Class

AMQP支持两种事务
1 自动事务, 每一次发送信息和确认都是作为一个独立的事务被执行.
2 服务端恩地事务, 服务端会缓存发送的信息和确认信息, 然后提交它们给需要的客户端.
事务类("tx")允许应用使用第二种事务, 也就是服务端事务. 该类支持的语法如下:
1 应用在需要事务的通道请求服务端事务
2 应用正常工作
3 应用提交或者回滚事务
4 应用重复上述流程
事务只覆盖了发送信息和确认信息, 不包括发送. 因此, 事务回滚不会重新编排信息或者重新发送信息, 客户端有权在后面的事务确认该信息.

2.3 AMQP Transport Architecture

这部分解释了命令如何映射到协议上的.

2.3.1 General Description 总体描述

AMQP是一个二进制协议. 信息会组织到不同类型的帧中. 帧保存了协议的方法和其他信息. 所有的帧都有相同的结构: 帧头, 负载, 帧尾. 结构负载的格式依赖于帧的类型.
我们假设一个可依赖的面向流的网络传输层(TCP/IP或者其他相当的).
在一个单独连接中, 可以由多个独立的控制线程, 这些线程称作通道. 每一个结构都会用通道号编号. 由于帧的相互交错, 不同的通道可以共享一个连接. 对于任意一个通道, 每个帧都以严格的顺序执行, 可以用来驱动协议解析器(通常是一个状态机)(For any given channel, frames run in a strict sequence that can be used to drive a protocol parser (typically a state machine)?).
我们使用很小的数据类型集来构造帧, 比如比特, 实数, 字符串, 字段表. 帧的字段会编排的很紧密, 而不会让解析复杂和很慢. 通过协议说明生成帧层相对来说比较简单.
wire-level格式化被设计为可伸缩的而且足够通用, 可以被其他高级协议使用. 我们认为AMQP随着时间推移是可扩展的, 可改良的, 并且wire-level格式将会支持这一点.

2.3.2 Data Types 数据类型

AMQP数据类型在方法帧中使用, 它们如下:
1 实数(1~8字节), 用来表示长度, 数量等. 实数一般是正数, 而且可能在帧是没有对齐的.
2 比特, 用来表示开关值, 比特包装为字节.
3 短字符串, 用来保存短文本属性. 短字符串最大255个字节
4 长字符串, 用来保存二进制数据块
5 属性表 保存键值对. 值可以是字符串和数字等.

2.3.3 Protocol Negotiation 协议协商

AMQP客户端和服务端会协商协议. 这个意思是说, 当客户端连接的时候, 服务端会建议一些可选条件, 客户端可以接收或者修改. 当两端对输出达成一致时, 连接会继续. 协商是一个非常有用的技术, 因为它可以让我们判断假设和预期.
在AMQP中, 我们可协商一些方面如下:
1 实际的协议和版本, 服务端可能在一个端口有多个协议.
2 参数加密和两端的权限. 这属于前面说的方法层
3 最大帧长度, 通道数量, 还有其他可选的限制
同意约定的限制可能会使双方提前分配主要的缓存, 避免死锁. 每一个进入的帧要么遵守约定的限制(这样是安全的), 要么超过限制, 超过限制是错误的, 必须断开连接. 这非常符合"要么正常工作, 要么不工作"的AMQP哲学.
双方在协商中最少需要达成以下内容:
1 服务端必须告诉客户端正在执行的限制
2 客户端响应而且可能会减少这次连接的限制

2.3.4 Delimiting Frames 帧的分割

TCP/IP是一个流式协议, 也就是说它没有内置的可以分割帧的机制. 现有的协议有以下几种不同的解决方法:
1 一次连接只发送一阵. 简单但是慢
2 在流中给帧加分隔符. 简单但是解析慢
3 计算帧的长度, 并在发送的时候在每个帧的开始标记长度. 这样简单而且快速, 我们选择这种

2.3.5 Frame Details 帧的细节

所有的帧都包括一个7字节的头, 任意字节的负载, 和一个字节的帧尾.


image.png

为了读取帧, 我们需要:
1 读头信息, 检查帧类型和通道
2 依靠帧类型, 我们读负载并执行
3 最后读帧尾
在考虑性能的实际使用场景中, 我们会使用"预读缓冲" 或者"收集读"来避免多个独立系统读取一个帧的情况.

2.3.5.1 Method Frames 方法帧

方法帧用来承载高级协议的命令. 一个方法帧表示一个命令. 方法帧的负载如下:


image.png

执行一个方法帧, 我们需要:
1 读取方法帧的负载
2 拆箱为一个结构体. 一个给定的方法通常都有同样的结构, 所以我们可以快速的拆箱.
3 检查在当前上下文中是否允许使用该方法
4 检查方法参数是否有效
5 执行方法
方法帧主体被够潮成一个AMQP数据列表. 编码直接由协议生成, 所以特别快

2.3.5.2 Content Frames 内容帧

内容是客户端通过AMQP服务器发送给另一个客户端的应用数据. 内容大概就是一系列属性加上一个二进制数据块. 属性由基础类定义, 这些组成了内容的头信息帧. 数据部分可以是任意大小的. 可能超过了多个数据块, 每一个都是内容主体帧.
观察一个通道, 我们可能会看到如下内容:


image.png

某些方法(比如 Basic.Publish)正式定义为会携带内容的方法. 当一个终端发送这样一个方法帧, 那么经常会在后面携带一个内容头和零个或多个内容体.
一个内容头格式如下:


image.png

我们把内容体放到不一样的帧中(而不是放到方法中), 以便AMQP可以支持零复制技术, 在该技术中内容从不会被编码. 我们把内容属性放在他们自己的帧中, 这样接收者就可以选择丢弃他们不需要的内容.

2.3.5.3 Heartbeat Frames 心跳帧

心跳是一种被设计来撤销TCP/IP特性的技术, 也就是说它可以将关闭了一段时间的断连接恢复连接. 在某些场景下, 我们需要迅速知道某一个端点是不是离线了, 或者其他不能响应的原因(比如死循环). 因为心跳可以在一个比较低的层级来实现, 所以我们在传输层以一种特殊类型的帧来实现, 而不是一个类方法.

2.3.6 Error Handling 异常处理

AMQP使用异常来处理错误. 任何可以操作错误(比如消息队列找不到, 没有权限等)最后都会是一个通道异常. 任何结构错误(无效参数, 方法序列化错误)都是一个连接异常. 异常会关闭通道或者连接, 然后返回一个代码和文按告知客户端应用. 我们使用三位数的应答码, 再加上文字, 就像HTTP协议和其他协议做的那样.

2.3.7 Closing Channels and Connections 关闭通道和连接

一个连接或者通道在客户端发送"Open", 服务端发送"Open-Ok"后会认为是打开的. 从这点来看, 一端想要关闭连接必须使用握手协议.
无论是正常还是由于异常关闭连接都需要特别小心. 突然地关闭一般不会被马上感知到, 而且后带来后续异常, 我们可能丢失错误响应码. 正确的做法是在所有关闭的时候都使用握手协议, 以便我们在关闭的时候可以确保其他客户端都已经知道是怎么回事了.
当一端决定关闭连接时, 它会发送一个关闭方法. 接收端必须响应一个"可以关闭"的信息, 然后双方可以关闭自己的连接. 注意, 如果某一端忽略了关闭, 当两端同时发送关闭可能会导致死锁.

2.4 AMQP Client Architecture AMQP客户端结构

我们可以直接从应用读写AMQP帧, 但是这肯定是一个糟糕的设计. 就算是最简单的AMQP通信也要比HTTP复杂, 应用开发人员为了发送信息给消息队列不需要知道二进制帧的结构. AMQP客户端结构建议由如下几个方面:
1 结构层. 在这一层以某种语言的结构获取AMQP协议方法, 然后序列化他们为wire-level结构. 该层可以直接通过AMQP说明构造(这是一个由XML实现的协议建模语言, 专门为了AMQP设计).
2 连接管理层. 这一层读写AMQP帧, 管理全部的连接和会话逻辑. 在这一层, 我们可以封装打开连接和会话, 错误处理, 内容的传输等逻辑. 这一层的大部分可以由AMQP自动创建. 例如,规范定义了哪些方法携带内容,因此可以机械地生成逻辑“发送方法,然后可选地发送内容”
3 API层. 这一层暴露应用使用的API. 这一层可能会反应一些存在的标准, 或者一些高级的AMQP方法, 在这一部分需要我们做好映射关系. AMQP方法的设计使得这个映射既简单又有效. API层可能会分为多层, 比如创建一个更高级的API层.
另外, 通常还有I/O层, 可以很简单, 也可以很复杂. 下图展示了建议的结构图


image.png

在本文中, 当我们说"客户端API"时, 我们指的是应用层之下的. 我们会使用"客户端API"和"应用"来指两个事, "应用"使用"客户端API"和中间件服务端通信.

你可能感兴趣的:(消息中间件 一 之 AMQP译文(上))