springboot webflux websocket+redis实现集群聊天室

由于websocket的session不能直接序列化然后存储到redis之类的缓存数据库中实现session共享,本例采用的是redis的发布/订阅机制来实现集群聊天室,原理就是当其中一个节点接收到消息时做处理(ps:消息中带用户信息用于判断用户在哪个节点),并将消息发布到对应的channel,订阅了这个channel的都会收到消息,接收到消息的节点判断用户是不是缓存在当前节点中,然后做对应消息处理就行,直接上代码吧!

pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>webflux-websocket-chat</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>webflux-websocket-chat</name>
    <description>Demo project for Spring Boot</description>

    <properties>

    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.44</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.10</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.tuckey</groupId>
            <artifactId>urlrewritefilter</artifactId>
            <version>4.0.4</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.3.3.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>4.6.1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

配置文件application.yml

# 服务端口
server:
  port: 8081

# redis ip 端口
spring:
  redis:
    host: 127.0.0.1
    port: 6379

# redis聊天topic
chat:
  topic: chatTopic

# 日志打印
logging:
  level:
    ROOT: info

redis工具类 RedisUtil

package com.example.webflux.util;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.Map;

/**
 * @program: webflux-websocket-chat
 * @description:
 * @author: 71ang~
 * @create: 2020-07-14 14:45
 * @vsersion: V1.0
 */
@Component
public class RedisUtil {
    @Autowired
    private RedisTemplate redisTemplate;
    public static RedisTemplate redis;

    @PostConstruct
    public void getRedisTemplate() {
        redis = this.redisTemplate;
    }

    public static Map getOfMap(String key) {
        return redis.opsForHash().entries(key);
    }

    public static void putOfMap(String key, Map map) {
        redis.opsForHash().putAll(key, map);
    }

    public static boolean delete(String key) {
        return redis.delete(key);
    }

    public static void convertAndSend(String channel, Object message) {
        redis.convertAndSend(channel,message);
    }
}

channel配置类

package com.example.webflux.redis;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * @program: webflux-websocket-chat
 * @description: 聊天配置
 * @author: Yang Mingqiang
 * @create: 2020-09-17 14:58
 * @vsersion: V1.0
 */
@Data
@Component
@ConfigurationProperties(prefix = "chat")
public class ChatConfig {
    private String topic;
}

redis监听容器

package com.example.webflux.redis;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.stereotype.Component;

/**
 * @program: webflux-websocket-chat
 * @description:
 * @author: 71ang~
 * @create: 2020-07-14 19:00
 * @vsersion: V1.0
 */
@Component
public class RedisListener {

    @Autowired
    private ChatConfig chatConfig;

    @Bean
    RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory, MessageListenerAdapter listenerAdapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();

        container.setConnectionFactory(connectionFactory);

        // 所有订阅该主题的节点都能收到消息
        container.addMessageListener(listenerAdapter, new PatternTopic(chatConfig.getTopic()));

        return container;
    }
}

redis监听消息处理

package com.example.webflux.redis;

import com.alibaba.fastjson.JSONObject;
import com.example.webflux.websocket.ChatHandler;
import com.example.webflux.websocket.WebSocketClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.Map;

/**
 * @program: webflux-websocket-chat
 * @description: redis消息订阅发布
 * @author: 71ang~
 * @create: 2020-07-14 18:53
 * @vsersion: V1.0
 */
@Slf4j
@Component
public class RedisListenerHandler extends MessageListenerAdapter {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 消息订阅处理
     * @param message
     * @param pattern
     */
    @Override
    public void onMessage(Message message, byte[] pattern) {
        JdkSerializationRedisSerializer serializer = new JdkSerializationRedisSerializer();

        byte[] body = message.getBody();
        String rawMsg;
        try {
            rawMsg = String.valueOf(serializer.deserialize(body));

            JSONObject msgObj = JSONObject.parseObject(rawMsg);
            String roomName = msgObj.getString("roomName");
            String userId = msgObj.getString("userId");
            String msg = msgObj.getString("message");

            // 发送消息
            ChatHandler.sendToAll(roomName,userId,msg);
        } catch (Exception e) {
            return;
        }
    }
}

spring上下文工具类

package com.example.webflux.util;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * @program: vue-cli-rest
 * @description: spring上下文工具类
 * @author: Yang Mingqiang
 * @create: 2020-07-06 09:56
 * @vsersion: V1.0
 */
@Component
public class SpringContextUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContextUtil.applicationContext = applicationContext;
    }

    /**
     * Description:
     * 〈获取applicationContext〉
     *
    []
     * @return : org.springframework.context.ApplicationContext
     */
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    /**
     * Description:
     * 〈通过name获取 Bean.〉
     *
    [name]
     * @return : java.lang.Object
     */
    public static Object getBean(String name) {
        return getApplicationContext().getBean(name);
    }

    /**
     * Description:
     * 〈通过class获取Bean.〉
     *
    [clazz]
     * @return : T
     */
    public static <T> T getBean(Class<T> clazz) {
        return getApplicationContext().getBean(clazz);
    }

    /**
     * Description:
     * 〈通过name,以及Clazz返回指定的Bean〉
     *
    [name, clazz]
     * @return : T
     */
    public static <T> T getBean(String name, Class<T> clazz) {
        return getApplicationContext().getBean(name, clazz);
    }
}

websocket配置类

package com.example.webflux.websocket;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class WebSocketConfig {

    @Bean
    public HandlerMapping handlerMapping() {
        Map<String, WebSocketHandler> map = new HashMap<>();
        map.put("/chat", new ChatHandler());

        SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
        mapping.setUrlMap(map);
        mapping.setOrder(-1);
        return mapping;
    }

    @Bean
    public WebSocketHandlerAdapter handlerAdapter() {
        return new WebSocketHandlerAdapter();
    }
}

WebSocketClient

package com.example.webflux.websocket;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.reactive.socket.WebSocketMessage;
import org.springframework.web.reactive.socket.WebSocketSession;
import reactor.core.publisher.FluxSink;

import java.io.Serializable;

/**
 * @program: webflux-websocket-chat-websocket-chat
 * @description:
 * @author: 71ang~
 * @create: 2020-07-14 13:39
 * @vsersion: V1.0
 */
@Slf4j
@Data
public class WebSocketClient implements Serializable {
    private static final long serialVersionUID = 3126044575672218399L;
    private FluxSink<WebSocketMessage> sink;
    private WebSocketSession session;

    public WebSocketClient(FluxSink<WebSocketMessage> sink, WebSocketSession session) {
        this.sink = sink;
        this.session = session;
    }

    public void sendData(String data) {
        sink.next(session.textMessage(data));
    }
}

ChatHandler

package com.example.webflux.websocket;

import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSONObject;
import com.example.webflux.redis.ChatConfig;
import com.example.webflux.util.RedisUtil;
import com.example.webflux.util.SpringContextUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.reactive.socket.HandshakeInfo;
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.WebSocketSession;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.net.InetSocketAddress;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
public class ChatHandler implements WebSocketHandler {

    public static ConcurrentHashMap<String,Map<String, WebSocketClient>> roomCacheMap = new ConcurrentHashMap<>();

    @Override
    public Mono<Void> handle(WebSocketSession session) {
        HandshakeInfo handshakeInfo = session.getHandshakeInfo();
        InetSocketAddress remoteAddress = handshakeInfo.getRemoteAddress();
        String params = handshakeInfo.getUri().getQuery();

        // Map<String, String> paramMap = Stream.of(params.split("&")).collect(Collectors.toMap(param -> param.split("=")[0], param -> param.split("=")[1]));
        HashMap<String, String> paramMap = HttpUtil.decodeParamMap(params, "UTF-8");

        String roomName = paramMap.get("roomName");
        String userId = paramMap.get("userId");

        //出站
        Mono<Void> output = session.send(Flux.create(sink -> handleClient(roomName, userId, new WebSocketClient(sink, session))));

        //入站
        Mono<Void> input = session.receive()
                .doOnSubscribe(conn -> {
                    log.info("建立连接:{},用户ip:{},房间号:{},用户:{}", session.getId(),
                            remoteAddress.getHostName(), roomName, userId);
                })
                .doOnNext(msg -> {
                    String message = msg.getPayloadAsText();
                    broadcast(roomName, userId, message);
                })
                .doOnComplete(() -> {
                    log.info("关闭连接:{}", session.getId());
                    exitRoom(session, roomName, userId);
                }).doOnCancel(() -> {
                    log.info("关闭连接:{}", session.getId());
                    exitRoom(session, roomName, userId);
                }).then();

        return Mono.zip(input, output).then();
    }

    private void exitRoom(WebSocketSession session, String roomName, String userId) {
        session.close().toProcessor().then();
        broadcast(roomName, userId, "退出房间!");
        removeUser(roomName, userId);
    }

    private void removeUser(String roomName, String userId) {
        log.info("用户:{},退出房间:{}!", userId, roomName);
        Map<String, WebSocketClient> socketClientCacheMap = roomCacheMap.get(roomName);
        socketClientCacheMap.remove(userId);
        if (socketClientCacheMap.isEmpty()) {
            log.info("房间:{}没人了,关闭房间!", roomName);
            roomCacheMap.remove(roomName);
        }
    }

    private void handleClient(String roomName, String userId, WebSocketClient client) {
        if (!roomCacheMap.containsKey(roomName)) {
            log.info("用户:{},创建房间:{}", userId, roomName);
            Map<String, WebSocketClient> socketClientCacheMap = new HashMap<>();
            socketClientCacheMap.put(userId, client);
            roomCacheMap.put(roomName, socketClientCacheMap);
        } else {
            Map<String, WebSocketClient> socketClientCacheMap = roomCacheMap.get(roomName);
            if (!socketClientCacheMap.containsKey(userId)) {
                log.info("用户:{},进入房间:{}", userId, roomName);
                socketClientCacheMap.put(userId, client);
            }
        }
    }

    /**
     * 发布消息广播
     */
    public void broadcast(String roomName, String userId, String message) {
        JSONObject msgObj = new JSONObject();
        msgObj.put("roomName",roomName);
        msgObj.put("userId",userId);
        msgObj.put("message",message);

        ChatConfig chatConfig = SpringContextUtil.getBean(ChatConfig.class);
        RedisUtil.convertAndSend(chatConfig.getTopic(),msgObj.toJSONString());

        sendToAll(roomName, userId, message);
    }

    /**
     * 发送消息给除了自己的所有用户
     * @param roomName
     * @param userId
     * @param message
     */
    public static void sendToAll(String roomName, String userId, String message) {
        Map<String, WebSocketClient> clients = roomCacheMap.get(roomName);
        clients.forEach((user, client) -> {
            if (!userId.equals(user)) {
                log.info("用户:{}发送消息:{}",userId,message);
                client.sendData(userId + ":" + message);
            }
        });
    }
}

在线websocket测试

在这里插入图片描述
在这里插入图片描述

源码github地址

注意事项

1、环境为jdk11
2、其中使用了lombok,要安装lombok插件
3、其中有一些依赖可能会下载不到,推荐两个maven镜像地址
在maven的setting.xml里添加:

<mirror>
		<id>alimaven</id>
		<name>aliyun maven</name>
		<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
		<mirrorOf>central</mirrorOf>
	</mirror>
<mirror>
    <id>repo2</id>
    <name>Mirror from Maven Repo2</name>
    <url>http://repo2.maven.org/maven2/</url>
    <mirrorOf>central</mirrorOf>
</mirror>
  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值