基于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>

 

 

这里提供一个简单的示例,使用Spring Boot作为后端和Vue.js作为前端来实现在线聊天。 1. 创建Spring Boot项目 首先,我们需要创建一个Spring Boot项目。在这里,我们使用Spring Initializr创建一个Maven项目。在dependencies中添加Web和WebSocket依赖。 2. 添加WebSocket配置类 在/src/main/java/目录下创建一个config包,并创建一个WebSocketConfig类。在这个类中,我们配置了一个WebSocketHandler,用于处理WebSocket请求。 ```java @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new ChatWebSocketHandler(), "/chat").setAllowedOrigins("*"); } private class ChatWebSocketHandler extends TextWebSocketHandler { private final List<WebSocketSession> sessions = new CopyOnWriteArrayList<>(); @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { sessions.add(session); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { sessions.remove(session); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { for (WebSocketSession s : sessions) { if (s.isOpen()) { s.sendMessage(message); } } } } } ``` 3. 创建一个ChatController类 在/src/main/java/目录下创建一个controller包,并创建一个ChatController类。在这个类中,我们处理聊天页面的请求。 ```java @RestController @RequestMapping("/chat") public class ChatController { @GetMapping public ModelAndView chat() { return new ModelAndView("chat"); } } ``` 4. 创建一个ChatMessage类 在/src/main/java/目录下创建一个model包,并创建一个ChatMessage类。这个类用于表示聊天消息。 ```java public class ChatMessage { private String sender; private String content; public ChatMessage() { } public ChatMessage(String sender, String content) { this.sender = sender; this.content = content; } // getter and setter } ``` 5. 创建一个Vue.js聊天页面 在/src/main/resources/static/目录下创建一个chat.html文件。在这个文件中,我们引入Vue.js和Sock.js,并创建一个Vue实例。 ```html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Chat</title> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1.4.0/dist/sockjs.min.js"></script> </head> <body> <div id="app"> <div v-for="message in messages"> {{ message.sender }}: {{ message.content }} </div> <input v-model="content" @keyup.enter="send"> </div> <script> new Vue({ el: '#app', data: { messages: [], content: '' }, created: function () { var socket = new SockJS('/chat'); var self = this; socket.onmessage = function (event) { var message = JSON.parse(event.data); self.messages.push(message); }; }, methods: { send: function () { var message = { sender: 'user', content: this.content }; this.messages.push(message); var socket = new SockJS('/chat'); socket.send(JSON.stringify(message)); this.content = ''; } } }); </script> </body> </html> ``` 6. 运行应用程序 现在,我们可以运行应用程序,并访问http://localhost:8080/chat,看到一个简单的聊天页面。在多个浏览器窗口中打开该页面,并开始聊天。 代码示例:https://github.com/zhangchaoxu/spring-boot-vue-chat
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值