君子之交淡如水|

庄周de蝴蝶

园龄:6年9个月粉丝:0关注:0

SpringBoot 集成 STOMP 实现一对一聊天的两种方法

前言

之前写过一篇SpringBoot 配置基于 wss 和 STOMP 的 WebSocket,而本文则将介绍两种实现单点聊天的方法,如果对配置基于 STOMPwssWebSocket 不太熟悉,建议先回看一下,本文的完整代码同样也已上传到GitHub

效果

在介绍最终的实现之前,先看一下效果,为了方便展示,使用了 iframe,以便可以同时展示四个窗口:

demo1

实现

为了实现能够将信息发给特定的用户,本文主要借用了 spring-messagingSimpMessagingTemplate 消息模板来实现,而下面的两种方法也是基于该消息模板的 convertAndSendconvertAndSendToUser 方法来实现。

基于 convertAndSendToUser 方法的实现

为了使用 convertAndSendToUser 方法能指定发送信息给特定用户,首先需要添加一个自定义的处理器,用于生成用户唯一的标识:

public class CustomHandshakeHandler extends DefaultHandshakeHandler {
@Override
protected Principal determineUser(
ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
// 获取例如 wss://localhost/websocket/1 订阅地址
// 中的最后一个用户 id 参数作为用户的标识,
// 为实现发送信息给指定用户做准备
String uri = request.getURI().toString();
String uid = uri.substring(uri.lastIndexOf("/") + 1);
return () -> uid;
}
}

以上自定义处理器用于设置用户唯一的标识为用户的 uid,用户只要在连接 websocket 时,在订阅地址 wss://localhost/websocket/ 后加上用户的 id,即可作为用户的唯一标识。

然后就是启用 websocket 消息代理的设置:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 开启一个简单的基于内存的消息代理
// 将消息返回到订阅了带 /chat 前缀的目的客户端
config.enableSimpleBroker("/chat");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 注册一个 /websocket/{id} 的 WebSocket 终端
// {id} 用于让用户连接终端时都可以有自己的路径
// 作为 Principal 的标识,以便实现向指定用户发送信息
registry.addEndpoint("/websocket/{id}")
.setHandshakeHandler(new CustomHandshakeHandler());
}
}

完成以上的配置后就已经有了一个 websocket终端,下面就介绍对消息的处理,为了便于处理消息,所有的消息都封装成了以下实体:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class MessageEntity {
private Long from;
private Long to;
private String message;
private Date time;
}

其中 from 为发送者的 idto 为接收者的 idmessage 为具体的消息,time 为消息的发送时间。

然后再介绍发送消息的接口:

@RestController
public class ChatController {
private final MessageService messageService;
@Autowired
public ChatController(MessageService messageService) {
this.messageService = messageService;
}
// 这里的 @MessageMapping 可以当成 @RequestMapping,
// 当有信息 (sendMsg 方法中的 messageEntity 参数即为客服端发送的信息实体)
// 发送到 /sendMsg 时会在这里进行处理
@MessageMapping("/sendMsg")
public void sendMsg(MessageEntity messageEntity) {
messageService.sendToUser(messageEntity);
}
}

最后是消息模板发送信息方法:

@Service
public class MessageService {
private final SimpMessagingTemplate simpMessagingTemplate;
@Autowired
public MessageService(SimpMessagingTemplate simpMessagingTemplate) {
this.simpMessagingTemplate = simpMessagingTemplate;
}
public void sendToUser(MessageEntity messageEntity) {
// convertAndSendToUser 方法可以发送信给给指定用户,
// 底层会自动将第二个参数目的地址 /chat/contact 拼接为
// /user/username/chat/contact,其中第二个参数 username 即为这里的第一个参数
// username 也是前文中配置的 Principal 用户识别标志
simpMessagingTemplate.convertAndSendToUser(
String.valueOf(messageEntity.getTo()),
"/chat/contact",
messageEntity
);
}
}

进行以上后端的后端配置(省略了 wss 的配置,如果不清楚,可以参考前言里的文章),即完成了后端代码的编写,下面再来介绍前端界面的编写:

首先是单个聊天界面的代码:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>主页</title>
<script src="/webjars/stomp-websocket/stomp.min.js"></script>
</head>
<body>
<label><input id="uid"/></label>
<button onclick="login()" id="login">登录</button>
<label><input id="msg" placeholder="信息后加 -id,发给指定人"/></label>
<button onclick="sendMsg()">发送</button>
<div id="user"></div>
<div id="greet"></div>
<script>
let stompClient
function login() {
// 根据输入的 id 号模拟不同用户的订阅
let socket = new WebSocket(`wss://localhost/websocket/${document.getElementById('uid').value}`)
stompClient = Stomp.over(socket)
stompClient.connect({}, function () {
// 所有想要接收给指定用户发送的信息的订阅地址都必须加上/user前缀
// 这里是为了配合后台的 convertAndSendToUser 方法,如果使用
// convertAndSend,就不需要 /user 前缀了,下面会再介绍
stompClient.subscribe(`/user/chat/contact`, function (frame) {
let entity = JSON.parse(frame.body)
showGreeting(`收到用户${entity.from}的信息: ${entity.message}`)
})
})
document.getElementById('user').innerText = `当前用户为:${document.getElementById('uid').value}`
function showGreeting(clientMessage) {
document.getElementById("greet").innerText += `${clientMessage}\n`
}
}
function sendMsg() {
const msg = document.getElementById('msg').value
stompClient.send("/sendMsg", {}, JSON.stringify({
from: document.getElementById('uid').value,
to: msg.substring(msg.lastIndexOf('-') + 1),
message: msg.substring(0, msg.lastIndexOf('-')),
time: new Date()
}))
}
</script>
</body>
</html>

然后是为了有四个聊天界面的代码:

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>聊天</title>
<style>
html, body, #app {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
iframe {
width: 49%;
height: 49%;
border: aquamarine 1px solid;
}
</style>
</head>
<body>
<div id="app">
<iframe src="https://localhost/index"></iframe>
<iframe src="https://localhost/index"></iframe>
<iframe src="https://localhost/index"></iframe>
<iframe src="https://localhost/index"></iframe>
</div>
</body>
</html>

进行了以上的配置后,就可以模拟实现简单的单点聊天了,下面再介绍使用消息模板的 convertAndSend 方法来实现单点聊天。

基于convertAndSend 方法的实现

基于convertAndSend 方法的实现不同于基于 convertAndSendToUser 时主要是通过后端进行配置,如果使用 convertAndSend 就只需要在前端订阅时进行控制即可,这么说可能不太清晰,下面就具体展示:

基于convertAndSend 就不再需要自定义处理器了,终端也不再需要/{id} 了:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig2 implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/chat");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/websocket");
}
}

消息接口处理的代码还是一样:

@RestController
public class ChatController2 {
private final MessageService2 messageService;
@Autowired
public ChatController2(MessageService2 messageService) {
this.messageService = messageService;
}
@MessageMapping("/sendMsg2")
public void sendMsg(MessageEntity messageEntity) {
messageService.sendToUser(messageEntity);
}
}

不过消息模板的方法实现就有所不同了:

@Service
public class MessageService2 {
private final SimpMessagingTemplate simpMessagingTemplate;
@Autowired
public MessageService2(SimpMessagingTemplate simpMessagingTemplate) {
this.simpMessagingTemplate = simpMessagingTemplate;
}
public void sendToUser(MessageEntity messageEntity) {
simpMessagingTemplate.convertAndSend("/chat/contact/" + messageEntity.getTo(), messageEntity);
}
}

这里通过在发送地址后拼接目的用户的 id,然后再配合前端订阅时的处理即可实现发送发送信息给特定用户,下面是前端代码:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>主页</title>
<script src="/webjars/stomp-websocket/stomp.min.js"></script>
</head>
<body>
<label><input id="uid"/></label>
<button onclick="login()" id="login">登录</button>
<label><input id="msg" placeholder="信息后加 -id,发给指定人"/></label>
<button onclick="sendMsg()">发送</button>
<div id="user"></div>
<div id="greet"></div>
<script>
let stompClient
function login() {
let socket = new WebSocket(`wss://localhost/websocket`)
stompClient = Stomp.over(socket)
stompClient.connect({}, function () {
// 由于使用了 convertAndSend, 这里就不再需要加 /user 前缀了
// 只要在订阅地址后加上自己的 id 即可发送给自己的信息
stompClient.subscribe(`/chat/contact/${document.getElementById('uid').value}`, function (frame) {
let entity = JSON.parse(frame.body)
showGreeting(`收到用户${entity.from}的信息: ${entity.message}`)
})
})
document.getElementById('user').innerText = `当前用户为:${document.getElementById('uid').value}`
function showGreeting(clientMessage) {
document.getElementById("greet").innerText += `${clientMessage}\n`
}
}
function sendMsg() {
const msg = document.getElementById('msg').value
stompClient.send("/sendMsg2", {}, JSON.stringify({
from: document.getElementById('uid').value,
to: msg.substring(msg.lastIndexOf('-') + 1),
message: msg.substring(0, msg.lastIndexOf('-')),
time: new Date()
}))
}
</script>
</body>
</html>

以上便是基于 STOMP 实现单点聊天的两种方法,如果有不清楚的地方也可以留言反馈。

总结

本文通过一个简单的例子介绍了两种实现单点聊天的方法,在下一篇文章将会通过一个基于 SpringBoot + Vue 的比较完善的例子来展示单点聊天的实现,不过下一篇的例子并未使用本文的 STOMP,而只是用了基本的 WebSocket 通信,不过只要按照本文的例子,也很容易对其进行改造,希望本文能够对你有所帮助。

本文作者:庄周de蝴蝶

本文链接:https://www.cnblogs.com/butterfly-fish/p/17762295.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   庄周de蝴蝶  阅读(218)  评论(0编辑  收藏  举报  
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起