webflux websocket 实现简单im聊天

java 复制代码
package com.kongjs.im.socket;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.data.redis.connection.ReactiveSubscription;
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.ReactiveRedisMessageListenerContainer;
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.socket.HandshakeInfo;
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.WebSocketMessage;
import org.springframework.web.reactive.socket.WebSocketSession;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

//@Component
public class ImMessageHandler implements WebSocketHandler, InitializingBean {

    private static final Logger log = LoggerFactory.getLogger(ImMessageHandler.class);

    // 本地内存:userId -> WebSocketSession(半状态架构核心)
    private static final Map<String, WebSocketSession> USER_SESSIONS = new ConcurrentHashMap<>();

    // sessionId -> userId(断开连接清理用)
    private static final Map<String, String> SESSION_USER = new ConcurrentHashMap<>();

    @Resource
    private ReactiveRedisMessageListenerContainer container;
    @Resource
    private ReactiveStringRedisTemplate reactiveStringRedisTemplate;
    @Resource
    private ReactiveMongoTemplate reactiveMongoTemplate;

    private static final String CHAT_MSG = "im:chat:msg:realtime";
    private static final String USER_CHANNELS = "im:user:channels:%s";
    private static final String USER_CHANNEL = "im:channel:user:%s";    // 单聊
    private static final String GROUP_CHANNEL = "im:channel:group:%s";  // 群聊
    private static final String ROOM_CHANNEL = "im:channel:room:%s";    // 房间
    private static final ChannelTopic TOPIC = ChannelTopic.of(CHAT_MSG);


    @Override
    public Mono<Void> handle(WebSocketSession session) {
        HandshakeInfo handshakeInfo = session.getHandshakeInfo();
        HttpHeaders headers = handshakeInfo.getHeaders();
        Mono<String> userIdMono = ReactiveSecurityContextHolder.getContext()
                .filter(a -> !ObjectUtils.isEmpty(a.getAuthentication()) && StringUtils.hasText(a.getAuthentication().getName()))
                .map(a -> a.getAuthentication().getName())
                .doOnNext(userId -> {
                    // 2. 建立映射
                    USER_SESSIONS.put(userId, session);
                    SESSION_USER.put(session.getId(), userId);
                });;

        Mono<Void> input = userIdMono.flatMap(userId ->
                session.receive()
                        .concatMap(message -> sendMsg(userId, session, message))
                        .then()
        );

        Flux<WebSocketMessage> outMessage = userIdMono.flatMapMany(userId ->
                        reactiveStringRedisTemplate.opsForHash()
                                .entries(USER_CHANNELS.formatted(userId))
                                .filter(entry -> !ObjectUtils.isEmpty(entry.getKey()) && !ObjectUtils.isEmpty(entry.getValue()))
                                .collectMap(entry -> entry.getKey().toString(), entry -> entry.getValue().toString())
                                .flatMapMany(map ->
                                        container.receive(TOPIC)
                                                .filter(msg -> filterMsg(userId, map, msg))
                                                .map(ReactiveSubscription.Message::getMessage)
                                                .doOnNext(msg -> reactiveMongoTemplate.save(msg, "message:realtime")
                                                        .doOnError(e -> log.error("消息保存失败", e))
                                                        .subscribe())
                                ))
                .map(session::textMessage);

        Mono<Void> output = session.send(outMessage);
        Mono<Void> cleanup = session.closeStatus()
                .doOnNext(s -> {
                    String userId = SESSION_USER.remove(session.getId());
                    if (userId != null) {
                        USER_SESSIONS.remove(userId);
                    }
                }).then();
        return input.and(output).and(cleanup).then();
    }

    private Mono<?> sendMsg(String userId, WebSocketSession session, WebSocketMessage message) {
        if (message.getType() != WebSocketMessage.Type.TEXT) return Mono.empty();
        try {
            DataBuffer payload = message.getPayload();
            byte[] bytes = new byte[payload.readableByteCount()];
            payload.read(bytes);
            DataBufferUtils.release(payload);
            JSONObject json = JSON.parseObject(bytes);
            json.put("from", userId);
            return reactiveStringRedisTemplate.convertAndSend(CHAT_MSG, message.getPayloadAsText());
        } catch (Exception e) {
            return sendError(session, "400", "消息格式错误").then(Mono.empty());
        }
    }

    private static boolean filterMsg(String userId, Map<String, String> entry, ReactiveSubscription.Message<String, String> msg) {
        try {
            JSONObject json = JSON.parseObject(msg.getMessage());
            String sendType = json.getString("chatType");
            if (!StringUtils.hasText(sendType)) {
                return false;
            }
            String sendTarget = json.getString("to");
            if (sendType.equals("user")) {
                return sendTarget.equals(userId);
            }
            Object target = entry.get(sendType);
            if (ObjectUtils.isEmpty(target)) {
                return false;
            }
            for (String s : target.toString().split(",")) {
                if (s.equals(sendTarget)) {
                    return true;
                }
            }
            return false;
        } catch (Exception e) {
            log.debug("过滤消息异常{}", e.getMessage());
            return false;
        }
    }

    private Mono<Void> sendError(WebSocketSession session, String code, String msg) {
        if (!session.isOpen()) return Mono.empty();
        JSONObject json = new JSONObject();
        json.put("code", code);
        json.put("msg", msg);
        WebSocketMessage webSocketMessage = session.textMessage(json.toString());
        return session
                .send(Mono.just(webSocketMessage))
                .onErrorResume(err -> {
                    log.debug("发生错误{}", err.getMessage());
                    return Mono.empty();
                });
    }

    @Override
    public void afterPropertiesSet() throws Exception {

    }
}
相关推荐
迪康Defender25 分钟前
AI 重构终端安全运营:智能分析中枢 AI Insight 模块架构与落地场景深度解析
运维·网络·人工智能·其他·安全·重构·架构
0x531 小时前
Java网络编程(二)
java·服务器·网络
AI领路人.1 小时前
[特殊字符] 用 OpenClaw 实现一个 C++ 复刻版 QuantClaw
websocket·单元测试·discord·rest api·内存占用·c++17
微硬创新1 小时前
老旧产线改造:耐达讯自动化16路4‑20mA转PROFINET的工程实践
人工智能·网络协议·自动化·信息与通信
西安景驰电子3 小时前
《PTP精确时间协议系列》第一篇:从原理到应用,全面解读IEEE 1588
linux·运维·服务器·开发语言·网络·windows·php
ZGi.ai3 小时前
多模型回答不稳定:路由规则与评测方法
网络·人工智能·大模型评测·多模型·模型路由·zgi·模型网关
80s7774 小时前
独享住宅IP、长效代理ip是什么?有哪些作用?
网络·网络协议·tcp/ip
IPdodo_4 小时前
静态 IP、住宅 IP 和固定出口有什么区别?开发场景选型指南
服务器·网络·网络协议·tcp/ip·代理ip
_ZHOURUI_H_5 小时前
Unity TCP底层极限拆解:粘包、半包、CRC、序列号、双缓冲到底怎么一起工作的?
网络·网络协议·tcp/ip·游戏·unity·游戏引擎·游戏开发
长江&后浪6 小时前
防火墙的描述
开发语言·网络·php