当前位置: 首页 > news >正文

【项目实战】基于netty-websocket-spring-boot-starter实现WebSocket服务器长链接处理

一、背景

项目中需要建立客户端与服务端之间的长链接,首先就考虑用WebSocket,再来SpringBoot原来整合WebSocket方式并不高效,因此找到了netty-websocket-spring-boot-starter 这款脚手架,它能让我们在SpringBoot中使用Netty来开发WebSocket服务器,并像spring-websocket的注解开发一样简单

二、netty-websocket-spring-boot-starter 入门介绍

2.1 核心注解

2.1.1 @ServerEndpoint

当ServerEndpointExporter类通过Spring配置进行声明并被使用,它将会去扫描带有@ServerEndpoint注解的类 被注解的类将被注册成为一个WebSocket端点 所有的配置项都在这个注解的属性中 ( 如:@ServerEndpoint(“/ws”) )

2.1.2 @OnOpen

当有新的WebSocket连接完成时,对该方法进行回调 注入参数的类型:Session、HttpHeaders…

2.1.3 @OnClose

当有WebSocket连接关闭时,对该方法进行回调 注入参数的类型:Session

2.1.4 @OnError

当有WebSocket抛出异常时,对该方法进行回调 注入参数的类型:Session、Throwable

2.1.5 @OnMessage

当接收到字符串消息时,对该方法进行回调 注入参数的类型:Session、String

2.2 核心配置

属性属性说明
pathWebSocket的path,也可以用value来设置
hostWebSocket的host,"0.0.0.0"即是所有本地地址
portWebSocket绑定端口号。如果为0,则使用随机端口(端口获取可见 多端点服务)
maxFramePayloadLength最大允许帧载荷长度
allIdleTimeSeconds与IdleStateHandler中的allIdleTimeSeconds一致,并且当它不为0时,将在pipeline中添加IdleStateHandler

三、实践netty-websocket-spring-boot-starter

3.1引入POM文件

主要添加包括以下依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId>
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope>
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency><dependency><groupId>org.yeauty</groupId><artifactId>netty-websocket-spring-boot-starter</artifactId><version>0.9.5</version>
</dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.4.6</version>
</dependency>

3.2 在主程序类中排除数据库使用

/*** 主程序启动类*/
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class WebsocketApplication {public static void main(String[] args) {SpringApplication.run(WebsocketApplication.class, args);}}

3.3 开启WebSocket支持

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

3.4 定义WebSocketServer服务器(核心代码)

在端点类上加上@ServerEndpoint注解,并在相应的方法上加上@OnOpen、@OnMessage、@OnError、@OnClose注解, 代码如下:

@ServerEndpoint(port = "${ws.port}", path = "/demo/{version}", maxFramePayloadLength = "6553600", allIdleTimeSeconds = "300")
public class WebSocketServer {private static Log LOGGER = LogFactory.get();// concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>();// 与某个客户端的连接会话,需要通过它来给客户端发送数据private Session session;// 接收用户IDprotected StringBuilder userInfo = new StringBuilder();@Autowiredprivate RedisTemplate<String, String> redisTemplate;/*** 连接建立成功调用的方法*/@OnOpenpublic void onOpen(Session session,HttpHeaders headers,@RequestParam String req,@RequestParam MultiValueMap<String, Object> reqMap,@PathVariable String arg,@PathVariable Map<String, Object> pathMap) {this.session = session;// 加入set中webSocketSet.add(this);// 在线数加1addOnlineCount();LOGGER.debug("UserId = {}, 通道ID={}, 当前连接人数={}", userInfo.toString(), getSessionId(session), getOnlineCount());}/*** 收到客户端消息后调用的方法*/@OnMessagepublic void onMessage(Session session, String message) {JSONObject jsonData = JSONUtil.parseObj(message);if (!jsonData.containsKey("command")) {LOGGER.debug("UserId = {}, 通道ID={}, 上行内容={}, 上行请求非法,缺少command参数, 处理结束",userInfo.toString(), getSessionId(session), message);return;}String userId = jsonData.getStr("userId");this.userInfo = new StringBuilder(userId);String command = jsonData.getStr("command");Class<?> service = Command.getService(command);if (Objects.isNull(service)) {errorMessage(command);LOGGER.error("UserId = {}, 通道ID={}, 解析指令执行出错!", userInfo.toString(), getSessionId(session));return;}LOGGER.info("UserId = {}, 通道ID={}, 处理类={}, 开始处理,请求内容={}",userInfo.toString(), getSessionId(session), service, jsonData.toString());BaseMessage baseMessage = getBaseMessage(service, session, command);if (baseMessage == null) {return;}try {jsonData.set("SessionId", getSessionId(session));JSON resp = baseMessage.handlerMessage(userInfo, jsonData);resp.putByPath("command", command);resp.putByPath("userId", userId);String value = resp.toString();//将结果写回客户端, 实现服务器主动推送ChannelFuture future = sendMessage(value);LOGGER.info("UserId = {}, 通道ID = {}, 返回内容 = {}, future = {}, 处理结束",userInfo.toString(), getSessionId(session), value, future.toString());} catch (Exception e) {LOGGER.error("UserId = {}, 通道ID={}, 解析执行出错信息={}", userInfo.toString(), getSessionId(session), e.getMessage());}}/*** 连接关闭调用的方法*/@OnClosepublic void onClose(Session session) {// 从set中删除webSocketSet.remove(this);// 在线数减1subOnlineCount();String userId = this.userInfo.toString();LOGGER.warn("UserId = {}, 通道ID = {}, 有一连接关闭!当前在线人数={}", userId, getSessionId(session), getOnlineCount());userInfo.delete(0, userInfo.length());if (ObjectUtil.isNotNull(userId)) {String keyStr = ConstDef.ONLINE_USER_TYPE + userId;redisTemplate.delete(keyStr);}session.close();}/*** 出错方法*/@OnErrorpublic void onError(Session session, Throwable cause) {if (Objects.nonNull(this.session) && Objects.nonNull(cause) && !(cause instanceof EOFException)) {LOGGER.error("UserId = {}, 通道ID={}, 出错信息={}", userInfo.toString(), this.session.id(), cause.toString());}if (Objects.nonNull(session) && session.isOpen()) {session.close();}}/*** 通过class获取Bean*/private BaseMessage getBaseMessage(Class<?> service, Session session, String command) {BaseMessage baseMessage;try {baseMessage = (BaseMessage) SpringUtils.getBean(service);} catch (Exception e) {LOGGER.error("UserId = {}, 通道ID = {}, 未找到协议头 = {} 的处理类", userInfo.toString(), getSessionId(session), service);errorMessage(command);return null;}return baseMessage;}/*** 获取通道ID*/private String getSessionId(Session session) {return session.id().asShortText();}/*** 协议错误*/public void errorMessage(String command) {JSONObject retObj = new JSONObject();retObj.set("code", ConstDef.ERROR_CODE_10001);retObj.set("msg", ConstDef.ERROR_CODE_10001_DESP);retObj.set("command", command);try {sendMessage(retObj.toString());} catch (IOException e) {LOGGER.error("UserId = {}, 通道ID={}, 解析执行出错信息={}", userInfo.toString(), getSessionId(session), e.getMessage());}}/*** 实现服务器主动推送*/public ChannelFuture sendMessage(String message) throws IOException {return this.session.sendText(message);}/*** 在线用户数*/public long getOnlineCount() {String onlineCountValue = redisTemplate.opsForValue().get(ConstDef.ONLINE_COUNT_KEY);if (StrUtil.isBlank(onlineCountValue) || !NumberUtil.isNumber(onlineCountValue)) {return 0L;}return Long.parseLong(onlineCountValue);}/*** 在线数+1*/private void addOnlineCount() {redisTemplate.opsForValue().increment(ConstDef.ONLINE_COUNT_KEY);}/*** 在线数-1*/private void subOnlineCount() {redisTemplate.opsForValue().decrement(ConstDef.ONLINE_COUNT_KEY);}
}

3.5 定义接口

/*** 消息处理接口*/
public interface BaseMessage {Log LOGGER = LogFactory.get();/*** 处理类、处理方法*/JSON handlerMessage(StringBuilder vin, JSONObject jsonData);
}

3.6 定义接口实现类 (业务处理逻辑)

该类是各业务的处理逻辑类,是接口类的具体实现。

@Component
@Configuration
public class QueryAllActivityListMessage implements BaseMessage {@Overridepublic JSON handlerMessage(StringBuilder userId, JSONObject jsonData) {LOGGER.debug("开始处理QueryAllActivityListMessage请求, 参数={}", JSONUtil.toJsonStr(jsonData));String resp = "我是服务器端返回的处理结果!";LOGGER.info("UserId = {}, param={}, QueryAllActivityListMessage回复 = {}", userId.toString(), jsonData, resp);JSONObject respStr = new JSONObject();return respStr.set("handleResult", resp);}
}

3.7 定义枚举Command

每增加一个业务接口的实现,就需要在这个枚举类注册一下。

/*** 指令-服务 枚举*/
public enum Command {/*** 业务1处理逻辑*/queryAllActivityList("queryAllActivityList", QueryAllActivityListMessage.class, "业务1处理逻辑");/*** 业务2处理逻辑*///略/*** 业务3处理逻辑*///略/*** 服务编码*/private String processCode;/*** 服务接口类*/private Class<?> service;/*** 接口描述*/private String desc;Command(String processCode, Class<?> service, String desc) {this.processCode = processCode;this.service = service;this.desc = desc;}public Class<?> getService() {return service;}public static Class<?> getService(String processCode) {for (Command command : Command.values()) {if (command.processCode.equals(processCode)) {return command.getService();}}return null;}
}

3.8 编写SpringUtils 工具类

用于搜索Bean, 通过class获取Bean

/*** SpringUtils 工具类,用于搜索*/
@Component
public class SpringUtils implements ApplicationContextAware {private static ApplicationContext applicationContext;@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {if (SpringUtils.applicationContext == null) {SpringUtils.applicationContext = applicationContext;}}/*** 获取applicationContext*/public static ApplicationContext getApplicationContext() {return applicationContext;}/*** 通过class获取Bean*/public static <T> T getBean(Class<T> clazz) {return getApplicationContext().getBean(clazz);}/*** 通过name获取 Bean.*/public static Object getBean(String name) {return getApplicationContext().getBean(name);}/*** 通过name,以及Clazz返回指定的Bean*/public static <T> T getBean(String name, Class<T> clazz) {return getApplicationContext().getBean(name, clazz);}}

3.9 定义常量定义类 + 返回码

/*** 常量定义类 + 返回码*/
public class ConstDef {/*** 返回码*/public static final int ERROR_CODE_10001 = 10001;public static final String ERROR_CODE_10001_DESP = "请求参数不合法";/*** 按UserId决定,用户在线类型,车机或者手机*/public static final String ONLINE_USER_TYPE = "ONLINE_USER_TYPE_";/*** 在线用户数*/public static final String ONLINE_COUNT_KEY = "ONLINE_COUNT_KEY";
}

四、功能验证

打开WebSocket客户端,连接到ws://127.0.0.1:9095/demo/1

从截图来看,WebSocket服务端能正常接受并处理来自客户端的请求,验证成功!

在这里插入图片描述

相关文章:

【项目实战】基于netty-websocket-spring-boot-starter实现WebSocket服务器长链接处理

一、背景 项目中需要建立客户端与服务端之间的长链接&#xff0c;首先就考虑用WebSocket&#xff0c;再来SpringBoot原来整合WebSocket方式并不高效&#xff0c;因此找到了netty-websocket-spring-boot-starter 这款脚手架&#xff0c;它能让我们在SpringBoot中使用Netty来开发…...

BC双驱、ChatGPT大火,AI独角兽撬开盈利大门?

配图来自Canva可画 放眼AI行业&#xff0c;各大AI玩家长期亏损、“钱”景堪忧。 回看过去一年&#xff0c;部分AI独角兽的亏损问题愈发尖锐——云从科技2022年净亏损同比扩大至8.5亿元&#xff1b;寒武纪2022年净亏损11.6亿元&#xff0c;较上年同期扩大41.4%&#xff1b;地平…...

1/4车、1/2车、整车悬架H2/H∞控制仿真合集

目录 前言 1. 1/4悬架系统 1.1数学模型 1.2 H2/H∞求解反馈阵阵 1.3仿真分析 2. 1/2悬架系统 2.1数学模型 2.2 H2/H∞求解反馈阵阵 2.3仿真分析 3. 整车悬架系统 3.1数学模型 整车7自由度主动悬架数学模型 3.2 H2/H∞求解反馈阵阵 3.3仿真分析 4.总结 参考文献 …...

Git使用教程、命令

Git使用教程、命令 基本配置 git的配置文件位置&#xff1a; win: c:\users\<userName>\.gitconfig linux: /home/<userName>/.gitconfig # 个人/etc/gitconfig # 系统全局# 修改git init时的默认分支为master&#x…...

《c++ primer笔记》第九章 顺序容器

前言 知识点很多&#xff0c;这里只记录遗忘的。从这章开始会对前面章节的内容进行一个扩充&#xff0c;如果以前的忘了读起来会有点吃力。总的来说&#xff0c;本章节难度不大。 文章目录一、概述二、容器库概览2.1容器定义和初始化2.2赋值三、顺序容器操作3.1添加元素3.2删除…...

QML动画(弹动和翻转效果)

Flickable&#xff08;弹动&#xff09; QML中提供了一个Flickable元素&#xff0c;可以将其子项设置在一个可以拖拽和弹动的界面上&#xff0c;使得子项目的视图可以滚动。在传统的用户界面中&#xff0c;可以使用标准控件&#xff08;如滚动条和箭头按钮&#xff09;滚动视图…...

GPS启动方式、定位速度、定位精度介绍

前面文章介绍了GPS定位基础知识 GPS定位知识介绍 (qq.com) 本文主要介绍GPS启动方式。 定位过程中最重要的辅助信息是时间、星历、位置。 根据辅助信息不同,...

深度学习零基础学习之路——第五章 个人数据集的制作

Python深度学习入门 第一章 Python深度学习入门之环境软件配置 第二章 Python深度学习入门之数据处理Dataset的使用 第三章 数据可视化TensorBoard和TochVision的使用 第四章 UNet-Family中Unet、Unet和Unet3的简介 第五章 个人数据集的制作 深度学习数据集的制作Python深度学…...

女神节 | PHP和Java算什么,女工程师才是最美最好的语言!

世界上第一个程序员是女性 第一个发现Bug的也是女性 在智领云有一群追求快乐和独立的女性工程师 她们多有魅力&#xff1f; 工位上她们专注于数据与代码 平日里郊游、瑜伽、插花、科学养娃一件不落 不仅用0和1编织数字世界 也在用心装点自己的生活 今天是国际劳动妇女节…...

【Python】装饰器

一、装饰器的作用 装饰器能够为已经存在的对象添加额外的功能。 二、什么是装饰器 装饰器本质是一个python函数&#xff0c;它可以让其他函数在不需要做任何代码变动的前提下增加额外功能&#xff0c;装饰器的返回值也是一个函数对象。 三、装饰器的应用场景 插入日志、性能…...

Spring事务及传播机制

概念 在MySQL中介绍过&#xff0c;当同一时间出现一起读写数据的情况&#xff0c;可能会导致最终的结果出错&#xff0c;因此可以使用事务来提高隔离级别 而Spring中也可以实现事务 手动添加事务 使用SpringBoot中的DataSourceTransactionManager对象可以获取事务&#xff0…...

43-Golang中的goroutine!!!

Golang中的goroutine进程和线程说明并发和并行并发并行Go协程和Go主线程案例小结goroutine的调度机制MPG模式基本介绍MPG模式运行的状态1MPG模式运行的状态2设置GOlang运行的CPU数不同 goroutine之间如何通讯使用全局变量加锁同步改进程序进程和线程说明 1.进程就是程序在操作…...

[深入理解SSD系列 闪存实战2.1.5] NAND FLASH基本读操作及原理_NAND FLASH Read Operation源码实现

前言 上面是我使用的NAND FLASH的硬件原理图,面对这些引脚,很难明白他们是什么含义, 下面先来个热身: 问1. 原理图上NAND FLASH只有数据线,怎么传输地址? 答1.在DATA0~DATA7上既传输数据,又传输地址 当ALE为高电平时传输的是地址, 问2. 从NAND FLASH芯片手册可知,要…...

pandas库中的read_csv函数读取数据时候的路径问题详解(ValueError: embedded null character)

read_csv()函数不仅是R语言中的一个读取csv文件的函数&#xff0c;也是pandas库中的一个函数。pandas是一个用于数据分析和处理的python库。它的read_csv函数可以读取csv文件里的数据&#xff0c;并将其转化为pandas里面的DataFrame对象。它由很多参数可以设置&#xff0c;例如…...

【量化交易笔记】4.移动平均值的实现

上一讲已经讲A股的数据下载到本地或保存数据库&#xff0c;我们可以随时使用。 移动平均MA(Moving Average) &#xff0c;是用统计分析的方法&#xff0c;将一定时期内的证券价格&#xff08;指数&#xff09;加以平均&#xff0c;并把不同时间的平均值连接起来&#xff0c;形成…...

2023年3月份的野兔在线工具系统版本更新

这个是野兔在线工具系统中文版更新&#xff0c;这次更新的功能&#xff0c;和修改的问题还是比较多的&#xff0c;也修复系统部分功能&#xff0c;应该也是目前市面上在线工具比较多的一个系统了。系统名称&#xff1a;野兔在线工具系统系统语言&#xff1a;中文版系统源码&…...

科技成果赋智中小企业深度行 边界无限靖云甲ADR入选十大优秀案例

近日&#xff0c;国家工业信息安全发展研究中心、青岛市工业和信息化局、青岛市民营经济发展局、青岛市即墨区人民政府、青岛蓝谷管理局联合举办的科技成果赋智中小企业“深度行”活动&#xff08;青岛站&#xff09;成功举办&#xff0c;同步举行了赋智“深度行”活动&#xf…...

我们的理性何处安放

每天工作压力和各种人相处都让我们非常忙碌&#xff0c;我们上大学&#xff0c;努力工作&#xff0c;都是想获得更好的人生场景&#xff0c;素养&#xff0c;提升自身的认知&#xff0c;这样就是对我们大多数人生最负责任。如何让自己理性与人为善&#xff0c;并能被人温柔以待…...

RecyclerView的详细使用

首先就是了解ListView和RecyclerView的区别1.ListView相比RecycleView的优点a.ListView实现添加HeaderView和FooderView有直接的方法b.分割线可以直接设置c.ListView实现onItemClickListence和onItemLongClickListence有直接的方法2.RecyclerView相比ListView的优点a.封装了Vie…...

一、向量及其线性运算

&#x1f64c;作者简介&#xff1a;数学与计算机科学学院出身、在职高校高等数学专任教师&#xff0c;分享学习经验、生活、 努力成为像代码一样有逻辑的人&#xff01; &#x1f319;个人主页&#xff1a;阿芒的主页 ⭐ 高等数学专栏介绍&#xff1a;本专栏系统地梳理高等数学…...

label-studio的使用教程(导入本地路径)

文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...

2025年能源电力系统与流体力学国际会议 (EPSFD 2025)

2025年能源电力系统与流体力学国际会议&#xff08;EPSFD 2025&#xff09;将于本年度在美丽的杭州盛大召开。作为全球能源、电力系统以及流体力学领域的顶级盛会&#xff0c;EPSFD 2025旨在为来自世界各地的科学家、工程师和研究人员提供一个展示最新研究成果、分享实践经验及…...

连锁超市冷库节能解决方案:如何实现超市降本增效

在连锁超市冷库运营中&#xff0c;高能耗、设备损耗快、人工管理低效等问题长期困扰企业。御控冷库节能解决方案通过智能控制化霜、按需化霜、实时监控、故障诊断、自动预警、远程控制开关六大核心技术&#xff0c;实现年省电费15%-60%&#xff0c;且不改动原有装备、安装快捷、…...

C++ Visual Studio 2017厂商给的源码没有.sln文件 易兆微芯片下载工具加开机动画下载。

1.先用Visual Studio 2017打开Yichip YC31xx loader.vcxproj&#xff0c;再用Visual Studio 2022打开。再保侟就有.sln文件了。 易兆微芯片下载工具加开机动画下载 ExtraDownloadFile1Info.\logo.bin|0|0|10D2000|0 MFC应用兼容CMD 在BOOL CYichipYC31xxloaderDlg::OnIni…...

CRMEB 中 PHP 短信扩展开发:涵盖一号通、阿里云、腾讯云、创蓝

目前已有一号通短信、阿里云短信、腾讯云短信扩展 扩展入口文件 文件目录 crmeb\services\sms\Sms.php 默认驱动类型为&#xff1a;一号通 namespace crmeb\services\sms;use crmeb\basic\BaseManager; use crmeb\services\AccessTokenServeService; use crmeb\services\sms\…...

毫米波雷达基础理论(3D+4D)

3D、4D毫米波雷达基础知识及厂商选型 PreView : https://mp.weixin.qq.com/s/bQkju4r6med7I3TBGJI_bQ 1. FMCW毫米波雷达基础知识 主要参考博文&#xff1a; 一文入门汽车毫米波雷达基本原理 &#xff1a;https://mp.weixin.qq.com/s/_EN7A5lKcz2Eh8dLnjE19w 毫米波雷达基础…...

日常一水C

多态 言简意赅&#xff1a;就是一个对象面对同一事件时做出的不同反应 而之前的继承中说过&#xff0c;当子类和父类的函数名相同时&#xff0c;会隐藏父类的同名函数转而调用子类的同名函数&#xff0c;如果要调用父类的同名函数&#xff0c;那么就需要对父类进行引用&#…...

若依登录用户名和密码加密

/*** 获取公钥&#xff1a;前端用来密码加密* return*/GetMapping("/getPublicKey")public RSAUtil.RSAKeyPair getPublicKey() {return RSAUtil.rsaKeyPair();}新建RSAUti.Java package com.ruoyi.common.utils;import org.apache.commons.codec.binary.Base64; im…...

加密通信 + 行为分析:运营商行业安全防御体系重构

在数字经济蓬勃发展的时代&#xff0c;运营商作为信息通信网络的核心枢纽&#xff0c;承载着海量用户数据与关键业务传输&#xff0c;其安全防御体系的可靠性直接关乎国家安全、社会稳定与企业发展。随着网络攻击手段的不断升级&#xff0c;传统安全防护体系逐渐暴露出局限性&a…...

node.js的初步学习

那什么是node.js呢&#xff1f; 和JavaScript又是什么关系呢&#xff1f; node.js 提供了 JavaScript的运行环境。当JavaScript作为后端开发语言来说&#xff0c; 需要在node.js的环境上进行当JavaScript作为前端开发语言来说&#xff0c;需要在浏览器的环境上进行 Node.js 可…...