spring boot 实现直播聊天室(二)
spring boot 实现直播聊天室(二)
技术方案:
- spring boot
- netty
- rabbitmq
目录结构
引入依赖
<dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId><version>4.1.96.Final</version>
</dependency>
SimpleNettyWebsocketServer
netty server 启动类
@Slf4j
public class SimpleNettyWebsocketServer {private SimpleWsHandler simpleWsHandler;public SimpleNettyWebsocketServer(SimpleWsHandler simpleWsHandler) {this.simpleWsHandler = simpleWsHandler;}public void start(int port) throws InterruptedException {NioEventLoopGroup boss = new NioEventLoopGroup(1);NioEventLoopGroup work = new NioEventLoopGroup(2);try {ServerBootstrap bootstrap = new ServerBootstrap();bootstrap.group(boss, work).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<NioSocketChannel>() {@Overrideprotected void initChannel(NioSocketChannel ch) throws Exception {ChannelPipeline pipeline = ch.pipeline();//HTTP协议编解码器,用于处理HTTP请求和响应的编码和解码。其主要作用是将HTTP请求和响应消息转换为Netty的ByteBuf对象,并将其传递到下一个处理器进行处理。pipeline.addLast(new HttpServerCodec());//用于HTTP服务端,将来自客户端的HTTP请求和响应消息聚合成一个完整的消息,以便后续的处理。pipeline.addLast(new HttpObjectAggregator(65535));pipeline.addLast(new IdleStateHandler(30,0,0));//处理请求参数pipeline.addLast(new SimpleWsHttpHandler());pipeline.addLast(new WebSocketServerProtocolHandler("/n/ws"));pipeline.addLast(simpleWsHandler);}});Channel channel = bootstrap.bind(port).sync().channel();log.info("server start at port: {}", port);channel.closeFuture().sync();} finally {boss.shutdownGracefully();work.shutdownGracefully();}}
}
NettyUtil: 工具类
public class NettyUtil {public static AttributeKey<String> G_U = AttributeKey.valueOf("GU");/*** 设置上下文参数* @param channel* @param attributeKey* @param data* @param <T>*/public static <T> void setAttr(Channel channel, AttributeKey<T> attributeKey, T data) {Attribute<T> attr = channel.attr(attributeKey);if (attr != null) {attr.set(data);}}/*** 获取上下文参数 * @param channel* @param attributeKey* @return* @param <T>*/public static <T> T getAttr(Channel channel, AttributeKey<T> attributeKey) {return channel.attr(attributeKey).get();}/*** 根据 渠道获取 session* @param channel* @return*/public static NettySimpleSession getSession(Channel channel) {String attr = channel.attr(G_U).get();if (StrUtil.isNotBlank(attr)){String[] split = attr.split(",");String groupId = split[0];String username = split[1];return new NettySimpleSession(channel.id().toString(),groupId,username,channel);}return null;}
}
处理handler
SimpleWsHttpHandler
处理 websocket 协议升级时地址请求参数 ws://127.0.0.1:8881/n/ws?groupId=1&username=tom
, 解析groupId 和 username ,并设置这个属性到上下文
/*** @Date: 2023/12/13 9:53* 提取参数*/
@Slf4j
public class SimpleWsHttpHandler extends ChannelInboundHandlerAdapter {@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {if (msg instanceof FullHttpRequest request){//ws://localhost:8080/n/ws?groupId=xx&username=tomString decode = URLDecoder.decode(request.uri(), StandardCharsets.UTF_8);log.info("raw request url: {}", decode);Map<String, String> queryMap = getParams(decode);String groupId = MapUtil.getStr(queryMap, "groupId", null);String username = MapUtil.getStr(queryMap, "username", null);if (StrUtil.isNotBlank(groupId) && StrUtil.isNotBlank(username)) {NettyUtil.setAttr(ctx.channel(), NettyUtil.G_U, groupId.concat(",").concat(username));}//去掉参数 ===> ws://localhost:8080/n/wsrequest.setUri(request.uri().substring(0,request.uri().indexOf("?")));ctx.pipeline().remove(this);ctx.fireChannelRead(request);}else{ctx.fireChannelRead(msg);}}/*** 解析 queryString* @param uri* @return*/public static Map<String, String> getParams(String uri) {Map<String, String> params = new HashMap<>(10);int idx = uri.indexOf("?");if (idx != -1) {String[] paramsArr = uri.substring(idx + 1).split("&");for (String param : paramsArr) {idx = param.indexOf("=");params.put(param.substring(0, idx), param.substring(idx + 1));}}return params;}
}
SimpleWsHandler
处理消息
@Slf4j
@ChannelHandler.Sharable
public class SimpleWsHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {@Autowiredprivate PushService pushService;/*** 在新的 Channel 被添加到 ChannelPipeline 中时被调用。这通常发生在连接建立时,即 Channel 已经被成功绑定并注册到 EventLoop 中。* 在连接建立时被调用一次** @param ctx* @throws Exception*/@Overridepublic void handlerAdded(ChannelHandlerContext ctx) throws Exception {NettySimpleSession session = NettyUtil.getSession(ctx.channel());if (session == null) {log.info("handlerAdded channel id: {}", ctx.channel().id());} else {log.info("handlerAdded channel group-username: {}-{}", session.group(), session.identity());}}/*** 连接断开时,Netty 会自动触发 channelInactive 事件,并将该事件交给事件处理器进行处理* 在 channelInactive 事件的处理过程中,会调用 handlerRemoved 方法** @param ctx* @throws Exception*/@Overridepublic void handlerRemoved(ChannelHandlerContext ctx) throws Exception {NettySimpleSession session = NettyUtil.getSession(ctx.channel());if (session!=null){log.info("handlerRemoved channel group-username: {}-{}", session.group(), session.identity());}offline(ctx.channel());}@Overrideprotected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {//todo msg 可以是json字符串,这里仅仅只是纯文本NettySimpleSession session = NettyUtil.getSession(ctx.channel());if (session!=null){MessageDto messageDto = new MessageDto();messageDto.setSessionId(session.getId());messageDto.setGroup(session.group());messageDto.setFromUser(session.identity());messageDto.setContent(msg.text());pushService.pushGroupMessage(messageDto);}else {log.info("channelRead0 session is null channel id: {}-{}", ctx.channel().id(),msg.text());}}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {log.info("SimpleWsHandler 客户端异常断开 {}", cause.getMessage());//todo offlineoffline(ctx.channel());}@Overridepublic void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {if (evt instanceof IdleStateEvent idleStateEvent) {if (idleStateEvent.state().equals(IdleStateEvent.READER_IDLE_STATE_EVENT)) {log.info("SimpleWsIdleHandler channelIdle 5 秒未收到客户端消息,强制关闭: {}", ctx.channel().id());//todo offlineoffline(ctx.channel());}} else if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {String attr = NettyUtil.getAttr(ctx.channel(), NettyUtil.G_U);if (StrUtil.isBlank(attr)) {ctx.writeAndFlush("参数异常");offline(ctx.channel());} else {//todo 可以做用户认证等等//记录用户登陆sessionNettySimpleSession session = NettyUtil.getSession(ctx.channel());Assert.notNull(session, "session 不能为空");SessionRegistry.getInstance().addSession(session);}}super.userEventTriggered(ctx,evt);}/*** 用户下线,处理失效 session* @param channel*/public void offline(Channel channel){NettySimpleSession session = NettyUtil.getSession(channel);if (session!=null){SessionRegistry.getInstance().removeSession(session);}channel.close();}}
PushService
推送服务抽取
public interface PushService {/*** 组推送* @param messageDto*/void pushGroupMessage(MessageDto messageDto);}@Service
public class PushServiceImpl implements PushService {@Autowiredprivate MessageClient messagingClient;@Overridepublic void pushGroupMessage(MessageDto messageDto) {messagingClient.sendMessage(messageDto);}
}
NettySimpleSession
netty session 封装
public class NettySimpleSession extends AbstractWsSession {private Channel channel;public NettySimpleSession(String id, String group, String identity, Channel channel) {super(id, group, identity);this.channel = channel;}@Overridepublic void sendTextMessage(MessageDto messageDto) {String content = messageDto.getFromUser() + " say: " + messageDto.getContent();// 不能直接 write content, channel.writeAndFlush(content);// 要封装成 websocketFrame,不然不能编解码!!!channel.writeAndFlush(new TextWebSocketFrame(content));}
}
启动类
@Slf4j
@SpringBootApplication
public class DemoApplication {public static void main(String[] args) {SpringApplication.run(DemoApplication.class, args);}@Beanpublic SimpleWsHandler simpleWsHandler(){return new SimpleWsHandler();}@PostConstructpublic void init() {new Thread(() -> {log.info(">>>>>>>> start netty ws server....");try {new SimpleNettyWebsocketServer(simpleWsHandler()).start(8881);} catch (InterruptedException e) {log.info(">>>>>>>> SimpleNettyWebsocketServer start error", e);}}).start();}}
其他代码参考 spring boot 实现直播聊天室
测试
websocket 地址 ws://127.0.0.1:8881/n/ws?groupId=1&username=tom
good luck!
相关文章:

spring boot 实现直播聊天室(二)
spring boot 实现直播聊天室(二) 技术方案: spring bootnettyrabbitmq 目录结构 引入依赖 <dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId><version>4.1.96.Final</version> </dependency>Si…...
alibaba fastjson GET List传参 和 接收解析
之前一直都是 get传的都是单字符串(例如 xxxxxxxxx?name{name};name“woaini”;),并没有传list的. GET List传参 问题场景 String url"xxxxxxxx?id{id}"; HashMap<String,Object> param new HashMap<>(); param.pu…...

API自动化测试是什么?我们该如何做API自动化测试呢?
API测试已经成为测试工作中的常规任务之一。为了提高测试效率并减少重复的手工操作,API自动化测试变得越来越重要。本文总结了API自动化测试方面的经验和心得,旨在与读者分享。 掌握自动化技能已经成为高级测试工程师的必备技能。敏捷和持续测试改变了传…...

PyTorch 的 10 条内部用法
欢迎阅读这份有关 PyTorch 原理的简明指南[1]。无论您是初学者还是有一定经验,了解这些原则都可以让您的旅程更加顺利。让我们开始吧! 1. 张量:构建模块 PyTorch 中的张量是多维数组。它们与 NumPy 的 ndarray 类似,但可以在 GPU …...

Django、Echarts异步请求、动态更新
前端页面 <!DOCTYPE html> <html><head><meta charset"utf-8"><title>echarts示例</title> <script src"jquery.min.js"></script><script type "text/javascript" src "echarts.m…...

Mac部署Odoo环境-Odoo本地环境部署
Odoo本地环境部署 安装Python安装Homebrew安装依赖brew install libxmlsec1 Python运行环境Pycharm示例配置 Mac部署Odoo环境-Odoo本地环境部署 安装Python 新机,若系统没有预装Python,则安装需要版本的Python 点击查询Python官网下载 安装Homebrew 一…...
【✅面试编程题:如何用队列实现一个栈】
✅面试编程题:如何用队列实现一个栈 💡典型回答 💡典型回答 使用两个队列可以实现一个栈,一个队列用来存储栈中的元素,另一个队列用来在pop操作时暂存元素。 上才艺: import java.util.LinkedList; impo…...

Windows本地的RabbitMQ服务怎么在Docker for Windows的容器中使用
1. 进入管理界面 windows安装过程请访问:Windows安装RabbitMQ、添加PHP的AMQP扩展 浏览器访问:http://127.0.0.1:15672/ 2. 创建虚拟主机 上面访问的是 RabbitMQ 的管理界面,可以在这个界面上进行一些操作,比如创建虚拟主机、…...

YOLOv5改进 | 2023卷积篇 | AKConv轻量级架构下的高效检测(既轻量又提点)
一、本文介绍 本文给大家带来的改进内容是AKConv是一种创新的变核卷积,它旨在解决标准卷积操作中的固有缺陷(采样形状是固定的),AKConv的核心思想在于它为卷积核提供了任意数量的参数和任意采样形状,能够使用任意数量…...

微信小程序:模态框(弹窗)的实现
效果 wxml <!--新增(点击按钮)--> <image classimg src"{{add}}" bindtapadd_mode></image> <!-- 弹窗 --> <view class"modal" wx:if"{{showModal}}"><view class"modal-conten…...

uniapp交互反馈api的使用示例
官方文档链接:uni.showToast(OBJECT) | uni-app官网 1.uni.showToast({}) 显示消息提示框。 常用属性: title:页面提示的内容 image:改变提示框默认的icon图标 duration:提示框在页面显示多少秒才让它消失 添加了image属性后。 注…...

XUbuntu22.04之HDMI显示器设置竖屏(一百九十八)
简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 优质专栏:多媒…...

如何用 Cargo 管理 Rust 工程系列 甲
以下内容为本人的学习笔记,如需要转载,请声明原文链接 微信公众号「ENG八戒」https://mp.weixin.qq.com/s/ceMTUzRjDoiLwjn_KfZSrg 这几年 Rust 可谓是炙手可热的新兴编程语言了,而且被投票为最受程序员喜爱的语言。它很现代,专门…...
Windows下ping IP+端口的方法
有两种方法: 1. windows 开通 telnet 参考: https://zhuanlan.zhihu.com/p/570982111 2. 安装插件 参考:Windows下ping IP端口的方法 推荐使用第二种。...
【python】os.getcwd()函数详解和示例
os.getcwd() 是 Python 的一个内建函数,用于获取当前工作目录的路径。这个函数属于 os 模块,需要导入这个模块才能使用它。 import os data_rootos.path.abspath(os.path.join(os.getcwd(),"../.."))# get data root path data_root1os.path.…...
Linux(二十一)——virtualenv安装成功之后,依然提示未找到命令(-bash: virtualenv: 未找到命令)
Linux(二十一)——virtualenv安装成功之后,依然提示未找到命令(-bash: virtualenv: 未找到命令) 解决办法: 创建软连接 ln -s /usr/local/python3/bin/virtualenv /usr/bin/virtualenv...

RNN介绍及Pytorch源码解析
介绍一下RNN模型的结构以及源码,用作自己复习的材料。 RNN模型所对应的源码在:\PyTorch\Lib\site-packages\torch\nn\modules\RNN.py文件中。 RNN的模型图如下: 源码注释中写道,RNN的数学公式: 表示在时刻的隐藏状态…...

Qt 文字描边(基础篇)
项目中有时需要文字描边的功能 1.基础的绘制文字 使用drawtext处理 void MainWindow::paintEvent(QPaintEvent *event) {QPainter painter(this);painter.setRenderHint(QPainter::Antialiasing, true);painter.setRenderHint(QPainter::SmoothPixmapTransform, true);painte…...
.360勒索病毒解密方法|勒索病毒解决|勒索病毒恢复|数据库修复
导言: 在数字化时代,.360勒索病毒如影随形,威胁个人和组织的数据安全。本文将深入介绍.360病毒的特征、威胁,以及如何有效地恢复被加密的数据文件,同时提供预防措施,助您更好地保护数字资产。如不幸感染这…...

Nginx(四层+七层代理)+Tomcat实现负载均衡、动静分离
一、Tomcat多实例部署 具体步骤请看我之前的博客 写文章-CSDN创作中心https://mp.csdn.net/mp_blog/creation/editor/134956765?spm1001.2014.3001.9457 1.1 访问测试多实例的部署 1.2 分别在三个tomcat服务上部署jsp的动态页面 mkdir /usr/local/tomcat/webapps/test vim …...
基于算法竞赛的c++编程(28)结构体的进阶应用
结构体的嵌套与复杂数据组织 在C中,结构体可以嵌套使用,形成更复杂的数据结构。例如,可以通过嵌套结构体描述多层级数据关系: struct Address {string city;string street;int zipCode; };struct Employee {string name;int id;…...

测试微信模版消息推送
进入“开发接口管理”--“公众平台测试账号”,无需申请公众账号、可在测试账号中体验并测试微信公众平台所有高级接口。 获取access_token: 自定义模版消息: 关注测试号:扫二维码关注测试号。 发送模版消息: import requests da…...

51c自动驾驶~合集58
我自己的原文哦~ https://blog.51cto.com/whaosoft/13967107 #CCA-Attention 全局池化局部保留,CCA-Attention为LLM长文本建模带来突破性进展 琶洲实验室、华南理工大学联合推出关键上下文感知注意力机制(CCA-Attention),…...
深入浅出:JavaScript 中的 `window.crypto.getRandomValues()` 方法
深入浅出:JavaScript 中的 window.crypto.getRandomValues() 方法 在现代 Web 开发中,随机数的生成看似简单,却隐藏着许多玄机。无论是生成密码、加密密钥,还是创建安全令牌,随机数的质量直接关系到系统的安全性。Jav…...

Keil 中设置 STM32 Flash 和 RAM 地址详解
文章目录 Keil 中设置 STM32 Flash 和 RAM 地址详解一、Flash 和 RAM 配置界面(Target 选项卡)1. IROM1(用于配置 Flash)2. IRAM1(用于配置 RAM)二、链接器设置界面(Linker 选项卡)1. 勾选“Use Memory Layout from Target Dialog”2. 查看链接器参数(如果没有勾选上面…...
【生成模型】视频生成论文调研
工作清单 上游应用方向:控制、速度、时长、高动态、多主体驱动 类型工作基础模型WAN / WAN-VACE / HunyuanVideo控制条件轨迹控制ATI~镜头控制ReCamMaster~多主体驱动Phantom~音频驱动Let Them Talk: Audio-Driven Multi-Person Conversational Video Generation速…...
4. TypeScript 类型推断与类型组合
一、类型推断 (一) 什么是类型推断 TypeScript 的类型推断会根据变量、函数返回值、对象和数组的赋值和使用方式,自动确定它们的类型。 这一特性减少了显式类型注解的需要,在保持类型安全的同时简化了代码。通过分析上下文和初始值,TypeSc…...

逻辑回归暴力训练预测金融欺诈
简述 「使用逻辑回归暴力预测金融欺诈,并不断增加特征维度持续测试」的做法,体现了一种逐步建模与迭代验证的实验思路,在金融欺诈检测中非常有价值,本文作为一篇回顾性记录了早年间公司给某行做反欺诈预测用到的技术和思路。百度…...

ubuntu22.04有线网络无法连接,图标也没了
今天突然无法有线网络无法连接任何设备,并且图标都没了 错误案例 往上一顿搜索,试了很多博客都不行,比如 Ubuntu22.04右上角网络图标消失 最后解决的办法 下载网卡驱动,重新安装 操作步骤 查看自己网卡的型号 lspci | gre…...

图解JavaScript原型:原型链及其分析 | JavaScript图解
忽略该图的细节(如内存地址值没有用二进制) 以下是对该图进一步的理解和总结 1. JS 对象概念的辨析 对象是什么:保存在堆中一块区域,同时在栈中有一块区域保存其在堆中的地址(也就是我们通常说的该变量指向谁&…...