SpringBoot集成WebSocket的基本实现

2023-10-08 17:05:16 浏览数 (2)

前言

WebSocket的用途是什么?

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

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

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

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

正文

具体实现

一、引入依赖

代码语言:txt复制
<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());

    }


     // 收到客户端消息后调用的方法
    @OnMessage

    public void onMessage(String message, Session session) {

        // 心跳检测,看连接是否意外断开

        // ps:现在uniapp等前端好像自动带有心跳包,但是web端一般还需要心跳包确保连接一直未断开

        if ("heart".equals(message)) {

            try {

                sendMessage("heartOk");

            } catch (IOException e) {

                e.printStackTrace();

            }

        }

    }


    @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;

    }


    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,到此,我们就解决了空指针的问题,真是泪目。

我正在参与2023腾讯技术创作特训营第二期有奖征文,瓜分万元奖池和键盘手表

0 人点赞