WebSocket+RabbitMQ实现单对单聊天

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);
		}
	}

基本框架大概可以了,由于现在非常瞌睡,可能一些细节没有补充,改天回头看的时候在来修改,顺便补充未完成的代码。
参考博文:移步这里,有什么比较模糊的也可以去看一下,诚挚感谢此博主博文。

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值