效果图
1.依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2.websocket配置
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
//定义几个前缀,重要
public void configureMessageBroker(MessageBrokerRegistry config) {
registry.setApplicationDestinationPrefixes("/app");
registry.enableSimpleBroker("/topic");
//默认也是/user/
registry.setUserDestinationPrefix("/user");
}
@Override
//websocket端点,及跨域
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/my-websocket").setAllowedOrigins("*").withSockJS();
}
}
3.监听各个事件,这些事件对于原生websocket各种事件(主要是用户上下线,进出入房间放Redis缓存session及各种系统通知)
package com.cloudride.modules.user.other;
import com.cloudride.common.constant.RedisConstant;
import com.cloudride.modules.user.util.WebSocketUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.*;
import java.util.Map;
import java.util.Set;
@Slf4j
@Component
public class STOMPConnectEventListener implements ApplicationListener {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private SimpMessagingTemplate messagingTemplate;
@Override
public void onApplicationEvent(ApplicationEvent event) {
//进入系统存入session
if (event instanceof SessionConnectEvent) {
SessionConnectEvent connectEvent = (SessionConnectEvent) event;
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(connectEvent.getMessage());
String userId = accessor.getNativeHeader("userId").get(0);
String sessionId = accessor.getSessionId();
redisTemplate.opsForHash().put(RedisConstant.SYSTEM_USER, userId, sessionId);
log.info("{}进入系统", userId);
} else if (event instanceof SessionConnectedEvent) {
Long size = redisTemplate.opsForHash().size(RedisConstant.SYSTEM_USER);
log.info("系统当前已有人数:{}", size);
//进入房间,存入session并群发通知
} else if (event instanceof SessionSubscribeEvent) {
SessionSubscribeEvent subscribeEvent = (SessionSubscribeEvent) event;
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(subscribeEvent.getMessage());
Map<String, String> systemUsers = redisTemplate.opsForHash().entries(RedisConstant.SYSTEM_USER);
Map<String, String> chatRoomUsers = redisTemplate.opsForHash().entries(RedisConstant.CHAT_ROOM_USER);
String fullDestination = (String) accessor.getMessageHeaders().get("simpDestination");
String currentUserSessionId = accessor.getSessionId();
String currentUserId = null;
for (Map.Entry<String, String> entry : systemUsers.entrySet()) {
if (currentUserSessionId.equals(entry.getValue())) {
currentUserId = entry.getKey();
redisTemplate.opsForHash().put(RedisConstant.CHAT_ROOM_USER, entry.getKey(), currentUserSessionId);
messagingTemplate.convertAndSendToUser(currentUserSessionId, fullDestination.replaceFirst("/user/", "/"), "系统消息:你已进入了聊天室,当前房间人数:" + (chatRoomUsers.size() + 1), WebSocketUtils.createMessageHeaders(entry.getValue()));
}
}
for (Map.Entry<String, String> entry : chatRoomUsers.entrySet()) {
messagingTemplate.convertAndSendToUser(entry.getValue(), fullDestination.replaceFirst("/user/", "/"), "系统消息:" + currentUserId + "进入了聊天室,当前房间人数:" + (chatRoomUsers.size() + 1), WebSocketUtils.createMessageHeaders(entry.getValue()));
}
//删除session并群发通知
} else if (event instanceof SessionUnsubscribeEvent) {
SessionUnsubscribeEvent unsubscribeEvent = (SessionUnsubscribeEvent) event;
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(unsubscribeEvent.getMessage());
Map<String, String> users = redisTemplate.opsForHash().entries(RedisConstant.SYSTEM_USER);
String currentUserSessionId = accessor.getSessionId();
String currentUserId = null;
for (Map.Entry<String, String> entry : users.entrySet()) {
if (currentUserSessionId.equals(entry.getValue())) {
redisTemplate.opsForHash().delete(RedisConstant.CHAT_ROOM_USER, entry.getKey());
currentUserId = entry.getKey();
messagingTemplate.convertAndSendToUser(currentUserSessionId, accessor.getSubscriptionId(), "系统消息:成功退出聊天室", WebSocketUtils.createMessageHeaders(entry.getValue()));
}
}
Map<String, String> chatRoomUsers = redisTemplate.opsForHash().entries(RedisConstant.CHAT_ROOM_USER);
for (Map.Entry<String, String> entry : chatRoomUsers.entrySet()) {
messagingTemplate.convertAndSendToUser(entry.getValue(), accessor.getSubscriptionId(), "系统消息:" + currentUserId + "退出了聊天室,当前房间人数:" + chatRoomUsers.size(), WebSocketUtils.createMessageHeaders(entry.getValue()));
}
//退出系统,删除session并群发通知
} else if (event instanceof SessionDisconnectEvent) {
SessionDisconnectEvent disconnectEvent = (SessionDisconnectEvent) event;
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(disconnectEvent.getMessage());
String currentUserSessionId = accessor.getSessionId();
Map<String, String> users = redisTemplate.opsForHash().entries(RedisConstant.SYSTEM_USER);
Set chatRoomUserIds = redisTemplate.opsForHash().entries(RedisConstant.CHAT_ROOM_USER).keySet();
String currentUserId = null;
for (Map.Entry<String, String> entry : users.entrySet()) {
if (currentUserSessionId.equals(entry.getValue())) {
redisTemplate.opsForHash().delete(RedisConstant.SYSTEM_USER, entry.getKey());
redisTemplate.opsForHash().delete(RedisConstant.CHAT_ROOM_USER, entry.getKey());
currentUserId=entry.getKey();
} else {
if (chatRoomUserIds.contains(entry.getKey())) {
messagingTemplate.convertAndSendToUser(entry.getValue(), "/topic/AAA", "系统消息:" +currentUserId + "下线了", WebSocketUtils.createMessageHeaders(entry.getValue()));
}
}
}
}
}
}
4.相当于一个聊天接口,但是不同于一般restful接口。入参封装在实体类SocketMessage,如果toUser为空则为群发
package com.cloudride.modules.user.controller;
import com.cloudride.common.constant.RedisConstant;
import com.cloudride.modules.user.entity.SocketMessage;
import com.cloudride.modules.user.util.WebSocketUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
@Controller
public class ChatController {
@Autowired
private SimpMessagingTemplate messagingTemplate;
@Autowired
private RedisTemplate redisTemplate;
@MessageMapping("/send")
public void send(SocketMessage message) throws Exception {
message.setDate(LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));
//群发
if (StringUtils.isBlank(message.getToUser())) {
Map<String, String> chatRoomUsers = redisTemplate.opsForHash().entries(RedisConstant.CHAT_ROOM_USER);
for (Map.Entry<String, String> entry : chatRoomUsers.entrySet()) {
if (!entry.getKey().equals(message.getFromUser())) {
messagingTemplate.convertAndSendToUser(entry.getValue(), "/topic/AAA", message, WebSocketUtils.createMessageHeaders(entry.getValue()));
}
}
} else {//私聊
String sessionId = (String) redisTemplate.opsForHash().get(RedisConstant.CHAT_ROOM_USER, message.getToUser());
messagingTemplate.convertAndSendToUser(sessionId, "/topic/AAA", message, WebSocketUtils.createMessageHeaders(sessionId));
}
}
}
5.实体类与工具类
@Getter
@Setter
public class SocketMessage {
private String fromUser;
private String toUser;
private String message;
private String date;
}
public class WebSocketUtils {
public static MessageHeaders createMessageHeaders(String sessionId) {
SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
headerAccessor.setSessionId(sessionId);
headerAccessor.setLeaveMutable(true);
return headerAccessor.getMessageHeaders();
}
}
6.Vue前端 vue-cli创建一个干净的Vue项目,需要安装3个js库
cnpm install sockjs-client --save
cnpm install stompjs --save
cnpm install net --save
将HellloWord.vue内容全部替换为
<template>
<div>
用户ID:<input v-model="userId">
<button @click="connect">登录聊天室</button>
<button @click="leave">退出聊天室</button>
<button @click="offline">下线</button>
发送给:
<input v-model="message.toUser">
消息:<input v-model="message.message">
<button @click="send">发送</button>
接受到消息:
<div v-html="info">
</div>
</div>
</template>
<script>
import SockJS from "sockjs-client"
import Stomp from "stompjs"
export default {
data() {
return {
userId: "",
stompClient: null,
message: {
toUser: '',
fromUser:'',
message: ''
},
info: ""
}
},
methods: {
connect() {
let socket = new SockJS('http://192.168.0.166:8181/my-websocket')
this.stompClient = Stomp.over(socket)
this.stompClient.connect({
"userId": this.userId
}, this.connectSuccess, this.connectError);
},
//注意/user前缀见后端配置类
connectSuccess(obj) {
this.stompClient.subscribe('/user/topic/AAA', (msg) => {
if (msg.body.startsWith("系统消息")) {
this.info += "<br>" + msg.body;
} else {
let body = JSON.parse(msg.body)
this.info += "<br>==>" + body.date + " "+body.fromUser+"说:" + body.message
}
})
},
connectError(err) {
console.log("网络异常")
},
//注意/app前缀,/app/send映射到后端chatController
send() {
this.message.fromUser=this.userId;
this.info += "<br> <== "+this.message.message;
this.stompClient.send("/app/send", {}, JSON.stringify(this.message));
},
offline() {
this.stompClient.disconnect();
this.info += "<br>成功退出系统";
},
//注意这里不用前缀
leave() {
this.stompClient.unsubscribe('/topic/AAA')
}
}
}
</script>