学习记录:在Spring boot中简单实现一个WebSocket Demo

背景

之前在项目里用过WebSocket做过"用户下单实时通知"的功能,但最近回过头来看,竟然完全没有一点印象。仔细一想,原因也很简单:当初只是看别人写的代码,自己真正动手实践较少。结果就是,学了很久,看似懂了,真正落到代码上却无从下手。

于是决定重新审视自己,将学习的脚印发布在网上(也是犹豫了很久才决定的,因为之前总觉得发博客必须是技术大佬才行,自己什么都不懂的菜鸡发出来的东西会污染了大伙的眼睛),现在虽然也会这么想,但是更多的是挑战和记录自己了------记录学习的过程,未来回头看也有迹可循,而不是一片模糊的回忆。人生嘛,不就是不断体验吗?既然想写,就从现在开始😋

所以,就从一个简单的WebSocket Demo开始,正式开启我的博客之旅。

一、简单理解

WebSocket是一种网络协议。它实现了服务端与客户端全双工通信,使得服务端可以主动向客户端推送消息,这适用于需要实时更新的场景,比如用户下单提醒、医疗看板数据实时刷新等。

二、实现步骤

1、导入依赖

创建一个Spring Boot项目,并在pom.xml中导入相关依赖

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

2、定义配置类

java 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebSocketConfiguration {

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

说明:关于为什么要定义配置类,我查找了网上一些资料,简单来说就是通过这个配置spring boot才能去扫描关于websocket的注解。

由于WebSocket的@ServerEndpoint注解属于Java EE标准,并不是Spring本身的组件,Spring Boot不会自动识别。

为了让Spring Boot能够扫描并注册这些WebSocket端点,我们需要声明ServerEndpointExporter这个 Bean。

它的作用就是负责把所有@ServerEndpoint类注册到容器中,否则WebSocket服务将无法被访问。

3、开发服务端组件

java 复制代码
import jakarta.websocket.*;
import org.springframework.stereotype.Component;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * WebSocket服务
 */
@Component
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {

    //存放会话对象
    private static Map<String, Session> sessionMap = new HashMap<>();

    /**
     * 连接建立成功调用的方法,并存储当前会话对象
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("sid") String sid) {
        System.out.println("客户端:" + sid + "建立连接");
        sessionMap.put(sid, session);
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message, @PathParam("sid") String sid) {
        System.out.println("收到来自客户端:" + sid + "的信息:" + message);
    }

    /**
     * 连接关闭调用的方法
     *
     * @param sid
     */
    @OnClose
    public void onClose(@PathParam("sid") String sid) {
        System.out.println("连接断开:" + sid);
        sessionMap.remove(sid);
    }

    @OnError
    public void onError(Session session, Throwable error, @PathParam("sid") String sid) {
        System.out.println("客户端:" + sid + "发生错误");
        error.printStackTrace();
    }

    /**
     * 群发
     *
     * @param message
     */
    public void sendToAllClient(String message) {
        Collection<Session> sessions = sessionMap.values();
        for (Session session : sessions) {
            try {
                //服务器向客户端发送消息
                session.getBasicRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

}

@ServerEndpoint:表示此类是一个WebSocket端点,value属性是必须的,用于设置路由。可以类比@RequestMapping这样就好理解多了。

@OnOpen:连接建立时触发,用于监听客户端的连接事件。可在这里保存session。

@OnMessage:收到消息时触发,用于监听客户端消息事件。类似Controller的"处理请求方法",但它处理的是WebSocket消息。

@OnClose:连接关闭时触发,用于处理连接断开事件。可在此移除session、释放资源。

@OnError:发生异常时触发,用于处理异常事件。该方法必须要有一个Throwable类型的参数,表示发生的异常。在这个方法中可以打印错误日志,清除错误会话。

4、前端页面(可选)

html 复制代码
<!DOCTYPE HTML>
<html>
<head>
    <meta charset="UTF-8">
    <title>WebSocket Demo</title>
</head>
<body>
    <input id="text" type="text" />
    <button onclick="send()">发送消息</button>
    <button onclick="closeWebSocket()">关闭连接</button>
    <div id="message">
    </div>
</body>
<script type="text/javascript">
    var websocket = null;
    var clientId = Math.random().toString(36).substr(2);

    //判断当前浏览器是否支持WebSocket
    if('WebSocket' in window){
        //连接WebSocket节点
        websocket = new WebSocket("ws://localhost:8080/ws/"+clientId);
    }
    else{
        alert('Not support websocket')
    }

    //连接发生错误的回调方法
    websocket.onerror = function(){
        setMessageInnerHTML("error");
    };

    //连接成功建立的回调方法
    websocket.onopen = function(){
        setMessageInnerHTML("连接成功");
    }

    //接收到消息的回调方法
    websocket.onmessage = function(event){
        setMessageInnerHTML(event.data);
    }

    //连接关闭的回调方法
    websocket.onclose = function(){
        setMessageInnerHTML("close");
    }

    //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function(){
        websocket.close();
    }

    //将消息显示在网页上
    function setMessageInnerHTML(innerHTML){
        document.getElementById('message').innerHTML += innerHTML + '<br/>';
    }

    //发送消息
    function send(){
        var message = document.getElementById('text').value;
        websocket.send(message);
    }
	
	//关闭连接
    function closeWebSocket() {
        websocket.close();
    }
</script>
</html>

注意:这里主要注意new WebSocket("ws://localhost:8080/ws/"+clientId),这是WebSocket对象的初始化,指定要连接服务器的地址。

三、测试结果

1️⃣启动服务器,并打开websocket.html进行双端连接

2️⃣客户端向服务器发送消息

3️⃣服务器向客户端发送消息

定义定时任务类,定时向客户端推送数据

java 复制代码
import com.sky.websocket.WebSocketServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Component
public class WebSocketTask {
    @Autowired
    private WebSocketServer webSocketServer;

    /**
     * 通过WebSocket每隔5秒向客户端发送消息
     */
    @Scheduled(cron = "0/5 * * * * ?")
    public void sendMessageToClient() {
        webSocketServer.sendToAllClient("这是来自服务端的消息:" + DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now()));
    }
}

注意要在启动类添加注解@EnableScheduling开启任务调度

此外还可以在其他接口中使用webSocketServer.sendToAllClient(),这样就是可以实时推送。比如在订单下单功能中调用此方法,就可以在用户下单后实时通知商家。

四、问题发现

上述的代码演示中,部分都是以往项目中使用到的。现在来看,确实存在一些问题。

问题描述

在一个商城交易系统中,假设有两个商家同时登录了浏览器,这时每个商家都会与服务器建立一个WebSocket连接,用于接收实时消息。当用户访问商家A的店铺并进行下单时,前端会调用下单接口完成订单创建。为了实现实时提醒功能,服务端希望通过WebSocket将"新订单"消息发送给商家A。然而,如果直接使用群发的方式,系统会把消息发送给所有在线商家,包括商家 B,这显然是业务上的错误。

因此,需要改成单发,即只发送给商家A。但问题来了:在用户下单的接口中,如何获取商家A的WebSocket session ID,从而精确地将消息发送给对应商家?

思考

单发需要WebSocket session ID,在下单功能中我如何获取到这个id标识?先思考在用户下单功能中我们能获得什么,能否从现有的资源中解决这个问题。我们可以得到商家的id,那么这个商家id是否可以作为商家与服务器进行WebSocket连接的session ID呢?

答案明了,在上述前端页面中,我们使用随机字符串clientId来作为session ID。那么我们可以进行业务规定,把clientId换成商户ID(merchantId),就能让WebSocket连接与业务绑定,这样在下单接口就能找到对应的WebSocket Session,然后只给那个商户推送消息。

伪代码

前端:

js 复制代码
//规定如下
var merchantId = "商户登录成功后的真实ID";
websocket = new WebSocket("ws://localhost:8080/ws/" + merchantId);

后端:

java 复制代码
/**
 * WebSocket服务
 */
@ServerEndpoint("/ws/{merchantId}")
public class WebSocketServer {
    
    //存放商户与服务器会话对象
    private static Map<String, Session> merchantSessions = new ConcurrentHashMap<>();
    
    /**
     * 连接建立成功调用的方法,并存储当前会话对象
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("merchantId") String merchantId) {
        merchantSessions.put(merchantId, session);
    }
    
    //其他方法...

    //单发
    public void sendToMerchant(String merchantId, String message) {
        Session session = merchantSessions.get(merchantId);
        if (session != null && session.isOpen()) {
            try {
                session.getBasicRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
java 复制代码
/**
 * 下单接口调用示例
 */
@PostMapping("/order/create")
public void createOrder(@RequestBody Order order) {
    // 创建订单....

    WebSocketServer.sendToMerchant(order.getMerchantId(), "您有新订单!");
}

这样在用户下单接口中就能根据商户ID找到对应的WebSocket会话,从而实现定向推送。

五、总结

整篇文章写下来,感觉又回到了当初一开始写项目的时候。磕磕碰碰,碰到困难时心烦意乱,解决问题时又上蹿下跳🥲,温故而知新,通过对WebSocket的回顾,进一步理解了如何实现用户下单实时通知功能。同时也极大的鼓舞了自信,原来认真投入写一篇博客可以带来这么多的正反馈,当然也有可能是三分钟热度哈😁。总之敢于记录自己,敢于分享自己,敢于尝试不同的体验。

最后: 本文仅作为学习记录分享使用,内容基于作者当前技术认知整理,若有不准确或描述不清之处,欢迎评论区指正,我会在能力范围内修正完善,谢谢阅读!🙏

参考

1、在 Spring Boot 中整合、使用 WebSocket - spring 中文网

2、万字详解,带你彻底掌握 WebSocket 用法(至尊典藏版)写的不错-CSDN博客

相关推荐
CesareCheung5 小时前
JMeter 进行 WebSocket 接口压测
python·websocket·jmeter
interception6 小时前
爬虫逆向:websocket实战案例,全国建筑市场
爬虫·websocket·网络协议
i_am_a_div_日积月累_6 小时前
websocket设置和断开机制
网络·websocket·网络协议
Jennifer33K8 小时前
WebSocket!!
网络·websocket·网络协议
寂寞旅行9 小时前
Nginx配置WSS安全WebSocket代理
websocket·nginx·安全
z***39621 天前
Nginx中如何配置WebSocket代理?
运维·websocket·nginx
j***48542 天前
Node.js实现WebSocket教程
websocket·网络协议·node.js
Ace_31750887762 天前
拼多多关键字搜索接口逆向:从 WebSocket 实时推送解析到商品数据结构化重建
数据结构·websocket·网络协议
v***43172 天前
Nginx WebSocket 长连接及数据容量配置
运维·websocket·nginx