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

参考资料

相关推荐
小灰灰__12 分钟前
IDEA加载通义灵码插件及使用指南
java·ide·intellij-idea
夜雨翦春韭15 分钟前
Java中的动态代理
java·开发语言·aop·动态代理
程序媛小果36 分钟前
基于java+SpringBoot+Vue的宠物咖啡馆平台设计与实现
java·vue.js·spring boot
追风林41 分钟前
mac m1 docker本地部署canal 监听mysql的binglog日志
java·docker·mac
芒果披萨1 小时前
El表达式和JSTL
java·el
不能再留遗憾了2 小时前
RabbitMQ 高级特性——消息分发
分布式·rabbitmq·ruby
duration~2 小时前
Maven随笔
java·maven
zmgst2 小时前
canal1.1.7使用canal-adapter进行mysql同步数据
java·数据库·mysql
跃ZHD2 小时前
前后端分离,Jackson,Long精度丢失
java
blammmp2 小时前
Java:数据结构-枚举
java·开发语言·数据结构