RabbitMQ六种模式介绍

参考:

RabbitMQ官网

鸟哥RabbitMQ教程

RabbitMQ常用交换机模式

RabbitMQ之RPC实现


导入RabbitMQ的依赖:

<dependency>
   <groupId>com.rabbitmq</groupId>
   <artifactId>amqp-client</artifactId>
   <version>3.4.1</version>
</dependency>

一、简单队列模式

图示:

(P) -> [|||] -> (C)

P:代表生产者

红色方块:代表队列

C:代表消费者

创建RabbitMQ连接工具类:

public class ConnectionUtil {
    public static void main(String[] args) throws Exception {
        System.out.println(getConnection());
    }

    public static Connection getConnection() throws Exception {
//        定义连接工厂
        ConnectionFactory factory = new ConnectionFactory();
//        服务地址
        factory.setHost("localhost");
//        端口号
        factory.setPort(5672);
//        vhost
        factory.setVirtualHost("testhost");
//        用户名
        factory.setUsername("admin");
//        密码
        factory.setPassword("admin");
        return factory.newConnection();
    }

}

1.发送消息

(P) -> [|||]

public class Send {

    private final static String QUEUE_NAME = "q_test_01";

    public static void main(String[] argv) throws Exception {
//      获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
//        从连接中创建通道
        Channel channel = connection.createChannel();
//        声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        //消息内容
        String message = "Hello World";
        channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
        System.out.println("[x] send " + message + "");
        //关闭通道
        channel.close();
        //关闭连接
        connection.close();
    }

}

2.接收消息

[|||] -> (C)

 

public class Recv {
    private final static String QUEUE_NAME = "q_test_01";

    public static void main(String[] argv) throws Exception {

        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        // 从连接中创建通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);

        // 监听队列
        channel.basicConsume(QUEUE_NAME, true, consumer);

        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [x] Received '" + message + "'");
        }
    }
}

二、Work模式

图示:

 1个生产者,1个队列,2个消费者

1.发送消息

public class Send {
    private final static String QUEUE_NAME = "test_queue_work";

    public static void main(String[] args) throws Exception {
        //获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        for (int i = 0; i < 100; i++) {
            //消息内容
            String message = "" + i;
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            System.out.println(" [x] Sent '" + message + "'");
            Thread.sleep(10);
        }
        channel.close();
        connection.close();
    }
}

2.接受消息

消费者1:

public class Recv1 {
    private final static String QUEUE_NAME = "test_queue_work";

    public static void main(String[] args) throws Exception {
        //获取到连接
        Connection connection = ConnectionUtil.getConnection();
        //获取到mq通道
        Channel channel = connection.createChannel();
        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        //同一时刻服务器只会发送一条消息给消费者
        channel.basicQos(1);
        //定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        //监听队列,false表示手动返回完成状态,true表示自动
        channel.basicConsume(QUEUE_NAME, true, consumer);

        //获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println("[y] Received" + message);
            //休眠
            Thread.sleep(10);
            //返回确认状态,注释掉表示使用自动确认模式
//            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
    }
}

消费者2:

public class Recv2 {

    private final static String QUEUE_NAME = "test_queue_work";

    public static void main(String[] argv) throws Exception {

        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 同一时刻服务器只会发一条消息给消费者
        //队列只有在收到消费者发回的上一条消息 ack 确认后,才会向该消费者发送下一条消息。
        channel.basicQos(1);

        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列,false表示手动返回完成状态,true表示自动
        channel.basicConsume(QUEUE_NAME, false, consumer);

        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [x] Received '" + message + "'");
            //休眠1秒
            Thread.sleep(10000);
            //下面这行注释掉表示使用自动确认模式,只有手动ack确认后,队列才会推送下一条消息到消费者
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
    }
}

备注:

1.消费者1开启了ack自动确认,

2.消费者2关闭了ack自动确认,使用的是手动ack确认,只有队列收到了上N(N为设置的Qos值)条消息的ack确认后才会发送下N条消息。

3.怎样才能做到按照每个消费者的能力分配消息呢?联合使用 Qos 和 Acknowledge 就可以做到。
basicQos 方法设置了当前信道最大预获取(prefetch)消息数量为1。消息从队列异步推送给消费者,消费者的 ack 也是异步发送给队列,从队列的视角去看,总是会有一批消息已推送但尚未获得 ack 确认,Qos 的 prefetchCount 参数就是用来限制这批未确认消息数量的。设为1时,队列只有在收到消费者发回的上一条消息 ack 确认后,才会向该消费者发送下一条消息。prefetchCount 的默认值为0,即没有限制,队列会将所有消息尽快发给消费者。
 

4.消息的确认模式
消费者从队列中获取消息,服务端如何知道消息已经被消费呢?

4.1 模式1:自动确认
只要消息从队列中获取,无论消费者获取到消息后是否成功消息,都认为是消息已经成功消费。
4.2 模式2:手动确认
消费者从队列中获取消息后,服务器会将该消息标记为不可用状态,等待消费者的反馈,如果消费者一直没有反馈,那么该消息将一直处于不可用状态。

开启手动ack确认:
 

5.两种分发方式

5.1 轮询分发 :

使用任务队列的优点之一就是可以轻易的并行工作。如果我们积压了好多工作,我们可以通过增加工作者(消费者)来解决这一问题,使得系统的伸缩性更加容易。在默认情况下,RabbitMQ将逐个发送消息到在序列中的下一个消费者(而不考虑每个任务的时长等等,且是提前一次性分配,并非一个一个分配)。平均每个消费者获得相同数量的消息。这种方式分发消息机制称为Round-Robin(轮询)。

使用轮询分发:两个消费者的Qos值设为相同的,关闭手动应答,改为自动应答。

5.2 公平分发 :

虽然上面的分配法方式也还行,但是有个问题就是:比如:现在有2个消费者,所有的奇数的消息都是繁忙的,而偶数则是轻松的。按照轮询的方式,奇数的任务交给了第一个消费者,所以一直在忙个不停。偶数的任务交给另一个消费者,则立即完成任务,然后闲得不行。而RabbitMQ则是不了解这些的。这是因为当消息进入队列,RabbitMQ就会分派消息。它不看消费者为应答的数目,只是盲目的将消息发给轮询指定的消费者。
使用公平分发:关闭自动应答,改为手动应答。
 

测试数据:

消费者1:休眠10ms(能力强)

消费者2:休眠100ms(能力弱)

消费者1和消费者2都开启了手动ack确认,消费者1消费了81条消息,消费者2消费了19条数据,消息顺序没规律。(公平分发)

消费者1和消费者2都开启了自动ack确认,消费者1消费了50条消息,消费者2消费了50条数据,奇偶轮询。(轮询分发)

三、订阅模式

特点:

1.1个生产者,多个消费者
2.每个消费者都有自己的一个队列
3.生产者没有将消息直接发送到队列,而是发送到了交换机
4.每个队列都要绑定到交换机
5.生产者发送的消息,经过交换机,到达队列,实现一个消息被多个消费者获取

发送消息:

public class Send {
    private final static String EXCHANGE_NAME = "test_exchange_fanout";

    public static void main(String[] args) throws Exception {
        //获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        //声明exchange,exchange一旦创建就不能改变,而服务器端创建了exchange,客户端也创建一遍就会报错
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
        //消息内容
        String message = "Hello World";
        //注意:消息发送到没有队列绑定的交换机时,消息将丢失,因为,交换机没有存储消息的能力,消息只能存在在队列中。
        channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
        System.out.println("sent " + message);
        channel.close();
        connection.close();

    }
}

接受消息:

消费者1:

public class Recv1 {
    private final static String QUEUE_NAME = "test_queue_work1";
    private final static String EXCHANGE_NAME = "test_exchange_fanout";

    public static void main(String[] args) throws Exception {
        //获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        //绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
        //同一时刻服务器只会发送一条消息给消费者
        channel.basicQos(1);
        //定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        //监听队列,手动返回完成
        channel.basicConsume(QUEUE_NAME, false, consumer);
        //获取消息
        while (true) {
            //消费者接受队列推送过来的消息
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [Recv] Received '" + message + "'");
            Thread.sleep(10);
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }


    }
}

消费者2:

public class Recv2 {

    private final static String QUEUE_NAME = "test_queue_work2";

    private final static String EXCHANGE_NAME = "test_exchange_fanout";

    public static void main(String[] argv) throws Exception {

        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");

        // 同一时刻服务器只会发一条消息给消费者
        channel.basicQos(1);

        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列,手动返回完成
        channel.basicConsume(QUEUE_NAME, false, consumer);

        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [Recv2] Received '" + message + "'");
            Thread.sleep(10);

            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
    }
}

备注:

1.一个消费者队列可以有多个消费者实例,只有其中一个消费者实例会消费 

2.在创建exchange时,一旦创建就不能改变,而服务端创建了exchange,客户端也创建一遍就会报错。

3.消息发送到没有队列绑定的交换机时,消息将丢失,因为,交换机没有存储消息的能力,消息只能存在在队列中。

在管理工具中查看队列和交换机的绑定关系:

这里写图片描述

交换机类型为:fanout(广播式交换机)

1.这种模式不需要指定Routing key路由键,一个交换机可以绑定多个队列queue,一个queue可同时与多个exchange交换机进行绑定;

2.如果消息发送到交换机上,但是这个交换机上面没有绑定的队列,那么这些消息将会被丢弃;

四、路由模式

交换机类型为:direct(直连交换机)

说明:

1.任何发送到Direct Exchange的消息都会被转发到指定RouteKey中指定的队列Queue;
2.生产者生产消息的时候需要执行Routing Key路由键;
3.队列绑定交换机的时候需要指定Binding Key,只有路由键与绑定键相同的话,才能将消息发送到绑定这个队列的消费者;
4.如果vhost中不存在RouteKey中指定的队列名,则该消息会被丢弃。

发送消息:

public class Send {
    private final static String EXCHANGE_NAME = "test_exchange_direct";

    public static void main(String[] args) throws Exception {
        //获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        //声明exchange,交换机类型为:direct
        channel.exchangeDeclare(EXCHANGE_NAME, "direct");
        //消息内容
        String message = "新增商品";
//        消息的key为:delete
        channel.basicPublish(EXCHANGE_NAME, "insert", null, message.getBytes());
        System.out.println("[x] sent " + message);
        channel.close();
        connection.close();
    }
}

接受消息:
消费者1:

public class Recv1 {
    private final static String QUEUE_NAME = "test_queue_direct_1";
    private final static String EXCHANGE_NAME= "test_exchange_direct";

    public static void main(String[] args) throws Exception {
        //获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        //声明队列
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);
        //绑定队列到交换机
        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"update");
        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"delete");
        //同一时刻服务器只会发送一条消息给消费者
        channel.basicQos(1);
        //定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        //监听队列,手动返回完成
        channel.basicConsume(QUEUE_NAME, false, consumer);
        //获取消息
        while (true) {
            //消费者接受队列推送过来的消息
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [Recv] Received '" + message + "'");
            Thread.sleep(10);
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }

    }
}

消费者2:

public class Recv2 {
    private final static String QUEUE_NAME = "test_queue_direct_2";
    private final static String EXCHANGE_NAME = "test_exchange_direct";

    public static void main(String[] args) throws Exception {
        //获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        //绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "insert");
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "delete");
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "update");

        //同一时刻服务器只会发送一条消息给消费者
        channel.basicQos(1);
        //定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        //监听队列,手动返回完成
        channel.basicConsume(QUEUE_NAME, false, consumer);
        //获取消息
        while (true) {
            //消费者接受队列推送过来的消息
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [Recv] Received '" + message + "'");
            Thread.sleep(10);
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }

    }
}

 

五、主题模式(通配符模式)


交换机类型为:topic

这里写图片描述

1.任何发送到Topic Exchange的消息都会被转发到所有满足Route Key与Binding Key模糊匹配的队列Queue上;
2.生产者发送消息的时候需要指定Route Key,同时绑定Exchange与Queue的时候也需要指定Binding Key;
3.#” 表示0个或多个关键字,“*”表示匹配一个关键字;
4.如果Exchange没有发现能够与RouteKey模糊匹配的队列Queue,则会抛弃此消息;
5.如果Binding中的Routing key *,#都没有,则路由键跟绑定键相等的时候才转发消息,类似Direct Exchange;

6.如果Binding中的Routing key为#或者#.#,则全部转发,类似Fanout Exchange;

发送消息:

public class Send {

    private final static String EXCHANGE_NAME = "test_exchange_topic";

    public static void main(String[] argv) throws Exception {
        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明exchange
        channel.exchangeDeclare(EXCHANGE_NAME, "topic");

        // 消息内容
        String message = "Hello World!!";
        channel.basicPublish(EXCHANGE_NAME, "routekey.1", null, message.getBytes());
        System.out.println(" [x] Sent '" + message + "'");

        channel.close();
        connection.close();
    }
}

接受消息:

消费者1:

public class Recv1 {

    private final static String QUEUE_NAME = "test_queue_topic_work_1";

    private final static String EXCHANGE_NAME = "test_exchange_topic";

    public static void main(String[] argv) throws Exception {

        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "routekey.*");

        // 同一时刻服务器只会发一条消息给消费者
        channel.basicQos(1);

        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列,手动返回完成
        channel.basicConsume(QUEUE_NAME, false, consumer);

        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [Recv_x] Received '" + message + "'");
            Thread.sleep(10);

            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
    }
}

消费者2:

public class Recv2 {
    private final static String QUEUE_NAME = "test_queue_topic_work_2";

    private final static String EXCHANGE_NAME = "test_exchange_topic";

    public static void main(String[] argv) throws Exception {

        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "*.*");

        // 同一时刻服务器只会发一条消息给消费者
        channel.basicQos(1);

        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列,手动返回完成
        channel.basicConsume(QUEUE_NAME, false, consumer);

        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [Recv2_x] Received '" + message + "'");
            Thread.sleep(10);

            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
    }
}

备注:

  同一条消息可以发送到多个队列上,一个队列上只有一个消费者实例会消费消息。

六:RPC模式

RPC的处理流程:

1.当客户端启动时,创建一个匿名的回调队列。
2.客户端为RPC请求设置2个属性:replyTo,设置回调队列名字;correlationId,标记request。
3.请求被发送到rpc_queue队列中。
4.RPC服务器端监听rpc_queue队列中的请求,当请求到来时,服务器端会处理并且把带有结果的消息发送给客户端。接收的队列就是replyTo设定的回调队列。
5.客户端监听回调队列,当有消息时,检查correlationId属性,如果与request中匹配,那就是结果了。

 

RPC服务端;

public class RPCServer {
    private static final String RPC_QUEUE_NAME = "rpc_queue";

    //具体处理方法
    private static int fib(int n) {
        if (n == 0)
            return 0;
        if (n == 1)
            return 1;
        return fib(n - 1) + fib(n - 2);
    }

    public static void main(String[] argv) throws Exception {

        //从工厂中获取连接
        Connection connection = ConnectionUtil.getConnection();
        //从连接中获取信道
        final Channel channel = connection.createChannel();
        //name:队列名字
        channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);

        channel.basicQos(1);
        QueueingConsumer consumer = new QueueingConsumer(channel);
        channel.basicConsume(RPC_QUEUE_NAME, false, consumer);
        System.out.println(" [x] Awaiting RPC requests");

        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            BasicProperties props = delivery.getProperties();
//          BasicProperties replyProps = new BasicProperties.Builder().correlationId(props.getCorrelationId()).build();
            AMQP.BasicProperties replyProps = new AMQP.BasicProperties.Builder()
                    .correlationId(props.getCorrelationId()).build();
            String message = new String(delivery.getBody());
            int n = Integer.parseInt(message);
            System.out.println(" [.] fib(" + message + ")");
            String repsonse = "" + fib(n);
            channel.basicPublish("", props.getReplyTo(), replyProps, repsonse.getBytes());
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
    }
}

RPC客户端:

public class RPCClient {
    private Connection connection;
    private Channel channel;
    private String requestQueueName = "rpc_queue";
    private String replyQueueName;

    public RPCClient() throws Exception {
        //建立一个连接和一个通道,并为回调声明一个唯一的'回调'队列
        connection = ConnectionUtil.getConnection();
        channel = connection.createChannel();
        //定义一个临时变量的接受队列名
        replyQueueName = channel.queueDeclare().getQueue();
    }
    //发送RPC请求
    public String call(String message) throws IOException, InterruptedException {
        //生成一个唯一的字符串作为回调队列的编号
        String corrId = UUID.randomUUID().toString();
        //发送请求消息,消息使用了两个属性:replyto和correlationId
        //服务端根据replyto返回结果,客户端根据correlationId判断响应是不是给自己的
        AMQP.BasicProperties props = new AMQP.BasicProperties.Builder().correlationId(corrId).replyTo(replyQueueName)
                .build();

        //发布一个消息,requestQueueName路由规则
        channel.basicPublish("", requestQueueName, props, message.getBytes("UTF-8"));

        //由于我们的消费者交易处理是在单独的线程中进行的,因此我们需要在响应到达之前暂停主线程。
        //这里我们创建的 容量为1的阻塞队列ArrayBlockingQueue,因为我们只需要等待一个响应。
        final BlockingQueue<String> response = new ArrayBlockingQueue<String>(1);

        // String basicConsume(String queue, boolean autoAck, Consumer callback)
        channel.basicConsume(replyQueueName, true, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                //检查它的correlationId是否是我们所要找的那个
                if (properties.getCorrelationId().equals(corrId)) {
                    //如果是,则响应BlockingQueue
                    response.offer(new String(body, "UTF-8"));
                }
            }
        });
        return response.take();
    }

    public void close() throws IOException {
        connection.close();
    }

    public static void main(String[] argv) {
        RPCClient fibonacciRpc = null;
        String response = null;
        try {
            fibonacciRpc = new RPCClient();

            System.out.println(" [x] Requesting fib(30)");
            response = fibonacciRpc.call("30");
            System.out.println(" [.] Got '" + response + "'");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (fibonacciRpc != null) {
                try {
                    fibonacciRpc.close();
                } catch (IOException _ignore) {
                }
            }
        }
    }

}

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章