SpringBoot 集成 WebSocket

ops/2024/9/20 3:59:28/ 标签: spring boot, websocket, 后端

前言

        最近在做一个 WebSocket 通信服务的软件,所以必须跟着学一学。

1、WebSocket 概述

        一般情况下,我们的服务器和服务器之间可以发送请求,但是服务器是不能向浏览器去发送请求的。因为设计之初并没有想到以后会出现服务端频繁向客户端发送请求的情况。

        全双工的通信协议(WebSocket 最大的特点是浏览器也可以往服务器发请求,服务器也可以向浏览器发请求)。

1.1、浏览器和服务器使用WebSocket通信流程

1. 浏览器发起http请求,请求建立 WebSocket 连接

这里的协议升级就是说,我想通过这个 http 连接去升级为 WebSocket 连接

2. 服务器响应统一协议更改

3. 相互发送数据

升级了协议之后浏览器就可以和服务器相互通信了: 

1.2、总结

  • WebSocket 协议是建立在 tcp 协议基础上的,所以不同语言也都支持 
  • tcp 协议是全双工协议,http 协议基于它是单向的
  • WebSocket 没有同源限制,所以前后端端口不一致也不影响信息的发送

2、Java 实现 WebSocket 的两种方式

2.1、基于注解实现WebSocket服务器端

服务终端类

  • @ServerEndpoint:监听连接(需要传递一个地址参数)
  • @OnOpen:连接成功
  • @OnClose:连接关闭
  • @OnMessage:收到消息

配置类

  • 把 Spring 中的 ServerEndpointExporter 对象注入进来

2.2.1、编写服务终端类 

// 监听哪些客户端来连接了WebSocket服务端
// 监听websocket地址 /myWs
@ServerEndpoint("/myWs")
@Component
@Slf4j
public class WebServerEndpoint {// 因为可能有多个客户端所以这里需要保证线程安全static Map<String,Session> sessionMap = new ConcurrentHashMap<>();// 建立连接时执行的操作@OnOpenpublic void onOpen(Session session){   // 每个websocket连接对于服务端来说都是一个SessionsessionMap.put(session.getId(),session);log.info("websocket is open");}/*** 收到客户端消息时执行的操作* @param text 接受到客户端的消息* @return 返回给客户端的消息*/@OnMessagepublic String onMessage(String text){log.info("收到一条新消息: " + text);return "收到 !";}// 连接关闭时执行的操作@OnClosepublic void onClose(Session session){sessionMap.remove(session.getId());log.info("websocket is close");}@Scheduled(fixedRate = 2000) // 每隔2s执行一次public static void sendMessage() throws IOException {for(String key: sessionMap.keySet()){ // 给所有客户端发送消息sessionMap.get(key).getBasicRemote().sendText("beat");}};
}

注意:这里监听的地址不可以是 "ws" 不然会报错,可能这是关键字吧,毕竟我们的协议就叫 ws 。

2.2.2、编写配置类

// 需要注入Bean的话必须声明为配置类
@Configuration
public class WebSocketConfig {@Beanpublic ServerEndpointExporter serverEndpointExporter(){return new ServerEndpointExporter();}
}

2.2、HTML + JS 实现客户端

 在 resources 目录下创建 static/ws-client.html

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>WebSocket Client</title>
</head>
<script>// 客户端和服务器连接的地址(我们服务端监听的地址)let ws = new WebSocket("ws://localhost:8080/myWs1")ws.onopen=function (){ // 连接打开的时候向服务器发送一条消息ws.send("hey man")}ws.onmessage=function (message) {console.log(message.data)}
</script>
<body>
<h1>WebSocket</h1>
</body>
</html>

测试:启动 SpringBoot 并访问  localhost:8080/ws-client.html

执行结果: 

2.3、基于 Spring 框架实现 WebSocket 服务器端

Spring 提供的类和框架

  • HttpSessionHandshakeInterceptor(抽象类):握手拦截器,在握手前后添加操作
  • AbstractWebSocketHandler(抽象类):WebSocket 处理程序,监听连接前、中、后
  • WebSocketConfigurer(接口):配置程序,比如配置监听哪个端口,配置自定义的握手拦截器,配置我们自定义的处理程序

2.3.1、编写握手拦截器类

/*** WebSocket 自定义握手拦截器*/
@Component
@Slf4j
public class MyInterceptor extends HttpSessionHandshakeInterceptor {@Overridepublic boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {log.info("握手前");log.info("远程地址 => " + request.getRemoteAddress());// 保留父类的操作return super.beforeHandshake(request, response, wsHandler, attributes);}@Overridepublic void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) {log.info("完成握手");// 完成握手后它就会把 HTTP 协议升级为 WebSocket 协议super.afterHandshake(request, response, wsHandler, ex);}
}

2.3.2、编写 WebSocket 处理程序

/*** WebSocket 自定义处理程序*/
@Slf4j
@Component
public class MyWsHandler extends AbstractWebSocketHandler {// WebSocketSession 对象可以封装一下吧用户的信息封装进去private static Map<String, SessionBean> sessionMap = new ConcurrentHashMap<>();// 线程安全的int值private static AtomicInteger clientIdMaker = new AtomicInteger(0);// 连接建立@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {super.afterConnectionEstablished(session);// 放在父方法调用之后SessionBean sessionBean = new SessionBean(session, clientIdMaker.getAndIncrement());sessionMap.put(session.getId(),sessionBean);log.info(sessionMap.get(session.getId()) + " connected");}// 收到消息@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {super.handleTextMessage(session, message);log.info(sessionMap.get(session.getId()).getClientId() + " : " + message.getPayload());}// 传输异常@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {super.handleTransportError(session, exception);// 如果异常就关闭 sessionif (session.isOpen())session.close();sessionMap.remove(session.getId());}// 连接关闭@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {super.afterConnectionClosed(session, status);log.info(sessionMap.get(session.getId()) + " closed");sessionMap.remove(session.getId());}@Scheduled(fixedRate = 2000) // 每隔2s执行一次public static void sendMessage() throws IOException {for(String key: sessionMap.keySet()){ // 给所有客户端发送消息sessionMap.get(key).getWebSocketSession().sendMessage(new TextMessage("beat"));}}
}

2.3.3、编写配置类

@Configuration
@EnableWebSocket    // 启用 spring 提供的 websocket 功能
public class MyWsConfig implements WebSocketConfigurer {@ResourceMyWsHandler myWsHandler; // 引入我们在MyWsHandler上声明的Bean(@Component)@ResourceMyInterceptor myInterceptor;@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {// 监听的地址registry.addHandler(myWsHandler,"/myWs1").addInterceptors(myInterceptor).setAllowedOrigins("*"); // 允许的源}
}

修改 html 中的 websocket 地址为 /myWs1 

 测试:访问 localhost:8080/ws-client.html

3、总结

服务器会和每个客户端维护一个连接 :

3、WebSocket 实现多人聊天室

3.1、需求

  • 进入聊天室
  • 群聊功能,任何人说话,所有人都能接受到消息
  • 退出群聊

3.2、实现

3.2.1、编写前端界面和通信逻辑 

        这里我们通过 js 实现了当用户点击发送按钮的时候,把文本框的内容通过 websocket 发送给指定的地址,我们的后台 websocket 程序收到后会全部群发给所有当前连接的客户端:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>WebSocket Client</title>
</head>
<script>// 客户端和服务器连接的地址(我们服务端监听的地址)let ws = new WebSocket("ws://localhost:8080/myWs1")ws.onopen=function (){ // 连接打开的时候向服务器发送一条消息// ws.send("hey man")}ws.onmessage=function (message) {// console.log(message.data)document.getElementById("talkMsg").innerHTML = message.data}function sendMsg(){// 发送文本框的信息ws.send(document.getElementById("message").value)// 发送完清空输入框document.getElementById("message").value=""}
</script>
<body>
<h1>WebSocket 多人聊天室</h1>
<p style="border: 1px solid black;width: 600px;height: 500px" id="talkMsg"></p>
<input id="message" />
<button id="sendBtn" onclick="sendMsg()">发 送</button>
</body>
</html>

 3.2.2、WebSocket 后台处理程序

         这里我们编写了一个 sendMessage 的方法来给所有已连接的客户端进行消息的群发,而我们上面的前端代码中设置当 websocket 收到消息后,会把文本域(<p>标签)中的内容替换掉(innerHTML),因为我们的消息是不断累加到 StringBuffer 当中的,而文本域内容的替换开销并不会造成视觉上的卡顿:

/*** WebSocket 自定义处理程序*/
@Slf4j
@Component
public class MyWsHandler extends AbstractWebSocketHandler {// WebSocketSession 对象可以封装一下吧用户的信息封装进去private static Map<String, SessionBean> sessionMap = new ConcurrentHashMap<>();// 线程安全的int值private static AtomicInteger clientIdMaker = new AtomicInteger(0);//private static StringBuffer sb = new StringBuffer();// 连接建立@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {super.afterConnectionEstablished(session);// 放在父方法调用之后SessionBean sessionBean = new SessionBean(session, clientIdMaker.getAndIncrement());sessionMap.put(session.getId(),sessionBean);log.info(sessionMap.get(session.getId()) + " connected");sb.append(sessionBean.getClientId()).append(" 进入了群聊<br/>");sendMessage(sessionMap);}// 收到消息@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {super.handleTextMessage(session, message);log.info(sessionMap.get(session.getId()).getClientId() + " : " + message.getPayload());sb.append(sessionMap.get(session.getId()).getClientId()).append(" : ").append(message.getPayload()).append("<br/>");sendMessage(sessionMap);}// 传输异常@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {super.handleTransportError(session, exception);// 如果异常就关闭 sessionif (session.isOpen())session.close();sessionMap.remove(session.getId());}// 连接关闭@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {super.afterConnectionClosed(session, status);Integer clientId = sessionMap.get(session.getId()).getClientId();log.info(sessionMap.get(session.getId()) + " closed");sessionMap.remove(session.getId());sb.append(clientId).append("退出了群聊<br/>");sendMessage(sessionMap);}public void sendMessage(Map<String,SessionBean> sessionMap){for(String sessionId: sessionMap.keySet()){try {sessionMap.get(sessionId).getWebSocketSession().sendMessage(new TextMessage(sb.toString()));} catch (IOException e) {e.printStackTrace();log.error(e.getMessage());}}}
}

3.2.3、测试

启动项目,并访问 http://localhost:8080/ws-client.html

        可以看到,当我们启动多个客户端(开启多个网页),就会显示多人加入群聊,当任意客户端发送消息后,所有客户端都可以看到,当有客户端退出后,同样所有人都可以看到。

4、WebSocket 使用场景

        总的来讲,websocket 主要的特点就是全双工通信吗,它最大的优势就是实时性特别好,因为传统的单工通信我们客户端需要不断的向服务器发送请求,要追求实时性就必须不断频繁发送请求,效率肯定是低下的。而基于 WebSocket 协议的全双工通信的话就不需要了,因为服务端下你在可以主动给客户端发送消息了,就不需要客户端不断的去发送请求了。

        比如股价分析,股票什么时候发生变动我们谁也说不清楚,但是难道让客户端每几百毫秒去访问一次服务器吗?那必不可能,最好的办法就是每当股价发生变动,让服务器主动给客户端发送消息,客户端收到消息以后再做更新。这样通信效率一下子刷就上来了。

4.1、网页在线客服

4.2、chatGpt

4.3、弹幕

4.4、实时股价分析

4.5、4399小游戏

4.6、实时统计

 总结

1、为什么需要 WebSocket?

        HTTP 请求只能从浏览器(客户端)发送服务器,不能从服务器发往浏览器(客户端),这就导致一些需要从服务器发往浏览器(客户端)的场景满足不了。

2、WebSocket 是什么?

        WebSocket 是 HTML5 支持的,它是基于现有的 HTTP 请求进行了协议升级的一个全双工通信协议。一般用于实时性要求比较高的场景。


http://www.ppmy.cn/ops/15131.html

相关文章

2024年华为OD机试真题-寻找最优的路测线路-Python-OD统一考试(C卷D卷)

题目描述: 评估一个网络的信号质量,其中一个做法是将网络划分为栅格,然后对每个栅格的信号质量计算。路测的时候,希望选择一条信号最好的路线(彼此相连的栅格集合)进行演示。现给出R行C列的整数数组Cov,每个单元格的数值S即为该栅格的信号质量(已归一化,无单位,值越大…

面试八股——常见数据结构

二叉搜索树 在二叉搜索树中&#xff0c;对于任意一个节点&#xff0c;左子树中的所有节点的值都小于该节点的值&#xff0c;右子树所有节点的值都大于该节点的值。 红黑树 红黑树是一种自平衡的二叉搜索树。 红黑树包含以下性质&#xff1a; 散列表 散列表的工作原理&#x…

EasyUI-Resizable 可调整尺寸

<div id"rr" style"width:100px;height:100px;border:1px solid #ccc;"></div>$(#rr).resizable({maxWidth:800,maxHeight:600 }); 属性 名称 类型 描述 默认值 disabled boolean 如果设置为 true&#xff0c;则禁止调整尺寸。 false …

vue 请求php接口 header 传自定义参数 提示cors 跨域问题

前端地址 http://192.168.0.125:4021 请求后端地址的时候报 from origin http://192.168.0.125:4021 has been blocked by CORS policy: Request header field userid is not allowed by Access-Control-Allow-Headers in preflight response. 大概意思是请求 header里有个…

数据结构 - 顺序表

一. 线性表的概念 线性表&#xff08;linear list&#xff09;是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构&#xff0c;常见的线性表&#xff1a;顺序表、链表、栈、队列、字符串... 线性表在逻辑上是线性结构&#xff0c;也就说是连续的…

极限电流型氧气传感器的压力补偿

在氧浓度恒定的条件下&#xff0c;对极限电流氧气传感器在不同压力下测试数据进行分析发现&#xff0c;气压变化将导致传感器测量值与真实值存在测试误差较大。针对该问题&#xff0c;提出了极限电流氧气传感器低压条件下压力补偿方法&#xff0c;并进行了实验验证。压力补偿后…

AIGC技术的探索与展望:跨界融合与科技变革

文章目录 前言一、AIGC技术的现状与特点二、AIGC技术在各个领域的应用三、AIGC技术对未来社会的影响四、AIGC技术的可能发展方向 前言 随着科技的飞速发展&#xff0c;人工智能与大数据的结合日益紧密&#xff0c;AIGC&#xff08;人工智能生成内容&#xff09;技术作为这一领域…

力扣——位运算系列

【LeetCode 137. 只出现一次的数字 II】 这题有两个思路&#xff1a; 1&#xff09;逐位考虑&#xff1b; 2&#xff09;状态机&#xff1b; 先来看逐位考虑的思路&#xff1a; 如果对于 nums 中的每个数只观察某一位&#xff0c;因为除了一个数&#xff08;后用 a 来代称&…

Docker容器:docker基础

目录 一.docker前言 云计算服务模式 关于 docker 产品 虚拟化产品有哪些&#xff1f; ① 寄居架构 ② 源生架构 虚拟化技术概述 主流虚拟化产品对比 1. VMware系列 2. KVM/OpenStack 3. Xen 4. 其他半/全虚拟化产品 二. docker 的相关知识 1. docker 的概念 doc…

用斐波那契数列感受算法的神奇(21亿耗时0.02毫秒)

目录 一、回顾斐波那契数列 二、简单递归方法 &#xff08;一&#xff09;解决思路 &#xff08;二&#xff09;代码展示 &#xff08;三&#xff09;性能分析 三、采用递归HashMap缓存 &#xff08;一&#xff09;解决思路 &#xff08;二&#xff09;代码展示 &…

基于adb操作安卓手机封装的python库

import re import shlex import subprocessclass ADBClient:def __init__(self, ip, port):"""初始化ADBClient实例。:param ip: 远程设备的IP地址。:param port: 远程设备的端口号。"""self.ip ipself.port portdef is_app_running(self, pac…

git版本库瘦身你知道吗

1.问题缘由 新开发一个git项目时&#xff0c;可能会使用之前的代码库作为基准&#xff0c;并且之前的代码库中可能有许多git提交记录&#xff08;.git文件夹下面有许多提交记录存储&#xff09;。这时需要处理以下2种情况。 1&#xff09;远程代码库中还未上传git提交记录&am…

使用FunASR处理语音识别

FunASR是阿里的一个语音识别工具&#xff0c;比SpeechRecognition功能多安装也很简单&#xff1b; 官方介绍&#xff1a;FunASR是一个基础语音识别工具包&#xff0c;提供多种功能&#xff0c;包括语音识别&#xff08;ASR&#xff09;、语音端点检测&#xff08;VAD&#xff…

Android 水滴屏、全屏适配

Android 水滴屏、全屏适配 何谓刘海屏&#xff1f;何谓水滴屏&#xff1f; 上述两种屏幕都可以统称为刘海屏&#xff0c;不过对于右侧较小的刘海&#xff0c;业界一般称为水滴屏或美人尖。 目前国内流行的手机厂商主要有&#xff1a;vivo、oppo、华为、小米。各厂商对刘海屏的…

GHO文件安装到Vmware的两种姿势

1、使用 Ghost11.5.1.2269 将gho转换为vmdk文件(虚拟机硬盘)&#xff0c;Vmware新建虚拟机自定义配置&#xff0c;然后添加已有的虚拟硬盘文件。 注意ghost的版本&#xff0c;如果你是用Ghost11.5备份的gho文件&#xff0c;再用Ghost12把gho文件转换为vmdk&#xff0c;则vmdk文…

深度学习之视觉特征提取器——VGG系列

VGG 提出论文&#xff1a;1409.1556.pdf (arxiv.org) 引入 距离VGG网络的提出已经约十年&#xff0c;很难想象在深度学习高速发展的今天&#xff0c;一个模型能够历经十年而不衰。虽然如今已经有VGG的大量替代品&#xff0c;但是笔者研究的一些领域仍然有大量工作选择使用VG…

vue2响应式 VS vue3响应式

Vue2响应式 存在问题&#xff1a; 新增属性&#xff0c;删除属性&#xff0c;界面不会更新。 直接通过下标修改数组界面不会自动更新。 Vue2使用object.defineProperty来劫持数据是否发生改变&#xff0c;如下&#xff1a; 能监测到获取和修改属性&#xff1a; 新增的属性…

服务器数据恢复—存储硬盘坏道,指示灯亮黄色的数据恢复案例

服务器数据恢复环境&故障&#xff1a; 一台某品牌EqualLogic PS系列某型号存储&#xff0c;存储中有一组由16块SAS硬盘组建的RAID5磁盘阵列&#xff0c;RAID5上划分VMFS文件系统存放虚拟机文件。存储系统上层一共分了4个卷。 raid5阵列中磁盘出现故障&#xff0c;有2块硬盘…

安全评估报告 项目安全风险评估报告 信息安全评估报告

一、安全评估报告的意义 安全评估报告是对特定环境、设施或系统安全性进行全 面分析、评估和预测的重要工具。它通过对潜在风险的识别、分析和评价&#xff0c;帮助决策者了解当前安全形势&#xff0c;制定科学的安全策略&#xff0c;从而有 效预防和减少安全事故的发生。安全…

Hive安装与配置实战指南

Hive安装与配置实战指南 在大数据领域中&#xff0c;Hive以其类SQL的查询语言HQL、可扩展的数据仓库能力和对Hadoop生态系统的良好集成&#xff0c;成为了数据分析和处理的重要工具。本文将指导您完成Hive的安装与配置&#xff0c;帮助您快速搭建起自己的Hive环境。 一、环境…