Springboot 集成websocket 并支持服务集群

1、新增配置类声明

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebsocketConfig {
    /**
     * 如果单元测试报错,请在类上加上以下注解内容
     * @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
     * @return
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }

}

2、新建websocket连接类

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;

/**
 * html页面与之关联的接口
 * var reqUrl = "http://localhost:8080/ws/device/{deviceType}/{deviceAddress}";
 * socket = new WebSocket(reqUrl.replace("http", "ws"));
 */
@Slf4j
@Component
@ServerEndpoint("ws/device/{deviceType}/{deviceAddress}")
public class OwonWebSocketServerEndpoint {

    private String KEY;
    private String DEVICE_TYPE;

    @OnOpen
    public void onOpen(Session session, @PathParam("deviceType") String deviceType, @PathParam("deviceAddress") String deviceAddress) {
        log.info("发现设备连接,deviceType:" + deviceType + ",deviceAddress:" + deviceAddress);
        DeviceDO userDevice = SpringContextUtils.getBean(rDeviceMapper.class).findByDeviceTypeAndDeviceAddress(deviceType, deviceAddress);
        if (userDevice == null) {
            try {
                session.close();
            } catch (IOException e) {
                // ignore
            }
            return;
        }
        this.KEY = deviceType + WsSessionManager.SPLIT + deviceAddress;
        this.DEVICE_TYPE = deviceType;
        SpringContextUtils.getBean(WsSessionManager.class).add(KEY, session);
        log.info(String.format("成功建立连接, key:" + this.KEY));
    }

    @OnClose
    public void onClose() {
        SpringContextUtils.getBean(WsSessionManager.class).remove(this.KEY);
        log.info("成功关闭连接, key:" + KEY);
    }

    @OnMessage
    public void onMessage(Session session, String message) {
        log.info("收到消息, message:" + message);
    }

    @OnError
    public void onError(Session session, Throwable error) {
        log.info("发生错误:" + this.KEY);
        error.printStackTrace();
    }

    /**
     * 指定发消息
     *
     * @param message
     */
    public void sendMessage(String deviceType, String deviceAddress, String message) {
        SpringContextUtils.getBean(MessageSendManager.class).sendMessage(deviceType, deviceAddress, message);
    }

3、新建session管理类

import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.websocket.Session;
import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;


@Slf4j
@Service
public class WsSessionManager {

    @Value("${eureka.instance.instance-id}")
    private String instanceId;
    @Autowired
    private JedisUtil jedisUtil;

    public static final String SPLIT = "@#@";

    /**
     * 保存连接 session 的地方
     */
    private static ConcurrentHashMap<String, Session> SESSION_POOL = new ConcurrentHashMap<>();

    /**
     * 添加 session
     *
     * @param key
     * @param session
     */
    public void add(String key, Session session) {
        // 添加 session
        SESSION_POOL.put(key, session);
        // 记录session所在的机器
        String url = getLocalServerUrl();
        jedisUtil.set(key, url, 24 * 3600);
    }

    /**
     * 设置链接机器地址
     *
     * @param key
     */
    public void setServerUrl(String key) {
        // 记录session所在的机器
        String url = getLocalServerUrl();
        jedisUtil.set(key, url, 24 * 3600);
    }

    /**
     * 删除 session,会返回删除的 session
     *
     * @param key
     * @return
     */
    public Session remove(String key) {

        // 删除 session
        Session session = SESSION_POOL.remove(key);
        // 删除记录的机器地址
        jedisUtil.del(key);
        return session;
    }

    /**
     * 删除并同步关闭连接
     *
     * @param key
     */
    public void removeAndClose(String key) {

        Session session = remove(key);
        if (session != null) {
            try {
                // 关闭连接
                session.close();
            } catch (IOException e) {
                // todo: 关闭出现异常处理
                e.printStackTrace();
            }
        }
    }

    /**
     * 获得 session
     *
     * @param key
     * @return
     */
    public Session get(String key) {

        // 获得 session
        return SESSION_POOL.get(key);
    }

    /**
     * 获取本机的地址
     *
     * @return
     */
    public String getLocalServerUrl() {
        return "http://" + instanceId;
    }

    /**
     * 组装session key
     *
     * @param deviceType 设备类型
     * @param devId  设备id
     * @return
     */
    public String getSessionKey(String deviceType, String devId) {
        return deviceType + SPLIT + devId;
    }

    /**
     * 获取redis 里面存储的链接地址
     *
     * @param sessionKey
     * @return
     */
    public String getServerUrl(String sessionKey) {
        return jedisUtil.get(sessionKey);
    }

    /**
     * 获取所有sessionKey
     *
     * @return
     */
    public List<String> getAllSessionKeys() {
        Enumeration<String> keys = SESSION_POOL.keys();
        if (keys.hasMoreElements()) {
            return Collections.list(keys);
        }
        return Lists.newArrayList();
    }

}

4、新建消息发送类

import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSON;
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.Session;

@Slf4j
@Component
public class MessageSendManager {

    @Autowired
    private WsSessionManager wsSessionManager;

    /**
     * 发送消息,先看本机,本机没有链接就转发
     * @param deviceType
     * @param deviceAddress
     * @param message
     * @return
     */
    public Boolean sendMessage(String deviceType, String deviceAddress, String message) {
        //先尝试找本机
        Session session = wsSessionManager.get(wsSessionManager.getSessionKey(deviceType, deviceAddress));
        if (null != session) {
            synchronized (session) {
                try {
                    session.getAsyncRemote().sendText(message);
                    log.info("MessageSendManager sendMsg 消息发送成功 deviceType={},deviceAddress={},payload={}", deviceType, deviceAddress, JSON.toJSONString(message));
                    return true;
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return false;
            }
        } else {
            // 转发到链接所在的机器
            String url = wsSessionManager.getServerUrl(wsSessionManager.getSessionKey(deviceType, deviceAddress));
            if (StringUtils.isBlank(url)) {
                log.info("MessageSendManager sendMsg 找不到链接地址 deviceType={},deviceAddress={}", deviceType, deviceAddress);
                return false;
            }
            // 本机地址
            String localUrl = wsSessionManager.getLocalServerUrl();
            if (StringUtils.equals(url, localUrl)) {
                log.info("MessageSendManager sendMsg 本机找不到 deviceType={},deviceAddress={}", deviceType, deviceAddress);
                return false;
            }
            // 转发到其他机器
            transferByMsg(url, deviceType, deviceAddress, message);
            return true;
        }
    }

    /**
     * 发送消息,本机
     * @param message
     * @return
     */
    public Boolean sendMessageByKey(String key, String message) {
        //先尝试找本机
        Session session = wsSessionManager.get(key);
        if (null != session) {
            synchronized (session) {
                try {
                    session.getAsyncRemote().sendText(message);
                    log.info("MessageSendManager sendMsg 消息发送成功 key={},payload={}", key, JSON.toJSONString(message));
                    return true;
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return false;
            }
        }
        return false;
    }

    /**
     * 发送消息,先看本机,本机没有链接就转发
     * @param deviceType
     * @param deviceAddress
     * @param message
     * @return
     */
    public Boolean sendMsgToClient(String deviceType, String deviceAddress, String message) {
        //先尝试找本机
        Session session = wsSessionManager.get(wsSessionManager.getSessionKey(deviceType, deviceAddress));
        if (null != session) {
            synchronized (session) {
                try {
                    session.getAsyncRemote().sendText(message);
                    log.info("MessageSendManager sendMsg 消息发送成功 deviceType={},deviceAddress={},payload={}", deviceType, deviceAddress, JSON.toJSONString(message));
                    return true;
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return false;
            }
        }
        return false;
    }

    private void transferByMsg(String url, String deviceType, String deviceAddress, String message) {
        String urlString = url + "/device/msg/dispatch";
        HttpUtil.post(urlString, JSON.toJSONString(new WsMsgDispatchDTO(deviceType, deviceAddress, message)));
    }

}

5、新建转发消息接收类

@RestController
@RequestMapping("/device")
public class DeviceMsgDispatchController {

    @Autowired
    private MessageSendManager manager;

    /**
     * 消息转发处理
     * @param dto
     * @return
     */
    @RequestMapping(value = "/msg/dispatch", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public BaseResult<Boolean> dispatch(@RequestBody WsMsgDispatchDTO dto) {
        return BaseResult.success(manager.sendMsgToClient(dto.getDeviceType(), dto.getDeviceAddress(), dto.getMessage()));
    }

}
相关推荐
爱码少年17 分钟前
springboot中责任链模式之简单应用
spring boot·责任链模式
F-2H1 小时前
C语言:指针4(常量指针和指针常量及动态内存分配)
java·linux·c语言·开发语言·前端·c++
苹果酱05671 小时前
「Mysql优化大师一」mysql服务性能剖析工具
java·vue.js·spring boot·mysql·课程设计
武昌库里写JAVA1 小时前
【MySQL】7.0 入门学习(七)——MySQL基本指令:帮助、清除输入、查询等
spring boot·spring·毕业设计·layui·课程设计
_oP_i2 小时前
Pinpoint 是一个开源的分布式追踪系统
java·分布式·开源
mmsx2 小时前
android sqlite 数据库简单封装示例(java)
android·java·数据库
武子康2 小时前
大数据-258 离线数仓 - Griffin架构 配置安装 Livy 架构设计 解压配置 Hadoop Hive
java·大数据·数据仓库·hive·hadoop·架构
豪宇刘3 小时前
MyBatis的面试题以及详细解答二
java·servlet·tomcat
秋恬意3 小时前
Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别
java·数据库·mybatis
FF在路上4 小时前
Knife4j调试实体类传参扁平化模式修改:default-flat-param-object: true
java·开发语言