利用Redis发布订阅模式结合websocket进行跨项目实时推送信息

       之前有个需求,就是要在用户从门户下单购买服务类型商品的时候,需要发送通知信息到管理端,在管理端有个站内信功能,在站内信可以实时获取推送提醒,由于门户网站和管理系统是两个独立的项目,所以需要将门户网站产生的订单信息以及提醒事项推送到管理端,使管理端可以获取到门户网站的信息,然后再利用websocket实时推送给前端。由于本项目中只是简单的信息提示,数据量不是特别大,而且业务不算复杂,所以感觉没有必要选用消息队列,这样会增加一个中间件的维护,由于项目本身使用了Redis,所以选用Redis来进行消息的传递。门户网站项目和管理系统需要连接同一个Redis的同一个库。

       后端的主要代码有门户网站Redis发布订阅信息服务类:

   门户端需要在Redis配置类中创建一个发布信息的通道

	@Bean
	public ChannelTopic topic() {
		return new ChannelTopic("topic");
	}

然后创建一个发布信息的服务类:

import com.alibaba.fastjson.JSONObject;
import com.xiaoifeng1010.core.dto.SendMsg;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

/**
 * @author xiaomifeng1010
 * @version 1.0
 * @date: 
 * @Description
 */
@Component
@Setter(onMethod_ = @Autowired)
@Slf4j
public class RedisService {

    private RedisTemplate<String, Object> redisTemplate;

    public Boolean sentMsg(SendMsg sendMsg){
        try {
            String msgContent = sendMsg.getMsgContent();
            log.info("redis开始发送信息到channel:{}", msgContent);
            String jsonString = JSONObject.toJSONString(sendMsg, false);
            log.info("发送的对象:{}",jsonString);
            redisTemplate.convertAndSend("topic",jsonString);
            log.info("redis发送信息到channel:{}",msgContent);
            return true;
        } catch (Exception e) {
           log.error("redis发布消息出错:{}",e);
           return false;
        }
    }
}

消息对象:

import lombok.Data;

import java.io.Serializable;

/**
 * @author xiaomifeng1010
 * @version 1.0
 * @date: 
 * @Description
 */
@Data
public class SendMsg implements Serializable {
    private static final long serialVersionUID = 1881467659037505830L;

    private String msgContent;

    private Integer itemType;
}

itemType(消息事项类型)设置了枚举类:

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;

import java.util.Objects;
import java.util.stream.Stream;

/**
 * @author xiaomifegn1010
 * @version 1.0
 * @date:
 * @Description 推送站内信息类别
 */
@AllArgsConstructor
@Getter
public enum PushMsgTypeEnum {

    TYPE_NOTICEMENT(1,"提醒"),
    PRODUCT_ORDER(2,"服务商品"),
    CUSTOMER_APPEAL(3,"客户申诉"),
    DECLARATION(4,"公司声明");


    public static String _name = "推送信息类别";

    private final Integer code;

    private final String name;

    public static String getName(Integer code) {
        if (Objects.isNull(code)){
            return StringUtils.EMPTY;
        }
        return  Stream.of(values()).filter(bean -> bean.code.equals(code))
                .map(PushMsgTypeEnum::getName).findFirst().orElse(StringUtils.EMPTY);

    }

}

然后在各项业务发生时,就可以调用发送信息服务进行发送消息了。

在管理端呢,就需要有一个监听信息的监听器:

import com.dcboot.module.customerManagement.config.WebsocketConfig;
import com.dcboot.module.customerManagement.msg.server.WebSocketServer;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.RedisTemplate;

/**
 * @author xiaomifeng1010
 * @version 1.0
 * @date: 
 * @Description
 */
@Slf4j
public class ConsumerRedisListener implements MessageListener {

    @Autowired
    private RedisTemplate<String, Object> gateWayRedisTemplate;

    /**
     * Callback for processing received objects through Redis.
     *
     * @param message message must not be {@literal null}.
     * @param pattern pattern matching the channel (if specified) - can be {@literal null}.
     */
    @SneakyThrows
    @Override
    public void onMessage(Message message, byte[] pattern) {
        String msg = String.valueOf(gateWayRedisTemplate.getValueSerializer().deserialize(message.getBody()));
        log.info("收到redis中订阅的消息:{}", msg);
        log.info("发送websocket信息");
        WebSocketServer.broadCastInfo(msg);
    }
}

还需要将监听器以及订阅的通道配置到redis中: 

import com.xiaomifeng1010.module.customerManagement.listener.ConsumerRedisListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;

/**
 * @author xiaomifeng1010
 * @version 1.0
 * @date: 
 * @Description
 */
@Configuration
public class RedisChannelConfig {

    @Autowired
    private LettuceConnectionFactory connectionFactory;

    @Bean
    public ConsumerRedisListener consumeRedis() {
        return new ConsumerRedisListener();
    }

    @Bean
    public ChannelTopic topic() {
        return new ChannelTopic("topic");
    }

    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer() {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.addMessageListener(consumeRedis(), topic());
        return container;
    }


}

在管理端监听到Redis订阅中的信息,然后使用WebSocketServer向浏览器推送信息

使用websocket需要在项目中引入websocket集成的springboot的依赖

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

websocket基本配置:

package com.xiaomifeng1010.module.core.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * @author xiaomifeng1010
 * @version 1.0
 * @date: 
 * @Description 启用websocket
 * 首先要注入ServerEndpointExporter,这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint。
 * 要注意,如果使用独立的servlet容器,而不是直接使用springboot的内置容器,就不要注入ServerEndpointExporter,
 * 因为它将由容器自己提供和管理。
 */
@Configuration
public class WebsocketConfig  {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

注意如果使用stomp协议方式,则配置类是这样的:比如官网配置:

import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/portfolio").withSockJS();  
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.setApplicationDestinationPrefixes("/app"); 
        config.enableSimpleBroker("/topic", "/queue"); 
    }
}

stomp方式进行websocket通信的案例可以参考我的博客:springboot整合websocket实现简单聊天功能 

然后创建websocket服务:

package com.xiaomifeng1010.module.core.msg.server;

import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.xiaomifeng1010.api.cms.msg.entity.StationMsg;
import com.xiaomifeng1010.api.cms.msg.service.StationMsgService;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author xiaomifeng1010
 * @version 1.0
 * @date: 
 * @Description
 */
@Component
@Slf4j
@ServerEndpoint(value = "/ws/msg")
public class WebSocketServer {

    private static StationMsgService stationMsgService;

    /**
     * 用来统计连接客户端的数量
     */
    private static final AtomicInteger OnlineCount = new AtomicInteger(0);
    /**
     * concurrent包的线程安全Set,用来存放每个客户端对应的Session对象。
     */
    private static CopyOnWriteArraySet<WebSocketServer> SessionSet = new CopyOnWriteArraySet<>();

    private static ConcurrentHashMap<String, Session> userInfoMap = new ConcurrentHashMap<>();

    private Session session;


    @Autowired
    public void setService(StationMsgService stationMsgService) {
        WebSocketServer.stationMsgService = stationMsgService;
    }


    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session) throws IOException {
        this.session = session;
        //加入set中
        SessionSet.add(this);

        // 在线数加1
        int cnt = OnlineCount.incrementAndGet();
        log.info("有连接加入,当前连接数为:{}", cnt);
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        SessionSet.remove(this);
        int cnt = OnlineCount.decrementAndGet();
        log.info("有连接关闭,当前连接数为:{}", cnt);
    }

    /**
     * 出现错误
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("发生错误:{},Session ID: {}", error.getMessage(), session.getId());
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param userAccount 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String userAccount,Session session) throws IOException {
        log.info("session对象:{}",session);
        log.info("来自客户端的用户账号:{}", userAccount);
        if (StringUtils.isNotBlank(userAccount)){
            userInfoMap.put(userAccount, session);
            log.info("userInfoMap:{}",userInfoMap);
        }
        sendMessage("收到来自客户端的消息内容:" + "你好客户端", 1);
        // broadCastInfo(message); 群发消息
    }

    /**
     * 发送消息
     *
     * @param message 消息
     */
    public void sendMessage(String message, Integer msgType) throws IOException {
        log.info("session信息:{}", session.getId());
        log.info("发送站内信息:{}", message);
        long count = stationMsgService.count(Wrappers.<StationMsg>lambdaQuery()
                .eq(StationMsg::getReadFlag, 0));
        Map<String, Object> messageVO = Maps.newLinkedHashMap();
        messageVO.put("msgContent", message);
        messageVO.put("unReadCount", count);
        messageVO.put("msgType", msgType);
        String sendMessage = JSONUtil.toJsonStr(messageVO);
        session.getBasicRemote().sendText(sendMessage);

    }

    /**
     * 群发消息
     *
     * @param message 消息
     * @param msgType 消息类型  1站内信  2 token过期通知
     */
    public static void broadCastInfo(String message,String userAccount,Integer msgType) throws IOException {
                if (StringUtils.isNotBlank(userAccount)){
                    Session session = userInfoMap.get(userAccount);
                    log.info("获取到的session用户:{}",session);
                    if (session != null && session.isOpen()) {
                        WebSocketServer webSocketServer = new WebSocketServer();
                        webSocketServer.session=session;
                        webSocketServer.sendMessage(message, msgType);
                    }
                }

    }

}

由于使用的SpringSecurity在进行用户及权限管理,所以无法再websocket通信中直接使用SpringSecurity的相关api去获取用户信息,由于推送的信息具有用户属性,不同的用户只能接收到自己权限内的信息通知,所以websocket连接需要知道是哪个用户,然后将该用户的信息推送给该用户,所以在websocket建立了连接后,需要前端发送一次通信信息,把用户信息推给后端,后端保存在HashMap中,并与session进行绑定,然后就可以针对具体用户各自推送信息。

注意:在websocket中使用 注入Spring管理的bean的时候,比如直接@Autowired private  StationMsgService stationMsgService;会报空指针异常,自动注入不成功,其实并不是service 不能被注入,其实是因为Spring管理的bean默认是单例的,和 websocket (多对象)是相冲突,所以需要在websocket的工具类中将StationMsgService声明为静态的,通过set方式注入就可以了,让多个websocket对象共用同一个StationMsgService

开发环境以http协议配置即可,在生产环境中需要升级https,对应的ws协议也需要升级到wss,前端在连接路径中需要修改协议,后端服务则需要配置修改Nginx配置即可

下边贴上Nginx配置

    map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}

上边这段映射配置放在http块下 

    location ^~ /msgApi/ws {
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_buffering off;
        #rewrite ^/dcbootApi/(.*)$ /$1 break;
        proxy_pass http://127.0.0.1:8080/msgApi/ws;
         proxy_http_version 1.1;
         proxy_connect_timeout 5s;
         proxy_read_timeout 600s;
         proxy_send_timeout 30s;
         proxy_set_header Upgrade $http_upgrade; 
         proxy_set_header Connection "$connection_upgrade";  
    }

location是在server块中的,配置的是websocket请求的代理(请求路径中有ws可以区分),主要是最下边两行的配置:

 proxy_set_header Upgrade $http_upgrade; 
 proxy_set_header Connection "$connection_upgrade"; 

$http_upgrade和$connection_upgrade就是引用的上边map配置的值

相比较stomp方式,不使用stomp客户端的时候,前端也不需要额外引入socket.js和stomp.js,可以使用原生写法,例如:

写一个自定义的js文件websocket.js

var wsObj = null;
var wsUri = null;
var userId = -1;

var lockReconnect = false;//避免重复连接
var wsCreateHandler = null;

function createWebSocket() {
	var host = window.location.host; // 带有端口号
	userId = GetQueryString("userId");
	// wsUri = "ws://" + host + "/websocket?userId=" + userId;
	wsUri = "ws://" + host + "/websocket/" + userId;

	try {
		wsObj = new WebSocket(wsUri);
		initWsEventHandle();
	} catch (e) {
		writeToScreen("执行关闭事件,开始重连");
		reconnect();
	}
}

function initWsEventHandle() {
	try {
		wsObj.onopen = function (evt) {
			heartCheck.start();
			onWsOpen(evt);
		};

		wsObj.onmessage = function (evt) {
			heartCheck.start();
			onWsMessage(evt);
		};

		wsObj.onclose = function (evt) {
			writeToScreen("执行关闭事件,开始重连");
			//reconnect();
			onWsClose(evt);
		};
		wsObj.onerror = function (evt) {
			writeToScreen("执行error事件,开始重连");
			reconnect();
			onWsError(evt);
		};
	} catch (e) {
		writeToScreen("绑定事件没有成功");
		reconnect();
	}
}

function onWsOpen(evt) {
	writeToScreen("CONNECTED");
}

function onWsClose(evt) {
	writeToScreen("DISCONNECTED");
}

function onWsError(evt) {
	writeToScreen(evt.data);
}

function writeToScreen(message) {
	if(DEBUG_FLAG)
	{
		$("#debuggerInfo").val($("#debuggerInfo").val() + "\n" + message);
	}
}

function reconnect() {
	if(lockReconnect) {
		return;
	};
	writeToScreen("1秒后重连");
	lockReconnect = true;
	//没连接上会一直重连,设置延迟避免请求过多
	wsCreateHandler && clearTimeout(wsCreateHandler);
	wsCreateHandler = setTimeout(function () {
		writeToScreen("重连..." + wsUri);
		createWebSocket();
		lockReconnect = false;
		writeToScreen("重连完成");
	}, 1000);
}

var heartCheck = {
	//15s之内如果没有收到后台的消息,则认为是连接断开了,需要再次连接
	timeout: 5000,
	timeoutObj: null,
	serverTimeoutObj: null,
	//重启
	reset: function(){
		clearTimeout(this.timeoutObj);
		clearTimeout(this.serverTimeoutObj);
		this.start();
	},
	//开启定时器
	start: function(){
		var self = this;
		this.timeoutObj && clearTimeout(this.timeoutObj);
		this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);
		this.timeoutObj = setTimeout(function(){
			writeToScreen("发送ping到后台");
			try
			{
				wsObj.send("ping");
			}
			catch(ee)
			{
				writeToScreen("发送ping异常");
			}
			//内嵌计时器
			self.serverTimeoutObj = setTimeout(function(){
				//如果onclose会执行reconnect,我们执行ws.close()就行了.如果直接执行reconnect 会触发onclose导致重连两次
				writeToScreen("没有收到后台的数据,关闭连接");
				//wsObj.close();
				reconnect();
			}, self.timeout);

		}, this.timeout)
	},
}

function GetQueryString(name) {
	var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
	var r = window.location.search.substr(1).match(reg); //获取url中"?"符后的字符串并正则匹配
	var context = "";
	if (r != null)
		context = r[2];
	reg = null;
	r = null;
	return context == null || context == "" || context == "undefined" ? "" : context;
}

然后在html文件中引用这个js文件即可:

<!DOCTYPE html>
<html>

<head>
<meta charset="UTF-8">
<title>Floor View</title>
<script src="/js/websocket.js"></script>
<script src="/js/jquery.min.js"></script>
<script id="code">
    var DEBUG_FLAG = true;
    $(function()
    {
        //启动websocket
        createWebSocket();

    });

    // 当有消息推送后触发下面事件
    function onWsMessage(evt) {
        var jsonStr = evt.data;
        writeToScreen(jsonStr);
    }

    function writeToScreen(message) {
        if(DEBUG_FLAG)
        {
            $("#debuggerInfo").val($("#debuggerInfo").val() + "\n" + message);
        }
    }

    function sendMessageBySocket()
    {
      var msg = $("#msg").val();
      wsObj.send(msg);
    }
</script>
</head>

<body style="margin: 0px;padding: 0px;overflow: hidden; ">
  <!-- 显示消息-->
  <textarea id="debuggerInfo" style="width:100%;height:200px;"></textarea>
  <!-- 发送消息-->
  <div>消息:<input type="text" id="msg"></input></div>
  <div><input type="button" value="发送消息" onclick="sendMessageBySocket()"></input></div>
</body>
</html>

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值