WebSocket+RabbitMQ实现点对点聊天
此功能为笔者在做毕业设计的时候需要实现的一个功能,记录一下完整的过程以及踩得坑。(会在流程中穿插笔者遇到的问题)
项目是前后端分离项目,服务端使用的SpringBoot,就先从服务端开始:
首先引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>1.5.2.RELEASE</version>
</dependency>
创建一个socket管理器
public class SocketManager {
private static Logger log = Logger.getLogger(SocketManager.class);
private static ConcurrentHashMap<String, WebSocketSession> manager = new ConcurrentHashMap<String, WebSocketSession>();
public static void add(String key, WebSocketSession webSocketSession) {
log.info("新添加webSocket连接 {} " + key);
manager.put(key, webSocketSession);
}
public static void remove(String key) {
log.info("移除webSocket连接 {} " + key);
manager.remove(key);
}
public static WebSocketSession get(String key) {
log.info("获取webSocket连接 {}" + key);
return manager.get(key);
}
}
重写WebSocketHandlerDecoratorFactory类
import org.apache.log4j.Logger;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.WebSocketHandlerDecorator;
import org.springframework.web.socket.handler.WebSocketHandlerDecoratorFactory;
import java.security.Principal;
/**
* 服务端和客户端在进行握手挥手时会被执行
*/
@Component
public class WebSocketDecoratorFactory implements WebSocketHandlerDecoratorFactory {
private static Logger log = Logger.getLogger(WebSocketDecoratorFactory.class);
@Override
public WebSocketHandler decorate(WebSocketHandler handler) {
return new WebSocketHandlerDecorator(handler) {
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
log.info("有人连接啦 sessionId = {}" + session.getId());
Principal principal = session.getPrincipal();
if (principal != null) {
log.info("key = {} 存入" + principal.getName());
// 身份校验成功,缓存socket连接
SocketManager.add(principal.getName(), session);
}
super.afterConnectionEstablished(session);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
log.info("有人退出连接啦 sessionId = {}" + session.getId());
Principal principal = session.getPrincipal();
if (principal != null) {
// 身份校验成功,移除socket连接
SocketManager.remove(principal.getName());
}
super.afterConnectionClosed(session, closeStatus);
}
};
}
}
接下来需要重写DefaultHandshakeHandler类(若不重写,session.getPrincipal()的结果是null)
import cn.hlsxn.fullmarks.model.User;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.support.DefaultHandshakeHandler;
import java.security.Principal;
import java.util.Map;
@Component
public class PrincipalHandshakeHandler extends DefaultHandshakeHandler {
// Custom class for storing principal
@Override
protected Principal determineUser(ServerHttpRequest request,
WebSocketHandler wsHandler,
Map<String, Object> attributes) {
// Generate principal with UUID as name
User user = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return new StompPrincipal(user.getUsername());
}
}
注意:return new StompPrincipal(user.getUsername());传入的参数应该是唯一的字符串,可以选择使用uuid,笔者由于数据库设计username是唯一的,所以直接使用username。
准备工作已经差不多了,进入主题
import cn.hlsxn.fullmarks.controller.chat.PrincipalHandshakeHandler;
import cn.hlsxn.fullmarks.controller.chat.WebSocketDecoratorFactory;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Autowired
private WebSocketDecoratorFactory webSocketDecoratorFactory;
@Autowired
private PrincipalHandshakeHandler principalHandshakeHandler;
private static Logger log = Logger.getLogger(WebSocketConfig.class);
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws/ep")
.setAllowedOrigins("*")
.setHandshakeHandler(principalHandshakeHandler)
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue","/topic");
//这个默认就是user,可以选择不用写
// registry.setUserDestinationPrefix("/user");
}
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.addDecoratorFactory(webSocketDecoratorFactory);
super.configureWebSocketTransport(registration);
}
接下来写通信相关
首先通信实体类,消息的类
import java.util.Date;
public class ChatMsg {
private String from;//发送的username
private String to;//接收者
private String content;//内容
private Date date;//时间
private String fromNickname;//昵称
public String getFromNickname() {
return fromNickname;
}
public void setFromNickname(String fromNickname) {
this.fromNickname = fromNickname;
}
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getTo() {
return to;
}
public void setTo(String to) {
this.to = to;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
}
消息转发类
import cn.hlsxn.fullmarks.model.ChatMsg;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
import java.util.Date;
@Controller
public class WsController {
@Autowired
SimpMessagingTemplate messagingTemplate;
@MessageMapping("/ws/chat")
public void handleChat(ChatMsg chatMsg) {
chatMsg.setDate(new Date());
messagingTemplate.convertAndSendToUser(chatMsg.getTo(), "/queue/chat", chatMsg);
}
}
这里再穿插一个没有解决的问题:这个方法本来的面目是下边这样的
@MessageMapping("/ws/chat")
public void handleMsg(Authentication authentication, ChatMsg chatMsg) {
User user = (User) authentication.getPrincipal();
chatMsg.setFrom(user.getUsername());
chatMsg.setFromNickname(user.getUname());
chatMsg.setDate(new Date());
simpMessagingTemplate.convertAndSendToUser(chatMsg.getTo(), "/queue/chat", chatMsg);
}
但是出了某些奇怪的错误,一访问就报错,接下来,既然authentication对象不能通过方法传入参数,那就继续改成了下个版本
@MessageMapping("/ws/chat")
public void handleMsg(ChatMsg chatMsg) {
User user = (User)SecurityContextHolder.getContext().getAuthentication()..getPrincipal();
chatMsg.setFrom(user.getUsername());
chatMsg.setFromNickname(user.getUname());
chatMsg.setDate(new Date());
simpMessagingTemplate.convertAndSendToUser(chatMsg.getTo(), "/queue/chat", chatMsg);
}
这个版本还是获取不到authentication对象,报空指针异常,找了很久解决问题的方法,毫无例外,全部失败,这怎么办呢,发个消息,谁发的都存不进去了。阔怕,既然解决不了问题,那就解决提出问题的人,服务端不加,在前端传入的时候就加进去,所以有了上上上边的最终版本
服务端还有最最最最最最最最重要的一步,也是笔者竟然可以忽略了的一步,忘了配置文件配置RabbitMQ
没有RabbitMQ的请移步安装教程,这是另一个网站的安装教程,很详细。
spring:
rabbitmq:
username: guest
password: guest
host: 127.0.0.1
port: 5672
服务端真的基本大功告成了,
接下来讲前端
首先,请执行npm install sockjs-client --save和npm install stompjs --save安装依赖
npm install sockjs-client --save
npm install stompjs --save
前端连接和接手消息使用的是store,
直接上代码把,store.js
import Vue from 'vue'
import Vuex from 'vuex'
import SockJS from "sockjs-client";
import Stomp from "stompjs";
Vue.use(Vuex)
const now = new Date();
const store = new Vuex.Store({
// 这里放全局参数
state: {
},
//这里是set方法
mutations: {
//这里边的方法是同步方法,不能实时监控的,所以websocket不能放在这里边
},
actions: {
connect(context){
context.state.stomp = Stomp.over(new SockJS("/ws/ep"));
context.state.stomp.connect({}, frame=> {
context.state.stomp.subscribe("/user/queue/chat", message=> {
alert("信息接收成功");
var msg = JSON.parse(message.body);
//做接收到消息的操作,这里暂时没有实现
});
}, failedMsg=> {
});
},
}
})
export default store;
然后在main.js中要注册这个方法
部分代码
import router from './router'
router.beforeEach((to, from, next) => {
if (to.path == '/' || to.path == '/codeLogin' || to.path == '/register') {
next();
} else {
if (window.sessionStorage.getItem("user")) {
//下边这句进行执行与服务端连接那个方法
store.dispatch('connect');
next();
} else {
next('/?redirect=' + to.path);
}
}
使用发送消息比较简单,主要方法
data(){
return {
content:'nihao',
to:'',
user : JSON.parse(window.sessionStorage.getItem("user"))
}
},
methods:{
send(){
let msgObj = new Object();
//但是服务端没有解决的拿到这里解决了
msgObj.from = this.user.username;
alert(this.user.username);
msgObj.fromNickname = this.user.uname;
msgObj.to = this.to;
msgObj.content = this.content;
this.stomp.send('/ws/chat', {}, JSON.stringify(msgObj));
alert(this.content);
}
}
基本框架大概可以了,由于现在非常瞌睡,可能一些细节没有补充,改天回头看的时候在来修改,顺便补充未完成的代码。
参考博文:移步这里,有什么比较模糊的也可以去看一下,诚挚感谢此博主博文。