SpringBoot 集成 WebSocket

本文介绍了WebSocket的基本概念、特点和优势,展示了如何在SpringBoot项目中实现WebSocket,包括配置WebSocketConfig和核心服务类WebSocketServer。在实际业务中遇到的问题是,当试图通过@Autowired注入Service时,由于WebSocket的多实例特性导致空指针异常。解决方案是将Service改为静态变量并通过setter方法注入。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一. 什么是 WebSocket

WebSocket 是一种全新的协议。它将 TCP 的 Socket(套接字)应用在了web page上,从而使通信双方建立起一个保持在活动状态的连接通道,并且属于全双工通信(双方同时进行双向通信)

二. WebSocket 的特点

WebSocket 的最大特点是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。

其他特点包括:

  • 建立在 TCP 协议之上,服务器端的实现比较容易。
  • 与 HTTP 协议有着良好的兼容性。默认端口是 80 和 443 ,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
  • 数据格式比较轻量,性能开销小,通信高效。
  • 可以发送文本,也可以发送二进制数据。
  • 没有同源限制,客户端可以与任意服务器通信。
  • 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

三. WebSocket 的优势

目前,很多网站都使用 Ajax 轮询方式来实现消息推送。

轮询是指在特定的的时间间隔(如每秒),由浏览器对服务器发出 HTTP 请求,然后由服务器返回最新的数据给客户端的浏览器。

这种传统的模式带来了很明显的缺点,即浏览器需要不断地向服务器发出请求,然而 HTTP 请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,会浪费很多的带宽资源。

而 WebSocket 允许服务端主动向客户端推送数据,这就使得客户端和服务器之间的数据交换变得更加简单。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

四. WebSocket 的实现

1.建SpringBoot项目

2.pom.xml

<!-- SpringBoot Websocket -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

3.新建配置类 WebSocketConfig,开启 WebSocket 支持

package org.springblade.common.config;

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

/**
 * @ClassName: WebSocketConfig
 * @author: 〆、dyh
 * @since: 2022/8/23 20:26
 */
@Configuration
public class WebSocketConfig {
    /**
     * 	注入ServerEndpointExporter,
     * 	这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

4.实现核心服务类 WebSocketServer

package org.springblade.modules.home.stats.service;

import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springblade.modules.rpa.service.SunPurchasePayService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet;

//注册成组件
@Component
//定义websocket服务器端,它的功能主要是将目前的类定义成一个websocket服务器端。注解的值将被用于监听用户连接的终端访问URL地址
@ServerEndpoint("/websocket/{tenantId}")
@Slf4j
public class WebSocketServer {

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

    private static SunPurchasePayService sunPurchasePayService;

    @Autowired
    public void setSunPurchasePayService(SunPurchasePayService sunPurchasePayService) {
        WebSocket.sunPurchasePayService = sunPurchasePayService;
    }


    //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
    //虽然@Component默认是单例模式的,但springboot还是会为每个websocket连接初始化一个bean,所以可以用一个静态set保存起来。
    //  注:底下WebSocket是当前类名
    private static CopyOnWriteArraySet<WebSocket> webSockets = new CopyOnWriteArraySet<>();
    // 用来存在线连接数
    private static Map<String, Session> sessionPool = new HashMap<String, Session>();

    /**
     * 链接成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam(value = "tenantId") String tenantId) {
        try {
            this.session = session;
            webSockets.add(this);
            sessionPool.put(tenantId, session);
            log.info("【websocket消息】有新的连接,总数为:" + webSockets.size());
        } catch (Exception e) {
        }
    }

    /**
     * 链接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        try {
            webSockets.remove(this);
            log.info("【websocket消息】连接断开,总数为:" + webSockets.size());
        } catch (Exception e) {
        }
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message
     */
    @OnMessage
    public void onMessage(String message, @PathParam(value = "tenantId") String tenantId) {
        log.info("tenantId:{}", tenantId);
        log.info("message:" + message);

        JSONObject jsonObject = JSONObject.parseObject(message);

        if (StringUtils.isNotBlank(jsonObject.getString("type")) && "getBase64Str".equals(jsonObject.getString("type"))) {
            sunPurchasePayService.getBase64Str();
        }

        JSONObject pong = new JSONObject();
        pong.put("type", "pong");
        sendAllMessage(JSONObject.toJSONString(pong));
        log.info("【websocket消息】收到客户端消息:{}" + message);
    }

    /**
     * 发送错误时的处理
     *
     * @param error
     */
    @OnError
    public void onError(Throwable error) {
        log.error("用户错误,原因:" + error.getMessage());
        error.printStackTrace();
    }

    // 此为广播消息
    public void sendAllMessage(String message) {
        log.info("【websocket消息】广播消息:" + message);
        for (WebSocket webSocket : webSockets) {
            try {
                if (webSocket.session.isOpen()) {
                    webSocket.session.getAsyncRemote().sendText(message);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    // 此为单点消息
    public void sendOneMessage(String tenantId, String message) {
        Session session = sessionPool.get(tenantId);
        if (session != null && session.isOpen()) {
            try {
                log.info("【websocket消息】 单点消息:" + message);
                session.getAsyncRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    // 此为单点消息(多人)
    public void sendMoreMessage(String[] tenantIds, String message) {
        for (String userId : tenantIds) {
            Session session = sessionPool.get(userId);
            if (session != null && session.isOpen()) {
                try {
                    log.info("【websocket消息】 单点消息:" + message);
                    session.getAsyncRemote().sendText(message);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

五.WebSocket 的问题描述

在具体的业务场景中,需要等用户连接成功后,从库表中先获取10条数据,作为默认的初始化数据进行显示。

我们想当然的通过 @Autowired 注解将对应 Service 进行依赖注入。却发现报了空指针的异常,也就是说,所需要的 Service 没有被成功注入。

当前写法如下:

//注册成组件
@Component
//定义websocket服务器端,它的功能主要是将目前的类定义成一个websocket服务器端。注解的值将被用于监听用户连接的终端访问URL地址
@ServerEndpoint("/websocket/{tenantId}")
@Slf4j
public class WebSocketServer {
	@Autowired
   private SunPurchasePayService sunPurchasePayService;
}

六.WebSocket 的问题分析

Spring管理采用单例模式(singleton),而 WebSocket 是多对象的,即每个客户端对应后台的一个 WebSocket 对象,也可以理解成 new 了一个 WebSocket,这样当然是不能获得自动注入的对象了,因为这两者刚好冲突。

@Autowired 注解注入对象操作是在启动时执行的,而不是在使用时,而 WebSocket 是只有连接使用时才实例化对象,且有多个连接就有多个对象。

所以我们可以得出结论,这个 Service 根本就没有注入到 WebSocket 当中。

七.WebSocket 的解决方案

使用 static 静态对象

将需要注入的 Service 改为静态,让它属于当前类,然后通过 setSunPurchasePayService 方法进行注入即可解决。


    private static SunPurchasePayService sunPurchasePayService;

    @Autowired
    public void setSunPurchasePayService(SunPurchasePayService sunPurchasePayService) {
        WebSocket.sunPurchasePayService = sunPurchasePayService;
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值