一、后端
1、安装依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2、定义消息实体类(根据特定业务来定义)
@Data
@AllArgsConstructor
public class ChatMessage {
String sender;
String message;
}
3、定义配置文件类
package com.example.stomptest.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
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.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
@Slf4j
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
/**
* 注册Stomp服务端点
* @param registry
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// addEndpoint 设置与客户端建立连接的url
registry.addEndpoint("/ws")
// 设置允许跨域
.setAllowedOriginPatterns("*")
// 允许SocketJs使用,是为了防止某些浏览器客户端不支持websocket协议的降级策略
.withSockJS();
}
/**
* 配置消息代理
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 客户端发送消息的请求前缀
registry.setApplicationDestinationPrefixes("/app");
// 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送
registry.enableSimpleBroker("/topic", "/queue");
// 服务端通知客户端的前缀,可以不设置,默认为user
registry.setUserDestinationPrefix("/user");
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
// log.info("--websocket信息发送前--");
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (accessor != null) {
// 判断是否是连接Command 如果是,需要获取token对象
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
String token = accessor.getFirstNativeHeader("token");//下方可执行解析token逻辑
log.info("toekn信息:"+token);
// final String token = accessor.getFirstNativeHeader(Constant.HEADER_TOKEN);
// if (!TokenUtil.validateToken(token)) {
// return null;
// }
// final LoginUser user = TokenUtil.getUserFromToken(token);
// UserUtil.setUser(user);
// sendToUser 需要与这里的user获取的principal一样
// accessor.setUser(new SocketUser(user));
accessor.setUser(() -> "test");
log.info("websocket 连接成功");
}
}
return message;
}
});
}
}
4、定义监视器
package com.example.stomptest.handle;
import com.example.stomptest.entity.ChatMessage;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionConnectEvent;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
@Component
@Slf4j
public class WebSocketEventListener {
@Resource
private SimpMessageSendingOperations messagingTemplate;
/**
* 连接建立事件
* @param event
*/
@EventListener
public void handleWebSocketConnectListener(SessionConnectEvent event) {
log.info("建立一个新的连接");
}
@EventListener
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
// StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
//
// String username = (String) headerAccessor.getSessionAttributes().get("login");
log.info("用户断开连接");
}
}
5、定义控制器测试用
package com.example.stomptest.controller;
import com.example.stomptest.entity.ChatMessage;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
@RestController
@Slf4j
public class ChatController {
@Resource
private SimpMessagingTemplate messagingTemplate;
@MessageMapping("/sendToAll")
// @SendTo("/topic/notice")
public String sendToAll(String message) {
String result="服务端通知: " + message;
messagingTemplate.convertAndSend("/topic/notice",result);
return result;
}
/**
* 点对点发送消息
* <p>
* 模拟 张三 给 李四 发送消息场景
* @param username 接收消息的用户
* @param message 消息内容
*/
@MessageMapping("/sendToUser/{username}")
public void sendToUser(@DestinationVariable String username, String message) {
String sender="1910";
String receiver = username; // 接收人
log.info("发送人:{}; 接收人:{}", sender, receiver);
// 发送消息给指定用户 /user/{username}/queue/greeting
messagingTemplate.convertAndSendToUser(receiver, "/queue/message", new ChatMessage(sender, message));
}
}
二、前端
1、安装依赖
npm i stompjs -S
npm i sockjs-client -S
2、测试代码
<template>
<el-button type="primary" @click="startSub">订阅广播</el-button>
<el-input v-model="input" placeholder="Please input" />
<el-button type="primary" @click="sendMessage">群发消息</el-button>
<el-button type="primary" @click="subscribeMessagePoint">点对点订阅消息</el-button>
<el-button type="primary" @click="sendMessagePoint">点对点发消息</el-button>
<el-button type="primary" @click="connect">建立连接</el-button>
<el-button type="primary" @click="disconnect">关闭连接</el-button>
</template>
<script setup>
import { ref, onMounted, onBeforeMount, onBeforeUnmount, nextTick, reactive, watch } from 'vue';
import SockJS from 'sockjs-client/dist/sockjs.min.js';
import Stomp from 'stompjs';
const stompClient = ref(null);
const input = ref('');
onMounted(() => {
connect();
});
const connect = () => {
let socket = new SockJS('http://localhost:3301/ws');
stompClient.value = Stomp.over(socket);
stompClient.value.connect(
{
login: 'test',//对于点对点通信必不可少
token: '3224sdsdfgdfdfsfddfsf',
},
() => {
console.log('连接成功');
},
(error) => {
console.log('连接失败: ' + error);
}
);
};
const disconnect = () => {
if (stompClient.value && stompClient.value.connected) {
stompClient.value.disconnect(() => {
console.log('断开连接');
});
}
};
const startSub = () => {
//订阅广播消息
stompClient.value.subscribe('/topic/notice', (res) => {
console.log('订阅广播成功:' + res.body);
});
};
const sendMessage = () => {
//广播发布消息
stompClient.value.send('/app/sendToAll', {}, input.value);
};
const subscribeMessagePoint = () => {
//订阅用户消息
stompClient.value.subscribe('/user/queue/message', (res) => {
let response = JSON.parse(res.body);
console.log('订阅点对点成功:');
console.log(response);
console.log(response.message);
});
};
const sendMessagePoint = () => {
//给指定用户发送消息
stompClient.value.send('/app/sendToUser/test', {}, input.value);
};
</script>
<style lang="scss" scoped></style>