SpringBoot 集成WebSocket详解

一、WebSocket概述

1、WebSocket简介

WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。

2、为什么需要WebSocket

HTTP 是基于请求响应式的,即通信只能由客户端发起,服务端做出响应,无状态,无连接。

无状态:每次连接只处理一个请求,请求结束后断开连接。
无连接:对于事务处理没有记忆能力,服务器不知道客户端是什么状态。
通过HTTP实现即时通讯,只能是页面轮询向服务器发出请求,服务器返回查询结果。轮询的效率低,非常浪费资源,因为必须不停连接,或者 HTTP 连接始终打开。

WebSocket的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话。

WebSocket特点:

(1)建立在 TCP 协议之上,服务器端的实现比较容易。
(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
(3)数据格式比较轻量,性能开销小,通信高效。
(4)可以发送文本,也可以发送二进制数据。
(5)没有同源限制,客户端可以与任意服务器通信。
(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

二、SpringBoot整合WebSocket

创建 SpringBoot项目,引入 WebSocket依赖,前端这里比较简陋。

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
            <version>2.7.12</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
            <version>2.7.12</version>
        </dependency>

application.yml:

server:
  port: 8081

spring:
  thymeleaf:
    mode: HTML
    cache: true
    prefix: classpath:/templates/
    encoding: UTF-8
    suffix: .html
    check-template-location: true
    template-resolver-order: 1

1、WebSocketConfig

启用 WebSocket的支持也是很简单。

/**
 * WebSocket配置类。开启WebSocket的支持
 */
@Configuration
public class WebSocketConfig {

    /**
     * bean注册:会自动扫描带有@ServerEndpoint注解声明的Websocket Endpoint(端点),注册成为Websocket bean。
     * 要注意,如果项目使用外置的servlet容器,而不是直接使用springboot内置容器的话,就不要注入ServerEndpointExporter,因为它将由容器自己提供和管理。
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}

2、WebSocketServer
这里就是重点了,核心都在这里。

因为WebSocket是类似客户端服务端的形式(采用ws协议),那么这里的WebSocketServer其实就相当于一个ws协议的Controller
直接@ServerEndpoint("/imserver/{userId}") 、@Component启用即可,然后在里面实现@OnOpen开启连接,@onClose关闭连接,@onMessage接收消息等方法。
新建一个ConcurrentHashMap用于接收当前userId的WebSocket或者Session信息,方便IM之间对userId进行推送消息。单机版实现到这里就可以。
集群版(多个ws节点)还需要借助 MySQL或者 Redis等进行订阅广播方式处理,改造对应的 sendMessage方法即可。

/**
 * WebSocket的操作类
 */
@Component
@Slf4j
/**
 * html页面与之关联的接口
 * var reqUrl = "http://localhost:8081/websocket/" + cid;
 * socket = new WebSocket(reqUrl.replace("http", "ws"));
 */
@ServerEndpoint("/websocket/{sid}")
public class WebSocketServer {

    /**
     * 静态变量,用来记录当前在线连接数,线程安全的类。
     */
    private static AtomicInteger onlineSessionClientCount = new AtomicInteger(0);

    /**
     * 存放所有在线的客户端
     */
    private static Map<String, Session> onlineSessionClientMap = new ConcurrentHashMap<>();

    /**
     * 连接sid和连接会话
     */
    private String sid;
    private Session session;

    /**
     * 连接建立成功调用的方法。由前端<code>new WebSocket</code>触发
     *
     * @param sid     每次页面建立连接时传入到服务端的id,比如用户id等。可以自定义。
     * @param session 与某个客户端的连接会话,需要通过它来给客户端发送消息
     */
    @OnOpen
    public void onOpen(@PathParam("sid") String sid, Session session) {
        /**
         * session.getId():当前session会话会自动生成一个id,从0开始累加的。
         */
        log.info("连接建立中 ==> session_id = {}, sid = {}", session.getId(), sid);
        //加入 Map中。将页面的sid和session绑定或者session.getId()与session
        //onlineSessionIdClientMap.put(session.getId(), session);
        onlineSessionClientMap.put(sid, session);

        //在线数加1
        onlineSessionClientCount.incrementAndGet();
        this.sid = sid;
        this.session = session;
        sendToOne(sid, "连接成功");
        log.info("连接建立成功,当前在线数为:{} ==> 开始监听新连接:session_id = {}, sid = {},。", onlineSessionClientCount, session.getId(), sid);
    }

    /**
     * 连接关闭调用的方法。由前端<code>socket.close()</code>触发
     *
     * @param sid
     * @param session
     */
    @OnClose
    public void onClose(@PathParam("sid") String sid, Session session) {
        //onlineSessionIdClientMap.remove(session.getId());
        // 从 Map中移除
        onlineSessionClientMap.remove(sid);

        //在线数减1
        onlineSessionClientCount.decrementAndGet();
        log.info("连接关闭成功,当前在线数为:{} ==> 关闭该连接信息:session_id = {}, sid = {},。", onlineSessionClientCount, session.getId(), sid);
    }

    /**
     * 收到客户端消息后调用的方法。由前端<code>socket.send</code>触发
     * * 当服务端执行toSession.getAsyncRemote().sendText(xxx)后,前端的socket.onmessage得到监听。
     *
     * @param message
     * @param session
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        /**
         * html界面传递来得数据格式,可以自定义.
         * {"sid":"user-1","message":"hello websocket"}
         */
        JSONObject jsonObject = JSON.parseObject(message);
        String toSid = jsonObject.getString("sid");
        String msg = jsonObject.getString("message");
        log.info("服务端收到客户端消息 ==> fromSid = {}, toSid = {}, message = {}", sid, toSid, message);

        /**
         * 模拟约定:如果未指定sid信息,则群发,否则就单独发送
         */
        if (toSid == null || toSid == "" || "".equalsIgnoreCase(toSid)) {
            sendToAll(msg);
        } else {
            sendToOne(toSid, msg);
        }
    }

    /**
     * 发生错误调用的方法
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("WebSocket发生错误,错误信息为:" + error.getMessage());
        error.printStackTrace();
    }

    /**
     * 群发消息
     *
     * @param message 消息
     */
    private void sendToAll(String message) {
        // 遍历在线map集合
        onlineSessionClientMap.forEach((onlineSid, toSession) -> {
            // 排除掉自己
            if (!sid.equalsIgnoreCase(onlineSid)) {
                log.info("服务端给客户端群发消息 ==> sid = {}, toSid = {}, message = {}", sid, onlineSid, message);
                toSession.getAsyncRemote().sendText(message);
            }
        });
    }

    /**
     * 指定发送消息
     *
     * @param toSid
     * @param message
     */
    private void sendToOne(String toSid, String message) {
        // 通过sid查询map中是否存在
        Session toSession = onlineSessionClientMap.get(toSid);
        if (toSession == null) {
            log.error("服务端给客户端发送消息 ==> toSid = {} 不存在, message = {}", toSid, message);
            return;
        }
        // 异步发送
        log.info("服务端给客户端发送消息 ==> toSid = {}, message = {}", toSid, message);
        toSession.getAsyncRemote().sendText(message);
        /*
        // 同步发送
        try {
            toSession.getBasicRemote().sendText(message);
        } catch (IOException e) {
            log.error("发送消息失败,WebSocket IO异常");
            e.printStackTrace();
        }*/
    }

}

3、controller

controller中只有一个简单的界面跳转操作,其他的不需要。

@Controller
@RequestMapping("/demo")
public class DemoController {

    /**
     * 跳转到websocketDemo.html页面,携带自定义的cid信息。
     * http://localhost:8081/demo/toWebSocketDemo/user-1
     *
     * @param cid
     * @param model
     * @return
     */
    @GetMapping("/toWebSocketDemo/{cid}")
    public String toWebSocketDemo(@PathVariable String cid, Model model) {
        model.addAttribute("cid", cid);
        return "websocketDemo";
    }

}

4、websocketDemo.html

新建一个文件,放到 templates目录下面。页面简单使用js代码调用WebSocket。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>666666</title>
</head>
<body>
    传递来的数据值cid:
    <input type="text" th:value="${cid}" id="cid"/>
    <p>【toUserId】:
    <div><input id="toUserId" name="toUserId" type="text" value="user-1"></div>
    <p>【toUserId】:
    <div><input id="contentText" name="contentText" type="text" value="hello websocket"></div>
    <p>【操作】:
    <div>
        <button type="button" onclick="sendMessage()">发送消息</button>
    </div>
</body>

<script type="text/javascript">
    var socket;
    if (typeof (WebSocket) == "undefined") {
        console.log("您的浏览器不支持WebSocket");
    } else {
        console.log("您的浏览器支持WebSocket");
        //实现化WebSocket对象,指定要连接的服务器地址与端口  建立连接

        var cid = document.getElementById("cid").value;
        console.log("cid-->" + cid);
        var reqUrl = "http://localhost:8081/websocket/" + cid;
        socket = new WebSocket(reqUrl.replace("http", "ws"));
        //打开事件
        socket.onopen = function () {
            console.log("Socket 已打开");
            //socket.send("这是来自客户端的消息" + location.href + new Date());
        };
        //获得消息事件
        socket.onmessage = function (msg) {
            console.log("onmessage--" + msg.data);
            //发现消息进入    开始处理前端触发逻辑
        };
        //关闭事件
        socket.onclose = function () {
            console.log("Socket已关闭");
        };
        //发生了错误事件
        socket.onerror = function () {
            alert("Socket发生了错误");
            //此时可以尝试刷新页面
        }
        //离开页面时,关闭socket
        //jquery1.8中已经被废弃,3.0中已经移除
        // $(window).unload(function(){
        //     socket.close();
        //});
    }

    function sendMessage() {
        if (typeof (WebSocket) == "undefined") {
            console.log("您的浏览器不支持WebSocket");
        } else {
            // console.log("您的浏览器支持WebSocket");
            var toUserId = document.getElementById('toUserId').value;
            var contentText = document.getElementById('contentText').value;
            var msg = '{"sid":"' + toUserId + '","message":"' + contentText + '"}';
            console.log(msg);
            socket.send(msg);
        }
    }

</script>
</html>

5、测试运行效果

(1)访问页面,建立连接

启动项目,访问 http://localhost:8081/demo/toWebSocketDemo/{cid} 跳转到页面,然后就可以和WebSocket交互了。

这里开启三个浏览器的窗口:

http://localhost:8081/demo/toWebSocketDemo/user-1

此时浏览器的console显示如下:

此时浏览器的network 显示如下:

服务端打印如下图所示内容:


http://localhost:8081/demo/toWebSocketDemo/user-2

此时浏览器的network 显示如下:

此时浏览器的network 显示如下:

服务端打印如下图所示内容:


http://localhost:8081/demo/toWebSocketDemo/user-3

此时浏览器的network 显示如下:

此时浏览器的network 显示如下:

服务端打印如下图所示内容:

(2)、在user-2给user-1发消息

此时user-2的浏览器network里面并没有再请求接口

 

此时查看服务端的console,截图如下:

 查看user-1的console,如下所示:

user-1的network并没有再请求接口。

(3)、给全员发消息

在user-3页面,给所有用户发消息

user-2页面console如下

user-1页面的console如下

 此时服务端的console打印如下:

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

mybatis(动态sql及分页)

mybatis-动态sql及分页 1. 动态sql简述2. 动态sql示例2.1 if2.2 choose2.3 foreach2.4 sql 及 include2.5 sql中的特殊字符 3. 后台分页实现4. 数据版本号处理并发问题 1. 动态sql简述 mybatis的动态sql语句是基于OGNL表达式的。可以方便的在sql语句中实现某些逻辑. 总体说来m…

Vue 封装的 axios 类的使用(小bug 改进)

http类 import { baseUrl } from "./config"; //引入config.js中的配置 import axios from "axios"; //引入axios import qs from "querystringify"; //form-Data请求时的工具类class Http{axios null;lastRequestIntercept null…

Android BUG 之 Error: Activity class {} does not exist

项目场景&#xff1a; 更换包名&#xff0c;运行报错 问题描述 原因分析&#xff1a; 在替换包名的时候要确认&#xff0c;配置文件跟build中的保持一致&#xff0c;在更换后还要将旧包的缓存数据清理掉 解决方案&#xff1a; 1 替换后删除 app 下的build 文件夹 2 Rebuild Pr…

Leetcode2976. 转换字符串的最小成本 I

Every day a Leetcode 题目来源&#xff1a;2976. 转换字符串的最小成本 I 解法1&#xff1a;最短路 建图&#xff0c;从 original[i] 向 changed[i] 连边&#xff0c;边权为 cost[i]。没边的边权设为 INF。 然后用 Floyd 算法求图中任意两点最短路&#xff0c;得到 g 矩阵…

MT6785安卓核心板_联发科MTK6785/Helio G95/曦力G95核心板定制

MT6785安卓核心板是基于MT6785(Helio G95)处理器&#xff0c;具备八核处理器结构&#xff0c;包括2颗主频为2.05GHz的Cortex A76处理器和6颗主频为2.0GHz的Cortex A55处理器&#xff0c;以及六颗Cortex-A55处理器。而在GPU方面&#xff0c;采用了Arm Mali-G76 MC4&#xff0c;频…

zabbix监控windows主机

下载安装zabbix agent安装包 Zabbix官网下载地址: https://www.zabbix.com/cn/download_agents?version5.0LTS&release5.0.40&osWindows&os_versionAny&hardwareamd64&encryptionOpenSSL&packagingMSI&show_legacy0 这里使用zabbix agent2 安装 …

22、Kubernetes核心技术 - 整合Rancher通过界面管理k8s集群

目录 一、概述 二、Rancher API Server 的功能 2.1、授权和角色权限控制 2.2、使用 Kubernetes 的功能 2.3、配置云端基础信息 2.4、查看集群信息 三、Rancher 安装 3.1、前置环境 3.2、通过 Docker 来进行安装Rancher 3.3、在 Rancher 的界面上绑定k8s集群 3.4、在 …

C语言入门教程,C语言学习教程(第二部分:C语言初探)二

十、C语言的三套标准&#xff1a;C89、C99和C11 我们今天使用的 Windows、Linux、Mac OS 等操作系统都是由一种叫做 Unix 的系统演化而来。Unix 作为80年代主流的操作系统&#xff0c;是整个软件工业的基础&#xff0c;是现代操作系统的开山鼻祖&#xff0c;C语言就是为 Unix …

html的全选反选

一、实验题目 html实现选择框的全选和反选 二、实验代码 <!DOCTYPE html> <html><head><meta charset"utf-8"><title>全选和反选</title></head><body><ul>兴趣爱好</ul><input id"all"…

国芯科技荣膺高工智能汽车“年度车规MCU高成长供应商”,加速产品精准化系列化布局

2023年12月13—15日&#xff0c;2023&#xff08;第七届&#xff09;高工智能汽车年会在上海召开&#xff0c;大会以“寻找拐点”为主题&#xff0c;通过超80场主题演讲及多场圆桌对话&#xff0c;为智能汽车赛道参与者「备战2024」提供全方位的决策支持。 作为汽车电子芯片领…

网络安全漏洞的常见类型

网络犯罪分子可以利用的常见网络安全漏洞包括凭证薄弱、缺乏数据加密、配置错误、软件过时和零日漏洞。这些漏洞通常会导致网络攻击&#xff0c;绕过组织的安全措施并窃取机密数据。组织需要识别并缓解这些漏洞&#xff0c;以防止安全漏洞。 继续阅读以了解有关网络安全漏洞的…

Apache ECharts | 一个数据可视化图表库

文章目录 1、简介1.1、主要特点1.2、使用场景 2、安装方式一&#xff1a;从下载的源代码或编译产物安装方法二&#xff1a;从 npm 安装方法三&#xff1a;⭐定制安装echarts.js 3、使用 官网&#xff1a; 英语&#xff1a;https://echarts.apache.org/en/index.html 中文&a…

ChatGLM2-6B 大语言模型本地搭建

ChatGLM模型介绍&#xff1a; ChatGLM2-6B 是清华 NLP 团队于不久前发布的中英双语对话模型&#xff0c;它具备了强大的问答和对话功能。拥有最大32K上下文&#xff0c;并且在授权后可免费商用&#xff01; ChatGLM2-6B的6B代表了训练参数量为60亿&#xff0c;同时运用了模型…

[VSCode] VSCode 常用快捷键

文章目录 VSCode 源代码编辑器VSCode 常用快捷键分类汇总01 编辑02 导航03 调试04 其他05 重构06 测试07 扩展08 选择09 搜索10 书签11 多光标12 代码片段13 其他 VSCode 源代码编辑器 官网&#xff1a;https://code.visualstudio.com/ 下载地址&#xff1a;https://code.visua…

在学习爬虫前的准备

1. 写一个爬虫程序需要分几步 获取网页内容。 我们会通过代码给一个网站服务器发送请求&#xff0c;它会返回给我们网页上的内容。 在我们平时使用浏览器访问服务器内容是&#xff0c;本质上也是向服务器发送一个请求&#xff0c;然后服务器返回网页上的内容。只不过浏览器还会…

Oracle VM VirtualBox xx needs the Micrsoft Visual C++ 2019错误

错误展示 解决方法 重修安装 Visual C 文件 1、前往官网 C 中 Windows 编程概述 | Microsoft Learn 2、找到对应的包 左边导航栏依次选择&#xff1a; 部署本机桌面应用程序-----重新分发Visual C 文件-----最新受支持的Visual C可再发型程序包下载 根据自己电脑系统进行选…

数据结构期末复习笔记

文章目录 数据结构期末复习第一章&#xff1a;数据结构绪论第二章&#xff1a;顺序表与单链表第三章&#xff1a;其它链表第四章&#xff1a;栈如何中缀转后缀后缀如何计算 第五章&#xff1a;队列第六章&#xff1a;串第七章&#xff1a;树的概念和遍历第八章&#xff1a;赫夫…

创建一个郭德纲相声GPTs

前言 在这篇文章中&#xff0c;我将分享如何利用ChatGPT 4.0辅助论文写作的技巧&#xff0c;并根据网上的资料和最新的研究补充更多好用的咒语技巧。 GPT4的官方售价是每月20美元&#xff0c;很多人并不是天天用GPT&#xff0c;只是偶尔用一下。 如果调用官方的GPT4接口&…

Win10 自带微软输入法怎么切换成简体字 快捷鍵是什么?

环境&#xff1a; Win10专业版 问题描述&#xff1a; 微軟輸入法怎麽切換中文簡體 快捷鍵&#xff0c;之前不小心按了快捷键 解决方案&#xff1a; 1.按CtrlShiftF快捷键转换简体字或繁体字 2.可以在“设置-时间和语言-区域和语言-语言-中文&#xff08;中华人民共和国&a…

为什么选择嬴图?

图数据库、图计算、图中台都是用图论的方式去构造实体间的关联关系&#xff0c;实体用顶点来表达&#xff0c;而实体间的关系用边来表达。图数据库的这种简洁、自由、高维但100%还原世界的数据建模的方式让实体间的关联关系的计算比SQL类的数据库高效成千上万倍。 图&#xff1…