消息系统的基础知识

在谈论消息和消息传递系统时,需要考虑四个基本要素:

  • 通信的方向,可以是单向的,也可以是请求/回复交换

  • 消息的目的,这也决定了其内容

  • 消息的计时,可以在上下文中(同步)或脱离上下文(异步)发送和接收

  • 消息的传递,可以直接发生或通过代理发生

在接下来的部分中,我们将介绍 我们将正式确定这些方面,为我们以后的讨论提供基础。

一种方式与请求/回复模式

消息传递系统中最基本的方面是通信的方向,这通常也决定了其语义。

最简单的通信模式是消息以单向方式从源推送到目的地; 这是一个微不足道的情况,不需要太多解释:

image 2024 05 08 13 28 51 334
Figure 1. 图 13.1:单向通信

单向通信的典型示例是使用 WebSocket 向连接的浏览器发送消息的电子邮件或 Web 服务器,或者将任务分配给一组工作人员的系统。

另一方面,我们有请求/回复交换模式,其中一个方向的消息总是与相反方向的消息匹配(不包括错误条件)。 这种交换模式的典型示例是调用 Web 服务或向数据库发送查询。 下图显示了这个简单且众所周知的场景:

image 2024 05 08 13 31 47 382

请求/回复模式似乎是一个实现起来很简单的模式,但是,正如我们稍后将看到的,当通信通道是异步的或涉及多个节点时,它会变得更加复杂。 看一下下图中所示的示例:

image 2024 05 08 13 32 20 002
Figure 2. Figure 13.3: Multi-node request/reply communication

通过图 13.3 所示的设置,我们可以更好地理解某些请求/回复模式的复杂性。 如果我们考虑任意两个节点之间通信的方向,我们可以肯定地说它是单向的。 然而,从全局角度来看,发起者发送请求并依次接收关联的响应,即使来自不同的节点。 在这些情况下,请求/回复模式与纯粹的单向循环的真正区别在于请求和回复之间的关系,该关系保存在发起者中。 回复通常在与请求相同的上下文中处理。

消息类型

消息本质上是连接不同软件组件的一种手段,而这样做的原因各不相同:可能是因为我们想要获取另一个系统或组件所持有的某些信息,可能是因为我们想要远程执行操作,也可能是因为我们想要通知某些同行刚刚发生了某些事情。

根据通信的原因,信息内容也会有所不同。一般来说,我们可以根据目的确定三种类型的消息:

  • 命令消息

  • 事件消息

  • 文档消息

命令消息

您应该已经熟悉命令消息,因为它本质上是一个序列化的命令对象(我们在第 9 章行为设计模式的命令部分中了解了这一点)

此类消息的目的是触发接收方执行操作或任务。 为此,命令消息必须包含运行任务的基本信息,其中通常包括操作名称和参数列表。 命令消息可用于实现远程过程调用(RPC)系统、分布式计算,或者可以更简单地用于请求某些数据。 RESTful HTTP 调用是命令的简单示例; 每个 HTTP 动词都有特定的含义,并与精确的操作相关联:GET,检索资源; POST,创建一个新的; PUT/PATCH,更新它; 并删除,以销毁它。

事件消息

事件消息用于通知另一个组件发生了某些事情。 它通常包含事件的类型,有时还包含一些细节,例如上下文、主题或涉及的参与者。

在 Web 开发中,例如,当我们利用 WebSocket 将通知从服务器发送到客户端以传达某些数据的更改或系统状态的突变时,我们会使用事件消息。

事件是分布式应用程序中非常重要的集成机制,因为它们使我们能够将系统的所有节点保持在同一页面上。

文档消息

文档消息主要用于在组件和机器之间传输数据。 一个典型的例子是用于传输数据库查询结果的消息。

文档消息与命令消息(也可能包含数据)的主要区别在于,消息不包含任何告诉接收者如何处理数据的信息。 另一方面,文档消息和事件消息之间的主要区别在于,特定事件与已发生的事情之间没有关联。 通常,对命令消息的回复是文档消息,因为它们通常仅包含所请求的数据或操作的结果。

现在我们知道如何对消息的语义进行分类,让我们了解用于移动消息的通信通道的语义。

异步消息传递、队列和流

读到这里,您应该已经熟悉异步操作的特性了。 事实证明,同样的原则也适用于消息传递和通信。

我们可以将同步通信比作电话:两个对等点必须同时连接到同一通道,并且它们应该实时交换消息。 通常,如果我们想给别人打电话,我们要么需要另一部电话,要么终止正在进行的通信以开始新的通信。

异步通信类似于短信:它不需要接收方在发送时就连接到网络; 我们可能会立即或在未知的延迟后收到回复,或者我们可能根本没有收到回复。 我们可能会依次向多个收件人发送多条短信,并以任意顺序接收他们的回复(如果有)。 简而言之,我们可以使用更少的资源获得更好的并行性。

异步通信的另一个重要特征是消息可以被存储,然后尽快或稍后传送。 当接收者太忙而无法处理新消息或当我们想要保证传递时,这可能很有用。 在消息传递系统中,这是通过使用消息队列来实现的,消息队列是一个协调消息生产者和消费者之间通信的组件,在消息传递到目的地之前存储任何消息,如下图所示:

image 2024 05 08 13 35 34 450
Figure 3. 图 13.4:消息队列

如果由于任何原因消费者崩溃、与网络断开连接或速度变慢,消息将累积在队列中并在消费者重新上线后立即调度。 队列可以位于生产者中,或者在生产者和消费者之间分割(在对等架构中),或者位于充当通信中间件的专用外部系统中(代理)。

与消息队列具有相似(但不相同!)目标的另一种数据结构是日志。 日志是一种仅附加的数据结构,它是持久的,并且可以在消息到达时或通过访问其历史记录来读取其消息。 在消息传递和集成系统的上下文中,这也称为数据流。

与队列相比,在流中,消息在检索或处理时不会被删除。 这样,消费者可以在消息到达时检索它们,或者可以随时查询流以检索过去的消息。 这意味着流在访问消息时提供了更多自由,而队列通常一次只向消费者公开一条消息。 最重要的是,一个流可以由多个消费者共享,这些消费者可以使用不同的方法访问消息(甚至是相同的消息)。

图 13.5 让您了解流的结构与消息队列的结构对比:

image 2024 05 08 13 36 31 825
Figure 4. 图 13.5:流

当我们使用这两种方法实现示例应用程序时,您将能够在本章后面更好地理解队列和流之间的区别。

消息传递系统中要考虑的最后一个基本元素是系统节点连接在一起的方式,可以直接连接,也可以通过中介连接。

点对点或基于代理的消息传递

消息可以以点对点的方式直接传递给接收者,或者通过称为消息代理的集中式中介系统传递给接收者。 代理的主要作用是将消息的接收者与发送者解耦。 下图显示了两种方法之间的架构差异:

image 2024 05 08 13 37 15 978
Figure 5. 图 13.6:点对点通信与消息代理

在对等架构中,每个节点都直接负责将消息传递给接收者。 这意味着节点必须知道接收者的地址和端口,并且它们必须就协议和消息格式达成一致。 代理从等式中消除了这些复杂性:每个节点可以完全独立,并且可以与未指定数量的对等点进行通信,而无需直接了解其详细信息。

代理还可以充当不同通信协议之间的桥梁。 例如,流行的 RabbitMQ 代理 (nodejsdp.link/rabbitmq) 支持高级消息队列协议 (AMQP)、消息队列遥测传输 (MQTT) 和简单/流文本导向消息传递协议 (STOMP),从而支持多个应用程序支持不同的消息传递协议 进行互动。

MQTT (nodejsdp.link/mqtt) 是一种轻量级消息传递协议,专为机器对机器通信(例如物联网)而设计。 AMQP (nodejsdp.link/amqp) 是一种更复杂的消息传递协议,旨在成为专有消息传递中间件的开源替代方案。 STOMP (nodejsdp.link/stomp) 是一个轻量级的基于文本的协议,来自“HTTP 设计学院”。 这三个协议都是应用层协议并且基于 TCP/IP。

除了解耦和互操作性方面的优势之外,代理还可以提供附加功能,例如持久队列、路由、消息转换和监控,更不用说许多代理支持的开箱即用的广泛消息传递模式。

当然,没有什么可以阻止我们使用点对点架构来实现所有这些功能,但不幸的是,需要付出更多的努力。 尽管如此,选择点对点方法而不是经纪人可能有不同的原因:

  • 通过删除代理,我们从系统中消除了单点故障

  • 必须扩展代理,而在对等架构中,我们只需要扩展应用程序的单个节点

  • 无需中介即可交换消息 可以大大减少通信的延迟。

通过使用点对点消息传递系统,我们可以拥有更大的灵活性和功能,因为我们不受任何特定技术、协议或体系结构的约束。

现在我们已经了解了消息传递系统的基础知识,接下来让我们探讨一些最重要的消息传递模式。 让我们从发布/订阅模式开始。