SpringBoot集成WebSocket的基本实现

2023年 10月 7日 58.6k 0

前言

WebSocket的用途是什么?

想象一个场景,有一些数据实时变化,前端需要在数据变化时刷新界面

此时我们第一反应,前端定时使用HTTP协议调用后端接口,刷新界面。OK,需求实现,下班回家!

然后我们就被前端套麻袋打了一顿。

那么如何优雅的让前端知道数据发生了变化呢?就需要用到WebSocket由后端将数据推送给前端

正文

具体实现

一、引入依赖


    org.springframework.boot
    spring-boot-starter-websocket
    3.0.4

二、配置WebSocket

创建一个config类,配置类代码为固定写法,主要用于告诉SpringBoot我有使用WebSocket的需求,

注意我加了@ServerEndpoint注解的类

/**
 * ServerEndpointExporter 是springBoot的用于自动注册和暴露 WebSocket 端点的类
 * 暴露ServerEndpointExporter类后,所有使用@ServerEndpoint("/websocket")的注解都可以用来发送和接收WebSocket请求
 */
@Component
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

三、WebSocket逻辑实现

话不多说,直接上代码

@Component // 交给Spring管理
@ServerEndpoint("/websocket") // 告知SpringBoot,这是WebSocket的实现类
@Slf4j
public class WebSocketServer {
    //静态变量,用来记录当前在线连接数
    private static AtomicInteger onlineCount = new AtomicInteger(0);
    //concurrent包的线程安全Set,用来存放每个客户端对应的WebSocket对象。
    private static CopyOnWriteArraySet webSocketSet = new CopyOnWriteArraySet();

    //与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;
    
    private List ids = new ArrayList();

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session) {
        this.session = session;
        webSocketSet.add(this);
        
        // ps:后端接参示例代码
        // 这样接参,前端对应传参方式为
        //    var _client = new window.WebSocket(_this.address + "?tunnelId=" + tunnelId);
        Map map = session.getRequestParameterMap();
        String id = map.get("tunnelId").get(0);
        ids = Arrays.asList(id.split(","));
        
        addOnlineCount();           //在线数加1
        try {
            sendMessage("连接成功");
        } catch (IOException e) {
            log.error("websocket IO异常");
        }
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        webSocketSet.remove(this);  //从set中删除
        subOnlineCount();           //在线数减1
        log.info("有一连接关闭!当前在线人数为" + getOnlineCount());
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        // 心跳检测,看连接是否意外断开
        // ps:现在uniapp等前端好像自动带有心跳包,但是web端一般还需要心跳包确保连接一直未断开
        if ("heart".equals(message)) {
            try {
                sendMessage("heartOk");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("发生错误");
        error.printStackTrace();
    }

    /**
     * 实现服务器主动推送
     */
    public void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
    }

    /**
     * 群发自定义消息
     */
    public static void sendInfo(String message, String id) {

        for (WebSocketServer item : webSocketSet) {
            try {
                if (id == null) {
                    item.sendMessage(message);
                } else if (item.ids.contains(id)) {
                    item.sendMessage(message);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static int getOnlineCount() {
        return onlineCount.get();
    }

    public static void addOnlineCount() {
        WebSocketServer.onlineCount.incrementAndGet();
    }

    public static void subOnlineCount() {
        if (getOnlineCount() > 0) {
            WebSocketServer.onlineCount.decrementAndGet();
        }

    }
}

ok,到这里,一个基本的WebSocket服务端就搭建完成了

下面是前端测试代码(前端就是一个html的demo)




    
    
    
    
    
    websocket测试页面



    
        
            
                
                    ws地址
                    
                    
                        连接
                    
                
            
        
        
            
                
                    消息
                    
                    
                        发送
                    
                
            
        
        
            
                
                
            
        
    






    $(function () {
        var _socket;

        $("#connect").click(function () {
            var tunnelId = "2"; // 设置需要传递的参数
            _socket = new _websocket($("#address").val(), tunnelId);
            _socket.init();
        });

        $("#send").click(function () {
            var _msg = $("#msg").val();
            output("发送消息:" + _msg);
            _socket.client.send(_msg);
        });
    });

    function output(e) {
        var _text = $("#log").html();
        $("#log").html(_text + "" + e);
    }

    function _websocket(address, tunnelId) {
        this.address = address;
        this.tunnelId = tunnelId;
        console.log(address)
        console.log(tunnelId)
        this.client;

        this.init = function () {
            if (!window.WebSocket) {
                this.websocket = null;
                return;
            }

            var _this = this;
            // var _client = new window.WebSocket(_this.address + "/" + _this.tunnelId);// 路径传参(没跑通)
            // 注意这里的名字要和后端接参数的名字对应上
            var _client = new window.WebSocket(_this.address + "?tunnelId=" + tunnelId);

            _client.onopen = function () {
                output("websocket打开");
                $("#msg-panel").show();
            };

            _client.onclose = function () {
                _this.client = null;
                output("websocket关闭");
                $("#msg-panel").hide();
            };

            _client.onmessage = function (evt) {
                output(evt.data);
            };

            _this.client = _client;
        };

        return this;
    }



进阶

以上内容实现了基本的推送消息到前端,也是网上大部分文章讲解的深度,但是实际开发中,笔者不可能不进行Spring的依赖注入,然后查询数据库拿到一些数据。此时我们就会发现,为什么空指针啊???为什么啊?

下面是笔者当时的排查思路

第一步:空指针?bean没被Spring管理呗。

看我三下五除二,要不就是@Component注解没加,要不就是SpringBoot启动类的扫描路径有问题,根本难不倒我

?都加了啊,为什么还是不行啊?开始怀疑人生

后来,因为我同时和小程序端还有web端对接,突然反应过来会不会是因为Spring默认单例,只会创造一个对象,但是WebSocket大概率都会有多个客户端,按照这个方向去尝试的话,直接手动获取bean对象是不是就不会空指针了呢?

我写了一个工具类获取bean对象

@Component
public final class SpringUtils implements BeanFactoryPostProcessor, ApplicationContextAware 
{
    private static ConfigurableListableBeanFactory beanFactory;

    private static ApplicationContext applicationContext;

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

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

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

在我们的WebSocket类中使用以下代码进行依赖注入

EmergencyTypeService emergencyTypeService = SpringUtils.getBean(EmergencyTypeService.class);

ok,到此,我们就解决了空指针的问题,真是泪目。

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论