RabbitMQ实践——搭建多人聊天服务

大纲

《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。

java 复制代码
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;
        });
    }

聊天室创建完毕后,会通知所有登录的用户。

java 复制代码
    @PostMapping("/create")
    public void create(@RequestParam String admin, @RequestParam String roomName) {
        chatRoomV2.createChatRoom(admin, roomName);
        core.notifyEveryone(roomName + " is created");
    }

监听Stream(聊天室)

java 复制代码
    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层要做特殊处理

java 复制代码
    @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,以标记是谁发送的。

java 复制代码
    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

参考资料

相关推荐
源码宝4 分钟前
基于java语言+ Vue+ElementUI+ MySQL8.0.36数字化产科管理平台源码,妇幼信息化整体解决方案
java·源代码·产科电子病历系统源码·医院产科信息管理系统源码·数字化产科管理平台源码
coisini.cn18 分钟前
基于CentOS Stream 9平台搭建RabbitMQ3.13.4以及开机自启
linux·运维·服务器·rabbitmq·centos stream 9
龙洋静19 分钟前
RabbitMQ中常用的三种交换机【Fanout、Direct、Topic】
分布式·rabbitmq
java66666888820 分钟前
使用RabbitMQ实现可靠的消息传递机制
分布式·rabbitmq·ruby
执键行天涯21 分钟前
【RabbitMQ问题踩坑】RabbitMQ设置手动ack后,消息队列有多条消息,只能消费一条,就不继续消费了,这是为什么 ?
rabbitmq·ack
szc176722 分钟前
RabbitMq 消息确认和退回机制
分布式·rabbitmq
FREE技术35 分钟前
基于java+springboot+vue实现的畅销图书推荐系统(文末源码+lw+ppt)23-500
java·vue.js·spring boot
ToBeWhatYouWannaBe.37 分钟前
代码随想录-Day49
java·数据结构·算法·leetcode
彧A1 小时前
数据库的学习(4)
java·开发语言·数据库
Jinyi5031 小时前
Spring Boot 高级配置:如何轻松定义和读取自定义配置
java·spring boot·后端·spring·java-ee·maven·intellij-idea