Websocket在Java中的实践——使用STOMP和RabbitMQ搭建聊天室

《Websocket在Java中的实践——整合Rabbitmq和STOMP》一文中,我们让Spring的Broker作为客户端的代理,订阅了RabbitMQ的队列。本文,我们将基于这篇文章的部分内容,搭建一个简单的多人聊天室功能。

Rabbitmq

开启STOMP支持

《Websocket在Java中的实践——整合Rabbitmq和STOMP》中“STOMP”部分。

服务端

依赖

《Websocket在Java中的实践——整合Rabbitmq和STOMP》中“依赖”部分。

参数

《Websocket在Java中的实践——整合Rabbitmq和STOMP》中“参数”部分。

参数映射类

《Websocket在Java中的实践——整合Rabbitmq和STOMP》中“参数映射类”部分。

用户信息类

这个类主要用于保存用户ID和其对应的通道之间的映射。当连接建立后,我们会把用户ID和其通道保存到这个结构体中;当连接断开后,我们会将其移除。

package com.nyctlc.stomprbmqchatroom.component;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.messaging.MessageChannel;
import org.springframework.stereotype.Component;

@Component
public class UserChannel {
    private final ConcurrentHashMap<String, MessageChannel> userMap = new ConcurrentHashMap<>();

    public MessageChannel getChannel(String userId) {
        return userMap.get(userId);
    }

    public void addChannel(String userId, MessageChannel channel) {
        userMap.put(userId, channel);
    }

    public void removeChannel(String userId) {
        userMap.remove(userId);
    }

    public ConcurrentHashMap<String, MessageChannel> getUsers() {
        return userMap;
    }
}

配置类

这次比较大的改动来源于配置类。
我们希望用户在握手时,通过/handshake/{uid}来携带用户id。这个时候我们就要使用类似于《Websocket在Java中的实践——握手拦截器》中介绍的拦截器

package com.nyctlc.stomprbmqchatroom.config;

import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.server.HandshakeInterceptor;

import com.nyctlc.stomprbmqchatroom.component.RabbitMQProperties;
import com.nyctlc.stomprbmqchatroom.component.UserChannel;

import jakarta.servlet.http.HttpSession;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    
    @Autowired
    private RabbitMQProperties rabbitMQProperties;

    @Autowired
    private UserChannel userChannel;

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/handshake/{uid}").addInterceptors(handshakeInterceptor());
    }

拦截器的逻辑是:从端点路径中取出uid,然后将其放置到消息头中。
开启代理Broker的代码和《Websocket在Java中的实践——整合Rabbitmq和STOMP》一致。

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.setApplicationDestinationPrefixes("/send");
        registry.enableStompBrokerRelay("/topic")
                .setRelayHost(rabbitMQProperties.getRabbitmqHost())
                .setRelayPort(Integer.parseInt(rabbitMQProperties.getRabbitmqStompPort()))
                .setClientLogin(rabbitMQProperties.getRabbitmqUsername())
                .setClientPasscode(rabbitMQProperties.getRabbitmqPassword())
                .setSystemLogin(rabbitMQProperties.getRabbitmqUsername())
                .setSystemPasscode(rabbitMQProperties.getRabbitmqPassword());
    }

最后,我们要对连接建立和断开的情况做些处理,这样才能知道哪些用户连接上了服务器,哪些和服务器断开了。

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(new ChannelInterceptor() {
            @Override
            public Message<?> preSend(Message<?> message, MessageChannel channel) {
                StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
                if (accessor != null && StompCommand.DISCONNECT.equals(accessor.getCommand())) {
                    System.out.println("STOMP Connection Closed");
                    String uid = (String) accessor.getSessionAttributes().get("uid");
                    userChannel.removeChannel(uid);
                } else if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) {
                    System.out.println("STOMP Connection Established");
                    String uid = (String) accessor.getSessionAttributes().get("uid");
                    userChannel.addChannel(uid, channel);
                }
                return message;
            }
        });
    }
}

消息接收类

当收到用户通过/send/msg-from-user端点发来的消息后,我们将消息转发给其他连接上的用户。

package com.nyctlc.stomprbmqchatroom.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.stereotype.Controller;

import com.nyctlc.stomprbmqchatroom.service.ChatService;

@Controller
public class WebSocketController {

    @Autowired
    private ChatService chatService;
    
    @MessageMapping("/msg-from-user")
    public String handle(@Payload String msg, SimpMessageHeaderAccessor headerAccessor) {
        String uid = (String) headerAccessor.getSessionAttributes().get("uid");
        System.out.println("Received message: " + msg + " from user ID: " + uid);
        chatService.send2Others(msg, uid);
        return msg;
    }
}

消息发送类

这段代码就是向RabbitMQ的默认交换器发送消息,路由键就是用户ID。这样,非消息发送者都将收到消息。

package com.nyctlc.stomprbmqchatroom.service;

import org.json.JSONObject;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.nyctlc.stomprbmqchatroom.component.UserChannel;

@Service
public class ChatService {
    
    @Autowired
    private UserChannel userChannel;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void send2Others(String msg, String uid) {
        for (String key : userChannel.getUsers().keySet()) {
            if (!key.equals(uid)) {
                JSONObject json = new JSONObject(msg);
                json.put("uid", uid);
                rabbitTemplate.convertAndSend("amq.topic", key, json.toString());
            }
        }
    }
}

测试

网页端

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>STOMP over WebSocket Example with StompJs.Client</title>
    <script src="https://cdn.jsdelivr.net/npm/@stomp/stompjs"></script>
</head>
<body>
    <h2>STOMP over WebSocket Example with StompJs.Client</h2>
    <label for="username">Username:</label>
    <input type="text" id="username" placeholder="Enter your username"/>
    <button id="connectButton">Connect</button>
    <button id="disconnectButton" style="display: none;">Disconnect</button>
    <form id="messageForm">
        <input type="text" id="messageInput" placeholder="Type a message..."/>
        <button type="submit">Send</button>
    </form>
    <div id="messages"></div>

    <script>
        var client = null;

        function connect() {
            var username = document.getElementById('username').value; // 获取用户名
            client = new StompJs.Client({
                brokerURL: 'ws://localhost:8080/handshake/'+username, // WebSocket服务端点
                connectHeaders: {},
                debug: function (str) {
                    console.log(str);
                },
                reconnectDelay: 5000,
                heartbeatIncoming: 4000,
                heartbeatOutgoing: 4000,
            });

            client.onConnect = function(frame) {
                console.log('Connected: ' + frame);
                client.subscribe('/topic/'+username, function(message) { // 订阅端点
                    var jsonObj = JSON.parse(message.body);
                    var messageOutput = jsonObj.uid + " said: "+ jsonObj.content;
                    showMessageOutput(messageOutput);
                });
                document.getElementById('connectButton').style.display = 'none';
            };

            client.onStompError = function(frame) {
                console.error('Broker reported error: ' + frame.headers['message']);
                console.error('Additional details: ' + frame.body);
            };

            client.activate();

            document.getElementById('connectButton').style.display = 'none';
            document.getElementById('disconnectButton').style.display = 'inline';
        }

        function sendMessage(event) {
            event.preventDefault(); // 阻止表单默认提交行为
            var messageContent = document.getElementById('messageInput').value.trim();
            if(messageContent && client && client.connected) {
                var chatMessage = { content: messageContent };
                client.publish({destination: "/send/msg-from-user", body: JSON.stringify(chatMessage)}); // 发送端点
                document.getElementById('messageInput').value = '';
            }
        }

        function showMessageOutput(message) {
            var messagesDiv = document.getElementById('messages');
            var messageElement = document.createElement('div');
            messageElement.appendChild(document.createTextNode(message));
            messagesDiv.appendChild(messageElement);
        }

        function disconnect() {
            if (client !== null) {
                client.deactivate();
            }
            document.getElementById('connectButton').style.display = 'inline';
            document.getElementById('disconnectButton').style.display = 'none';
        }

        document.getElementById('messageForm').addEventListener('submit', sendMessage);

        document.getElementById('connectButton').addEventListener('click', connect);
        document.getElementById('disconnectButton').addEventListener('click', disconnect);
    </script>
</body>
</html>

案例

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

breaksoftware

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值