基于SockJS stomp的聊天系统——前后端

效果图

 

 

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>

 

 


版权声明:本文为qq_39506978原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。