大纲
在《RabbitMQ实践——搭建单人聊天服务》一文中,我们搭建了Tom和Jerry两人的聊天服务。在这个服务中,它们都向Fanout交换器发送消息。而Fanout会将消息路由到它们两各自监听的队列。这样它们就可以得到全部消息。
如果是多人聊天,比如10个人聊天,按上述方案,需要Fanout交换器绑定10个队列。这就会使得结构变得非常复杂。
这是因为Classic类型队列在消费者确认读取消息后,会将消息从队列中删除。这样就需要我们使用fanout向多个队列路由消息,以供不同消费者消费。如果多个消费者消费同一个队列,则会导致每个消费者得到的都是部分信息。这就不符合我们理解的聊天场景。
但是我们可以使用Stream类型队列来解决这个问题。
Stream类型队列和之前的Classic队列的不同点是:Stream队列并不会清除消息。消息会一直存在于Stream队列中,消费者可以从指定位置开始读取消息。这样我们只要有一个Stream队列保存消息,所有消费者都从队列中读取消息即可。
用户登录
关于用户登录的流程我们在《RabbitMQ实践——搭建单人聊天服务》中已经有详细的介绍。即上图中黑色字体1、2、3、4、5的步骤。
创建聊天室
我们会创建一个以聊天室名称命名的交换器和Stream类型队列。即上图中黑色字体6、7、8、9的步骤。
需要注意的是Stream类型队列创建方案和Classic类型类似,只需要多指定"x-queue-type"=“stream”。但是对于Durable(持久化)只能设置为True,exclusive只能设置为False,autoDelete只能设置为False。
package com.rabbitmq.chat.service;
import java.util.Collections;
import java.util.Date;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
@Service
public class ChatRoomV2 {
@Autowired
private RabbitTemplate rabbitTemplate;
public void createChatRoom(String admin, String roomName) {
createChatRoom(roomName);
}
private void createChatRoom(String roomName) {
rabbitTemplate.execute(action -> {
action.exchangeDeclare(roomName, "fanout", false, true, null);
action.queueDeclare(roomName, true, false, false,
Collections.singletonMap("x-queue-type", "stream"));
action.queueBind(roomName, roomName, "");
return null;
});
}
聊天室创建完毕后,会通知所有登录的用户。
@PostMapping("/create")
public void create(@RequestParam String admin, @RequestParam String roomName) {
chatRoomV2.createChatRoom(admin, roomName);
core.notifyEveryone(roomName + " is created");
}
监听Stream(聊天室)
public Flux<String> receive(String username, String roomName) {
return Flux.create(emitter -> {
rabbitTemplate.execute(channel -> {
channel.basicQos(100);
Date timestamp = new Date(System.currentTimeMillis());
channel.basicConsume(roomName, false, username,
false, true,
Collections.singletonMap("x-stream-offset", timestamp),
(consumerTag, message) -> {
String senderOfMessage = message.getProperties().getHeaders().get("username").toString();
String show = "You Said: ";
if (!senderOfMessage.equals(username)) {
show = senderOfMessage + " Said: ";
}
show += new String(message.getBody());
System.out.println(show);
emitter.next(show);
channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
},
consumerTag -> { }
);
return null;
});
});
}
我们将"x-stream-offset"设置为当前毫秒数,是表示我们只读取当前时间之后发布的消息。这也符合聊天室的业务特点:不能读取历史消息。
当我们收到消息后,我们会获取消息Header中的自定义字段username,它标志了消息的发布者。如果发布者和读取者是同一人,我们将展示内容前面新增“You Said:”;如果是别人说的,则标记发布者的名称。
由于我们使用了WebFlux响应式编程,所以Controller层要做特殊处理
@GetMapping(value = "/receive", produces = "text/event-stream")
public Flux<String> receive(@RequestParam String username, @RequestParam String roomName) {
return chatRoomV2.receive(username, roomName);
}
发送消息
每个聊天室用户只要给之前创建的Fanout交换器发送消息即可。在这一步,我们给他们发送的消息Header中新增了字段username,以标记是谁发送的。
public void send(String username, String roomName, String message) {
Message msg = MessageBuilder.withBody(message.getBytes())
.setHeader("username", username)
.build();
rabbitTemplate.send(roomName, "", msg);
}
实验
登录
Tom侧
Jerry侧
创建聊天室
Jerry侧
Jerry申请创建一个聊天室
在管理后台,我们看到对应的交换器和Stream都创建出来了。
同时在刚才的登录接口界面,Jerry收到了通知
Tom侧
Tom也会收到通知
进入聊天室
Tom和Jerry在收到通知后,可以通过receive接口进入聊天室,监听聊天室内容变化。
Jerry侧
Tom侧
发送消息
Jerry发送消息
Jerry侧聊天室
Tom侧聊天室
Tom发送消息
Jerry侧聊天室
Tom侧聊天室
代码工程
https://github.com/f304646673/RabbitMQDemo