SpringBoot+WebSocket+Redis 实现消息推送

环境:

       SpringBoot 2.0.0.3 + JDK 1.8 + IDEA + Redis(spring-boot-starter-data-redis) + Nginx1.14

坑点:

  1. 程序以war包运行。websocket配置问题
  2. 服务器开启了Nginx代理,导致websocket 404
  3. websocket 短时间 自动关闭。
  4. redis 消息发布/订阅模式 发布对象,接受乱码。这个最后也没解决。如果有哪位大佬知道如何解决。请留言。

 

  1. websocket 配置

    
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Component;
    import javax.websocket.*;
    import javax.websocket.server.PathParam;
    import javax.websocket.server.ServerEndpoint;
    import java.io.IOException;
    import java.util.concurrent.CopyOnWriteArrayList;
    
    /**
     * @description: websocket类
     * @create: 2019-11-21 13:56
     **/
    @Component
    @Slf4j
    @ServerEndpoint("/webService/websocket/{userId}")
    public class WebSocketServer {
        // 消息会话
        private Session session;
        //消息接收者id
        private String userId;
        // 每个客户的WebSocketServer 需要保证线程安全
        private static CopyOnWriteArrayList list = new CopyOnWriteArrayList<>();
    
        /**
         * @Description: 建立连接时调用
         * @param session
         * @param userId
         * @Date: 2019/11/21  15:11
         */
        @OnOpen
        public void onOpen(Session session, @PathParam("userId")String userId){
            log.info("【websocket消息推送模块】--用户" + userId +"连接服务。");
            this.session = session;
            this.userId = userId;
            list.add(this);
        }
    
        /**
         * @Description: 关闭连接时调用
         * @param session
         * @param userId
         * @Date: 2019/11/21  15:11
         */
        @OnClose
        public void onClose(Session session,@PathParam("userId")String userId){
            log.info("【websocket消息推送模块】--用户" + userId +"断开服务。");
            list.remove(this);
        }
    
        /**
         * @Description: 接收到前台推送的消息时调用
         * @param message
         * @param session
         * @Date: 2019/11/21  15:15
         */
        @OnMessage
        public void onMessage(String message, Session session,@PathParam("userId")String userId){
            //目前用不到
            log.info("【websocket消息推送模块】--接收到用户" + userId +"推送的消息:" + message);
            //可以根据userId,推送给具体人
            sendInfo(message,userId);
        }
    
        @OnError
        public void onError(Session session,Throwable error){
            log.info("【websocket消息推送模块】--用户" + userId +"连接异常。");
            error.printStackTrace();
        }
    
        /**
         * @Description: 发送消息
         * @param message
         * @Date: 2019/11/21  15:20
         */
        public void sendMessage(String message) throws IOException {
            session.getBasicRemote().sendText(message);
        }
    
        /**
         * @Description: 群发消息
         * @param message
         * @param userId
         * @Author: 
         * @Date: 2019/11/21  15:35
         */
        public static void sendInfo(String message, @PathParam("userId")String userId){
            log.info("【websocket消息推送模块】--用户" + userId +"发送消息");
            list.stream().forEach(item->{
                if(item.userId.equalsIgnoreCase(userId.trim())){
                    try {
                        log.info("【websocket消息推送模块】--推送消息给用户" + userId);
                        item.sendMessage(message);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    
    }
    

     注意:如果是以jar包方式运行需要加上以下配置。如果以war方式运行则不需要添加以下配置,会和tomcat冲突。

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

    另外因为服务器端使用了Nginx代理,所以需要在Nginx 中添加以下配置解决404问题。默认websocket 连接60秒会自动断开

    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 600s; # 解决连接自动关闭问题。

    如果对最大连接数和排队数有要求,可以配置Tomcat 中的server.xml

      

     

  2. redis 配置 

          用到了 订阅/发布模式 。

         redis 配置



@Configuration
@Slf4j
public class RedissonConfig {
    @Autowired
    private MessageReceiver messageReceiver;
    // 消息处理器 集合
    private static ConcurrentHashMap listenerMap = new ConcurrentHashMap<>();
    
    /**
     * @Description: 自定义redis 模板
     * @param lettuceConnectionFactory
     * @Author: 
     * @Date: 2019/11/9  16:53
     */
    @Bean
    public RedisTemplate redisTemplate(LettuceConnectionFactory lettuceConnectionFactory){
        //配置redisTemplate 序列化方式
        RedisTemplate template = new RedisTemplate<>();
        template.setConnectionFactory(lettuceConnectionFactory);
        // 使用Jackson2JsonRedisSerialize 替换默认序列化(默认采用的是JDK序列化)
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        //java 对象 和 json 之间转换的框架
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        //使用enableDefaultTyping()枚举指定什么样的类型(类)默认输入应该使用。
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(mapper);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        //key 采用String序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //  hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }

    /**
     * @Description: 注入消息监听器 支持自定义通道和自定义消息处理器.
     * 用法:1.在枚举类中添加 通道名称
     * 2.定义消息监听器和消息处理器。并注入到setListenerMap 中
     * @param connectionFactory 连接工厂
     * @Author: 
     * @Date: 2019/11/11  14:54
     */
    @Bean
    @DependsOn({"listenerAdapter"})
    public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory){
        if(listenerMap == null || listenerMap.size() == 0){
            log.info("Redis 注册消息监听器失败!");
            throw new RRException("Redis 注册消息监听器失败!");
        }
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        log.info("listenerMap == " + JSONObject.toJSONString(listenerMap));
        //循环遍历,添加通道
        Arrays.asList(Constant.RedisTopic.values()).stream().forEach(o -> {
            if(listenerMap.containsKey(o.getName())){
                container.addMessageListener(listenerMap.get(o.getName()),new PatternTopic(o.getName()));
            }
        });
        //发布时,需要序列化对象
        Jackson2JsonRedisSerializer seria = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        seria.setObjectMapper(objectMapper);
        container.setTopicSerializer(seria);
        return container;
    }


    /**
     * @Description: 消息处理器 不同topic主题通用同一消息处理器。如果新增消息处理器
     * @param
     * @Author: 
     * @Date: 2019/11/11  16:10
     */
    @Bean("listenerAdapter")
    public MessageListenerAdapter listenerAdapter(MessageReceiver receiver){
        //定义消息处理器
        MessageListenerAdapter adapter = new MessageListenerAdapter(receiver,"receiveMessage1");
        //添加到listenerMap中
        setListenerMap(Constant.RedisTopic.TOPIC_ONE.getName(),adapter);
        return adapter;
    }


    /**
     * @Description: 消息处理器 不同topic主题通用同一消息处理器。如果新增消息处理器
     * @param
     * @Author: 
     * @Date: 2019/11/11  16:10
     */
    @Bean("listenerAdapter2")
    public MessageListenerAdapter listenerAdapter2(MessageReceiver receiver){
        //定义消息处理器
        MessageListenerAdapter adapter = new MessageListenerAdapter(receiver,"receiveMessage2");
        //添加到listenerMap中
        setListenerMap(Constant.RedisTopic.TOPIC_TWO.getName(),adapter);
        return adapter;
    }


    /**
     * @Description: 设置消息处理器集合
     * @param topicName
     * @param adapter
     * @Author: 
     * @Date: 2019/11/11  18:30
     */
    public void setListenerMap(String topicName,MessageListenerAdapter adapter){
        if(StringUtils.isBlank(topicName)){
            log.info("Redis 设置消息处理器失败! -- 主题名称:topicName为空");
            throw new RRException("Redis 设置消息处理器失败!");
        }
        if(adapter == null){
            log.info("Redis 设置消息处理器失败! -- 消息处理器 MessageListenerAdapter:adapter为空");
            throw new RRException("Redis 设置消息处理器失败!");
        }
        if(listenerMap == null){
            listenerMap = new ConcurrentHashMap<>();
        }
        listenerMap.put(topicName,adapter);
    }
}

 

 配置消息接受器



/**
 * @description: redis 消息处理器
 * @author: 
 * @create: 2019-11-12 11:10
 **/
@Component
@Slf4j
public class MessageReceiver {
    @Autowired
    private MessageContentService messageContentService;
    @Autowired
    private MessageUserService messageUserService;
    private static CopyOnWriteArrayList list = new CopyOnWriteArrayList<>();
    public void receiveMessage1(String messageId){
        //调用webSocket 推送 消息 以及消息接受者
       log.info("receiveMessage1接收到的消息为:" + messageId);
        if(StringUtils.isNotBlank(messageId)){
            list.add(messageId);
            doSendMessage();
        }
    }
   

    public synchronized void doSendMessage(){
        //处理具体业务
    }
}

3. 发布消息 

    这里用切面处理 注解:



import java.lang.annotation.*;

/**
 * @Description: redis 消息订阅注解
 * @Author: 
 * @Date: 2019/11/21  10:22
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisMessage {
    /**
     * 消息内容
     */
    String content() default "";
    /**
     * 用户id,多个用,隔开
     */
    String userId() default "";
}

 

切面:



/**
 * @description:
 * @author: 
 * @create: 2019-11-21 10:27
 **/
@Component
@Aspect
@Slf4j
public class RedisMessageAspect {
    @Autowired
    private MessageContentService messageContentService;
    @Autowired
    private MessageUserService messageUserService;

    @Autowired
    private StringRedisTemplate template;
    @Pointcut("@annotation(com.test.common.annotation.RedisMessage)")
    public void defaoltPointCut(){

    }

    @AfterReturning(value = "defaoltPointCut()",returning = "result")
    public void afterReturn(JoinPoint joinPoint, Object result){
        log.info("Redis 消息订阅 注解开始执行");
        if(result != null){
            if(result instanceof Map){
                Map map = (Map)result;
                String content = map.get(Constant.MessageReturn.CONTENT.getName()).toString();
                // 推送的用户
                if(map.get(Constant.MessageReturn.USER_ID.getName()) instanceof  List){
                    List userIds = (List)map.get(Constant.MessageReturn.USER_ID.getName());
                    StringBuffer userId = new StringBuffer();
                    for (String str : userIds){
                        userId.append(str + Constant.Separator.REDIS_USER.getName());
                    }
                    if(StringUtils.isNotBlank(content) && StringUtils.isNotBlank(userId.toString())){
                        //把数据存入数据库
                        Long messageId = saveMessage(content, userId.toString());
                        if(messageId != null){
                            // 发布订阅
                            publishMessage(StringTools.stringOf(messageId));
                        }
                    }
                }
            }
        }
        log.info("Redis 消息订阅 注解结束");
    }


    /**
     * @Description: 保存 消息、用户
     * @param content
     * @param userId
     * @Author: 
     * @Date: 2019/11/21  10:38
     */
    public Long saveMessage(String content,String userId){
        //保存消息到数据库。或者redis
    }

    /**
     * @Description: redis 发布消息
     * @param message
     * @Author: chenxue
     * @Date: 2019/11/21  10:39
     */
    public void publishMessage(String message){
        template.convertAndSend(Constant.RedisTopic.TOPIC_ONE.getName(),message);
    }
}

 

你可能感兴趣的:(java,SpringBoot,redis)