SpringBoot开发系列(7)-开发WebSocket的一点经验

简介:

1、前言

在某些项目场景中,WebSocket是个利器,但毕竟常规应用场景不多。趁现在还记得些,把一些开发过程中总结的一些经验记下来,以免过个一年半载再次需要用到时忘却了。之前已经写过一篇《WebSocket,不再轮询》,讲了一些WebSocket的概念和应用场景,而本文这次偏实战,讲解的代码会比较多一些。

代码包括WebSocket的服务端和客户端,以及如何写WebSocket的单元测试。其中还会针对一些 “坑” ,做重点分析。

2、WebSocket服务端

WebSocket服务端,即提供WebSocket服务的程序。SpringBoot开发WebSocket,常规有两种方式 - 申明式和编程式,前者最简单,我用的就是申明式。

2.1、pom.xml

        <!--websocket 服务端-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

2.2、注册Bean

在包含@Configuration类(启动类也包含该注解)中配置ServerEndpointExporter,配置后会自动注册所有“@ServerEndpoint”注解声明的Websocket Endpoint。

    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }

2.3、WebSocket服务端类

根据上文,写WebSocket的类需要通过“@ServerEndpoint”注解声明。
MyWebSocketService .java

@Component
@ServerEndpoint(value = "/xxx/{userId}")
@Slf4j
public class MyWebSocketService {
    private String userId = "anonymous";
    private static int onlineCount = 0;
    private Session session;
    private static ConcurrentHashMap<String, Session> sessionPool = new ConcurrentHashMap<>();


    @OnOpen
    public void onOpen(Session curSession, @PathParam("userId") String curUserId) {
        this.session = curSession;
        this.userId = curUserId;
        sessionPool.put(curUserId, curSession);
        addOnlineCount();
        log.info(curUserId + "有一连接加入!当前在线人数为" + onlineCount);
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        if (sessionPool.get(this.userId) != null) {
            sessionPool.remove(userId);
            subOnlineCount();
            log.info(userId + "有一连接关闭!当前在线人数为" + getOnlineCount());
        }
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message) {
        handleMessage(message);
    }

    /**
     * @param curSession
     * @param error
     */
    @OnError
    public void onError(Session curSession, Throwable error) {
        log.error(error.getMessage(), error);
    }

    public static synchronized int getOnlineCount() {
        return onlineCount;
    }

    public static synchronized void addOnlineCount() {
        onlineCount++;
    }

    public static synchronized void subOnlineCount() {
        onlineCount--;
    }

    /**
     * 只发送给单一用户
     *
     * @param curUserId
     * @param socketMessage
     */
    public void sendMessageSingle(String curUserId, SocketMessage socketMessage) {
        Session curSession = sessionPool.get(curUserId);
        if (curSession != null) {
            try {
                String response = JSON.toJSONString(socketMessage);
                curSession.getBasicRemote().sendText(response);
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            }
        }
    }

  
    /**
     * 处理客户端发送的消息
     * websocket 初始化早,无法注入Bean
     *
     * @param message
     * @return
     */
    private void handleMessage(String message) {
        try {
            SocketMessage request = JSON.parseObject(message, SocketMessage.class);
            switch (ModuleEnum.valueOf(request.getModule())) {
                case HEART_CHECK:
                    this.session.getBasicRemote().sendText(
                            JSON.toJSONString(new SocketMessage(ModuleEnum.HEART_CHECK.name(), "回复心跳检查")));
                    break;
                case ACTION_MAP_SWITCH:
                    MapMapper mapMapper =ApplicationContextRegister.getApplicationContext().getBean(MapMapper.class);
                    mapMapper.updateAllBranchVisible();
                    sendMessageSingle(chlWeb, request);
                    break;
                 //case 等等,其他处理逻辑
                default:
                    break;
            }

        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }

}

2.4、单例与多例的冲突

上述代码中有一步是要调用dao层的方法,handleMessage方法中。

//...
                case ACTION_MAP_SWITCH:
                    MapMapper mapMapper =ApplicationContextRegister.getApplicationContext().getBean(MapMapper.class);
                    mapMapper.updateAllBranchVisible();
//...

正常我们开发SpringBoot,都是借助Spring容器的IOC的特性,将Service、Dao等直接依赖注入,类似于下面。

    @Autowired
    private MapMapper mapMapper;

//...
                case ACTION_MAP_SWITCH:
                    mapMapper.updateAllBranchVisible();
//...

但是这么写会报错,在执行 mapMapper.updateAllBranchVisible(); 方法时报空指针,即MapMapper的Bean没有注入进来。所以本文是通过Spring容器上下文,用工厂类的方式创建MapMapper的Bean。
ApplicationContextRegister.java

@Component
public class ApplicationContextRegister implements ApplicationContextAware {
    private static ApplicationContext applicationContext;

    @Override
    public void  setApplicationContext(ApplicationContext curApplicationContext) {
        applicationContext = curApplicationContext;
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

}

要弄懂原因,首先要了解Spring注入Bean的方式:

有些人可能不知道,Spring默认实例化的Bean是单例模式,这就意味着在Spring容器加载时,就注入了MapMapper的实例,不管再调用多少次接口,加载的都是这个Bean同一个实例。

而WebSocket是多例模式,在项目启动时第一次初始化实例时,MapMapper的实例的确可以加载成功,但可惜这时WebSocket是无用户连接的。当有第一个用户连接时,WebSocket类会创建第二个实例,但由于Spring的Dao层是单例模式,所以这时MapMapper对应的实例为空。后续每连接一个新的用户,都会再创建新的WebSocket实例,当然MapMapper的实例都为空。

3、WebSocket客户端

一般很少有人在SpringBoot里面写WebSocket的客户端,通常都是后端提供服务,前端来作为客户端通讯。但是如果你的应用场景是后端之间的长连接交互,还是会用到的。或者,当你需要给你的服务端写单元测试时,这个后面再说。

3.1、pom.xml

        <!--websocket 客户端-->
        <dependency>
            <groupId>org.java-websocket</groupId>
            <artifactId>Java-WebSocket</artifactId>
            <version>1.3.8</version>
        </dependency>

3.2、WebSocket客户端类

配置WebSocket客户端的方法更简单,继承并实现WebSocketClient 类。

MyWebSocketClient.java

import lombok.extern.slf4j.Slf4j;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;

@Slf4j
public class MyWebSocketClient extends WebSocketClient {
public MyWebSocketClient(URI uri){
    super(uri);
}

    @Override
    public void onOpen(ServerHandshake serverHandshake) {
        log.info("客户端连接成功");
    }

    @Override
    public void onMessage(String s) {
        log.info("客户端接收到消息:"+s);
    }

    @Override
    public void onClose(int i, String s, boolean b) {
        log.info("客户端关闭成功");
    }

    @Override
    public void onError(Exception e) {
        log.error("客户端出错");
    }

    public static void main(String[] args) {
        try {
            MyWebSocketClient myWebSocketClient = new MyWebSocketClient(new URI("ws://localhost:9000/xxx/user1"));
            myWebSocketClient.connect();
            while (!WebSocket.READYSTATE.OPEN.equals(myWebSocketClient.getReadyState())) {
                log.info("WebSocket客户端连接中,请稍等...");
                Thread.sleep(500);
            }
            myWebSocketClient.send("{\"module\":\"HEART_CHECK\",\"message\":\"请求心跳\"}");
            myWebSocketClient.close();
        } catch (Exception e) {
            log.error("error", e);
        }
    }

}

4、WebSocket单元测试

客户要求我们的SpringBoot程序发布前,要通过sonar的质量检查,其中有一项就是 “保证单元测试的覆盖率超过50%” 。普通Http接口的单元测试我们都知道,实在不会也可以百度出来。可是你很难百度出来,WebSocket接口如何做单元测试?

后来我想,单元测试嘛,无非就是监听后端服务的路由,调用一下程序的方法。那我能不能写一个测试类,通过创建WebSocket客户端的方式,模拟前端来测试服务端的逻辑?实际上我研究 “3、WebSocket客户端“ ,就是为了提高这个单元测试的覆盖率。

4.1、WebEnvironment

我们在写Junit的测试类时,通常都会如下文一样,通过@SpringBootTest获取启动类,加载SpringBoot配置。但是如果我们的项目里面有WebSocket,这样会报无法启动WebSocket的错误。

@RunWith(SpringRunner.class)
@SpringBootTest
public class CompositeControllerTest{
    @Test
    public void websocketClient() {
        int num = new Integer(1);
        Assert.assertEquals(num, 1);
    }
}

@SpringBootTest注解实际有一个webEnvironment的属性,SpringBootTest.WebEnvironment有下列四种:

  1. MOCK(默认) : 加载一个WebApplicationContext并提供一个模拟servlet环境。嵌入式servlet容器在使用此注释时不会启动。如果servlet API不在你的类路径上,这个模式将透明地回退到创建一个常规的非web应用程序上下文。可以与@AutoConfigureMockMvc结合使用,用于基于MockMvc的应用程序测试。
  2. RANDOM_PORT : 加载一个EmbeddedWebApplicationContext并提供一个真正的servlet环境。嵌入式servlet容器启动并在随机端口上侦听。
  3. DEFINED_PORT : 加载一个EmbeddedWebApplicationContext并提供一个真正的servlet环境。嵌入式servlet容器启动并监听定义的端口(即从application.properties或默认端口8080)。
  4. NONE : 使用SpringApplication加载ApplicationContext,但不提供任何servlet环境(模拟或其他)。

我们在测试使用websocket时是需要完整的容器,所以可以选 RANDOM_PORT或DEFINED_PORT。

4.2、测试类

为了方便测试我们使用 SpringBootTest.WebEnvironment.DEFINED_PORT,监听固定的端口。

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@AutoConfigureMockMvc
@Slf4j
class CompositeControllerTest {
    @Autowired
    private MockMvc mockMvc;
    @Autowired
    private WebApplicationContext webApplicationContext;

    @Before
    public void before(){
        mockMvc= MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
    }

// ...  等等

    /**
     * 创建 WebSocket 的客户端做测试
     * @throws Exception
     */
    @Test
    void websocketClient() throws Exception{
        MyWebSocketClient myWebSocketClient = new MyWebSocketClient(new URI("ws://localhost:port/xxxx/user1"));
        myWebSocketClient.connect();
        while (!WebSocket.READYSTATE.OPEN.equals(myWebSocketClient.getReadyState())){
            log.info("WebSocket客户端连接中,请稍等...");
            Thread.sleep(500);
        }
        Map<String,String> requestMap=new HashMap<>();
        requestMap.put("HEART_CHECK","{\"module\":\"HEART_CHECK\",\"message\":\"请求心跳\"}");
        requestMap.put("KEY1","VALUE1");
        requestMap.put("KEY2","VALUE2");
        requestMap.put("KEY3","VALUE3");

        for(String key: requestMap.keySet()){
            myWebSocketClient.send(requestMap.get(key));
        }
        //测试 onError、onMessage、onClose
        // ...  等等
        myWebSocketClient.close();
    }
}

OK,在最后生成的sonar测试报告里面,我们可以看到WebSocket的代码基本都被覆盖到,单元测试的覆盖率提高到了90%,我的任务达到了。

文献参考

1.spring boot整合Websocket笔记

https://yq.aliyun.com/articles/637898?spm=a2c4e.11153940.0.0.537f1d36hwhtLi

相关文章
|
4天前
|
JSON API 数据安全/隐私保护
95%开发者不知道的调试黑科技:Apipost让WebSocket开发效率翻倍的秘密
在现代Web开发中,WebSocket提供全双工通信,适用于实时交互场景,如IM系统、聊天和客服系统。尽管调试工具众多,但文档设计一直是其短板。本文介绍如何使用Apipost实现WebSocket的高效调试与文档设计。Apipost不仅简化了连接建立、消息发送等调试操作,还通过分组功能优化了消息管理。其文档设计功能支持在同一endpoint下区分业务逻辑,生成清晰易维护的文档,并可一键分享。此外,文章还提供了WebSocket实战技巧,涵盖连接保持、消息格式选择、错误处理及安全性保障等内容,助力开发者提升开发效率。
|
14天前
|
JSON API 数据安全/隐私保护
95%开发者不知道的调试黑科技:Apipost让WebSocket开发效率翻倍的秘密
在现代Web开发中,WebSocket因其全双工通信特性广泛应用于实时交互场景,但调试和文档设计工具却相对匮乏。Apipost最新推出的“调试与设计模块”解决了这一痛点,不仅简化了WebSocket的调试流程,还优化了文档设计,支持消息分组、精细化文档管理和一键分享功能,极大提升了开发效率。此外,Apipost提供了链接保持、消息格式选择、错误处理及安全性保障等实战技巧,助力开发者更好地应对WebSocket开发挑战。
|
18天前
|
消息中间件 XML 前端开发
springBoot集成websocket实时消息推送
本文介绍了如何在Spring Boot项目中集成WebSocket实现实时消息推送。首先,通过引入`spring-boot-starter-websocket`依赖,配置`WebSocketConfig`类来启用WebSocket支持。接着,创建`WebSocketTest`服务器类,处理连接、消息收发及错误等事件,并使用`ConcurrentHashMap`管理用户连接。最后,前端通过JavaScript建立WebSocket连接,监听消息并进行相应处理。此方案适用于需要实时通信的应用场景,如聊天室、通知系统等。
|
5月前
|
开发框架 前端开发 网络协议
Spring Boot结合Netty和WebSocket,实现后台向前端实时推送信息
【10月更文挑战第18天】 在现代互联网应用中,实时通信变得越来越重要。WebSocket作为一种在单个TCP连接上进行全双工通信的协议,为客户端和服务器之间的实时数据传输提供了一种高效的解决方案。Netty作为一个高性能、事件驱动的NIO框架,它基于Java NIO实现了异步和事件驱动的网络应用程序。Spring Boot是一个基于Spring框架的微服务开发框架,它提供了许多开箱即用的功能和简化配置的机制。本文将详细介绍如何使用Spring Boot集成Netty和WebSocket,实现后台向前端推送信息的功能。
1234 1
|
5月前
|
前端开发 Java C++
RSocket vs WebSocket:Spring Boot 3.3 中的两大实时通信利器
本文介绍了在 Spring Boot 3.3 中使用 RSocket 和 WebSocket 实现实时通信的方法。RSocket 是一种高效的网络通信协议,支持多种通信模式,适用于微服务和流式数据传输。WebSocket 则是一种标准协议,支持全双工通信,适合实时数据更新场景。文章通过一个完整的示例,展示了如何配置项目、实现前后端交互和消息传递,并提供了详细的代码示例。通过这些技术,可以大幅提升系统的响应速度和处理效率。
|
5月前
|
监控 小程序 前端开发
小程序全栈开发中的WebSocket实时通信实践
【10月更文挑战第3天】随着移动互联网的发展,小程序因便捷的用户体验和社交传播能力,成为企业拓展业务的新渠道。本文探讨了小程序全栈开发中的WebSocket实时通信实践,包括其实时通信、长连接及双向通信的特点,并通过实时聊天、推送、游戏和监控等功能的实现,展示了WebSocket在小程序中的应用。开发者需注意安全性、性能及兼容性等问题,以保障小程序的稳定运行和用户体验。
99 7
|
7月前
|
数据处理 开发者 监控
揭秘实时Web应用开发:WebSocket与Akka Streams如何让Play Framework如虎添翼?
【8月更文挑战第31天】实时Web应用需求日益增长,覆盖了从即时通讯到在线游戏等多个领域。Play Framework结合WebSocket与Akka Streams,简化了高效实时应用的开发。WebSocket提供全双工通信,使服务器能主动向客户端推送消息;Akka Streams支持声明式数据流处理,有效避免系统因数据处理不及时而崩溃。本文通过示例代码展示了如何利用这些技术构建实时股票报价系统,展现了其在实时数据处理方面的强大能力。掌握这一技术组合,将大幅提升你在实时Web应用开发中的效率与稳定性。
80 0
|
7月前
|
开发框架 网络协议 Java
SpringBoot WebSocket大揭秘:实时通信、高效协作,一文让你彻底解锁!
【8月更文挑战第25天】本文介绍如何在SpringBoot项目中集成WebSocket以实现客户端与服务端的实时通信。首先概述了WebSocket的基本原理及其优势,接着详细阐述了集成步骤:添加依赖、配置WebSocket、定义WebSocket接口及进行测试。通过示例代码展示了整个过程,旨在帮助开发者更好地理解和应用这一技术。
533 1
|
7月前
|
JavaScript 前端开发 网络协议
WebSocket在Java Spring Boot+Vue框架中实现消息推送功能
在现代Web应用中,实时消息提醒是一项非常重要的功能,能够极大地提升用户体验。WebSocket作为一种在单个TCP连接上进行全双工通信的协议,为实现实时消息提醒提供了高效且低延迟的解决方案。本文将详细介绍如何在Java Spring Boot后端和Vue前端框架中利用WebSocket实现消息提醒功能。
322 0
|
7月前
|
小程序 Java API
springboot 微信小程序整合websocket,实现发送提醒消息
springboot 微信小程序整合websocket,实现发送提醒消息