在《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>
案例