SpringBoot快速入门WebSocket(JSR-356附Demo源码)
现在我想写一篇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;// 静态变量保存SessionMapprivate final static ConcurrentHashMap<Long, Session> sessions = new ConcurrentHashMap<>();@Autowired testController testController;private static testController testController2;@Autowiredpublic void setMyService(testController controller) {WebSocketServer.testController2 = controller; // 静态变量中转}@OnOpenpublic 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);}@OnClosepublic void onClose(Session session){System.out.println("连接关闭了: id="+ userId);removeSession(userId);}@OnMessagepublic void onMessage(String message,Session session){System.out.println(message);try {session.getBasicRemote().sendText("你传来的消息是"+message);} catch (IOException e) {throw new RuntimeException(e);}}// 添加Sessionpublic void addSession(Long userId, Session session) {sessions.put(userId, session);}// 移除Sessionpublic static void removeSession(Long userId) {sessions.remove(userId);}// 获取Sessionpublic 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 {@Beanpublic 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"];// 从数组中随机选择一个channelfunction 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 URLconst 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;@Autowiredpublic void setMyService(testController controller) {WebSocketServer.testController2 = controller; // 静态变量中转}// 在onOpen中来测试一下@OnOpenpublic 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
相关文章:

SpringBoot快速入门WebSocket(JSR-356附Demo源码)
现在我想写一篇Java快速入门WebSocket,就使用 JSR-356的websocket,我想分以下几点, 1. websocket介绍, 1.1 介绍 什么是WebSocket? WebSocket 是一种基于 TCP 的全双工通信协议,允许客户端和服务器在单个长连接上实…...
JDBC执行sql过程
1. 加载数据库驱动 JDBC 通过 驱动(Driver) 实现与不同数据库的通信。驱动需提前加载到 JVM: 手动加载(JDBC 4.0 前): Class.forName("com.mysql.cj.jdbc.Driver"); // MySQL 驱…...
VNC windows连接ubuntu桌面
✅ 步骤 1:安装 VNC 服务器 首先,我们需要在 Winux 系统上安装一个 VNC 服务器。这里我们使用 tigervnc 作为例子,它是一个常用的 VNC 服务器软件。 打开终端并更新你的软件包: sudo apt update安装 tigervnc 服务器:…...
CSS中的@import指令
一、什么是import指令? import 是CSS提供的一种引入外部样式表的方式,允许开发者在CSS文件中引入其他CSS文件,或者在HTML的<style>标签中引入外部样式。与常见的<link>标签相比,import 提供了一种更“CSS原生”的样式…...

【安装配置教程】ubuntu安装配置Kodbox
目录 一、引言 二、环境配置 1. 服务器配置 2. 必备组件 三、安装基础环境 1. 安装 PHP 8.1 及扩展 2. 安装 MySQL 数据库 3.安装 Redis(可选,提升缓存性能) 4. 配置nginx文件 4.1. 创建 Kodbox 站点目录 4.2. 编写 Ng…...
【软件设计师:数据库】13.数据库控制与安全
一、数据库语言SQL SQL是结构化查询语言(Structured Query Language)的缩写,其功能包括数据查询、数据操纵、数据定义和数据控制四个部分。 SQL 语言简洁、方便实用、功能齐全,已成为目前应用最广的关系数据库语言。SQL既是自含式语言(联机交互),又是嵌入式语言(宿主语…...

LabVIEW车牌自动识别系统
在智能交通快速发展的时代,车牌自动识别系统成为提升交通管理效率的关键技术。本案例详细介绍了基于 LabVIEW 平台,搭配大恒品牌相机构建的车牌自动识别系统,该系统在多个场景中发挥着重要作用,为交通管理提供了高效、精准的解决方…...
el-menu 折叠后小箭头不会消失
官方示例 <template><el-radio-group v-model"isCollapse" style"margin-bottom: 20px"><el-radio-button :value"false">expand</el-radio-button><el-radio-button :value"true">collapse</el-ra…...

c语言第一个小游戏:贪吃蛇小游戏01
hello啊大家好 今天我们用一个小游戏来增强我们的c语言! 那就是贪吃蛇 为什么要做一个贪吃蛇小游戏呢? 因为这个小游戏所涉及到的知识有c语言的指针、数组、链表、函数等等可以让我们通过这个游戏来巩固c语言,进一步认识c语言。 一.我们先…...
6. HTML 锚点链接与页面导航
在开发长页面或文档类网站时,锚点链接(Anchor Links)是一个非常实用的功能。通过学习 HTML 锚点技术,将会掌握如何在同一页面内实现快速跳转,以及如何优化长页面的导航体验。以下是基于给定素材的学习总结和实践心得 一、什么是锚点链接? 锚点链接(也称为页面内链接)允…...

[项目总结] 抽奖系统项目技术应用总结
🌸个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 🏵️热门专栏: 🧊 Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm1001.2014.3001.5482 🍕 Collection与…...
Axios替代品Alova
介绍alova | Alova.JS Multipart 实体请求 | Axios中文文档 | Axios中文网 1. 极致的轻量与性能 Tree-shaking优化:仅打包使用到的功能模块 零依赖:基础包仅 4KB(Axios 12KB) 2. 智能请求管理(开箱即用࿰…...
Python OpenCV性能优化与部署实战指南
在计算机视觉领域,OpenCV作为开源视觉库的标杆,其性能表现直接影响着从工业检测到AI模型推理的各类应用场景。本文结合最新技术趋势与生产实践,系统性梳理Python环境下OpenCV的性能优化策略与部署方案。 一、性能优化核心技术矩阵 1.1 内存…...
k8s的flannel生产实战与常见问题排查
关于 Kubernetes Flannel 插件的详细教程及生产环境实战指南,涵盖核心概念、安装配置、常见问题排查与优化策略 Flannel通信流程 一、Flannel 概述 Flannel 是 Kubernetes 最常用的 CNI(Container Network Interface)插件之一,…...
删除链表倒数第N个节点
Leetcode(19): 给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。 分析: 首要目标就是找到第N个节点的前一个节点,因为只有通过这个节点(cur)才可进行对…...
互联网大厂Java面试实录:Spring Boot与微服务架构在电商场景中的应用解析
💪🏻 1. Python基础专栏,基础知识一网打尽,9.9元买不了吃亏,买不了上当。 Python从入门到精通 😁 2. 毕业设计专栏,毕业季咱们不慌忙,几百款毕业设计等你选。 ❤️ 3. Python爬虫专栏…...
UGUI如何使用EventTrigger
前言 在 Unity 的 UGUI 系统中,EventTrigger 是一个强大的组件,允许开发者监听和处理多种 UI 交互事件。以下是详细的使用方法、示例代码、优缺点分析以及注意事项。 一、EventTrigger 基本用法 1. 添加 EventTrigger 组件 在 Unity 编辑器中选中 UI 对象(如 But…...
从代码学习深度学习 - 单发多框检测(SSD)PyTorch版
文章目录 前言工具函数数据处理工具 (`utils_for_data.py`)训练工具 (`utils_for_train.py`)检测相关工具 (`utils_for_detection.py`)可视化工具 (`utils_for_huitu.py`)模型类别预测层边界框预测层连接多尺度预测高和宽减半块基础网络块完整的模型训练模型读取数据集和初始化…...

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

更换内存条会影响电脑的IP地址吗?——全面解析
在日常电脑维护和升级过程中,许多用户都会遇到需要更换内存条的情况。与此同时,不少用户也担心硬件更换是否会影响电脑的网络配置,特别是IP地址的设置。本文将详细探讨更换内存条与IP地址之间的关系,帮助读者理解这两者之间的本质…...
SQLite数据库加密(Java语言、python语言)
1. 背景与需求 SQLite 是一种轻量级的关系型数据库,广泛应用于嵌入式设备、移动应用、桌面应用等场景。为了保护数据的隐私与安全,SQLite 提供了加密功能(通过 SQLCipher 扩展)。在 Java 中,可以使用 sqlite-jdbc 驱动与 SQLCipher 集成来实现 SQLite 数据库的加密。 本…...
RISC-V入门资料
以下是获取 RISC-V 相关资料的权威渠道和推荐资源,涵盖技术文档、开发工具、社区支持等: 1. 官方资料 RISC-V 国际基金会官网 https://riscv.org 核心文档:ISA 规范(包括基础指令集(RV32I/RV64I)、扩展指令…...
C++访问权限控制符
访问权限控制符 在C中,访问权限控制符是用来限制类或结构体成员(例如:变量、函数等)的访问级别的。C提供了三种访问权限级别: Public 访问权限: 公共成员可以在任何地方被访问,包括类的内部、…...

VMware安装CentOS Stream10
文章目录 安装下载iso文件vmware安装CentOS Stream创建新虚拟机安装CentOS Stream10 安装 下载iso文件 官方地址:跳转链接 vmware安装CentOS Stream 创建新虚拟机 参考以下步骤 安装CentOS Stream10 指定ISO文件 开启虚拟机选择Install CentOS Stream 10 鼠…...
互联网大厂Java求职面试:云原生与AI融合下的系统设计挑战-2
互联网大厂Java求职面试:云原生与AI融合下的系统设计挑战-2 第一轮提问:云原生架构选型与微服务治理 面试官(技术总监):郑薪苦,我们先从一个基础问题开始。你了解Spring Cloud和Kubernetes在微服务架构中…...

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

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

MIT XV6 - 1.3 Lab: Xv6 and Unix utilities - primes
接上文 MIT XV6 - 1.2 Lab: Xv6 and Unix utilities - pingpong primes 继续实验,实验介绍和要求如下 (原文链接 译文链接) : Write a concurrent prime sieve program for xv6 using pipes and the design illustrated in the picture halfway down this page and…...
Android学习总结之kotlin协程面试篇
一、协程基础概念与原理类真题 真题 1:协程是线程吗?为什么说它是轻量级的?(字节跳动 / 美团) 解答: 本质区别: 线程是操作系统调度的最小单位(内核态),协…...

从前端视角看网络协议的演进
别再让才华被埋没,别再让github 项目蒙尘!github star 请点击 GitHub 在线专业服务直通车GitHub赋能精灵 - 艾米莉,立即加入这场席卷全球开发者的星光革命!若你有快速提升github Star github 加星数的需求,访问taimili…...