Bootstrap

RabbitMQ入门

目录

MQ 相关概念

什么是MQ

MQ 的作用

什么是RabbitMQ

RabbitMQ的安装

安装 erlang 

安装 RabbitMQ

安装 RabbitMQ 管理界面

开放云服务器端口

访问 RabbitMQ 管理界面 

RabbitMQ 的用户角色

RabbitMQ的工作流程

Producer 和 Consumer

Connection 和 Channel

Virrtual host

Queue

Exchange

AMQP

管理界面操作

 添加用户

设置虚拟机操作权限

更新或删除用户

退出当前用户

创建虚拟主机

入门代码

引入依赖

生产者代码实现

创建连接

创建 Channel

声明队列

发送消息

释放资源 

消费者代码实现

消费消息

完整代码

生产者代码

消费者代码

常见问题

IP、端口号不正确或未开放端口号

账号或密码错误

用户对虚拟机没有操作权限

队列不存在

资源释放的顺序


MQ 相关概念

什么是MQ

MQ(Message Queue),即 消息队列,因此,它本质上也是一个队列,遵循先进先出(FIFO),队列中存放的内容是 消息(message),消息可以非常简单,如字符串,也可以比较复杂,如内嵌对象

MQ常用于在分布式系统中传递数据,允许不同的应用程序或服务之间进行异步通信

系统之间的调用方式通常有两种

1. 同步通信

直接调用对方的服务,数据从一端发出后可以立即到达另一端

2. 异步通信

数据从一端出发,先进入到容器中进行临时存储,触发某个条件后,再从容器中发送到另一端。而容器的一个具体实现就是 MQ

因此,MQ的主要工作就是接收并转发消息

MQ 的作用

(1)解耦系统

通过消息队列,生产者和消费者可以不直接联系。生产者发送消息到队列,消费者从队列中读取消息并进行处理。这样,生产者不需要等待消费者处理完毕,就可以继续生产消息。这样各个系统或服务之间通过消息传递数据,而不需要知道对方的实现细节,减少了组件之间的直接依赖

(2)流量控制

通过消息队列,可以暂时缓解系统负载的压力。当系统接收到大量的请求时,生产者可以将请求放入队列中,消费者逐步从队列中处理这些请求,避免瞬间高并发对系统造成的压力

(3)负载均衡

多个消费者可以从同一个队列中获取消息进行处理,从而实现负载均衡。消息队列将消息均匀地分配给多个消费者,提高系统的处理能力和效率

(4)消息分发

当多个系统需要对统一数据做出响应时,可以使用MQ进行消息分发

(5)延迟通知

消息队列可以在特定时间后再处理某些任务,比如定时任务调度、预约任务等

......

RabbitMQ 则是 MQ 的一种实现

什么是RabbitMQ

RabbitMQ 是一个开源的消息中间件(Message Broker),实现了消息队列的功能,允许不同的应用程序或服务之间通过消息进行异步通信。RabbitMQ 采用 Erlang 语言实现了 AMQP(Advanced Message Queuing Protocol,高级消息队列协议)标准,提供了可靠的消息传递、队列管理、消息路由等功能

RabbitMQ的安装

此处使用的是 ubuntu 来进行 RabbitMQ 的安装

RabbitMQ 包含在标准的 Ununtu 仓库中,但其包含的版本通常比最新的 RabbitMQ 发行版落后很多,因此,RabbitMQ 团队制作了软件包,可参考官网:Installing on Debian and Ubuntu | RabbitMQ

由于这种方式比较复杂,在这里就使用 Ubuntu 仓库中的版本进行安装

我们先更新软件包:

apt-get update

安装 erlang 

RabbitMQ 需要 Erlang 语言的支持,因此在安装 RabbitMQ 之前需要安装erlang:

apt-get install erlang

 安装完成后,可以通过 erl 命令查看 erlang 版本:

可以使用 halt(). 命令或 ctrl + c 退出:

接下来,我们继续安装 RabbitMQ

安装 RabbitMQ

apt-get install rabbitmq-server

确认是否安装成功:

systemctl status rabbitmq-server

接下来,我们继续安装 RabbitMQ 管理界面

安装 RabbitMQ 管理界面

rabbitmq-plugins enable rabbitmq_management

 

安装完成

接下来,就可以启动服务了(若服务已经启动,则不再需要):

service rabbitmq-server start

接下来,我们就可以访问 RabbitMQ 的管理界面了

若是使用的是云服务器,则需要先开放端口 15672

开放云服务器端口

找到防火墙或是安全组,点击添加规则:

 成功开放 15672 端口

访问 RabbitMQ 管理界面 

然后就可以通过 IP:port (如:http://49.232.238.62:15672/)访问界面了:

其默认的用户名和密码都为 guest

 

但是却提示:用户只能通过 localhost 登录

这是因为 RabbitMQ 从 3.3.0 开始禁止使用 guest 权限通过除了 localhost 外的访问

那该如何登录呢?

可以添加管理员用户,然后以管理员用户的账号进行登录

添加用户:

rabbitmqctl add_user 账号 密码

添加用户 admin,密码 123456

rabbitmqctl add_user admin 123456

添加成功

接下来,我们为用户添加权限:

 rabbitmqctl set_user_tags 账号 角色名称

为 admin 添加超级管理员角色:

rabbitmqctl set_user_tags admin administrator

添加成功 

接下来,我们就来了解 RabbitMQ 的用户角色

RabbitMQ 的用户角色

 RabbitMQ 的用户角色分为:Administrator、Monitoring、Policymaker、Management、Impersonator 和 None

(1)Administrator(管理员)

管理员角色拥有完全的权限可以登录管理控制台,进行系统配置、管理用户、管理虚拟主机、队列、交换机、绑定等,具有最高权限

(2)Monitoring(监控)

监控角色只拥有读取权限可以登录管理控制台,主要用于查看 RabbitMQ 的状态和监控数据

(3)Policymaker(策略制定者)

策略制定者角色主要负责管理 RabbitMQ 的策略配置可以登录管理控制台,比如定义如何在虚拟主机中应用策略

(4)Management(管理)

管理角色通常具有在管理 UI(Web 界面)中进行操作的权限仅可登录管理控制台允许用户管理系统资源

(5)Impersonator(角色代理人)

代理人角色允许一个用户模拟或“冒充”另一个用户执行操作,无法登录管理控制台,用于调试或特殊的操作场景,如运维人员需要模拟其他用户的行为来进行故障排除或审计

(6)None(无权限)

无权限角色(其他用户)表示用户没有任何特殊权限,不被授权访问任何资源,无法登录管理控制台

使用设置的用户名和密码(admin 123456)进行登录:

登录成功: 

 接下来,我们先来学习 RabbitMQ 的工作流程

RabbitMQ的工作流程

RabbitMQ 是一个消息中间件,也是一个生产者消费者模型,负责接收、存储并转发消息

要理解图中的流程,我们需要先来了解一些基本的概念

Producer 和 Consumer

Producer生产者,是 RabbitMQ 的 客户端,向 RabbitMQ 发送消息

Consumer消费者,是 RabbitMQ 的 客户端,从 RabbitMQ 接收消息

Broker:就是 RabbitMQ Server,主要用于接收和发送消息

生产者(Producer)创建消息,并将消息发送到 RabbitMQ 中(消息通常是一个带有一定业务逻辑的数据,如 JSON 字符串)消息可以带有一定的标签,RabbitMQ 会根据标签进行路由,将消息发送给符合条件的消费者(Consumer)

消费者(Consumer)连接到 RabbitMQ 服务器就可以消费消息了,在消费的过程中,标签会被丢掉,消费者只会接收到消息,并不会知道消息的生产者是谁

对于 RabbitMQ 而言,一个 RabbitMQ Broker 可以简单看做一个 RabbitMQ 服务节点,或是 RabbitMQ 服务实例,也可以将一个 RabbitMQ Broker 看做一台 RabbitMQ 服务器

Connection 和 Channel

Connection:是客户端与 RabbitMQ 服务器之间的网络连接,通常是通过 TCP 协议建立的。在 RabbitMQ 中,连接是用于承载所有消息通道的基础设施,负责传输客户端和服务器之间的所有数据和控制信息

Channel:是在一个 Connection 上进行消息传递的 "虚拟通道"一个 Connection 上可以创建多个 Channel,每个 Channel 都是独立的虚拟连接,消息的发送和接收都是基于 Channel 的。通道在 RabbitMQ 中是轻量级的资源,相比 Connection,它的创建和销毁开销较小

Virrtual host

Virtual Host(虚拟主机):在 RabbitMQ 中,Virtual Host 是一种用于隔离不同应用程序之间消息传递的机制。它允许在同一 RabbitMQ 服务器实例中创建多个逻辑上的隔离空间,每个虚拟主机可以有独立的队列、交换机、绑定、权限等设置

类似于 MySQL 中的 database,一个 MySQL 服务器可以有多个 database

Queue

Queue(队列):是 RabbitMQ 的内部对象,用于存储消息

多个消费者可以订阅同一个队列

Exchange

Exchange(交换机) :负责接收生产者发送的消息,并根据特定的规则将这些消息路由到一个或多个 Queue 中

交换机是消息路由的中心,会根据类型和规则来确定如何转发接收到的消息

我们通过一个例子来进一步理解:

寄件人(生产者)的包裹(消息)被送到到物流中心(交换机)后,物流中心(交换机)会根据收件人(消费者)的地址(一定的规则)将快递分发到不同的站点(队列),然后再送到收件人(消费者)手中

其中,进行分发的过程,就是交换机根据类型和规则来将消息转发到队列的过程

此时,我们再来看流程图:

Producer 生产了一条消息 

Producer 需要连接到 RabbitMQ Broker,建立一个连接(Connection),开启一个信道(Channel)

接着,Producer 需要声明一个交换机(Exchange),用于将要发送的消息路由到指定的队列中

Producer 还需要声明一个队列(Queue),用于存放消息

次数,Producer 就可以将消息发送到 RabbitMQ Broker 中了

RabbitMQ Broker 接收消息,并存入对应的队列(Queue)中,若未找到对应队列,则根据生产者的配置,选择丢弃或是退回给生产者

我们还是以送快递为例,进一步进行理解:

RabbitMQ 可以看做物流公司,而 Broker 就像是物流公司的总部,负责协调和管理所有的物流站点,确保快递的安全,高效地运送快递

Virtual host 像是物流公司为不同的客户划分的独立运输中心,每个运输中心都有自己的仓库(Queue),分拣规则(Exchange)和运输路线(Connection 和 Channel),确保不同客户的包裹处理互不干扰,同时可以提供定制化的服务

Queue 就类似于快递站的一个个仓库,用来存放等待派送的包裹,每个仓库都有一个或多个快递员(消费者)负责从仓库中取出包裹并将其派送

Connection 就像是快递员与快递站之间的通信线路,快递员需要通过这个线路来接收和派送快递(消息)

Channel 就像是快递员在运送包裹时的多个并行线路,这样,快递员就可以同时处理多个包裹,如一边派送包裹,一边接收新的包裹

接下来,我们来继续学习 AMQP

AMQP

AMQP (Advanced Message Queuing Protocol) 是一种开放标准的消息传递协议,旨在支持不同平台和编程语言之间的消息通信。AMQP定义了一套确定消息交换的功能,包括交换机(Exchange)、队列(Queue)等。这些组件共同工作,使得生产者能够将消息发送到交换机,然后由队列接收并等待消费者接收。AMQP还定义了一个网络协议,允许客户端应用通过该协议与消息代理和 AMQP 模型进行通信

RabbitMQ 是遵从 AMQP 协议的,也就是说,RabbitMQ 是 AMQP 的 Erlang 的实现,且RabbitMQ 还支持 STOWP2、MQTT2 等协议

因此,AMQP 的模型结构与 RabbitMQ 的模型结构是一样的

在了解了基本概念之后,我们来学习如何 RabbitMQ 的管理界面上的相关操作

管理界面操作

我们首先来看 admin,也就是与用户相关的操作

 添加用户

进行添加用户操作:

添加成功:

点击要操作的用户,查看用户详情: 

在用户详情页面,可以进行更新、删除等操作

设置虚拟机操作权限

上述我们添加用户 zhangsan 后,不能操作任何虚拟机

我们可以在用户详情页面对其进行设置:

设置成功

更新或删除用户

在详情页,还可以进行用户的更新(更新密码或权限)和删除操作

退出当前用户

在右上角,可以退出当前用户(log out)

创建虚拟主机

在 admin 页面中,点击右侧的 Virtual Hosts

添加虚拟主机: 

添加成功:

且此时登录的用户默认可以操作该虚拟主机

若需要对虚拟主机进行设置,可以点击需要设置的虚拟主机,进入详情页

添加 zhangsan 用户

添加成功:

在这里我们先简单学习用户和虚拟机相关的操作,而关于 连接、交换机、队列等操作,我们在后续学习中慢慢进行理解

在学习了上述基础后,我们就来尝试编写代码

入门代码

要编写代码,首先要分析清楚我们要实现的功能:

生产者生产消息,将消息发送给 RabbitMQ,RabbitMQ 将消息发送给消费者

因此,实现的步骤为:

(1)引入依赖

(2)编写生产者代码

(3)编写消费者代码

引入依赖

        <!-- https://mvnrepository.com/artifact/com.rabbitmq/amqp-client -->
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.20.0</version>
        </dependency>

生产者代码实现

生产者要向 RabbitMQ 发送消息,首先需要与 RabbitMQ 建立连接

创建连接

而建立连接需要的信息有:

(1)IP

(2)端口号

(3)账号

(4)密码

(5)需要使用的虚拟主机

RabbitMQ 默认用于与客户端连接的 TCP 端口号是 5672,因此,需要提前进行开放:

为了方便演示,我们直接在 main 方法中编写代码: 


public class Producer {
    public static void main(String[] args) throws IOException, TimeoutException {
        // 1. 创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 2. 设置参数
        factory.setHost("49.232.238.62"); // ip 的默认值为 localhost
        factory.setPort(5672); // 默认值为 5672
        factory.setVirtualHost("test01"); // 虚拟主机,默认值为 /
        // 账号
        factory.setUsername("admin"); // 用户名,默认为 guest
        factory.setPassword("123456"); // 密码,默认为 guest
        // 3. 创建连接 Connection
        Connection connection = factory.newConnection(); // 需要处理异常,在此处直接进行抛出
    }
}

 连接建立好后,我们接着来创建 Channel

创建 Channel

        // 4. 创建 Channel
        Channel channel = connection.createChannel();

声明队列

需要使用 queueDeclare 方法来声明队列

Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments) throws IOException;

我们来看这个方法中的参数和返回值:

参数: 

queue:队列的名称

durable:队列是否持久化,若设置为 true,则持久化的队列会存盘,服务器重启后,消息不会丢失

exclusive:是否独占,若设置为 true,则只有一个消费者能监听队列

autoDelete:是否自动删除,当没有消费者(Consumer)时,是否自动删除队列

arguments:用于设置队列的属性

返回值:

Queue.DeclareOk:声明确认方法,用于标识队列已成功声明

声明一个名为 simple.test 的持久化队列:

        // 5. 声明队列
        channel.queueDeclare("simple.test", true, false, false, null);

simple.test 队列不被一个消费者独占,不会自动删除,队列的属性不进行设置

声明队列后,若不存在 simple.test 队列,则会自动创建;若存在,则不进行创建,使用已有的

接下来,我们就可以向 RabbitMQ 发送消息了

发送消息

在前面我们了解到,生产者发送的消息,由 Exchange(交换机)接收,并根据特定的规则将这些消息路由到一个或多个 Queue 中

当一个新的 RabbitMQ 节点启动时,会预声明(declare)几个内置的交换机,内置交换机名称是空字符串(""),生产者发送的消息会根据队列名称直接路由到对应的队列

例如,有一个名为 "simple.test" 的队列,当我们使用内置的交换机时,可以看做生产者直接发送消息到  "simple.test" 队列中,消费者可以直接从  "simple.test" 队列中接收消息,而不需要关心交换机的存在。这种模式适合非常简单的应用场景,生产者和消费者之间的通信是一对一

在这里,我们就使用内置的交换机来将生产者的消息存储到  "simple.test" 队列中

通过 basicPublish 方法来进行发送

我们来看 basicPublish 方法:

void basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws IOException;

exchange:交换机名称,若使用内置的交换机,默认为 ""

routingKey:路由键,当使用内置交换机时,routingKey 为队列名称

props:配置相关信息

body:要发送的消息

        // 6. 通过 channel 发送消息到队列中
        String message = "test...";
        channel.basicPublish("", "simple.test", null, message.getBytes());
        System.out.println("消息:" + message + " 发送成功");

释放资源 

当我们发送完消息后,不要忘记释放资源

        // 7. 释放资源
        channel.close();
        connection.close();

当 Connection 关闭时,Channel 也会自动关闭,因此,Channel 可以不用关闭,但是显示关闭 Channel 是一个良好的习惯

生产者的代码编写完成后,我们就可以运行代码,并观察结果了

我们先打开 RabbitMQ 管理界面 中的 Queues and Streams 界面:

运行代码,再次观察:

此时,创建了 simple.test 队列,且队列中有一条消息

我们查看 simple.test 的详细信息:

点击 Get messages:  

点击 Get Message(s) 获取队列中的一条消息,可以看到, 生产者的消息成功发送到 simple.test 队列中

此时,我们注释掉释放资源的代码,再次运行程序:

 就可以在 Connections Channels 中查看到相关信息了:

接下来,我们继续编写消费者的代码

消费者代码实现

消费者要从 RabbitMQ 中获取消息,也需要先与其建立连接,并创建 Channel,同时声明队列 Queue

public class Consumer {
    public static void main(String[] args) throws IOException, TimeoutException {
        // 1. 创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 2. 设置参数
        factory.setHost("49.232.238.62"); // ip 的默认值为 localhost
        factory.setPort(5672); // 默认值为 5672
        factory.setVirtualHost("test01"); // 虚拟主机,默认值为 /
        // 账号
        factory.setUsername("admin"); // 用户名,默认为 guest
        factory.setPassword("123456"); // 密码,默认为 guest
        // 3. 创建连接 Connection
        Connection connection = factory.newConnection(); // 需要处理异常,在此处直接抛出,并不进行处理
        // 4. 创建 Channel
        Channel channel = connection.createChannel();
        // 5. 声明队列
        channel.queueDeclare("simple.test", true, false, false, null);
    }
}

也可以不声明队列,但是若队列不存在,就会抛出异常,因此,我们在编写消费者代码时也声明队列 simple.test

接下来,我们就可以消费队列中的消息了

消费消息

使用 basicConsume 方法来消费消息:

String basicConsume(String queue, boolean autoAck, Consumer callback) throws IOException;

queue:队列名称

autoAck:是否自动确认,消费者收到消息后,自动向 MQ 确认已经收到消息

callback:回调对象

我们重点来看 callback 这个回调对象,其类型为 Consumer

Consumer 是一个接口,用于定义消息消费者的行为,当我们需要从 RabbitMQ 接收消息时,需要提供一个实现了 Consumer 接口的对象

DefaultConsumer 是 RabbitMQ 提供的一个默认消费者,实现类 Consumer 接口

其中,handleDelivery 方法是其核心方法

当从队列中接收到消息后,会自动调用该方法,在方法中,我们可以实现如何处理接收到的消息,如打印消息内容、将消息存储到数据库等

其中的参数有:

consumerTag:消费者标签,通常是消费者的订阅队列时指定

envelope:包含消息的封包信息,如队列名称、交换机名称等

properties:配置信息

body:消息内容

我们定义 DefaultConsumer 对象,并重写 handleDelivery 方法,在方法中实现消息的处理逻辑:

        // 6. 消费消息
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 回调方法,当接收到消息后,自动执行该方法
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("成功接收到消息: " + new String(body));
            }
        };

在这里,我们就简单的打印接收到的消息

再使用  basicConsume 方法消费消息:

        channel.basicConsume("simple.test", true, consumer);

消费 simple.test 队列中的消息,当消费者接收到消息后,自动向 RabbitMQ 进行确认

最后,释放资源:

        // 7. 释放资源
        channel.close();
        connection.close();

消费者相当于是一个监听程序,需要监听队列中是否有消息需要消费,因此,在大多数情况下,不需要关闭资源

 我们运行消费者代码,并观察结果:

由于刚才生产者代码运行了两次,因此向队列中发送了两条消息,而这两条消息都成功被消费者接收

若我们不释放消费者的资源,则可以看到响应的 Connection Channel

 在 Queues and Streams 页面,点击右侧的 +/-: 

点击添加消费者数量: 

可以看到此时 simple.test 队列有一个消费者: 

完整代码

以下是生产者和消费者的完整代码:

生产者代码

public class Producer {
    public static void main(String[] args) throws IOException, TimeoutException {
        // 1. 创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 2. 设置参数
        factory.setHost("49.232.238.62"); // ip 的默认值为 localhost
        factory.setPort(5672); // 默认值为 5672
        factory.setVirtualHost("test01"); // 虚拟主机,默认值为 /
        // 账号
        factory.setUsername("admin"); // 用户名,默认为 guest
        factory.setPassword("123456"); // 密码,默认为 guest
        // 3. 创建连接 Connection
        Connection connection = factory.newConnection(); // 需要处理异常,在此处直接抛出,并不进行处理
        // 4. 创建 Channel
        Channel channel = connection.createChannel();
        // 5. 声明队列
        channel.queueDeclare("simple.test", true, false, false, null);
        // 6. 通过 channel 发送消息到队列中
        String message = "test...";
        channel.basicPublish("", "simple.test", null, message.getBytes());
        System.out.println("消息:" + message + " 发送成功");
        // 7. 释放资源
        channel.close();
        connection.close();
    }
}

消费者代码

public class Consumer {
    public static void main(String[] args) throws IOException, TimeoutException {
        // 1. 创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 2. 设置参数
        factory.setHost("49.232.238.62"); // ip 的默认值为 localhost
        factory.setPort(5672); // 默认值为 5672
        factory.setVirtualHost("test01"); // 虚拟主机,默认值为 /
        // 账号
        factory.setUsername("admin"); // 用户名,默认为 guest
        factory.setPassword("123456"); // 密码,默认为 guest
        // 3. 创建连接 Connection
        Connection connection = factory.newConnection(); // 需要处理异常,在此处直接抛出,并不进行处理
        // 4. 创建 Channel
        Channel channel = connection.createChannel();
        // 5. 声明队列
        channel.queueDeclare("simple.test", true, false, false, null);
        // 6. 消费消息
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 回调方法,当接收到消息后,自动执行该方法
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("成功接收到消息: " + new String(body));
            }
        };
        channel.basicConsume("simple.test", true, consumer);
        // 7. 释放资源
        channel.close();
        connection.close();
    }
}

常见问题

IP、端口号不正确或未开放端口号

若连接的 IP 或是端口号不正确,又或是未开放端口号,则会连接不上,等待一段时间,程序就会抛出异常:

连接超时,没有进一步的信息

此时,就需要检查 IP 和 端口号是否正确,以及端口号是否已经开放

账号或密码错误

当账号或密码错误时,会抛出异常:

此时就需要检查用户名和密码是否正确

用户对虚拟机没有操作权限

进入 admin 详情页,删除 admin 对 test01 虚拟主机的操作权限:

此时再次进行访问:

当前用户对 test01 虚拟主机没有操作权限

此时我们就需要检查当前用户的操作权限

队列不存在

找到 simple.test 队列,进入详情页:

将其删除: 

并注释掉队列声明的代码,此时再次运行程序:

在虚拟主机 test01 上不存在 simple.test 队列

因此,我们最好每次都将声明队列加上,防止队列不存在时抛出异常,影响后续程序的执行

资源释放的顺序

在关闭资源时,一定是先关闭 Channel,再关闭 Connectiion

若先关闭 Connection,再关闭 Channel,就会出现问题:

这是因为当 Connection 关闭时,Channel 也会自动关闭,而此时再尝试关闭 Channel,自然会报错

;