传统@ServerEndpoint方式开发WebSocket应用和SpringBoot构建WebSocket应用程序

2023-11-01

小Hub领读:

通过websocket的两种使用方式,让你更加深入理解用法。很多人不懂websocket怎么辨别是谁发送过来的,文中说到实现WebSocketConfigurer接口,定义拦截器可以绑定用户信息,还有其他很多,细细品,对比看比较好!

文末有源码地址~

对了,看完记得点个[ 在看 ]支持一下哈。


作者:编码妙妙屋

https://www.skypyb.com/2019/02/jishu/java/813/

WebSocket 一次握手就可以使客户端和服务端建立长连接,并进行双向数据传输。

由于其双向传输特性,服务端可主动向客户端发送信息,实时性很高。

而与 HTTP 协议比起来 WebSocket 协议每次数据传输的头信息都较小,节约带宽。

在获取实时数据这方面时,那是比 ajax 轮询方式高到不知道哪去了。

在 SpringBoot 架构下进行 WebSocket 服务开发的话, 首先还是要导入这个

就算是使用 Tomcat 7 的 @ServerEndpoint 进行 WebSocket 开发, 也得导。不然在 SpringBoot 环境下会有 bug。

普通 java web 应用开发 WebSocket 就不需要了,@ServerEndpoint 直接用就行。

compile('org.springframework.boot:spring-boot-starter-websocket:2.0.4.RELEASE')

maven 仓库: spring-boot-starter-websocket

这是使用 @ServerEndpoint 进行的传统 WebSocket 开发:

由于我是在 SpringBoot 环境, 所以得先写个能扫描 @ServerEndpoint 的配置, 不然在客户端连接的时候会一直连不上。不是在 SpringBoot 下开发的可以跳过这一环节。

ServerEndpointExporter 这个类偏偏还是 spring-boot-starter-websocket 提供的。所以必须要导入这个依赖。

package com.skypyb.websocket;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfigOne {
    /**
     * 这个bean会自动注册使用了@ServerEndpoint注解声明的对象
     * 没有的话会报404
     *
     * @return
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

@ServerEndpoint 注解中写上客户端连接的地址。

在类层次上, 还得加上 @Component 注解才行。普通 java web 项目不用加。

这个方式开发的 WebSocket 服务器,每个连接加入都会为该连接新建一个服务器对象绑定。

package com.skypyb.websocket;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
/**
 * @ServerEndpoint 该注解可以将类定义成一个WebSocket服务器端,
 * @OnOpen 表示有浏览器链接过来的时候被调用
 * @OnClose 表示浏览器发出关闭请求的时候被调用
 * @OnMessage 表示浏览器发消息的时候被调用
 * @OnError 表示报错了
 */
@ServerEndpoint("/ws/serverOne")
@Component
public class WebSocketServerOne {
    //concurrent包下线程安全的Set
    private static final CopyOnWriteArraySet<WebSocketServerOne> SESSIONS = new CopyOnWriteArraySet<>();
    private Session session;
    @OnOpen
    public void onOpen(Session session) {
        this.session = session;
        SESSIONS.add(this);
        System.out.println(String.format("成功建立连接~ 当前总连接数为:%s", SESSIONS.size()));
        System.out.println(this);
    }
    @OnClose
    public void onClose() {
        SESSIONS.remove(this);
        System.out.println(String.format("成功关闭连接~ 当前总连接数为:%s", SESSIONS.size()));
    }
    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println(message);
    }
    @OnError
    public void onError(Session session, Throwable error) {
        System.out.println("发生错误");
        error.printStackTrace();
    }
    /**
     * 指定发消息
     *
     * @param message
     */
    public void sendMessage(String message) {
        try {
            this.session.getBasicRemote().sendText(message);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**
     * 群发消息
     *
     * @param message
     */
    public static void fanoutMessage(String message) {
        SESSIONS.forEach(ws -> ws.sendMessage(message));
    }
}

这时,一个传统的 WebSocket 应用就开发完毕了。由于我是在 SpringBoot 下开发的,所以有一些调整。

启动应用后搞个普通的 html 页面直接本地打开就可以试验是否能够连接上了。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>webSocket</title>
    <style type="text/css">
    </style>
</head>
<body>
<h1>WebSocket Demo.</h1>
<h1>WebSocket Demo..</h1>
<h1>WebSocket Demo...</h1>
<input type="button" onclick="websocket.send('666666666')" value="点我发消息"/>
</body>
<script type="application/javascript">
    var websocket = {
        send: function (str) {
        }
    };
    window.onload = function () {
        if (!'WebSocket' in window) return;
        webSocketInit();
    };
    function webSocketInit() {
        websocket = new WebSocket("ws://127.0.0.1:8080/ws/serverOne");
        //成功建立连接
        websocket.onopen = function () {
            websocket.send("成功连接到服务器");
        };
        //接收到消息
        websocket.onmessage = function (event) {
            console.log(event.data)
        };
        //连接发生错误
        websocket.onerror = function () {
            alert("WebSocket连接发生错误");
        };
        //连接关闭
        websocket.onclose = function () {
            alert("WebSocket连接关闭");
        };
        //监听窗口关闭事件,当窗口关闭时,主动关闭websocket连接
        window.onbeforeunload = function () {
            websocket.close()
        };
    }
</script>
</html>

不过… 我既然都导入了 spring-boot-starter-websocket 依赖,自然最好还是用 SprinBoot 推荐的方法比较好。

这是使用 SpringBoot 的形式构建 WebSocket 应用程序:

这是核心配置类。实现 WebSocketConfigurer 接口实现它提供的注册方法。

这个方法就厉害了, 可以配置 websocket 入口,允许访问的域、注册 Handler、定义拦截器等等等等。

@EnableWebSocket 用于开启注解接收和发送消息

package com.skypyb.springboot_websocket;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.HandshakeInterceptor;
import java.util.Map;
@Configuration
@EnableWebSocket
public class WebSocketConfigTwo implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new MyWebSocketHandler(), "/ws/serverTwo")//设置连接路径和处理
                .setAllowedOrigins("*")
                .addInterceptors(new MyWebSocketInterceptor());//设置拦截器
    }
    /**
     * 自定义拦截器拦截WebSocket请求
     */
    class MyWebSocketInterceptor implements HandshakeInterceptor {
        //前置拦截一般用来注册用户信息,绑定 WebSocketSession
        @Override
        public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
                                       WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
            System.out.println("前置拦截~~");
            if (!(request instanceof ServletServerHttpRequest)) return true;
//            HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
//            String userName = (String) servletRequest.getSession().getAttribute("userName");
            String userName = "Koishipyb";
            attributes.put("userName", userName);
            return true;
        }
        @Override
        public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
                                   WebSocketHandler wsHandler, Exception exception) {
            System.out.println("后置拦截~~");
        }
    }
}

关于他这个拦截器, 是非常重要的,最好还是设置一个。他可以在连接进入到 Handler 处理时进行一些操作。

比如从 session 中拿出用户登陆信息作为唯一标识符等等…

我把我的拦截器实现写成内部类了,反正也没多少东西。这个配置注册的处理器和拦截器,都是只有一个的,无论你多少连接进来,都是用相同的对象处理。

那么就不好用传统的 WebSocket 开发那样用个集合类容器来存了。用前置拦截设置进去的某些唯一标识作为 key,session 作为 value 用键值对映射类容器来存储连接是比较好的方案。

推荐阅读Github上最值得学习的100个Java开源项目,涵盖各种技术栈!

这是我实现的处理器, 也是 WebSocket 开发的核心:

需要实现 WebSocketHandler 接口, 该接口提供了五个方法。

  • 1、afterConnectionEstablished(): 建立新的 socket 连接后回调的方法。

  • 2、handleMessage(): 接收客户端发送的 Socket。

  • 3、handleTransportError(): 连接出错时,回调的方法。

  • 4、afterConnectionClosed(): 连接关闭时,回调的方法。

  • 5、supportsPartialMessages(): 这个是 WebSocketHandler 是否处理部分消息,没什么卵用 返回 false 就完事了。

package com.skypyb.springboot_websocket;
import org.springframework.web.socket.*;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class MyWebSocketHandler implements WebSocketHandler {
    private static final Map<String, WebSocketSession> SESSIONS = new ConcurrentHashMap<>();
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        String userName = session.getAttributes().get("userName").toString();
        SESSIONS.put(userName, session);
        System.out.println(String.format("成功建立连接~ userName: %s", userName));
    }
    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
        String msg = message.getPayload().toString();
        System.out.println(msg);
    }
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        System.out.println("连接出错");
        if (session.isOpen()) {
            session.close();
        }
    }
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
        System.out.println("连接已关闭,status:" + closeStatus);
    }
    @Override
    public boolean supportsPartialMessages() {
        return false;
    }
    /**
     * 指定发消息
     *
     * @param message
     */
    public static void sendMessage(String userName, String message) {
        WebSocketSession webSocketSession = SESSIONS.get(userName);
        if (webSocketSession == null || !webSocketSession.isOpen()) return;
        try {
            webSocketSession.sendMessage(new TextMessage(message));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**
     * 群发消息
     *
     * @param message
     */
    public static void fanoutMessage(String message) {
        SESSIONS.keySet().forEach(us -> sendMessage(us, message));
    }
}

至此,SpringBoot 与 WebSocket 已经集成完了。客户端仍然使用我上边那个 HTML 文件就行,访问地址改一下就完事了。

我的这个 WebSocket 项目整体源码地址https://github.com/skypyb/codeDemo/tree/master/WebSocket


(完)

MarkerHub文章索引:(点击阅读原文直达)
https://github.com/MarkerHub/JavaIndex
【推荐阅读】
小白教程,Springboot项目搭建(前端到数据库,超详细)
Spring 和 Spring Boot 最核心的 3 大区别,详解!
就几条命令,一键学会Docker部署SpringBoot项目
使用 SpringBoot2.X 实现 Quartz 动态任务的分布式调度
从集成到ACK、消息重试、死信队列,Kafka你知多少?

好文!必须点赞

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

传统@ServerEndpoint方式开发WebSocket应用和SpringBoot构建WebSocket应用程序 的相关文章

  • 阿里Sentinel控制台源码修改-对接Apollo规则持久化

    改造背景 前面我们讲解了如何对接Apollo来持久化限流的规则 对接后可以直接通过Apollo的后台进行规则的修改 推送到各个客户端实时生效 但还有一个问题就是Sentinel控制台没有对接Apollo Sentinel控制台本来就可以修改
  • C++ inline用法

    1 引入 inline 关键字的原因 在 c c 中 为了解决一些频繁调用的小函数大量消耗栈空间 栈内存 的问题 特别的引入了 inline 修饰符 表示为内联函数 栈空间就是指放置程序的局部数据 也就是函数内数据 的内存空间 在系统下 栈
  • (fastjson)对象转JSON字符串 接收json字符串返回对象

  • 视频86免费影院-视频电影网聚平台

    这两年在互联网来讲 视频行业是比较火热的 各大视频分享网站 融资 风投 欢乐声一片 这表明中国的互联网用户随着网络带宽的加大对在线视频 电影还是比较喜欢的 正好在网上看到一个不错的网站程序 修改过后自己也来做一个视频网站 不过内容都是采集的
  • 建立Tahi IPv6测试环境

    首先说一下TAHI测试的相关术语 Tester Node TN 测试平台 A tester node for the conformance tests Node Under Test NUT 待测试机 A testee node for
  • oracle查看服务器名字,查看oracle数据库服务器的名字

    查看oracle数据库服务器的名字 windows 中 1 select name from v database 直接运行就可以查看了 2 查看tnsnames ora 的连接 有个SID SID就是服务名了 1 查看oracle的安装目
  • java 面向对象编程——简介

    目录 第一章 对象和类 一 面向对象的程序设计 1 抽象的数据类型 2 什么是类 3 总结 二 定义一个类 1 定义类的成员变量 2 定义类的成员的方法 3 类的成员变量和方法总结 4 创建并使用对象 第二章 方法 一 方法的重载 1 方法
  • Spring Cloud Alibaba版本选型

    Spring Cloud Alibaba版本选型 版本说明 https github com alibaba spring cloud alibaba wiki E7 89 88 E6 9C AC E8 AF B4 E6 98 8E
  • javac不是内部命令或外部命令

    JAVAC 不是内部或外部命令 也不是可运行的程序或批处理文件 今天在运行JAVA的时候突然出了这个错误 这可怎么办 刚接触JAVA的新手可能就不知道怎么解决 JAVAC 不是内部命令或外部命令 下面我就来说说 解决 JAVAC 不是内部命
  • Sed 介绍和教程

    Sed 介绍和教程 作者 Bruce Barnett 译者 Koala 原文地址 http www grymoire com Unix Sed html 注 译者不懂sed Sed 介绍 如果你想写一个程序对一个文件做一些改动 那就sed就
  • B站狂神说--ElasticSearch笔记

    课程 免费 网址 https www bilibili com video BV17a4y1x7zq spm id from 333 999 0 0 笔记来源 https www kuangstudy com bbs 14427364812
  • 知识图谱在金融领域的分析与应用

    本文首发于个人博客 www bobinsun cn 前言 知识图谱因其自身的图展示 图挖掘 图模型计算优势 可帮助金融从业人员进行业务场景的分析与决策 有利于建立客户画像 进行精准营销获客 发现信用卡套现 资金挪用等行为 更好的表达 分析金
  • 车规级MCU知识介绍

    一辆传统燃油车需要大约500到600颗芯片 轻混汽车大约需要1000颗 插电混动和纯电动汽车则需要至少2000颗芯片 这就意味着在智能电动汽车快速发展的过程中 不仅对先进制程芯片需求不断增加 而且对传统芯片需求也会持续增加 MCU就是这样
  • PXE装机报错汇总

    报错1 PXE E53 No boot filename received CLIENT MAC ADDR 88 0C 29 0D 88 3C GUID 564D6429 2E4A 0B83 6161 AE0A050D803C PXE MB
  • 【持续集成CI/持续部署CD】二、Docker安装Maven私服Nexus

    本文是关于通过 Docker 进行安装部署 Nexus3 私服的快速入门和简单使用案例 一 安装 1 通过 docker 获取最新版本的 nexus3 镜像 docker pull sonatype nexus3创建 docker 镜像到宿
  • Pytorch中交叉熵损失函数 nn.CrossEntropyLoss()计算过程

    pytorch的交叉熵损失函数是如何计算outputs和 labels之间的损失的 对于一个分类问题的CNN模型 最后一层的代码一般如下 nn Linear 2048 num classes 然后计算一次迭代损失的代码一般如下 loss f
  • 国教 2019级 算法设计与分析 作业集锦(期末作业)

    7 1 寻找第k小的数 20 分 给定若干整数 请设计一个高效的算法 确定第k小的数 输入格式 测试数据有多组 处理到文件尾 每组测试数据的第1行输入2个整数n k 1 k n 1000000 第2行输入n个整数 每个数据的取值范围在0到1
  • Linux内核的配置和编译

    文章目录 前言 1 内核介绍 2 linux内核源码目录结构 3 内核配置和编译实验 4 内核的配置原理 5 menuconfig 6 Kconfig文件详解 7 menuconfig的实验 前言 以下内容基于2 6 35 7版本内核 文件
  • MyBatis和Hibernate相比,优势在哪里

    看到现在好多Web项目用了MyBatis 没感觉MyBatis好到哪里了 从开发效率上讲 不管是Hibernate的反向工程 还是自动建表 关联映射都比MyBatis方便得多 难道仅仅是因为运行速度 Hibernate也有缓存啊 或者 二者
  • 生成带有目录的Markdown格式文档

    什么是Markdown Markdown 是一种轻量级的 标记语言 它的优点很多 目前也被越来越多的写作爱好者 撰稿者广泛使用 使用 Markdown 的优点 专注你的文字内容而不是排版样式 轻松的导出 HTML PDF 和本身的 md 文

随机推荐

  • 性能测试出现:java.net.NoRouteToHostException: Can‘t assign requested address (Address not available)解决方案

    前言 在性能测试中会常常遇到java net NoRouteToHostException Can t assign requested address Address not available 这个问题 什么原因导致的呢 这个原因不一定
  • 全网最全Log配置教程及框架性能比较,看这篇就够了!

    每天早上七点三十 准时推送干货 一 摘要 不管是使用何种编程语言 何种框架 日志输出几乎无处不再 也是任何商业软件中必不可少的一部分 总结起来 日志的用途大致可以归纳成以下三种 问题追踪 通过日志不仅仅包括我们程序的一些bug 也可以在安装
  • VS堆栈溢出异常

    总是会遇到报堆栈溢出异常未处理错误 解决过几次 但是下次还是忘记怎么处理 所以写在博客上做个笔记 错误如图 解决方法 项目解决方案里右键项目 选择属性 在堆栈保留处输入40000000 4G64位电脑经验值 8G内存就要输入80000000
  • TiDB数据库权限管理

    TiDB数据库权限管理 TiDB 的权限管理系统按照 MySQL 的权限管理进行实现 TiDB 支持大部分的 MySQL 的语法和权限类型 本文主要介绍 TiDB 权限相关操作 各项操作需要的权限以及权限系统的实现 权限相关操作 授予权限
  • 将vue默认下载项yarn切换成npm

    今天在使用vue下载时发现 之前默认的npm变成了yarn 不习惯 下面将介绍如何将vue下载默认项yarn切换成npm 第一步 需要找到文档下的 vuerc 第二步 用编辑器 或者可以打开文本的软件 将其打开 打开后是这样的 第三步 其实
  • IDEA的查询引用、调用关系图的功能

    Eclipse的 Call Hierarchy 可以查看一个Java方法或类成员变量的调用树 caller和callee两个方向 非常方便 在IDEA中类似功能被划分到了三个命令 IntelliJ IDEA中可以在主菜单中选择Navigat
  • Git报错 Incorrect username or password (access token) 的解决方式

    错误原因 在使用git的时候 出现 Incorrect username or password access token 这个报错主要就是代表本地保存的gitee或者GitHub的账号还有密码错误 而他们这些账号密码都保存到了window
  • 移动端网络优化

    http b codekk com detail Trinea E7 A7 BB E5 8A A8 E7 AB AF E7 BD 91 E7 BB 9C E4 BC 98 E5 8C 96 一个网络请求可以简单分为连接服务器 gt 获取数据
  • centos python 升级3.7 及pip

    centos 7 6 1810 python3 6升级3 7 查看系统版本 开始升级 1 下载Python 3 7 0 tgz软件包 2 解压编译 3 更改默认python版本 pip2消失的话 查看系统版本 cat etc redhat
  • OSI七层协议大白话解读

    互联网的本质就是一系列的网络协议 这个协议就叫OSI协议 一系列协议 按照功能不同 分工不同 人为的分层七层 实际上这个七层是不存在的 没有这七层的概念 只是人为的划分而已 区分出来的目的只是让你明白哪一层是干什么用的 每一层都运行不同的协
  • Python的学习记录

    Python The Zen of Python by Tim Peters Beautiful is better than ugly Explicit is better than implicit Simple is better t
  • Docker安装与配置阿里云镜像加速

    一 Docker简介 1 Docker是什么 产生背景 开发和运维之间因为环境不同而导致的矛盾 不同的操作系统 软件环境 应用配置等 DevOps 集群环境下每台服务器都配置相同的环境 太麻烦 解决 在我的机器上可以正常工作 的问题 Doc
  • Java IO流处理 面试题汇总

    说明 本节内容来源于网络汇总 输入输出流是相对于内存而言的 1 面试题汇总 1 java中有几种类型的流 2021 08 19更新 从编码方式上 分为字符流和字节流 如上图可知 字节流继承inputStream和OutputStream 字
  • 第二章:25+ Python 数据操作教程(第十三节NUMPY 教程与练习)持续更新

    NumPy Numerical Python 或 Numeric Python 的缩写 是 Python 中对数组和矩阵进行快速数学计算的最基本的软件包之一 在处理多维数据时它也非常有用 集成C C 和FORTRAN工具是一件幸事 它还提供
  • vue路由在使用keep-alive缓存之后第二次进入页面created和mounted不执行问题及解决

    一 keep alive介绍 1 什么是 keep alive Vue js 中 keep alive 是一个内置组件 可以在需要缓存的组件上添加keep alive 标签 使得这个组件被缓存起来 不会被多次渲染和销毁 keep alive
  • pytest 用例依赖

    应用场景 1 创建订单之前 需要先添加购物车 2 在执行订单接口用例之前 要保证添加购物车接口用例完成 并且是pass 应用办法 可以使用pytest插件 pytest插件介绍 1 官方插件地址介绍 https docs pytest or
  • 【Lua基础系列】rawset & rawget方法

    Lua基础系列 rawset rawget方法 大家好 我是Lampard 欢迎来到Lua基础系列的博客 前文再续 书接上一回 今天和大家讲解一下lua中的rawset rawget方法 Lua基础系列 index元方法 Lua基础系列 n
  • HTML表单学习之单选按钮却可以多选的问题及解决办法

    出现这种情况的原因是如图所示 按钮没有分组 通过在input元素中添加name属性可以将两个按钮设置为同一组 单选按钮正常使用的代码如下所示
  • Mybatis配置解析-03

    1 核心配置文件 JDBC中连接数据库的URL转义 jdbc mysql localhost 3306 mybatis useSSL true amp useUnicode true amp characterEncoding utf8 m
  • 传统@ServerEndpoint方式开发WebSocket应用和SpringBoot构建WebSocket应用程序

    小Hub领读 通过websocket的两种使用方式 让你更加深入理解用法 很多人不懂websocket怎么辨别是谁发送过来的 文中说到实现WebSocketConfigurer接口 定义拦截器可以绑定用户信息 还有其他很多 细细品 对比看比