前言
之前写过一篇SpringBoot 配置基于 wss 和 STOMP 的 WebSocket,而本文则将介绍两种实现单点聊天的方法,如果对配置基于 STOMP
和 wss
的 WebSocket
不太熟悉,建议先回看一下,本文的完整代码同样也已上传到GitHub。
效果
在介绍最终的实现之前,先看一下效果,为了方便展示,使用了 iframe
,以便可以同时展示四个窗口:
实现
为了实现能够将信息发给特定的用户,本文主要借用了 spring-messaging
的 SimpMessagingTemplate
消息模板来实现,而下面的两种方法也是基于该消息模板的 convertAndSend
和 convertAndSendToUser
方法来实现。
基于 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
为发送者的 id
,to
为接收者的 id
,message
为具体的消息,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
通信,不过只要按照本文的例子,也很容易对其进行改造,希望本文能够对你有所帮助。