热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

RabbitMQ交换机

一、交换机1.1作用Exchange(交换机)的作用就是接收消息并根据路由键转发消息到绑定的队列。1.2交换机常用属性属性含义Name交换机名称Type交换机类型,direct、t

一、交换机


1.1 作用

Exchange(交换机) 的作用就是接收消息并根据路由键转发消息到绑定的队列。

技术图片


1.2 交换机常用属性



































属性含义
Name交换机名称
Type交换机类型,direct、topic、fanout、headers等,它们本质都一样,只是消息转发的逻辑不同
Durability是否持久化,true 为持久化
Auto Delete当最后一个绑定到 Exchange 上的队列删除后,自动删除该 Exchange
Internal当前 Exchange 是否用于 RabbitMQ 内部使用,默认为 false
Arguments扩展参数,用于扩展 AMQP 协议自制定化使用

二、不同类型的交换机


2.1 Direct Exchange

**Direct Exchange (直连型交换机) ** 是根据消息携带的路由键(routing key)将消息投递给对应队列的,步骤如下:

1) 将一个队列绑定到某个交换机上,同时赋予该绑定一个路由键(routing key);
2) 当一个携带着路由值为 R 的消息被发送给直连交换机时,交换机会把它路由给绑定值同样为 R 的队列。

Direct 模式可以使用 RabbitMQ 自带的 Exchange 即 default Exchange ,所以不需要将 Exchange 进行任何绑定(binding) 操作,消息传递时,RouteKey 必须完全匹配才会被队列接受,否则消息会被丢弃。

我们之前的实例使用的就是 default Exchange

技术图片

实例:

消费者:

public class Consumer {
public static void main(String[] args) throws Exception {
// 1.创建连接工厂对象
ConnectionFactory cOnnectionFactory= new ConnectionFactory();
// 设置主机
connectionFactory.setHost("111.231.83.100");
// 设置端口
connectionFactory.setPort(5672);
// 设置虚拟主机
connectionFactory.setVirtualHost("/");
// 2.获取一个连接对象
final Connection cOnnection= connectionFactory.newConnection();
// 3.创建 Channel
final Channel channel = connection.createChannel();
String exchangeType = "direct";
// 4.声明交换机
String exchangeName = "directExchange";
channel.exchangeDeclare(exchangeName, exchangeType, true, false, false, null);
// 5.申明队列
String queueName = "directQueue";
channel.queueDeclare(queueName, false, false, false, null);
// 6.将交换机和队列进行绑定关系
// 注意:我们只绑定一个路由KEY,说明另一个路由不会被消费掉
String routingKey = "directA";
channel.queueBind(queueName, exchangeName, routingKey);
// 7.创建消费者
QueueingConsumer queueingCOnsumer= new QueueingConsumer(channel);
channel.basicConsume(queueName, true, queueingConsumer);
// 8.循环消费
System.err.println("消费端启动");
while (true) {
QueueingConsumer.Delivery delivery = queueingConsumer.nextDelivery();
String msg = new String(delivery.getBody());
System.err.println("消费端消费: " + msg);
}
}
}

生产者:

public class Producer {
public static void main(String[] args) throws Exception {
// 1.创建连接工厂对象
ConnectionFactory cOnnectionFactory= new ConnectionFactory();
// 设置主机
connectionFactory.setHost("111.231.83.100");
// 设置端口
connectionFactory.setPort(5672);
// 设置虚拟主机
connectionFactory.setVirtualHost("/");
// 2.获取一个连接对象
final Connection cOnnection= connectionFactory.newConnection();
// 3.创建 Channel
final Channel channel = connection.createChannel();
// 4.循环发送消息
// 声明交换机
String exchangeName = "directExchange";

// 发送消息
String routingKey1 = "directA";
String msg1 = "directA 消息";
channel.basicPublish(exchangeName, routingKey1, null, msg1.getBytes());
String routingKey2 = "directB";
String msg2 = "directB 消息";
channel.basicPublish(exchangeName, routingKey2, null, msg2.getBytes());
// 5.关闭资源
channel.close();
connection.close();
connectionFactory.clone();
}
}

先启动消费者,然后启动生产者,查看控制台输出:

消费端启动
消费端消费: directA 消息

可以看到未被绑定的 routingKey 消息未被消费掉。


2.2 Fanout Exchange

Fanout Exchange 是扇型交换机,它不处理路由键,只需要将队列绑定到交换机上。任何发送到 Fanout Exchange 的消息都会被转发到与该 Exchange 绑定(Binding)的所有 Queue上。它的转发消息是最快的。

技术图片

实例:

消费者

public class Consumer {
public static void main(String[] args) throws Exception {
// 1.创建连接工厂对象
ConnectionFactory cOnnectionFactory= new ConnectionFactory();
// 设置主机
connectionFactory.setHost("111.231.83.100");
// 设置端口
connectionFactory.setPort(5672);
// 设置虚拟主机
connectionFactory.setVirtualHost("/");
// 2.获取一个连接对象
final Connection cOnnection= connectionFactory.newConnection();
// 3.创建 Channel
final Channel channel = connection.createChannel();
String exchangeType = "fanout";
// 4.声明交换机
String exchangeName = "fanoutExchange";
channel.exchangeDeclare(exchangeName, exchangeType, true, false, false, null);
// 5.申明队列
String queueName1 = "fanoutQueue1";
channel.queueDeclare(queueName1, false, false, false, null);
String queueName2 = "fanoutQueue2";
channel.queueDeclare(queueName2, false, false, false, null);
// 6.将交换机和队列进行绑定关系,不需要设置 routingKey
channel.queueBind(queueName1, exchangeName, "");
channel.queueBind(queueName2, exchangeName, "");
// 7.创建消费者并消费
new Thread(() -> {
try {
handleConsumer(channel,queueName1);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
handleConsumer(channel,queueName2);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
private static void handleConsumer(Channel channel, String queueName) throws Exception {
QueueingConsumer queueingCOnsumer= new QueueingConsumer(channel);
channel.basicConsume(queueName, true, queueingConsumer);
// 7.循环消费
System.err.println(queueName + " 消费端启动");
while (true) {
QueueingConsumer.Delivery delivery = queueingConsumer.nextDelivery();
String msg = new String(delivery.getBody());
System.err.println(queueName + "消费端消费: " + msg);
}
}
}

生产者:

public class Producer {
public static void main(String[] args) throws Exception {
// 1.创建连接工厂对象
ConnectionFactory cOnnectionFactory= new ConnectionFactory();
// 设置主机
connectionFactory.setHost("111.231.83.100");
// 设置端口
connectionFactory.setPort(5672);
// 设置虚拟主机
connectionFactory.setVirtualHost("/");
// 2.获取一个连接对象
final Connection cOnnection= connectionFactory.newConnection();
// 3.创建 Channel
final Channel channel = connection.createChannel();

// 4.声明交换机
String exchangeName = "fanoutExchange";
// 5.循环发送消息
for (int i = 0; i <5; i++) {
String msg = "消息"+i;
channel.basicPublish(exchangeName, "", null, msg.getBytes());
}
// 6.关闭资源
channel.close();
connection.close();
connectionFactory.clone();
}
}

先启动消费者,然后启动生产者,查看控制台输出:

fanoutQueue1 消费端启动
fanoutQueue2 消费端启动
fanoutQueue1消费端消费: 消息0
fanoutQueue1消费端消费: 消息1
fanoutQueue2消费端消费: 消息0
fanoutQueue1消费端消费: 消息2
fanoutQueue2消费端消费: 消息1
fanoutQueue2消费端消费: 消息2
fanoutQueue1消费端消费: 消息3
fanoutQueue1消费端消费: 消息4
fanoutQueue2消费端消费: 消息3
fanoutQueue2消费端消费: 消息4

2.3 Topic Exchange

Topic Exchange(主题交换机) 会将所有发送到 Topic Exchange 的消息转发到所有关心 RouteKey 中指定的TopicQueue 上。

ExchangeRouteKey 和某 Topic 进行模糊匹配。使用这种类型,队列需要绑定一个 Topic。

注:可以使用通配符进行模糊匹配



  • # 匹配一个或多个词

  • * 匹配一个词

技术图片

实例:

消费者:

public class Consumer {
public static void main(String[] args) throws Exception {
// 1.创建连接工厂对象
ConnectionFactory cOnnectionFactory= new ConnectionFactory();
// 设置主机
connectionFactory.setHost("111.231.83.100");
// 设置端口
connectionFactory.setPort(5672);
// 设置虚拟主机
connectionFactory.setVirtualHost("/");
// 2.获取一个连接对象
final Connection cOnnection= connectionFactory.newConnection();
// 3.创建 Channel
final Channel channel = connection.createChannel();
String exchangeType = "topic";
// 4.声明交换机
String exchangeName = "topicExchange";
channel.exchangeDeclare(exchangeName, exchangeType, true, false, false, null);
// 5.申明队列
String queueName1 = "topicQueue1";
channel.queueDeclare(queueName1, false, false, false, null);
String queueName2 = "topicQueue2";
channel.queueDeclare(queueName2, false, false, false, null);
// 6.将交换机和队列进行绑定关系
// 匹配一个或多个词
String routingKey1 = "user.#";
channel.queueBind(queueName1, exchangeName, routingKey1);
// 匹配一个词
String routingKey2 = "order.*";
channel.queueBind(queueName2, exchangeName, routingKey2);
// 7.创建消费者并消费
new Thread(() -> {
try {
handleConsumer(channel,queueName1);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
handleConsumer(channel,queueName2);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
private static void handleConsumer(Channel channel, String queueName) throws Exception {
QueueingConsumer queueingCOnsumer= new QueueingConsumer(channel);
channel.basicConsume(queueName, true, queueingConsumer);
// 7.循环消费
System.err.println(queueName + " 消费端启动");
while (true) {
QueueingConsumer.Delivery delivery = queueingConsumer.nextDelivery();
String msg = new String(delivery.getBody());
System.err.println(queueName + "消费端消费: " + msg);
}
}

}

生产者:

public class Producer {
public static void main(String[] args) throws Exception {
// 1.创建连接工厂对象
ConnectionFactory cOnnectionFactory= new ConnectionFactory();
// 设置主机
connectionFactory.setHost("111.231.83.100");
// 设置端口
connectionFactory.setPort(5672);
// 设置虚拟主机
connectionFactory.setVirtualHost("/");
// 2.获取一个连接对象
final Connection cOnnection= connectionFactory.newConnection();
// 3.创建 Channel
final Channel channel = connection.createChannel();
// 4.声明交换机
String exchangeName = "topicExchange";
// 5.发送消息
// 模糊匹配多个词
channel.basicPublish(exchangeName, "user.a", null, "user.a".getBytes());
channel.basicPublish(exchangeName, "user.a.b", null, "user.a.b".getBytes());
// 模糊匹配一个词
channel.basicPublish(exchangeName, "order.a", null, "order.a".getBytes());
channel.basicPublish(exchangeName, "order.a.b", null, "order.a.b".getBytes());
// 6.关闭资源
channel.close();
connection.close();
connectionFactory.clone();
}
}

先启动消费者,然后启动生产者,查看控制台输出:

topicQueue2 消费端启动
topicQueue1 消费端启动
topicQueue1消费端消费: user.a
topicQueue1消费端消费: user.a.b
topicQueue2消费端消费: order.a

2.4 Headers Exchange

Headers Exchange(头交换机) 不处理路由键,而是根据发送的消息内容中的 headers 属性进行匹配。
在绑定 QueueExchange 时指定一组键值对;当消息发送到 RabbitMQ 时会取到该消息的 headersExchange 绑定时指定的键值对进行匹配;如果完全匹配则消息会路由到该队列,否则不会路由到该队列。headers 属性是一个键值对,可以是 Hashtable,键值对的值可以是任何类型。而 fanout,direct,topic 的路由键都需要要字符串形式的。

不经常使用,了解即可。


三、Dead Letter Exchange(死信交换机)


3.1 死信模式

死信模式指的是,当消费者不能处理接收到的消息时,将这个消息重新发布到另外一个队列中,等待重试或者人工干预,这个过程中的 exchangequeue 就是所谓的 Dead Letter ExchangeQueue


3.2 死信消息生成原因

消息变成死信有以下几种情况:



  • 消费者对消息使用了 **basicReject ** 或 者 basicNack 回复,并且 requeue 参数设置为 false,即不再将该消息重新在消费者间进行投递.

  • 消息由于消息有效期(per-message TTL)过期

  • 消息由于队列超过其长度限制而被丢弃


3.3 实例

死信消费者:

public class DeadLetterConsumer {
public static void main(String[] args) throws Exception {
// 1.创建连接工厂对象
ConnectionFactory cOnnectionFactory= new ConnectionFactory();
// 设置主机
connectionFactory.setHost("111.231.83.100");
// 设置端口
connectionFactory.setPort(5672);
// 设置虚拟主机
connectionFactory.setVirtualHost("/");
// 2.获取一个连接对象
final Connection cOnnection= connectionFactory.newConnection();
// 3.创建 Channel
final Channel channel = connection.createChannel();
// 4.声明死信交换机
String exchangeName = "deadLetterExchange";
String exchangeType = "topic";
channel.exchangeDeclare(exchangeName, exchangeType, true, false, false, null);
// 5.申明队列
String queueName = "deadLetterQueue";
channel.queueDeclare(queueName, false, false, false, null);
// 6.将交换机和队列进行绑定关系
String routingKey = "#";
channel.queueBind(queueName, exchangeName, routingKey);
// 7.创建消费者消费
QueueingConsumer queueingCOnsumer= new QueueingConsumer(channel);
channel.basicConsume(queueName, true, queueingConsumer);
System.err.println("死信消费端启动");
while (true) {
QueueingConsumer.Delivery delivery = queueingConsumer.nextDelivery();
String msg = new String(delivery.getBody());
System.err.println("死信消费端消费: " + msg);
}
}
}

业务消费者:

public class BusinessConsumer {
public static void main(String[] args) throws Exception {
// 1.创建连接工厂对象
ConnectionFactory cOnnectionFactory= new ConnectionFactory();
// 设置主机
connectionFactory.setHost("111.231.83.100");
// 设置端口
connectionFactory.setPort(5672);
// 设置虚拟主机
connectionFactory.setVirtualHost("/");
// 2.获取一个连接对象
final Connection cOnnection= connectionFactory.newConnection();
// 3.创建 Channel
final Channel channel = connection.createChannel();
String exchangeType = "topic";
// 4.声明交换机
String exchangeName = "businessExchange";
channel.exchangeDeclare(exchangeName, exchangeType, false, false, false, null);
// 5.申明队列
String queueName = "businessQueue";
Map arguments = new HashMap();
// 指定死信交换机名
arguments.put("x-dead-letter-exchange", "deadLetterExchange");
// 如果队列配置了参数 x-dead-letter-routing-key 的话,“死信”的路由key将会被替换成该参数对应的值。
// 如果没有设置,则保留该消息原有的路由key
// arguments.put("x-dead-letter-routing-key", "business.#");
channel.queueDeclare(queueName, true, false, false, arguments);
// 5.将交换机和队列进行绑定关系
String routingKey = "business.#";
channel.queueBind(queueName, exchangeName, routingKey);
// 6.创建消费者
QueueingConsumer queueingCOnsumer= new QueueingConsumer(channel);
// autoAck = false 设置手动确认消费
channel.basicConsume(queueName, false, queueingConsumer);
// 7.循环消费
System.err.println("业务消费端启动");
while (true) {
QueueingConsumer.Delivery delivery = queueingConsumer.nextDelivery();
String msg = new String(delivery.getBody());
System.err.println("业务消费端接收消息: " + msg);
// 设置消费失败
channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
}
}
}

生产者:

public class Producer {
public static void main(String[] args) throws Exception {
// 1.创建连接工厂对象
ConnectionFactory cOnnectionFactory= new ConnectionFactory();
// 设置主机
connectionFactory.setHost("111.231.83.100");
// 设置端口
connectionFactory.setPort(5672);
// 设置虚拟主机
connectionFactory.setVirtualHost("/");
// 2.获取一个连接对象
final Connection cOnnection= connectionFactory.newConnection();
// 3.创建 Channel
final Channel channel = connection.createChannel();
// 4.循环发送消息
// 声明交换机
String exchangeName = "businessExchange";
// 声明路由
String routingKey = "business.order";
for (int i = 0; i <5; i++) {
String msg = "business 消息" + i;
channel.basicPublish(exchangeName, routingKey, null, msg.getBytes());
}
// 5.关闭资源
channel.close();
connection.close();
connectionFactory.clone();
}
}


  1. 启动死信消费者

  2. 启动业务消费者
    3)启动生产者

  3. 观察控制台输出

业务消费者控制台输出:

业务消费端启动
业务消费端接收消息: business 消息0
业务消费端接收消息: business 消息1
业务消费端接收消息: business 消息2
业务消费端接收消息: business 消息3
业务消费端接收消息: business 消息4

死信消费者控制台输出:

死信消费端启动
死信消费端消费: business 消息0
死信消费端消费: business 消息1
死信消费端消费: business 消息2
死信消费端消费: business 消息3
死信消费端消费: business 消息4

可以看到处理失败的消息已经传递到死信交换机中,并被死信消费者消费。

从上面的代码,我们可以知道死信队列并不是什么特殊的队列,只不过是绑定在死信交换机上的队列。死信交换机也不是什么特殊的交换机,只不过是用来接受死信的交换机,所以可以为任何类型 Direct、Fanout、Topic。一般来说,会为每个业务队列分配一个独有的 路由key ,并对应的配置一个死信队列进行监听。


3.4 死信的处理方式

死信的产生是不可避免,我们需要从实际的业务角度和场景出发,对这些死信进行后续的处理,常见的处理方式大致有下面几种,



  1. 丢弃,如果不是很重要,可以选择丢弃

  2. 记录死信入库,然后做后续的业务分析或处理

  3. 通过死信队列,由负责监听死信的应用程序进行处理

RabbitMQ交换机



推荐阅读
  • 本文详细探讨了Java集合框架的使用方法及其性能特点。首先,通过关系图展示了集合接口之间的层次结构,如`Collection`接口作为对象集合的基础,其下分为`List`、`Set`和`Queue`等子接口。其中,`List`接口支持按插入顺序保存元素且允许重复,而`Set`接口则确保元素唯一性。此外,文章还深入分析了不同集合类在实际应用中的性能表现,为开发者选择合适的集合类型提供了参考依据。 ... [详细]
  • C++ STL 常见函数应用详解与实例解析
    本文详细解析了 C++ STL 中常见函数的应用,并通过具体实例进行说明。特别地,文章对迭代器(iterator)的概念进行了深入探讨,将其视为一种将迭代操作抽象化的工具,便于在不同容器间进行元素访问和操作。此外,还介绍了迭代器的基本类型、使用方法及其在算法中的应用,为读者提供了丰富的实践指导。 ... [详细]
  • C#中实现高效UDP数据传输技术
    C#中实现高效UDP数据传输技术 ... [详细]
  • Git基础操作指南:掌握必备技能
    掌握 Git 基础操作是每个开发者必备的技能。本文详细介绍了 Git 的基本命令和使用方法,包括初始化仓库、配置用户信息、添加文件、提交更改以及查看版本历史等关键步骤。通过这些操作,读者可以快速上手并高效管理代码版本。例如,使用 `git config --global user.name` 和 `git config --global user.email` 来设置全局用户名和邮箱,确保每次提交时都能正确标识提交者信息。 ... [详细]
  • 优化后的标题:数据网格视图(DataGridView)在应用程序中的高效应用与优化策略
    在应用程序中,数据网格视图(DataGridView)的高效应用与优化策略至关重要。本文探讨了多种优化方法,包括但不限于:1)通过合理的数据绑定提升性能;2)利用虚拟模式处理大量数据,减少内存占用;3)在格式化单元格内容时,推荐使用CellParsing事件,以确保数据的准确性和一致性。此外,还介绍了如何通过自定义列类型和优化渲染过程,进一步提升用户体验和系统响应速度。 ... [详细]
  • 为了在Fragment中直接调用Activity的方法,可以通过定义一个接口并让Activity实现该接口来实现。具体步骤包括:首先在Fragment中声明一个接口,并在Activity中实现该接口。接着,在Fragment中通过类型转换检查Activity是否实现了该接口,如果实现了则调用相应的方法。这种方法不仅提高了代码的解耦性,还增强了模块间的通信效率。此外,还可以通过ViewModel或LiveData等现代Android架构组件进一步优化这一过程,以实现更加高效和可靠的通信机制。 ... [详细]
  • 深入解析 OpenCV 2 中 Mat 对象的类型、深度与步长属性
    在OpenCV 2中,`Mat`类作为核心组件,对于图像处理至关重要。本文将深入探讨`Mat`对象的类型、深度与步长属性,这些属性是理解和优化图像操作的基础。通过具体示例,我们将展示如何利用这些属性实现高效的图像缩小功能。此外,还将讨论这些属性在实际应用中的重要性和常见误区,帮助读者更好地掌握`Mat`类的使用方法。 ... [详细]
  • PHP中元素的计量单位是什么? ... [详细]
  • 本文深入探讨了 iOS 开发中 `int`、`NSInteger`、`NSUInteger` 和 `NSNumber` 的应用与区别。首先,我们将详细介绍 `NSNumber` 类型,该类用于封装基本数据类型,如整数、浮点数等,使其能够在 Objective-C 的集合类中使用。通过分析这些类型的特性和应用场景,帮助开发者更好地理解和选择合适的数据类型,提高代码的健壮性和可维护性。苹果官方文档提供了更多详细信息,可供进一步参考。 ... [详细]
  • 在 HihoCoder 1505 中,题目要求从给定的 n 个数中选取两对数,使这两对数的和相等。如果直接对所有可能的组合进行遍历,时间复杂度将达到 O(n^4),因此需要考虑优化选择过程。通过使用哈希表或其他高效的数据结构,可以显著降低时间复杂度,从而提高算法的效率。具体实现中,可以通过预处理和存储中间结果来减少重复计算,进一步提升性能。 ... [详细]
  • 本文深入探讨了原型模式在软件设计中的应用与实现。原型模式通过使用已有的实例作为原型来创建新对象,而不是直接通过类实例化。这种方式不仅简化了对象的创建过程,还提高了系统的灵活性和效率。具体来说,原型模式涉及一个支持克隆功能的接口或基类,子类通过实现该接口来提供具体的克隆方法,从而实现对象的快速复制。此外,文章还详细分析了原型模式的优缺点及其在实际项目中的应用场景,为开发者提供了实用的指导和建议。 ... [详细]
  • 本文深入解析了 Apache 配置文件 `httpd.conf` 和 `.htaccess` 的优化方法,探讨了如何通过合理配置提升服务器性能和安全性。文章详细介绍了这两个文件的关键参数及其作用,并提供了实际应用中的最佳实践,帮助读者更好地理解和运用 Apache 配置。 ... [详细]
  • 前端技术实现调用摄像头进行拍照功能
    在公司项目中,为了实现调用摄像头进行拍照的功能,我们深入研究了HTML5的相关技术。尽管Java在许多方面表现出色,但在这一场景下,HTML5的灵活性和易用性更胜一筹。本文将分享具体的代码设计和实现细节,帮助开发者快速掌握这一功能。 ... [详细]
  • Python与R语言在功能和应用场景上各有优势。尽管R语言在统计分析和数据可视化方面具有更强的专业性,但Python作为一种通用编程语言,适用于更广泛的领域,包括Web开发、自动化脚本和机器学习等。对于初学者而言,Python的学习曲线更为平缓,上手更加容易。此外,Python拥有庞大的社区支持和丰富的第三方库,使其在实际应用中更具灵活性和扩展性。 ... [详细]
  • 本文介绍了如何通过掌握 IScroll 技巧来实现流畅的上拉加载和下拉刷新功能。首先,需要按正确的顺序引入相关文件:1. Zepto;2. iScroll.js;3. scroll-probe.js。此外,还提供了完整的代码示例,可在 GitHub 仓库中查看。通过这些步骤,开发者可以轻松实现高效、流畅的滚动效果,提升用户体验。 ... [详细]
author-avatar
余小刚玩guitarvp_996
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有