SpringBoot快速入门WebSocket(​​JSR-356附Demo源码)

news2025/5/12 23:43:47

现在我想写一篇Java快速入门WebSocket,就使用 JSR-356的websocket,我想分以下几点, 

1. websocket介绍, 

1.1 介绍   

什么是WebSocket?​

WebSocket 是一种基于 ​​TCP​​ 的​​全双工通信协议​​,允许客户端和服务器在​​单个长连接​​上实时交换数据。它是 HTML5 规范的一部分,通过 ​​JSR-356(Java API for WebSocket)​​ 在 Java 中标准化。

​核心特点​​:

  • ​双向通信​​:客户端和服务器可以主动发送消息。
  • ​低延迟​​:无需频繁建立/断开连接(HTTP的“握手”仅一次)。
  • ​轻量级​​:数据帧(Frame)结构比 HTTP 更高效。

因为是双向通信,因此WebSocket十分适合用于服务端与客户端需要实时通信的场景,如聊天室,游戏,

1.2 他与http有什么不同  

​特性​​WebSocket​​HTTP​
​连接模型​长连接(持久化)短连接(请求-响应后关闭)
​通信方向​全双工(双向实时通信)半双工(客户端主动发起请求)
​协议头​ws:// 或 wss://(加密)http:// 或 https://
​握手过程​首次通过 HTTP 升级协议,之后独立通信每次请求都需完整 HTTP 头
​适用场景​实时聊天、股票行情、游戏同步网页浏览、API 调用
​数据格式​支持二进制帧和文本帧通常是文本(JSON/XML/HTML)

​关键区别示例​​:

  • ​HTTP​​:如果客户端与服务端需要实时通信,由于http需要发起请求才能获得响应,而不能直接获取服务端的消息, 客户端不断轮询服务器(如每秒请求一次) → 高延迟、高负载。
  • ​WebSocket​​:建立一次连接,服务器可随时推送数据 → 实时性强、资源占用低。

2. 代码实战   

2.0 WebSocket 核心事件介绍

websocket主要有onOpen,onMessage,onError,onClose四种事件,由于是双向通信,所以不论是前端还是后端,都需要对这四种事件进行处理

websocket建立连接称之为握手,在握手成功后,才可以互通消息

事件名称触发时机前端用途后端用途备注
​onOpen​当WebSocket连接成功建立时(握手完成)1. 更新连接状态UI
2. 准备发送初始消息
1. 记录连接日志
2. 初始化会话数据
3. 将新连接加入连接池
前端和后端都会在连接建立后立即触发
​onMessage​当收到对方发送的消息时1. 处理服务器推送的数据
2. 更新页面内容
3. 触发业务逻辑
1. 处理客户端请求
2. 广播消息给其他客户端
3. 执行业务逻辑
可以处理文本和二进制数据
​onError​当连接发生错误时1. 显示错误提示
2. 尝试自动重连
3. 记录错误日志
1. 记录错误信息
2. 清理异常连接
3. 发送警报通知
错误可能来自网络问题或程序异常
​onClose​当连接关闭时1. 更新连接状态UI
2. 显示断开原因
3. 决定是否重连
1. 清理会话资源
2. 从连接池移除
3. 记录断开日志
可能是主动关闭或被动断开

同时后端还有一个较为核心的概念 session 你可以将其理解为双端之间的连接

由于在后端会同时存在多个与客户端的连接(来自不同客户端) ,后端发送消息时候,需要去获取到对应的session,才能将消息发送到指定的客户端

2.1 环境准备

  • JDK 8+​​(JSR-356 需要 Java EE 7 或 Jakarta EE 8)
  • ​支持 WebSocket 的服务器​​(如 Tomcat 9+、Jetty 9+、WildFly)
  • ​Maven/Gradle 依赖​​(以 Tomcat 为例):
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

2.2编写后端代码

后端代码中有一些您可能当前看的比较疑惑,但是后续我会讲,主要先关注websocket的核心事件即可

1.编写ServerEndpoint

ServerEndpoint,他可以类比于SpringMVC中的Controller, 在括弧中的字符串即为websocket通讯的地址,不同于Controller的是

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;

@ServerEndpoint("/test_path/websocket/{userId}/{channel}")
@Component
public class WebSocketServer {
    private Long userId;
    // 静态变量保存SessionMap
    private final static ConcurrentHashMap<Long, Session> sessions = new ConcurrentHashMap<>();

    @Autowired testController testController;


    private static testController testController2;
    @Autowired
    public void setMyService(testController controller) {
        WebSocketServer.testController2 = controller; // 静态变量中转
    }

    @OnOpen
    public void onOpen(Session session,
                       @PathParam("userId") Long userId,
                       @PathParam("channel") String channel){
        System.out.println(testController);
        System.out.println(testController2);
        this.userId = userId;
        System.out.println("连接已经建立: id="+userId+" channel="+channel);
        addSession(userId,session);
    }

    @OnClose
    public void onClose(Session session){
        System.out.println("连接关闭了: id="+ userId);
        removeSession(userId);
    }

    @OnMessage
    public void onMessage(String message,Session session){
        System.out.println(message);
        try {
            session.getBasicRemote().sendText("你传来的消息是"+message);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    // 添加Session
    public void addSession(Long userId, Session session) {
        sessions.put(userId, session);
    }

    // 移除Session
    public static void removeSession(Long userId) {
        sessions.remove(userId);
    }

    // 获取Session
    public static Session getSession(Long userId) {
        return sessions.get(userId);
    }

    // 向指定用户发送消息
    public static void sendMessageToUser(Long userId, String message) throws IOException {
        Session session = sessions.get(userId);
        if (session != null && session.isOpen()) {
            session.getBasicRemote().sendText(message);
        }
    }

    // 广播消息给所有用户
    public static void broadcast(String message) {
        sessions.forEach((id, session) -> {
            try {
                if (session.isOpen()) {
                    session.getBasicRemote().sendText(message);
                }
            } catch (IOException e) {
                removeSession(id); // 发送失败时移除失效session
            }
        });
    }
}

其中 @ServerEndpoint注解的类下的 @OnOpen,@OnClose,@OnMessage,@OnError会被自动识别,客户端一旦连接,发送消息,关闭等,会自动触发对应的方法

@OnMessage可以在多个方法上标注,但是需要传参类型不同,消息进来后会自动进入对应参数的方法(类似于方法的多个重写,需要参数不同)

这里由于客户端与服务端之间的操作主要由session完成,我通过userId将session存进了map

2.编写WebSocketConfig  

配置文件中, ServerEndpointExporter是最重要的,它不是 WebSocket 容器本身​​,而是 ​​Spring 与 WebSocket 容器之间的桥梁​​。它的核心职责是让 Spring 能感知并管理标准 JSR-356(Java WebSocket API)定义的端点。

在 Spring 中扫描 @ServerEndpoint类, 并向 WebSocket 容器注册这些端点


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

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

2.3 编写前端代码 

前端通过websocket与服务端连接的方法非常简单,只需要

new WebSocket(服务端路径);

一旦连接成功,连接会一直存在,不会断开,直至一方主动断开,这样中途通讯不需要新建立连接

前端代码一样需要实现onopen,onmessage,onerror,onclose

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>WebSocket 消息通信</title>
    <style>
        #content {
            width: 500px;
            height: 300px;
            border: 1px solid #ccc;
            padding: 10px;
            overflow-y: auto;
            font-family: Arial, sans-serif;
        }
        .message {
            margin: 5px 0;
            padding: 8px;
            border-radius: 5px;
            max-width: 70%;
            word-wrap: break-word;
        }
        .sent {
            background: #e3f2fd;
            margin-left: auto;
            text-align: right;
        }
        .received {
            background: #f1f1f1;
            margin-right: auto;
            text-align: left;
        }
        #text {
            width: 400px;
            padding: 8px;
        }
        #button {
            padding: 8px 15px;
            background: #4CAF50;
            color: white;
            border: none;
            cursor: pointer;
        }
    </style>
</head>
<body>
<div id="content"></div>
<input type="text" id="text" placeholder="输入要发送的消息">
<input type="button" id="button" value="发送">
</body>
</html>
<script>
    // 随机生成用户ID (1-10000)
    function generateRandomId() {
        return Math.floor(Math.random() * 10000) + 1;
    }
    const channels = ["pc", "Android", "ios"];

    // 从数组中随机选择一个channel
    function getRandomChannel() {
        return channels[Math.floor(Math.random() * channels.length)];
    }

    let socket;
    const contentDiv = document.getElementById('content');

    // 在content div中追加消息
    function appendMessage(text, isSent) {
        const messageDiv = document.createElement('div');
        messageDiv.className = `message ${isSent ? 'sent' : 'received'}`;
        messageDiv.textContent = text;
        contentDiv.appendChild(messageDiv);
        contentDiv.scrollTop = contentDiv.scrollHeight; // 自动滚动到底部
    }

    // 建立WebSocket连接
    function connectWebSocket() {
        const userId = generateRandomId();
        const channel = getRandomChannel();

        // 构建带参数的WebSocket URL
        const wsUrl = `ws://localhost:8080/test_path/websocket/${userId}/${channel}`;
        console.log(`连接参数: userId=${userId}, channel=${channel}`);
        appendMessage(`系统: 连接建立中 (用户ID: ${userId}, 设备: ${channel})`, false);

        socket = new WebSocket(wsUrl);

        socket.onopen = () => {
            appendMessage('系统: WebSocket连接已建立', false);
        };

        socket.onmessage = (event) => {
            appendMessage(`服务器: ${event.data}`, false);
        };

        socket.onerror = (error) => {
            appendMessage(`系统错误: ${error.message}`, false);
        };

        socket.onclose = () => {
            appendMessage('系统: 连接已关闭', false);
        };
    }

    // 发送消息函数
    function sendMessage() {
        const message = document.getElementById('text').value.trim();
        if (!message) {
            alert('请输入要发送的消息');
            return;
        }

        if (socket && socket.readyState === WebSocket.OPEN) {
            socket.send(message);
            appendMessage(`我: ${message}`, true);
            document.getElementById('text').value = '';
        } else {
            appendMessage('系统: 连接未准备好,请稍后再试', false);
        }
    }

    // 页面初始化
    window.onload = function() {
        connectWebSocket();

        // 按钮点击事件
        document.getElementById('button').addEventListener('click', sendMessage);

        // 回车键发送
        document.getElementById('text').addEventListener('keypress', function(e) {
            if (e.key === 'Enter') {
                sendMessage();
            }
        });
    };
</script>

2.4 额外测试代码

写一个Controller来主动向前端发送消息, 其中WebSocketServer中调用的静态方法

@RestController
public class testController {

    @PostMapping("/testPush")
    public void testPush(String text,Long userId) throws IOException {
        WebSocketServer.sendMessageToUser(userId,text);
    }

    @PostMapping("/testBroadcast")
    public void testBroadcast(String text) throws IOException {
        WebSocketServer.broadcast(text);
    }
}

在@ServerEndpoint类中, 我们尝试一下注入其他的Bean

public class WebSocketServer { 
    // .......
    @Autowired testController testController;

    private static testController testController2;

    @Autowired
    public void setMyService(testController controller) {
         WebSocketServer.testController2 = controller; // 静态变量中转
    }
    // 在onOpen中来测试一下
    @OnOpen
    public void onOpen(Session session,
                       @PathParam("userId") Long userId,
                       @PathParam("channel") String channel){
        System.out.println(testController);
        System.out.println(testController2);
        this.userId = userId;
        System.out.println("连接已经建立: id="+userId+" channel="+channel);
        addSession(userId,session);
    }
}

3.测试结果

服务端发送至客户端的消息将呈现在左侧,而客户端的消息将呈现在右侧

3.1 握手

启动项目,在打开前端页面时,会随机出id与channel,并自动连接服务端, 可以清晰的见到发起的握手请求 

同时通过服务端控制台可以看到,直接@autowire注入的Controller失败了,而静态变量注入的成功了

3.2 发送消息

在服务端的onMessage接收到消息后,代码中直接使用session向客户端发送了一条收到xx消息的推送,可以看到成功通信了

我们再来试一试从Controller中获取到session,主动从服务端向客户端发送消息呢

可以看到获取到了指定的session,然后发送至了指定的客户端了

4.本人写的时候的疑惑

4.1ServerEndpointExporter的作用

ServerEndpointExporter 是 Spring 整合标准 WebSocket(JSR-356)的关键桥梁,它相当于 WebSocket 版的 "路由注册器"它的存在解决了以下核心问题:

端点注册:将 @ServerEndpoint 类暴露给 WebSocket 容器 生态整合:让非 Spring 管理的 WebSocket 实例能使用部分Spring功能

没有它,@ServerEndpoint 就只是一个普通的注解,不会产生任何实际效果。

ServerEndpointExporter 可以让@ServerEndpoint 类调用部分Spring的功能

        如通过静态变量获取 Bean....... 其余请自行查阅

4.2为什么不能使用依赖注入

在Controller或者其他可能存在的bean中,为什么我不能通过@autowire 来注入被@ServerEndpoint注解的类呢? 在@ServerEndpoint注解的类中,又为什么不能使用@autowire注入其他bean呢

即使加了 @Component 注解,@ServerEndpoint 类也不会被 Spring 完全管理,这是由 WebSocket 的实现机制决定的。以下是关键点解析:

根本原因:双重生命周期管理 JSR-356(标准 WebSocket)和 Spring 是两套独立的规范。

@ServerEndpoint 的实例化由 WebSocket 容器(如 Tomcat)创建和管理,不是通过 Spring 容器创建的。

@Component 的局限性

虽然加了 @Component,但 Spring 只会将其注册为 Bean,不会接管它的生命周期,因此: Spring 的依赖注入(如 @Autowired)不会自动生效 Spring AOP、@PostConstruct 等 Spring 特性无法使用

 5.源码分享

Gitee: LiJing/websocketDemo

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2374268.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【安装配置教程】ubuntu安装配置Kodbox

目录 一、引言 二、环境配置 1. 服务器配置​ 2. 必备组件​ 三、安装基础环境​ 1. 安装 PHP 8.1 及扩展​ 2. 安装 MySQL 数据库 3.安装 Redis&#xff08;可选&#xff0c;提升缓存性能&#xff09; 4. 配置nginx文件 4.1. 创建 Kodbox 站点目录​ 4.2. 编写 Ng…

LabVIEW车牌自动识别系统

在智能交通快速发展的时代&#xff0c;车牌自动识别系统成为提升交通管理效率的关键技术。本案例详细介绍了基于 LabVIEW 平台&#xff0c;搭配大恒品牌相机构建的车牌自动识别系统&#xff0c;该系统在多个场景中发挥着重要作用&#xff0c;为交通管理提供了高效、精准的解决方…

c语言第一个小游戏:贪吃蛇小游戏01

hello啊大家好 今天我们用一个小游戏来增强我们的c语言&#xff01; 那就是贪吃蛇 为什么要做一个贪吃蛇小游戏呢&#xff1f; 因为这个小游戏所涉及到的知识有c语言的指针、数组、链表、函数等等可以让我们通过这个游戏来巩固c语言&#xff0c;进一步认识c语言。 一.我们先…

[项目总结] 抽奖系统项目技术应用总结

&#x1f338;个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 &#x1f3f5;️热门专栏: &#x1f9ca; Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm1001.2014.3001.5482 &#x1f355; Collection与…

机器视觉的平板电脑屏幕组件覆膜应用

在现代智能制造业中&#xff0c;平板电脑屏幕组件覆膜工序是确保产品外观和功能完整性的重要环节。随着技术的进步&#xff0c;传统的覆膜方式已经无法满足高速度、高精度的生产需求。而MasterAlign视觉系统的出现&#xff0c;将传统覆膜工艺转变为智能化、自动化的生产流程。在…

更换内存条会影响电脑的IP地址吗?——全面解析

在日常电脑维护和升级过程中&#xff0c;许多用户都会遇到需要更换内存条的情况。与此同时&#xff0c;不少用户也担心硬件更换是否会影响电脑的网络配置&#xff0c;特别是IP地址的设置。本文将详细探讨更换内存条与IP地址之间的关系&#xff0c;帮助读者理解这两者之间的本质…

VMware安装CentOS Stream10

文章目录 安装下载iso文件vmware安装CentOS Stream创建新虚拟机安装CentOS Stream10 安装 下载iso文件 官方地址&#xff1a;跳转链接 vmware安装CentOS Stream 创建新虚拟机 参考以下步骤 安装CentOS Stream10 指定ISO文件 开启虚拟机选择Install CentOS Stream 10 鼠…

基于Dify实现对Excel的数据分析

在dify部署完成后&#xff0c;大家就可以基于此进行各种应用场景建设&#xff0c;目前dify支持聊天助手&#xff08;包括对话工作流&#xff09;、工作流、agent等模式的场景建设&#xff0c;我们在日常工作中经常会遇到各种各样的数据清洗、格式转换处理、数据统计成图等数据分…

资产月报怎么填?资产月报填报指南

资产月报是企业对固定资产进行定期检查和管理的重要工具&#xff0c;它能够帮助管理者了解资产的使用情况、维护状况和财务状况&#xff0c;从而为资产的优化配置和决策提供依据。填写资产月报时&#xff0c;除了填报内容外&#xff0c;还需要注意格式的规范性和数据的准确性。…

MIT XV6 - 1.3 Lab: Xv6 and Unix utilities - primes

接上文 MIT XV6 - 1.2 Lab: Xv6 and Unix utilities - pingpong primes 继续实验&#xff0c;实验介绍和要求如下 (原文链接 译文链接) : Write a concurrent prime sieve program for xv6 using pipes and the design illustrated in the picture halfway down this page and…

从前端视角看网络协议的演进

别再让才华被埋没&#xff0c;别再让github 项目蒙尘&#xff01;github star 请点击 GitHub 在线专业服务直通车GitHub赋能精灵 - 艾米莉&#xff0c;立即加入这场席卷全球开发者的星光革命&#xff01;若你有快速提升github Star github 加星数的需求&#xff0c;访问taimili…

Docker中运行的Chrome崩溃问题解决

问题 各位看官是否在 Docker 容器中的 Linux 桌面环境&#xff08;如Xfce&#xff09;上启动Chrome &#xff0c;遇到了令人沮丧的频繁崩溃问题&#xff1f;尤其是在打开包含图片、视频的网页&#xff0c;或者进行一些稍复杂的操作时&#xff0c;窗口突然消失&#xff1f;如果…

【沉浸式求职学习day36】【初识Maven】

沉浸式求职学习 Maven1. Maven项目架构管理工具2.下载安装Maven3.利用Tomcat和Maven进入一个网站 Maven 为什么要学习这个技术&#xff1f; 在Java Web开发中&#xff0c;需要使用大量的jar包&#xff0c;我们手动去导入&#xff0c;这种操作很麻烦&#xff0c;PASS&#xff01…

【音视频工具】MP4BOX使用

这里写目录标题 使用介绍 使用 下面这个网站直接使用&#xff1a; MP4Box.js - JavaScript MP4 Reader/Fragmenter (gpac.github.io) 介绍 MMP4Box 是 GPAC 项目开发的一款命令行工具&#xff0c;专门用于处理 MP4 格式多媒体文件&#xff0c;也可操作 AVI、MPG、TS 等格…

Linux中常见开发工具简单介绍

目录 apt/yum 介绍 常用命令 install remove list vim 介绍 常用模式 命令模式 插入模式 批量操作 底行模式 模式替换图 vim的配置文件 gcc/g 介绍 处理过程 预处理 编译 汇编 链接 库 静态库 动态库&#xff08;共享库&#xff09; make/Makefile …

flow-matching 之学习matcha-tts cosyvoice

文章目录 matcha 实现cosyvoice 实现chunk_fmchunk_maskcache_attn stream token2wav 关于flow-matching 很好的原理性解释文章&#xff0c; 值得仔细读&#xff0c;多读几遍&#xff0c;关于文章Flow Straight and Fast: Learning to Generate and Transfer Data with Rectifi…

ubuntu22.04在 Docker容器中安装 ROS2-Humble

22.04 安装 docker 容器并实现rviz功能 1 docker pull命令拉取包含ROS-Humble的镜像&#xff1a; docker pull osrf/ros:humble-desktop-full-jammy docker images验证该镜像是否拉取成功。 使用镜像osrf/ros:humble-desktop-full-jammy创建并运行容器 sudo docker run -it…

【JavaWeb+后端常用部件】

回顾内容看&#xff1a; 一、获取请求参数的方法 参考&#xff1a;[JavaWeb]——获取请求参数的方式(全面&#xff01;&#xff01;&#xff01;)_java 获取请求参数-CSDN博客 Json格式的Body加备注RequestBody{id}动态路径加备注PathVariableid?&name?直接接收就好 i…

Redis 重回开源怀抱:开源精神的回归与未来展望

在开源软件的广袤天地里&#xff0c;Redis 一直是备受瞩目的明星项目。近期&#xff0c;Redis 宣布重新回归开源&#xff0c;这一消息犹如一颗石子投入平静的湖面&#xff0c;在技术社区激起层层涟漪。今天&#xff0c;就让我们深入了解 Redis 这一重大转变背后的故事、意义以及…

弹窗表单的使用,基于element-ui二次封装

el-dialog-form 介绍 基于element-ui封装的弹窗式表单组件 示例 git地址 https://gitee.com/chenfency/el-dialog-form.git 更新日志 2021-8-12 版本1.0.0 2021-8-17 优化组件&#xff0c;兼容element原组件所有Attributes及Events 2021-9-9 新增tip提示 安装教程 npm install …