Springboot+Netty+WebSocket搭建简单的消息通知
Springboot+Netty+WebSocket搭建简单的消息通知
一、快速开始
1、添加依赖
<dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId><version>4.1.36.Final</version>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2、添加配置
spring:http:encoding:force: truecharset: UTF-8application:name: spring-cloud-study-websocket-reidsfreemarker:request-context-attribute: request#prefix: /templates/suffix: .htmlcontent-type: text/htmlenabled: truecache: falsecharset: UTF-8allow-request-override: falseexpose-request-attributes: trueexpose-session-attributes: trueexpose-spring-macro-helpers: true#template-loader-path: classpath:/templates/
3、添加启动类
@SpringBootApplication
public class WebSocketApplication {public static void main(String[] args) {SpringApplication.run(WebSocketApplication.class);try {new NettyServer(12345).start();System.out.println("https://blog.csdn.net/moshowgame");System.out.println("http://127.0.0.1:6688/netty-websocket/index");}catch(Exception e) {System.out.println("NettyServerError:"+e.getMessage());}}
}
二、添加WebSocket部分代码
1、WebSocketServer
@Slf4j
@ServerEndpoint("/imserver/{userId}")
@Component
public class WebSocketServer {/**静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。*/private static AtomicInteger onlineCount = new AtomicInteger(0);/**concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。*/private static ConcurrentHashMap<String,WebSocketServer> webSocketMap = new ConcurrentHashMap<>();/**与某个客户端的连接会话,需要通过它来给客户端发送数据*/private Session session;/**接收userId*/private String userId="";/*** 连接建立成功调用的方法*/@OnOpenpublic void onOpen(Session session,@PathParam("userId") String userId) {this.session = session;this.userId=userId;if(webSocketMap.containsKey(userId)){webSocketMap.remove(userId);webSocketMap.put(userId,this);}else{webSocketMap.put(userId,this);onlineCount.incrementAndGet(); // 在线数加1}log.info("用户连接:"+userId+",当前在线人数为:" + onlineCount.get());try {sendMessage("连接成功");} catch (IOException e) {log.error("用户:"+userId+",网络异常!!!!!!");}}/*** 连接关闭调用的方法*/@OnClosepublic void onClose() {if(webSocketMap.containsKey(userId)){webSocketMap.remove(userId);onlineCount.decrementAndGet(); // 在线数减1}log.info("用户退出:"+userId+",当前在线人数为:" + onlineCount.get());}/*** 收到客户端消息后调用的方法** @param message 客户端发送过来的消息*/@OnMessagepublic void onMessage(String message, Session session) {log.info("用户消息:"+userId+",报文:"+message);//可以群发消息//消息保存到数据库、redisif(StrUtil.isNotBlank(message)){try {//解析发送的报文JSONObject jsonObject = JSON.parseObject(message);//追加发送人(防止串改)jsonObject.put("fromUserId",this.userId);String toUserId=jsonObject.getString("toUserId");//传送给对应toUserId用户的websocketif(StrUtil.isNotBlank(toUserId)&&webSocketMap.containsKey(toUserId)){webSocketMap.get(toUserId).sendMessage(jsonObject.toJSONString());}else{log.error("请求的userId:"+toUserId+"不在该服务器上");//否则不在这个服务器上,发送到mysql或者redis}}catch (Exception e){e.printStackTrace();}}}/**** @param session* @param error*/@OnErrorpublic void onError(Session session, Throwable error) {log.error("用户错误:"+this.userId+",原因:"+error.getMessage());error.printStackTrace();}/*** 实现服务器主动推送*/public void sendMessage(String message) throws IOException {this.session.getBasicRemote().sendText(message);}/*** 发送自定义消息* */public static void sendInfo(String message,@PathParam("userId") String userId) throws IOException {log.info("发送消息到:"+userId+",报文:"+message);if(StrUtil.isNotBlank(userId)&&webSocketMap.containsKey(userId)){webSocketMap.get(userId).sendMessage(message);}else{log.error("用户"+userId+",不在线!");}}public static synchronized AtomicInteger getOnlineCount() {return onlineCount;}
}
2、WebSocketConfig
@Configuration
public class WebSocketConfig { @Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter(); }
}
3、DemoController
import cn.vipthink.socket.server.WebSocketServer;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;import java.io.IOException;@RestController
public class DemoController {@GetMapping("index")public ResponseEntity<String> index(){return ResponseEntity.ok("请求成功");}@GetMapping("page")public ModelAndView page(){return new ModelAndView("index");}@RequestMapping("/push/{toUserId}")public ResponseEntity<String> pushToWeb(String message, @PathVariable String toUserId) throws IOException {WebSocketServer.sendInfo(message,toUserId);return ResponseEntity.ok("MSG SEND SUCCESS");}
}
6、添加templates/index.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><title>Netty-Websocket</title><script type="text/javascript">// by zhengkai.blog.csdn.netvar socket;if(!window.WebSocket){window.WebSocket = window.MozWebSocket;}if(window.WebSocket){socket = new WebSocket("ws://127.0.0.1:12345/ws");socket.onmessage = function(event){var ta = document.getElementById('responseText');ta.value += event.data+"\r\n";};socket.onopen = function(event){var ta = document.getElementById('responseText');ta.value = "Netty-WebSocket服务器。。。。。。连接 \r\n";};socket.onclose = function(event){var ta = document.getElementById('responseText');ta.value = "Netty-WebSocket服务器。。。。。。关闭 \r\n";};}else{alert("您的浏览器不支持WebSocket协议!");}function send(message){if(!window.WebSocket){return;}if(socket.readyState == WebSocket.OPEN){socket.send(message);}else{alert("WebSocket 连接没有建立成功!");}}</script>
</head>
<body>
<form onSubmit="return false;"><label>ID</label><input type="text" name="uid" value="${uid!!}" /> <br /><label>TEXT</label><input type="text" name="message" value="这里输入消息" /> <br /><br /> <input type="button" value="发送ws消息"onClick="send(this.form.uid.value+':'+this.form.message.value)" /><hr color="black" /><h3>服务端返回的应答消息</h3><textarea id="responseText" style="width: 1024px;height: 300px;"></textarea>
</form>
</body>
</html>
三、添加Netty部分
1、NettyServer
import cn.vipthink.socket.handler.WSWebSocketHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;public class NettyServer {private final int port;public NettyServer(int port) {this.port = port;}public void start() throws Exception {EventLoopGroup bossGroup = new NioEventLoopGroup();EventLoopGroup group = new NioEventLoopGroup();try {ServerBootstrap sb = new ServerBootstrap();sb.option(ChannelOption.SO_BACKLOG, 1024);sb.group(group, bossGroup) // 绑定线程池.channel(NioServerSocketChannel.class) // 指定使用的channel.localAddress(this.port)// 绑定监听端口.childHandler(new ChannelInitializer<SocketChannel>() { // 绑定客户端连接时候触发操作@Overrideprotected void initChannel(SocketChannel ch) throws Exception {System.out.println("收到新连接");//websocket协议本身是基于http协议的,所以这边也要使用http解编码器ch.pipeline().addLast(new HttpServerCodec());//以块的方式来写的处理器ch.pipeline().addLast(new ChunkedWriteHandler());ch.pipeline().addLast(new HttpObjectAggregator(8192));ch.pipeline().addLast(new WebSocketServerProtocolHandler("/ws", null, true, 65536 * 10));ch.pipeline().addLast(new WSWebSocketHandler());}});ChannelFuture cf = sb.bind().sync(); // 服务器异步创建绑定System.out.println(NettyServer.class + " 启动正在监听: " + cf.channel().localAddress());cf.channel().closeFuture().sync(); // 关闭服务器通道} finally {group.shutdownGracefully().sync(); // 释放线程池资源bossGroup.shutdownGracefully().sync();}}
}
2、WSChannelHandlerPool
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;/*** WSChannelHandlerPool* 通道组池,管理所有websocket连接*/
public class WSChannelHandlerPool {public WSChannelHandlerPool(){}public static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);}
3、WSWebSocketHandler
import cn.vipthink.socket.config.WSChannelHandlerPool;
import com.alibaba.fastjson.JSON;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;import java.util.HashMap;
import java.util.Map;public class WSWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {System.out.println("与客户端建立连接,通道开启!");//添加到channelGroup通道组WSChannelHandlerPool.channelGroup.add(ctx.channel());}@Overridepublic void channelInactive(ChannelHandlerContext ctx) throws Exception {System.out.println("与客户端断开连接,通道关闭!");//添加到channelGroup 通道组WSChannelHandlerPool.channelGroup.remove(ctx.channel());}@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {//首次连接是FullHttpRequest,处理参数 by zhengkai.blog.csdn.netif (null != msg && msg instanceof FullHttpRequest) {FullHttpRequest request = (FullHttpRequest) msg;String uri = request.uri();Map paramMap=getUrlParams(uri);System.out.println("接收到的参数是:"+ JSON.toJSONString(paramMap));//如果url包含参数,需要处理if(uri.contains("?")){String newUri=uri.substring(0,uri.indexOf("?"));System.out.println(newUri);request.setUri(newUri);}}else if(msg instanceof TextWebSocketFrame){//正常的TEXT消息类型TextWebSocketFrame frame=(TextWebSocketFrame)msg;System.out.println("客户端收到服务器数据:" +frame.text());sendAllMessage(frame.text());}super.channelRead(ctx, msg);}@Overrideprotected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {}private void sendAllMessage(String message){//收到信息后,群发给所有channelWSChannelHandlerPool.channelGroup.writeAndFlush( new TextWebSocketFrame(message));}private static Map getUrlParams(String url){Map<String,String> map = new HashMap<>();url = url.replace("?",";");if (!url.contains(";")){return map;}if (url.split(";").length > 0){String[] arr = url.split(";")[1].split("&");for (String s : arr){String key = s.split("=")[0];String value = s.split("=")[1];map.put(key,value);}return map;}else{return map;}}
}
四、启动服务
http://localhost:9999/demo/page
可以通过前端发送消息到后端,通过日志查看
相关文章:
Springboot+Netty+WebSocket搭建简单的消息通知
SpringbootNettyWebSocket搭建简单的消息通知 一、快速开始 1、添加依赖 <dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId><version>4.1.36.Final</version> </dependency> <dependency>…...
@RefreshScope静态变量注入
RefreshScope注解通常用于注入实例变量,而不是静态变量。由于静态变量与类直接关联,刷新操作无法直接影响它们。 如果你需要动态刷新静态变量的值,一种可行的方案是使用一个通过Value注解注入的实例变量,并在该实例变量的getter方…...
多维时序 | MATLAB实现SABO-CNN-GRU-Attention多变量时间序列预测
多维时序 | MATLAB实现SABO-CNN-GRU-Attention多变量时间序列预测 目录 多维时序 | MATLAB实现SABO-CNN-GRU-Attention多变量时间序列预测预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 多维时序 | MATLAB实现SABO-CNN-GRU-Attention多变量时间序列预测。 模型描…...
SAP 凭证项目文本 增强 demo2
SAP 凭证项目文本 增强 demo2 增强点 AC_DOCUMENT METHOD if_ex_ac_document~change_initial. DATA: ls_item TYPE accit, ls_exitem TYPE accit_sub, lv_sgtxt TYPE bseg-sgtxt, lv_bktxt TYPE bkpf-bktxt, lv_zuonr TYPE bseg-zuonr, lv_blart TYPE bkpf-blart, lv_zprono TY…...
一套基于C#语言开发的LIMS实验室信息管理系统源码
实验室信息管理系统(LIMS)是指帮助实验室组织和管理实验数据的计算机软件系统,它将实验室操作有机地组织在一起,以满足实验室工作流程的所有要求。它能以不同的方式支持实验室的工作,从简单的过程(如样品采集和入库)到复杂的流程(…...
kubesphere部署rocketmq5.x,并对外暴露端口
kubesphere是青云开源的k8s管理工具,用户可以方便的通过页面进行k8s部署的部署,rocketmq则是阿里开源的一款mq平台,现在版本为5.1.3版本,较比4.x版本的rocketmq有比较大的调整:比如客户端的轻量化(统一通过…...
5.8 汇编语言:汇编高效除法运算
通常情况下计算除法会使用div/idiv这两条指令,该指令分别用于计算无符号和有符号除法运算,但除法运算所需要耗费的时间非常多,大概需要比乘法运算多消耗10倍的CPU时钟,在Debug模式下,除法运算不会被优化,但…...
如何通过python来给手机发送一条短信?
要通过Python发送短信到手机,您可以使用不同的短信服务提供商的API。以下是一个使用Twilio和Sinch服务提供商的示例,您可以根据自己的选择来决定使用哪个。 使用Twilio发送短信: 首先,注册一个Twilio账户并获取您的账户SID、认证令牌和Twilio号码。 安装 twilio 包,如果您…...
无涯教程-PHP - IntlChar类
在PHP7中,添加了一个新的 IntlChar 类,该类试图公开其他ICU函数。此类定义了许多静态方法和常量,可用于操作unicode字符。使用此类之前,您需要先安装 Intl 扩展名。 <?phpprintf(%x, IntlChar::CODEPOINT_MAX);print (IntlCh…...
【Linux操作系统】Linux系统编程中信号捕捉的实现
在Linux系统编程中,信号是一种重要的机制,用于实现进程间通信和控制。当某个事件发生时,如用户按下CtrlC键,操作系统会向进程发送一个信号,进程可以捕获并相应地处理该信号。本篇博客将介绍信号的分类、捕获与处理方式…...
【PHP】基础语法变量常量
文章目录 PHP简介前置知识了解静态网站的特点动态网站特点 PHP基础语法代码标记注释语句分隔(结束)符变量变量的基本概念变量的使用变量命名规则预定义变量可变变量变量传值内存分区 常量基本概念常量定义形式命名规则使用形式系统常量魔术常量 PHP简介 PHP定义:一…...
Failed to resolve: com.github.mcxtzhang:SwipeDelMenuLayout:V1.3.0
在allprojects下的repositories闭包里面添加jcenter()和maven {url https://jitpack.io},具体可以看你的第三方框架需要添加什么仓库,大多数都只需要上面两个。 我的build.gradle(Project)完整内容如下: buildscript …...
常用 Python IDE 汇总(非常详细)从零基础入门到精通,看完这一篇就够了
写 Python 代码最好的方式莫过于使用集成开发环境(IDE)了。它们不仅能使你的工作更加简单、更具逻辑性,还能够提升编程体验和效率。 每个人都知道这一点。而问题在于,如何从众多选项中选择最好的 Python 开发环境。初级开发者往往…...
【Hive】HQL Map 『CRUD | 相关函数』
文章目录 1. Map 增删改查1.1 声明 Map 数据类型1.2 增1.3 删1.4 改1.5 查 2. Map 相关函数2.1 单个Map 3. Map 与 String3.1 Map 转 string3.2 string 转 Map 1. Map 增删改查 1.1 声明 Map 数据类型 语法:map<基本数据类型, 基本数据类型> 注意是<>…...
ELF修复基本工作原理
ELF修复基本工作原理 ELF(Executable and Linkable Format)是一种常见的可执行文件和可链接文件的格式,广泛用于Linux和UNIX系统中。ELF修复是指对ELF文件进行修改或修复,以确保其正确加载和执行。 ELF修复的基本工作原理如下: 识别ELF文件:首先,需要识别和验证目标文…...
matlab实现输出的几种方式(disp函数、fprintf函数、print函数)
matlab实现输出的几种方式(disp函数、fprintf函数、print函数) 输出为文本、文件、打印 1、disp函数 显示变量的值,如果变量包含空数组,则会返回 disp,但不显示任何内容。 矩阵 A [1 0]; disp(A)结果 字符串 S …...
C/C++数据库编程
文章目录 0. Mysql安装与开发环境配置1. win10 Navicat 连接虚拟机的MySQL需要关闭防火墙2. 由于找不到libmysql.dIl, 无法继续执行代码。重新安装程序可能会解决此问题。3. 测试连接数据库,并插入数据4. C封装MySQL增删改查操作 0. Mysql安装与开发环境配置 MySQL…...
通过python在unity里调用C#接口
log: 背景 最近在做虚拟人底层驱动sdk测试,因为后端使用的是C#,我个人更倾向于python编程辅助测试工作,测试sdk需要通过开发提供的接口方法文档,通过传测试场景参数调用方法进行单元测试 技术&工具 项目语言 C# 项目工具 unity 测试…...
C++笔记之左值与右值、右值引用
C笔记之左值与右值、右值引用 code review! 文章目录 C笔记之左值与右值、右值引用1.左值与右值2.右值引用——关于int&& r 10;3.右值引用——对比int&& r 10;和int& r 10;4.右值引用(rvalue reference)的概念 1.左值与右值 2.…...
JS逆向-某招聘平台token
前言 本文是该专栏的第56篇,后面会持续分享python爬虫干货知识,记得关注。 通常情况下,JS调试相对方便,只需要chrome或者一些抓包工具,扩展插件,就可以顺利完成逆向分析。目前加密参数的常用逆向方式大致可分为以下几种,一种是根据源码的生成逻辑还原加密代码,一种是补…...
RestClient
什么是RestClient RestClient 是 Elasticsearch 官方提供的 Java 低级 REST 客户端,它允许HTTP与Elasticsearch 集群通信,而无需处理 JSON 序列化/反序列化等底层细节。它是 Elasticsearch Java API 客户端的基础。 RestClient 主要特点 轻量级ÿ…...
MongoDB学习和应用(高效的非关系型数据库)
一丶 MongoDB简介 对于社交类软件的功能,我们需要对它的功能特点进行分析: 数据量会随着用户数增大而增大读多写少价值较低非好友看不到其动态信息地理位置的查询… 针对以上特点进行分析各大存储工具: mysql:关系型数据库&am…...
linux arm系统烧录
1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 (忘了有没有这步了 估计有) 刷机程序 和 镜像 就不提供了。要刷的时…...
Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决
Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决 问题背景 在一个基于 Spring Cloud Gateway WebFlux 构建的微服务项目中,新增了一个本地验证码接口 /code,使用函数式路由(RouterFunction)和 Hutool 的 Circle…...
AI,如何重构理解、匹配与决策?
AI 时代,我们如何理解消费? 作者|王彬 封面|Unplash 人们通过信息理解世界。 曾几何时,PC 与移动互联网重塑了人们的购物路径:信息变得唾手可得,商品决策变得高度依赖内容。 但 AI 时代的来…...
重启Eureka集群中的节点,对已经注册的服务有什么影响
先看答案,如果正确地操作,重启Eureka集群中的节点,对已经注册的服务影响非常小,甚至可以做到无感知。 但如果操作不当,可能会引发短暂的服务发现问题。 下面我们从Eureka的核心工作原理来详细分析这个问题。 Eureka的…...
Go 并发编程基础:通道(Channel)的使用
在 Go 中,Channel 是 Goroutine 之间通信的核心机制。它提供了一个线程安全的通信方式,用于在多个 Goroutine 之间传递数据,从而实现高效的并发编程。 本章将介绍 Channel 的基本概念、用法、缓冲、关闭机制以及 select 的使用。 一、Channel…...
Kubernetes 网络模型深度解析:Pod IP 与 Service 的负载均衡机制,Service到底是什么?
Pod IP 的本质与特性 Pod IP 的定位 纯端点地址:Pod IP 是分配给 Pod 网络命名空间的真实 IP 地址(如 10.244.1.2)无特殊名称:在 Kubernetes 中,它通常被称为 “Pod IP” 或 “容器 IP”生命周期:与 Pod …...
comfyui 工作流中 图生视频 如何增加视频的长度到5秒
comfyUI 工作流怎么可以生成更长的视频。除了硬件显存要求之外还有别的方法吗? 在ComfyUI中实现图生视频并延长到5秒,需要结合多个扩展和技巧。以下是完整解决方案: 核心工作流配置(24fps下5秒120帧) #mermaid-svg-yP…...
数据结构第5章:树和二叉树完全指南(自整理详细图文笔记)
名人说:莫道桑榆晚,为霞尚满天。——刘禹锡(刘梦得,诗豪) 原创笔记:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 上一篇:《数据结构第4章 数组和广义表》…...
