springmvc+websocket+redis 解决服务器集群问题

本文详细介绍了如何在Spring MVC框架下集成WebSocket与Redis,实现高效的消息推送和集群间的通信。通过具体的代码示例,展示了如何配置pom.xml依赖,设置Redis连接池,实现订阅监听,以及WebSocket端点的创建。此外,还对比了Redis与ActiveMQ在发布订阅机制上的差异。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在有的业务中,当更改状态时,可能需要大量的轮询来实现,用websocket能够很好的实现,但是因为工作中很多都是采取服务器集群来实现的,所以对集群情况下的websocket进行了学习,在围观大佬之后,进行了改造,使之贴合我们公司架构,springmvc。

github地址:https://github.com/onthewayw/springmvc_websocket_mq.git

1:pom.xml

<properties>
    <spring-version>4.3.18.RELEASE</spring-version>
    <servlet-version>3.1.0</servlet-version>
    <log4j-version>1.2.17</log4j-version>
    <slf4j-log4j12-version>1.7.9</slf4j-log4j12-version>
    <slf4j-api-version>1.7.9</slf4j-api-version>
    <junit-version>4.12</junit-version>
</properties>


<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>${spring-version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring-version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-messaging</artifactId>
        <version>${spring-version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-websocket</artifactId>
        <version>${spring-version}</version>
    </dependency>
    <dependency>
        <groupId>org.java-websocket</groupId>
        <artifactId>Java-WebSocket</artifactId>
        <version>1.3.0</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>${spring-version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>${spring-version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-beans</artifactId>
        <version>${spring-version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aop</artifactId>
        <version>${spring-version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-support</artifactId>
        <version>${spring-version}</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.5.3</version>
        <scope>runtime</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-redis</artifactId>
        <version>1.8.4.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>2.9.0</version>
    </dependency>
    <!--log4j-->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>${slf4j-log4j12-version}</version>
    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>${log4j-version}</version>
    </dependency>

    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>${slf4j-api-version}</version>
    </dependency>

    <!-- servlet
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>${servlet-version}</version>
        <scope>provided</scope>
    </dependency>-->

    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>${junit-version}</version>
    </dependency>

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.7</version>
    </dependency>
    <!-- 添加MQ依赖 -->
    <dependency>
        <groupId>com.rabbitmq</groupId>
        <artifactId>amqp-client</artifactId>
        <version>3.5.1</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.amqp</groupId>
        <artifactId>spring-rabbit</artifactId>
        <version>1.4.5.RELEASE</version>
    </dependency>
    <!--这里需要引入jackson库,否则jsonMessageConverter实例化就会报错,如果使用Gson需要实现jsonMessageConverter的父类方法自己手动转一次,然后jsonMessageConverter的配置换成自己实现的类-->
    <!--Jackson 核心库-->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-core</artifactId>
        <version>2.8.9</version>
    </dependency>
    <!--Jackson 序列化库-->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.8.9</version>
    </dependency>
    <!--Jackson 注解支持-->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-annotations</artifactId>
        <version>2.8.9</version>
    </dependency>
    <dependency>
        <groupId>javax.websocket</groupId>
        <artifactId>javax.websocket-api</artifactId>
        <version>1.0</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>jstl</artifactId>
        <version>1.2</version>
    </dependency>
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>servlet-api</artifactId>
        <version>2.5</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

2.redis.xml配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:redis="http://www.springframework.org/schema/redis"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                        http://www.springframework.org/schema/beans/spring-beans.xsd
                        http://www.springframework.org/schema/context
                        http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/redis http://www.springframework.org/schema/redis/spring-redis.xsd
">
    <context:component-scan base-package="com.wang.*" />
    <context:property-placeholder location="classpath:spring-rabbitMQ.properties" ignore-unresolvable="true"/>
    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <property name="maxIdle" value="${redis.maxIdle}" />
        <property name="minIdle" value="${redis.minIdle}" />
        <property name="maxTotal" value="${redis.maxTotal}" />
        <property name="testOnBorrow" value="true" />
    </bean>

    <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
          p:host-name="${redis.ip}" p:port="${redis.port}" p:password="${redis.password}"
          p:pool-config-ref="jedisPoolConfig" p:usePool="true"/>
    <bean id="stringRedisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate" p:connection-factory-ref="jedisConnectionFactory"/>
    <!-- Redis连接-->
    <!--<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
          p:host-name="192.168.19.129" p:port="6379" p:password="123456">
        <constructor-arg ref="jedisPoolConfig" />
    </bean>-->
    <!-- 缓存序列化方式 -->
    <bean id="keySerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer" />
    <bean id="valueSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer" />
    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="jedisConnectionFactory" />
        <property name="keySerializer" ref="keySerializer" />
        <property name="valueSerializer" ref="valueSerializer" />
        <property name="hashKeySerializer" ref="keySerializer" />
        <property name="hashValueSerializer" ref="valueSerializer" />
    </bean>
    <!--<bean id="redisListenerContainer" class="org.springframework.data.redis.listener.RedisMessageListenerContainer">
        <property name="connectionFactory" ref="jedisConnectionFactory"/>
    </bean>-->
    <bean id="serverEndpointExporter" class="org.springframework.web.socket.server.standard.ServerEndpointExporter"/>
    <!--序列化-->
    <bean id="jdkSerializer" class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" />
    <!-- 配置监听器,redis在订阅消息时,会根据 redis:listener标签指定的方法名和通道(topic)调用不同的方法-->
    <bean id="listener" class="com.wang.listener.SubscribeListener" />

    <redis:listener-container connection-factory="jedisConnectionFactory">
    <!--   the method attribute can be skipped as the default method name is
            "handleMessage"
        topic代表监听的通道,是一个正规匹配 -->

    <redis:listener ref="listener" serializer="jdkSerializer" method="handleMessage" topic="im-*" />
    </redis:listener-container>

</beans>

3 订阅监听类,对消息进行监听

public class SubscribeListener implements MessageListener {

    private final Logger logger = LoggerFactory.getLogger(SubscribeListener.class);

    private StringRedisTemplate redisTemplate;

    private Session session;

    public Logger getLogger() {
        return logger;
    }

    public StringRedisTemplate getRedisTemplate() {
        return redisTemplate;
    }

    public void setRedisTemplate(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public Session getSession() {
        return session;
    }

    public void setSession(Session session) {
        this.session = session;
    }

    @Override
    public void onMessage(Message message, byte[] pattern) {
        String msg = new String(message.getBody());
        logger.info(new String(pattern) + "主题发布:" + msg);
        if(null!=session){
            try {
                session.getBasicRemote().sendText(msg);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

4 websocket端点

@ServerEndpoint(value = "/im_webSocket/{topic}/{username}")
public class WebsocketEndpoint {
    /**
     * 因为@ServerEndpoint不支持注入,所以使用SpringUtils获取IOC实例
     */
    private StringRedisTemplate redisTemplate = SpringUtils.getBean(StringRedisTemplate.class);

    private RedisMessageListenerContainer redisMessageListenerContainer = SpringUtils.getBean(RedisMessageListenerContainer.class);

    //存放该服务器该ws的所有连接。用处:比如向所有连接该ws的用户发送通知消息。
    private static CopyOnWriteArraySet<WebsocketEndpoint> sessions = new CopyOnWriteArraySet<>();

    private Session session;

    @OnOpen
    public void onOpen(Session session, @PathParam("topic")String topic){
        System.out.println("java websocket:打开连接:topic------"+topic);
        this.session = session;
        sessions.add(this);
        SubscribeListener subscribeListener = new SubscribeListener();
        subscribeListener.setSession(session);
        subscribeListener.setRedisTemplate(redisTemplate);
        //设置订阅topic
        try{
            redisMessageListenerContainer.addMessageListener(subscribeListener,new ChannelTopic(topic));
        }catch (Exception e){
            e.printStackTrace();
        }

    }
    @OnMessage
    public void onMessage(Session session, String message,@PathParam("topic")String topic,@PathParam("username") String username){
        System.out.println("websocket 收到消息:"+message);
        PulishService pulishService = SpringUtils.getBean(PulishService.class);
        pulishService.publish(topic, message);
    }
    @OnClose
    public void onClose(Session session){
       System.out.println("java websocket:关闭连接");
        sessions.remove(this);
    }
    @OnError
    public void onError(Session session,Throwable error){
        System.out.println("java websocket 出现错误");
    }

    public Session getSession() {
        return session;
    }

    public void setSession(Session session) {
        this.session = session;
    }
}

5.service

@Component
public class PulishService {
    @Autowired(required = false)
    private StringRedisTemplate redisTemplate;
    public void publish(String channel, Object message){
        //该方法封装的 connection.publish(rawChannel, rawMessage);
        redisTemplate.convertAndSend(channel,message);
    }
}

6.controller

@Controller
@RequestMapping("webSocket")
public class WebsocketController {

    @RequestMapping("/index/{topic}/{username}")
    public ModelAndView index(@PathVariable("topic") String topic, @PathVariable("username") String username) {
        ModelAndView mv = new ModelAndView("/websocket_1");
        mv.addObject("topic", topic);
        mv.addObject("username", username);
        return mv;
    }
}

7.客户端

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8"></meta>
    <title>websocket+redis集群</title>
</head>
<input type="hidden" id="port" value='${port }'>
<input type="hidden" id="topic" value='${topic }'>
<input type="hidden" id="username" value='${username }'>
<body>
${topic} 频道 聊天中。。。<br/>
<input id="input_id" type="text" /><button onclick="sendMessage()">发送</button>    <button onclick="closeWebsocket()">关闭</button>
<div id="message_id"></div>
</body>
<script>
    var topic = document.getElementById("topic").value;
    var username = document.getElementById("username").value;
    var websocket = new WebSocket('ws://127.0.0.1:8081/im_webSocket/'+topic+'/'+username);
    console.log(websocket);
    websocket.onopen = function(event){
        setMessage("打开连接");
    }

    websocket.onclose = function(event){
        setMessage("关闭连接");
    }

    websocket.onmessage = function(event){
        setMessage(event.data);
    }

    websocket.onerror = function(event){
        setMessage("连接异常");
    }

    //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function(){
        closeWebsocket();
    }

    //关闭websocket
    function closeWebsocket(){
        //3代表已经关闭
        if(3!=websocket.readyState){
            websocket.close();
        }else{
            alert("websocket之前已经关闭");
        }
    }

    //将消息显示在网页上
    function setMessage(message){
        document.getElementById('message_id').innerHTML += message + '<br/>';
    }

    //发送消息
    function sendMessage(){
        //1代表正在连接
        if(1==websocket.readyState){
            var message = document.getElementById('input_id').value;
            //setMessage(message);
            websocket.send(message);
        }else{
            alert("websocket未连接");
        }
        document.getElementById('input_id').value="";
        document.getElementById('input_id').focus();
    }
</script>
</html>

在写的时候出现了如下错误:

org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.springframework.data.redis.listener.RedisMessageListenerContainer' available: expected single matching bean but found 2: redisListenerContainer,org.springframework.data.redis.listener.RedisMessageListenerContainer#0
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1041)

那是因为在这里多注入了一次。

因为@ServerEndpoint注解之下并不能用@Autowired进行注入,所以用springUtil工具类进行注入

springUtil工具类

package com.wang.util;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.stereotype.Repository;

@Repository
public final class SpringUtils implements BeanFactoryPostProcessor {

    private static ConfigurableListableBeanFactory beanFactory; // Spring应用上下文环境

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        SpringUtils.beanFactory = beanFactory;
    }

    public static ConfigurableListableBeanFactory getBeanFactory() {
        return beanFactory;
    }

    /**
     * 获取对象
     *
     * @param name
     * @return Object 一个以所给名字注册的bean的实例
     * @throws org.springframework.beans.BeansException
     *
     */
    @SuppressWarnings("unchecked")
    public static <T> T getBean(String name) throws BeansException {
        return (T) getBeanFactory().getBean(name);
    }

    /**
     * 获取类型为requiredType的对象
     *
     * @param clz
     * @return
     * @throws org.springframework.beans.BeansException
     *
     */
    public static <T> T getBean(Class<T> clz) throws BeansException {
        T result = (T) getBeanFactory().getBean(clz);
        return result;
    }

    /**
     * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true
     *
     * @param name
     * @return boolean
     */
    public static boolean containsBean(String name) {
        return getBeanFactory().containsBean(name);
    }

    /**
     * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException)
     *
     * @param name
     * @return boolean
     * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
     *
     */
    public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException {
        return getBeanFactory().isSingleton(name);
    }

    /**
     * @param name
     * @return Class 注册对象的类型
     * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
     *
     */
    public static Class<?> getType(String name) throws NoSuchBeanDefinitionException {
        return getBeanFactory().getType(name);
    }

    /**
     * 如果给定的bean名字在bean定义中有别名,则返回这些别名
     *
     * @param name
     * @return
     * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
     *
     */
    public static String[] getAliases(String name) throws NoSuchBeanDefinitionException {
        return getBeanFactory().getAliases(name);
    }

}

或者还有一种方法,如下:

在其中加入如上代码,就可以进行注入。

Redis发布订阅与ActiveMQ的比较

(1)ActiveMQ支持多种消息协议,包括AMQP,MQTT,Stomp等,并且支持JMS规范,但Redis没有提供对这些协议的支持; 
(2)ActiveMQ提供持久化功能,但Redis无法对消息持久化存储,一旦消息被发送,如果没有订阅者接收,那么消息就会丢失; 
(3)ActiveMQ提供了消息传输保障,当客户端连接超时或事务回滚等情况发生时,消息会被重新发送给客户端,Redis没有提供消息传输保障。 
总之,ActiveMQ所提供的功能远比Redis发布订阅要复杂,毕竟Redis不是专门做发布订阅的,但是如果系统中已经有了Redis,并且需要基本的发布订阅功能,就没有必要再安装ActiveMQ了,因为可能ActiveMQ提供的功能大部分都用不到,而Redis的发布订阅机制就能满足需求。

 

等有时间测试mq集成的例子。希望不足之处多多指正

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值