SpringBoot集成WebSocket的基本实现

前言

WebSocket的用途是什么?

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

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

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

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

正文

具体实现

一、引入依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
    <version>3.0.4</version>
</dependency>

二、配置WebSocket

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

注意我加了@ServerEndpoint注解的类

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

三、WebSocket逻辑实现

话不多说,直接上代码

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

    //与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;
    
    private List<String> 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<String, List<String>> 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)

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <!-- 最新版本的 Bootstrap 核心 CSS 文件 -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css"
          integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <title>websocket测试页面</title>
</head>
<body>
<div class="panel panel-default">
    <div class="panel-body">
        <div class="row">
            <div class="col-md-6">
                <div class="input-group">
                    <span class="input-group-addon">ws地址</span>
                    <input type="text" id="address" class="form-control" placeholder="ws地址"
                           aria-describedby="basic-addon1" value="ws://localhost:9700/tunnel/websocket">
                    <div class="input-group-btn">
                        <button class="btn btn-default" type="submit" id="connect">连接</button>
                    </div>
                </div>
            </div>
        </div>
        <div class="row" style="margin-top: 10px;display: none;" id="msg-panel">
            <div class="col-md-6">
                <div class="input-group">
                    <span class="input-group-addon">消息</span>
                    <input type="text" id="msg" class="form-control" placeholder="消息内容" aria-describedby="basic-addon1">
                    <div class="input-group-btn">
                        <button class="btn btn-default" type="submit" id="send">发送</button>
                    </div>
                </div>
            </div>
        </div>
        <div class="row" style="margin-top: 10px; padding: 10px;">
            <div class="panel panel-default">
                <div class="panel-body" id="log" style="height: 450px;overflow-y: auto;">
                </div>
            </div>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/jquery@1.12.4/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js"
        integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
        crossorigin="anonymous"></script>

<script type="text/javascript">
    $(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 + "<br>" + 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;
    }
</script>
</body>
</html>

进阶

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

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

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

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

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

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

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

java 复制代码
@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> T getBean(Class<T> clz) throws BeansException
    {
        T result = (T) beanFactory.getBean(clz);
        return result;
    }
}

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

java 复制代码
EmergencyTypeService emergencyTypeService = SpringUtils.getBean(EmergencyTypeService.class);

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

相关推荐
龙哥·三年风水1 天前
workman服务端开发模式-应用开发-vue-element-admin封装websocket
分布式·websocket·vue
ZoeLandia1 天前
WebSocket | 背景 概念 原理 使用 优缺点及适用场景
网络·websocket·网络协议
zquwei2 天前
SpringCloudGateway+Nacos注册与转发Netty+WebSocket
java·网络·分布式·后端·websocket·网络协议·spring
carterslam2 天前
解决:websocket 1002 connection rejected 426upgrade required
网络·websocket·网络协议
抓住鼹鼠不撒手2 天前
xterm.js结合websocket实现web ssh
前端·javascript·websocket
学前端的小朱2 天前
Echarts实现大屏可视化
websocket·echarts·nodejs·vue3·vite·koa·cors
龙少95434 天前
【Http,Netty,Socket,WebSocket的应用场景和区别】
java·后端·websocket·网络协议·http
m0_748232924 天前
前端在WebSocket中加入Token
前端·websocket·网络协议
等一场春雨4 天前
react websocket 全局访问和响应
前端·websocket·react.js
流穿5 天前
WebSocket vs SSE:实时通信技术的对比与选择
网络·websocket·网络协议·大语言模型·sse