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

Springboot项目集成Netty组件

系列文章目录

Springboot项目集成Netty组件
Netty新增解析数据包指定长度功能


文章目录

  • 系列文章目录
  • 前言
  • 一、Netty是什么?
  • 二、使用步骤
    • 1. 项目引入依赖
      • 1.1 项目基础版本信息:
      • 1.2 Netty依赖
    • 2. 项目配置
      • 2.1 在 yml 配置文件中配置以下:
      • 2.2 创建Netty配置类
    • 3. Netty 服务端
      • 3.1 Netty 服务端
      • 3.2 Netty channle注册类
      • 3.3 Netty 协议初始化解码器
      • 3.4 Netty 全局通道池
      • 3.5 WebSocket 协议通道数据处理
        • 3.5.1 WebSocket 处理URI参数
        • 3.5.2 WebSocket 数据处理类
      • 3.6 Socket 协议通道数据处理
      • 3.7 Netty 公共工具类
      • 3.8 Netty 启动类
  • 总结


前言

项目中有时候会需与其他客户端或者系统建立Sokcet连接 或者WebSocket连接,满足业务需求。Netty 是一个利用 Java 的高级网络的能力,隐藏其背后的复杂性而提供一个易于使用的 API 的客户端/服务器框架。我们可以便捷的利用此组件搭建自己的客户端/服务器框架,进行二次开发,满足自己的业务需求。

本文介绍了Springboot集成Netty的配置以及使用方式,项目是基于ruoyi分离版作为基础项目进行二次开发,如果需要完整项目,可以私聊我,如果有时间会整理出来。


一、Netty是什么?

Netty是由JBOSS提供的一个java开源框架,现为 Github上的独立项目。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
也就是说,Netty 是一个基于NIO的客户、服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户、服务端应用。Netty相当于简化和流线化了网络应用的编程开发过程,例如:基于TCP和UDP的socket服务开发。
“快速”和“简单”并不用产生维护性或性能上的问题。Netty 是一个吸收了多种协议(包括FTP、SMTP、HTTP等各种二进制文本协议)的实现经验,并经过相当精心设计的项目。最终,Netty 成功的找到了一种方式,在保证易于开发的同时还保证了其应用的性能,稳定性和伸缩性。

简单说: Netty是一个高性能、高可靠性的基于NIO封装的网络应用框架

二、使用步骤

1. 项目引入依赖

1.1 项目基础版本信息:

Java 版本1.8
SpringBoot 版本 2.2.13.RELEASE

1.2 Netty依赖

        <dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId><version>4.1.68.Final</version></dependency>

2. 项目配置

2.1 在 yml 配置文件中配置以下:

# netty-server 配置
netty:# 监听端口port: 8180# 队列池 - 最大队列数 200queue-max: 200# 线程队列容量 10pool-queue-init: 10# netty 请求路径path: /ws

2.2 创建Netty配置类

@Data
@Component
//application.yml此文件在ruoyi-admin模块中
@PropertySource(name="NettyProperties",value = {"classpath:application.yml"},ignoreResourceNotFound=false, encoding="UTF-8")
@ConfigurationProperties(prefix = "netty")
public class NettyProperties {private Integer port;private Integer queueMax;private String path;
}

3. Netty 服务端

3.1 Netty 服务端

import com.ruoyi.entrust.constant.NettyProperties;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;/*** @program: ruoyi* @Author: LiuZedi* @Date: 2021/12/26 18:18*/
@Slf4j
@Component
public class NettySocketServer {@Autowiredprivate NettyProperties properties;public void start() throws Exception {// Boss线程:由这个线程池提供的线程是boss种类的,用于创建、连接、绑定socket, (有点像门卫)然后把这些socket传给worker线程池。// 在服务器端每个监听的socket都有一个boss线程来处理。在客户端,只有一个boss线程来处理所有的socket。EventLoopGroup bossGroup = new NioEventLoopGroup();// Worker线程:Worker线程执行所有的异步I/O,即处理操作EventLoopGroup workgroup = new NioEventLoopGroup();try {// ServerBootstrap 启动NIO服务的辅助启动类,负责初始话netty服务器,并且开始监听端口的socket请求ServerBootstrap serverBootstrap = new ServerBootstrap();serverBootstrap.group(bossGroup, workgroup)// 设置非阻塞,用它来建立新accept的连接,用于构造serversocketchannel的工厂类.channel(NioServerSocketChannel.class)// ServerChannelInitializer 对出入的数据进行的业务操作,其继承ChannelInitializer.childHandler(new ServerChannelInitializer())//设置队列大小.option(ChannelOption.SO_BACKLOG, properties.getQueueMax())// 两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文.childOption(ChannelOption.SO_KEEPALIVE, true);ChannelFuture channelFuture = serverBootstrap.bind(properties.getPort()).sync();log.info("Netty服务器开启等待客户端连接, 监听端口:{}", properties.getPort());channelFuture.channel().closeFuture().sync();} catch (Exception e) {e.printStackTrace();} finally {// 关闭主线程组bossGroup.shutdownGracefully();// 关闭工作线程workgroup.shutdownGracefully();}}
}

3.2 Netty channle注册类

对出入的数据进行的业务操作进行初始化

目前通道支持连接协议 WebSocketSocket

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;/*** @program: ruoyi* @Author: LiuZedi* @Date: 2021/12/26 18:32** channle注册类*/
public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {@Overrideprotected void initChannel(SocketChannel ch)  {ChannelPipeline pipeline = ch.pipeline();//添加日志pipeline.addLast("logging",new LoggingHandler(LogLevel.WARN));// 判断 连接类型 此处 用来支持多种连接 目前支持 WebSocket 和 Socketpipeline.addLast("socketChoose",new SocketChooseHandler());}
}

3.3 Netty 协议初始化解码器

此类用于 解析多种协议,调用对应协议的处理器

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketFrameAggregator;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.CharsetUtil;
import lombok.extern.slf4j.Slf4j;import java.nio.ByteOrder;
import java.util.List;/*** @program: ruoyi* @Author: LiuZedi* @Date: 2022/1/18 10:34*** 协议初始化解码器.* 用来判定实际使用什么协议.*/
@Slf4j
public class SocketChooseHandler extends ByteToMessageDecoder {/** 默认暗号长度为23 */private static final int MAX_LENGTH = 23;/** WebSocket握手的协议前缀 */private static final String WEBSOCKET_PREFIX = "GET /";@Overrideprotected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {String protocol = getBufStart(in);in.resetReaderIndex();// 输出数据log.debug("获取的数据为: {}",protocol);if (protocol.startsWith(WEBSOCKET_PREFIX)) {log.info("解析 webSocket");//  websocket连接时,执行以下处理// HttpServerCodec:将请求和应答消息解码为HTTP消息ctx.pipeline().addLast("http-codec", new HttpServerCodec());// HttpObjectAggregator:将HTTP消息的多个部分合成一条完整的HTTP消息ctx.pipeline().addLast("aggregator", new HttpObjectAggregator(65535));// ChunkedWriteHandler:向客户端发送HTML5文件,文件过大会将内存撑爆ctx.pipeline().addLast("http-chunked", new ChunkedWriteHandler());ctx.pipeline().addLast("WebSocketAggregator", new WebSocketFrameAggregator(65535));//  若超过6000秒未收到约定心跳,则主动断开channel释放资源ctx.pipeline().addLast(new IdleStateHandler(6000,0,0));// 用于处理websocket URI参数ctx.pipeline().addLast(new WebSocketURIHandler());//用于处理websocket, /ws为访问websocket时的urictx.pipeline().addLast("ProtocolHandler", new WebSocketServerProtocolHandler("/ws"));ctx.pipeline().addLast(new WebSocketServerHandler());} else {log.info("解析 socket");//  常规TCP连接时,执行以下处理ctx.pipeline().addLast(new IdleStateHandler(60,0,0));// 数据读取使用小端模式ctx.pipeline().addLast("length-decoder",new LengthFieldBasedFrameDecoder(ByteOrder.LITTLE_ENDIAN,Integer.MAX_VALUE, 0, 4, 0, 4,true));ctx.pipeline().addLast(new StringEncoder(CharsetUtil.UTF_8));ctx.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));ctx.pipeline().addLast(new SocketServerHandler());}ctx.pipeline().remove(this.getClass());}private String getBufStart(ByteBuf in){int length = in.readableBytes();if (length > MAX_LENGTH) {length = MAX_LENGTH;}// 标记读位置in.markReaderIndex();byte[] content = new byte[length];in.readBytes(content);return new String(content);}
}

在处理 Socket协议的处理中,代码中使用了小端处理方案,具体原因可见我的另外一篇博客Netty新增解析数据包指定长度功能

3.4 Netty 全局通道池

由于作为Netty 服务器端,实际业务中会创建很多通道,有可能查找指定已有通道并进行数据交互。因此,开发此全局通道池,业务中可以进行保存通道,复用,以及查询通道进行交互等功能。

以下注释是 本人实际项目中的注释,理解后,根据实际项目进行对应修改,但是此类是通用

import io.netty.channel.Channel;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.AttributeKey;
import io.netty.util.concurrent.GlobalEventExecutor;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.stream.Collectors;/*** @program: ruoyi* @Author: LiuZedi* @Date: 2022/1/15 16:07*/
public class GlobalChannelPool {// 临时存放连接客户端public static ChannelGroup temporaryChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);// 存放已绑定用户信息连接客户端public static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);/*** 在集合中获取对应的通道** @param module  模块名 exam 和 entrust  可根据实际业务进行修改* @param protocol 连接类型 webSocket 和 socket  可根据实际业务进行修改* @param keyName  数据类型  exam模块key是mac | entrust模块key是uid   可根据实际业务进行修改* @param key 数据类型值 可根据实际业务进行修改* @return*/public static List<Channel> getChannelByKey(String module, String protocol ,String keyName, String key){if (StringUtils.isEmpty(module) && StringUtils.isEmpty(protocol) && StringUtils.isEmpty(keyName) && StringUtils.isEmpty(key)){return null;}List<Channel> channelList = null;String moduleName = "module";String protocolName = "protocol";try{// 先过滤 模块  examination | detection | entrustAttributeKey<String> moduleKey = AttributeKey.valueOf(moduleName);channelList = channels.stream().filter(channel -> channel.attr(moduleKey).get().equals(module)).collect(Collectors.toList());// 过滤 socket类型  webSocket 和 socketAttributeKey<String> protocolKey = AttributeKey.valueOf(protocolName);channelList = channelList.stream().filter(channel -> channel.attr(protocolKey).get().equals(protocol)).collect(Collectors.toList());// 过滤 数据类型  examination模块 身份证读卡器 key是mac | detection模块 身份证读卡器 key是mac | entrust模块 签名功能 key是signatureAttributeKey<String> dataKey = AttributeKey.valueOf(keyName);channelList = channelList.stream().filter(channel -> channel.attr(dataKey).get().equals(key)).collect(Collectors.toList());}catch (Exception e){return null;}return channelList;}/*** 通道添加属性 区分通道** @param channel 通道* @param module  模块名 exam 和 entrust* @param protocol 连接协议 webSocket 和 socket* @param keyName  数据类型  exam模块key是mac | entrust模块key是uid* @param key 数据类型值*/public static void addChannelAttrKey(Channel channel, String module, String protocol ,String keyName, String key){if (channel != null){// 存储 模块信息  examination | detection | entrustAttributeKey<String> moduleKey = AttributeKey.valueOf("module");channel.attr(moduleKey).set(module);// 存储 连接协议  webSocket 和 socketAttributeKey<String> protocolKey = AttributeKey.valueOf("protocol");channel.attr(protocolKey).set(protocol);// 存储 数据类型  examination模块 身份证读卡器 key是mac | detection模块 身份证读卡器 key是mac | entrust模块 签名功能 key是signatureAttributeKey<String> dataKey = AttributeKey.valueOf(keyName);channel.attr(dataKey).set(key);}}
}

3.5 WebSocket 协议通道数据处理

由于项目中,WebSocket请求链接可以携带参数,因此对于此参数需要提前处理,才可以进一步进行处理WebSocket链接中交互的数据。

3.5.1 WebSocket 处理URI参数

此函数用于处理 WebSocket中参数

import com.ruoyi.entrust.constant.NettyProperties;
import com.ruoyi.examination.utils.SpringContextUtil;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;/*** @program: ruoyi* @Author: LiuZedi* @Date: 2022/1/19 14:53*/
@Slf4j
public class WebSocketURIHandler extends SimpleChannelInboundHandler<WebSocketFrame> {private NettyProperties nettyProperties;public WebSocketURIHandler(){nettyProperties = (NettyProperties) SpringContextUtil.getBean("nettyProperties");}/*** 建立连接** channel 通道 action 活跃的 当客户端主动链接服务端的链接后,这个通道就是活跃的了。* 也就是客户端与服务端建立了通信通道并且可以传输数据*/@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {// 把新连接保存到临时连接表中GlobalChannelPool.temporaryChannels.add(ctx.channel());log.info("客户端与服务端连接开启:{},客户端名字:{}", ctx.channel().remoteAddress().toString(),ctx.name());}/*** 断开连接** channel 通道 Inactive 不活跃的 当客户端主动断开服务端的链接后,这个通道就是不活跃的。* 也就是说客户端与服务端关闭了通信通道并且不可以传输数据*/@Overridepublic void channelInactive(ChannelHandlerContext ctx) throws Exception {// 从连接列表中移除该连接GlobalChannelPool.channels.remove(ctx.channel());log.info("客户端与服务端连接关闭:{},客户端名字:{}", ctx.channel().remoteAddress().toString(),ctx.name());}@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {if (msg instanceof FullHttpRequest) {FullHttpRequest request = (FullHttpRequest) msg;String uri = request.uri();String origin = request.headers().get("Origin");log.info("uri:{},Origin:{}",uri,origin);Map<String, String> params = NettyUtil.getParams(uri);if (nettyProperties.getPath().equals(NettyUtil.getBasePath(uri))) {// 因为有可能携带了参数,导致客户端一直无法返回握手包,因此在校验通过后,重置请求路径Channel channel = ctx.channel();GlobalChannelPool.channels.add(channel);// 添加 AttributeKeyGlobalChannelPool.addChannelAttrKey(channel,params.get("module"),"webSocket",params.get("keyName"),params.get("key"));// 连接已绑定完成,把该链接移除临时存储区,避免积累过多,检索慢GlobalChannelPool.temporaryChannels.remove(channel);request.setUri(nettyProperties.getPath());log.info("webSocket处理完成,重新生成路径!!!");}else {log.warn("webSocket连接不正确。请求链接URI是:{}",uri);ctx.close();}}super.channelRead(ctx, msg);}@Overrideprotected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame msg) throws Exception {ctx.channel().writeAndFlush(msg.retain());}
}

3.5.2 WebSocket 数据处理类

package com.ruoyi.entrust.utils.netty;import io.netty.channel.*;
import io.netty.handler.codec.http.websocketx.*;
import lombok.extern.slf4j.Slf4j;/*** @program: ruoyi* @Author: LiuZedi* @Date: 2022/1/18 14:41*/@Slf4j
@ChannelHandler.Sharable
public class WebSocketServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {/*** 建立连接** channel 通道 action 活跃的 当客户端主动链接服务端的链接后,这个通道就是活跃的了。* 也就是客户端与服务端建立了通信通道并且可以传输数据*/@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {// 把新连接保存到临时连接表中GlobalChannelPool.temporaryChannels.add(ctx.channel());log.info("客户端与服务端连接开启:{},客户端名字:{}", ctx.channel().remoteAddress().toString(),ctx.name());}/*** 断开连接** channel 通道 Inactive 不活跃的 当客户端主动断开服务端的链接后,这个通道就是不活跃的。* 也就是说客户端与服务端关闭了通信通道并且不可以传输数据*/@Overridepublic void channelInactive(ChannelHandlerContext ctx) throws Exception {// 从连接列表中移除该连接GlobalChannelPool.channels.remove(ctx.channel());log.info("客户端与服务端连接关闭:{},客户端名字:{}", ctx.channel().remoteAddress().toString(),ctx.name());}/*** 接收客户端发送的消息 channel 通道 Read 读 简而言之就是从通道中读取数据,* 也就是服务端接收客户端发来的数据。但是这个数据在不进行解码时它是ByteBuf类型的*/@Overrideprotected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {log.info("接收到来自客户端的信息:{}", ctx.channel().remoteAddress().toString());if (msg != null) {// WebSocket消息处理String webSocketInfo = ((TextWebSocketFrame) msg).text().trim();log.info("收到webSocket消息:" + webSocketInfo);}else {  //数据处理为空log.warn("接收到来自客户端的信息:{}   warn:消息为空", ctx.channel().remoteAddress().toString());}}/**** channel 通道 Read 读取 Complete 完成 在通道读取完成后会在这个方法里通知,* 对应可以做刷新操作 ctx.flush()*/@Overridepublic void channelReadComplete(ChannelHandlerContext ctx) throws Exception {ctx.flush();}/*** exception 异常 Caught 抓住 抓住异常,当发生异常的时候,可以做一些相应的处理,比如打印日志、关闭链接*/@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {log.error("发生异常的连接是" + ctx.channel().id());log.error("netty服务端发送异常" + cause);cause.printStackTrace();ctx.close();}
}

3.6 Socket 协议通道数据处理

package com.ruoyi.entrust.utils.netty;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.constant.HttpStatus;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.entrust.constant.NettyDataParam;
import com.ruoyi.entrust.utils.JsonUtil;
import com.ruoyi.examination.domain.DetectionOrder;
import com.ruoyi.examination.domain.Order;
import com.ruoyi.examination.service.impl.DetectionOrderServiceImpl;
import com.ruoyi.examination.service.impl.ExaminationOrderServiceImpl;
import com.ruoyi.examination.utils.SpringContextUtil;
import io.netty.channel.*;
import com.ruoyi.examination.domain.Message;
import io.netty.handler.codec.http.websocketx.*;
import lombok.extern.slf4j.Slf4j;/*** @program: ruoyi* @Author: LiuZedi* @Date: 2022/1/17 21:09*/@Slf4j
@ChannelHandler.Sharable
public class SocketServerHandler extends SimpleChannelInboundHandler<Object> {/*** 建立连接** channel 通道 action 活跃的 当客户端主动链接服务端的链接后,这个通道就是活跃的了。* 也就是客户端与服务端建立了通信通道并且可以传输数据*/@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {// 把新连接保存到临时连接表中GlobalChannelPool.temporaryChannels.add(ctx.channel());log.info("客户端与服务端连接开启:{},客户端名字:{}", ctx.channel().remoteAddress().toString(),ctx.name());}/*** 断开连接** channel 通道 Inactive 不活跃的 当客户端主动断开服务端的链接后,这个通道就是不活跃的。* 也就是说客户端与服务端关闭了通信通道并且不可以传输数据*/@Overridepublic void channelInactive(ChannelHandlerContext ctx) throws Exception {// 从连接列表中移除该连接GlobalChannelPool.channels.remove(ctx.channel());log.info("客户端与服务端连接关闭:{},客户端名字:{}", ctx.channel().remoteAddress().toString(),ctx.name());}/*** 接收客户端发送的消息 channel 通道 Read 读 简而言之就是从通道中读取数据,* 也就是服务端接收客户端发来的数据。但是这个数据在不进行解码时它是ByteBuf类型的*/@Overrideprotected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {log.info("接收到来自客户端的信息:{}", ctx.channel().remoteAddress().toString());// WebSocket消息处理if (msg instanceof WebSocketFrame) {log.warn("Socket Handler 接收到WebSocket信息: 请检查!!!");}// Socket消息处理else{String requestData = (String)msg;log.info("收到socket消息:"+ requestData);}}/**** channel 通道 Read 读取 Complete 完成 在通道读取完成后会在这个方法里通知,* 对应可以做刷新操作 ctx.flush()*/@Overridepublic void channelReadComplete(ChannelHandlerContext ctx) throws Exception {ctx.flush();}/*** exception 异常 Caught 抓住 抓住异常,当发生异常的时候,可以做一些相应的处理,比如打印日志、关闭链接*/@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {log.error("发生异常的连接是" + ctx.channel().id());log.error("netty服务端发送异常" + cause);cause.printStackTrace();ctx.close();}
}

3.7 Netty 公共工具类

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.constant.HttpStatus;
import com.ruoyi.common.core.domain.AjaxResult;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import lombok.extern.slf4j.Slf4j;import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** @program: ruoyi* @Author: LiuZedi* @Date: 2022/1/18 16:59*/
@Slf4j
public class NettyUtil {/*** 将路径参数转换成Map对象,如果路径参数出现重复参数名,将以最后的参数值为准* @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;}/*** 获取URI中参数以外部分路径* @param uri* @return*/public static String getBasePath(String uri) {if (uri == null || uri.isEmpty())return null;int idx = uri.indexOf("?");if (idx == -1)return uri;return uri.substring(0, idx);}/*** 发布消息到对应 通道** @param channel 当前连接通道* @param message 所需发布消息* @param requestJsonObject 查询通道的参数* @param protocol 通道连接协议* @return 是否有对应通道*/public static boolean pushInfoToChannels(Channel channel, String message, JSONObject requestJsonObject, String protocol){boolean haveChannels = false;// 查询对应 连接通道List<Channel> channels = GlobalChannelPool.getChannelByKey(requestJsonObject.getString("module"),protocol,requestJsonObject.getString("keyName"),requestJsonObject.getString("key"));if (null == channels || channels.size() == 0){log.warn("没有发现对应{}客户端,模块为:{},请求连接模式为:{},KeyName为:{},Key为:{}", protocol,requestJsonObject.getString("module"),protocol,requestJsonObject.getString("keyName"),requestJsonObject.getString("key"));channel.writeAndFlush(JSON.toJSONString(AjaxResult.error(HttpStatus.NO_WEBSOCKET,"没有发现对应webSocket客户端")));}else {log.info("发现多个对应{}客户端,数量为:{},模块为:{},请求连接模式为:{},KeyName为:{},Key为:{}", protocol,channels.size(), requestJsonObject.getString("module"), protocol, requestJsonObject.getString("keyName"),requestJsonObject.getString("key"));haveChannels = true;}// 发送数据到对应的通道if (haveChannels){for (Channel ch: channels){ch.writeAndFlush(new TextWebSocketFrame(message));}}return haveChannels;}/*** 发布消息到对应 通道** @param message 所需发布消息* @param requestJsonObject 查询通道的参数* @param protocol 通道连接协议* @return 是否有对应通道*/public static boolean pushInfoToChannels(String message, JSONObject requestJsonObject, String protocol){boolean haveChannels = false;// 查询对应 连接通道List<Channel> channels = GlobalChannelPool.getChannelByKey(requestJsonObject.getString("module"),protocol,requestJsonObject.getString("keyName"),requestJsonObject.getString("key"));if (null == channels || channels.size() == 0){log.warn("没有发现对应{}客户端,模块为:{},请求连接模式为:{},KeyName为:{},Key为:{}", protocol,requestJsonObject.getString("module"),protocol,requestJsonObject.getString("keyName"),requestJsonObject.getString("key"));}else {log.info("发现多个对应{}客户端,数量为:{},模块为:{},请求连接模式为:{},KeyName为:{},Key为:{}", protocol,channels.size(), requestJsonObject.getString("module"), protocol, requestJsonObject.getString("keyName"),requestJsonObject.getString("key"));haveChannels = true;}// 发送数据到对应的通道if (haveChannels){for (Channel ch: channels){ch.writeAndFlush(new TextWebSocketFrame(message));}}return haveChannels;}
}

3.8 Netty 启动类

此处 Netty作为单独服务器启动,与SpringBoot使用端口并不一致

通过实现CommandLineRunner接口和添加@Component注解,使启动Netty服务器在项目启动之后执行

Spring boot的CommandLineRunner接口主要用于实现在应用初始化后,去执行一段代码块逻辑,这段初始化代码在整个应用生命周期内只会执行一次。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;/*** @program: ruoyi* @Author: LiuZedi* @Date: 2022/1/15 14:55*/
@Order(1)
@Component
public class NettyStart implements CommandLineRunner {@Autowiredprivate NettySocketServer nettySocketServer;@Overridepublic void run(String... args) throws Exception {nettySocketServer.start();}
}

总结

以上就是今天要讲的内容,本文介绍了Netty的使用,Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。我们可以快速使用此类方法进行使用开发自己的网络服务器。

之后我将写一篇文章,介绍如何使用Netty 实现 通过微信小程序 在网页上进行电子签字的功能。

相关文章:

Springboot项目集成Netty组件

系列文章目录 Springboot项目集成Netty组件 Netty新增解析数据包指定长度功能 文章目录系列文章目录前言一、Netty是什么&#xff1f;二、使用步骤1. 项目引入依赖1.1 项目基础版本信息&#xff1a;1.2 Netty依赖2. 项目配置2.1 在 yml 配置文件中配置以下&#xff1a;2.2 创建…...

python 中的import cfg问题

pip install cfg 报错: ERROR: Could not find a version that satisfies the requirement cfg (from versions: none) ERROR: No matching distribution found for cfg 要使用pip install cfg2才行...

[oeasy]python0088_字节_Byte_存储单位_KB_MB_GB_TB

编码进化 回忆上次内容 上次 回顾了 字符大战的结果 ibm 曾经的 EBCDIC 由于字符不连续的隐患 导致后续 出现 无数问题无法补救 7-bit 的 ASA X3.4-1963 字母序号连续 比较字符时 效率高判断字符 是否是字母 也很容易 获得了 IBM以外公司的 支持 为什么 ASA X3.4-1963 是 7…...

vue3.0 生命周期

目录前言&#xff1a;vue3.0生命周期图例1.beforeCreate2.created3.beforeMount/onBeforeMount4.mounted/onMounted5.beforeUpdate/onBeforeUpdate6.updated/onUpdated7.beforeUnmount/onBeforeUnmount8.unmounted/onUnmounted案例&#xff1a;总结前言&#xff1a; 每个Vue组…...

CGAL 数字类型

文章目录 一、简介二、内置数字类型三、CGAL中的数字类型参考资料一、简介 在CGAL汇总,数字类型必须满足特定的语法和语义要求,这样它们才能在CGAL代码中成功使用。一般来说,它们往往是代数结构概念的模型,如果它们对实数的子集模型,那么它们就也是RealEmbeddable模型。 二…...

如何将Python打包后的exe还原成.py?

将python打包好的exe解压为py文件&#xff0c;步骤如下&#xff1a;下载pyinstxtractor.py文件下载地址&#xff1a;https://nchc.dl.sourceforge.net/project/pyinstallerextractor/dist/pyinstxtractor.py并将pyinstxtractor.py放到和exe相同的目录文件下打开命令控制台cd 进…...

CJSON简单介绍

json简介 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。 易于人阅读和编写。同时也易于机器解析和生成。 它基于JavaScript Programming Language, Standard ECMA-262 3rd Edition - December 1999的一个子集&#xff0c;最新的定义可以参考ECMA-404_2nd_ed…...

算法训练营 day49 动态规划 爬楼梯 (进阶)零钱兑换 完全平方数

算法训练营 day49 动态规划 爬楼梯 &#xff08;进阶&#xff09;零钱兑换 完全平方数 爬楼梯 &#xff08;进阶&#xff09; 70. 爬楼梯 - 力扣&#xff08;LeetCode&#xff09; 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同…...

Vue:extends继承组件复用性

提到extends继承&#xff0c;最先想到的可能是ES6中的class、TS中的interface、面向对象编程语言中中的类和接口概念等等&#xff0c;但是我们今天的关注点在于&#xff1a;如何在Vue中使用extends继承特性。 目录 Vue&#xff1a;创建Vue实例的方式 构造函数方式&#xff1…...

ChatGPT 的一些思考

最近 ChatGPT3.5 在全世界范围内掀起了一次 AI 的潮流&#xff0c;ChatGPT1.0/ChatGPT2.0 当时也是比较火爆&#xff0c;但是那个当时感觉还是比较初级的应用&#xff0c;相当于是一个进阶版的微软小冰&#xff0c;给人的感觉是有一点智能&#xff0c;但不多。其实从早期版本开…...

GEE学习笔记 六十九:【GEE之Python版教程三】Python基础编程一

环境配置完成后&#xff0c;那么可以开始正式讲解编程知识。之前我在文章中也讲过&#xff0c;GEE的python版接口它是依赖python语言的。目前很多小伙伴是刚开始学习GEE编程&#xff0c;之前或者没有编程基础&#xff0c;或者是没有学习过python。为了照顾这批小伙伴&#xff0…...

大数据全系安装

内容版本号CentOS7.6.1810ZooKeeper3.4.6Hadoop2.9.1HBase1.2.0MySQL5.6.51HIVE2.3.7Sqoop1.4.6flume1.9.0kafka2.8.1scala2.12davinci3.0.1spark2.4.8flink1.13.5 1. 下载CentOS 7镜像 CentOS官网 2. 安装CentOS 7系统——采用虚拟机方式 2.1 新建虚拟机 2.2.1 [依次选择]-&…...

stable-diffusion-webui 安装使用

文章目录1.github 下载&#xff0c;按教程运行2.安装python 忘记勾选加入环境变量&#xff0c;自行加入&#xff08;重启生效&#xff09;3.环境变量添加后&#xff0c;清理tmp &#xff0c;venv重新运行4.运行报错&#xff0c;无法升级pip&#xff0c;无法下载包&#xff0c;5…...

3D点云处理:点云聚类--FEC: Fast Euclidean Clustering for Point Cloud Segmentation

文章目录 聚类结果一、论文内容1.1 Ground Surface Removal1.2 Fast Euclidean Clustering题外:欧几里得聚类Fast Euclidean Clustering二、参考聚类结果 原始代码中采用的是pcl中的搜索方式,替换为另外第三方库,速度得到进一步提升。 一、论文内容 论文中给出的结论:该…...

华为OD机试题 - 射击比赛(JavaScript)| 代码+思路+重要知识点

最近更新的博客 华为OD机试题 - 括号检查(JavaScript) 华为OD机试题 - 最小施肥机能效(JavaScript) 华为OD机试题 - 子序列长度(JavaScript) 华为OD机试题 - 众数和中位数(JavaScript) 华为OD机试题 - 服务依赖(JavaScript) 华为OD机试题 - 字符串加密(JavaScript)…...

流程引擎之Flowable简介

背景Flowable 是一个流行的轻量级的采用 Java 开发的业务流程引擎&#xff0c;通过 Flowable 流程引擎&#xff0c;我们可以部署遵循 BPMN2.0 协议的流程定义&#xff08;一般为XML文件&#xff09;文件&#xff0c;并能创建流程实例&#xff0c;查询和访问流程相关的实例与数据…...

AcWing:4861. 构造数列、4862. 浇花(C++)

目录 4861. 构造数列 问题描述&#xff1a; 实现代码&#xff1a; 4862. 浇花 问题描述&#xff1a; 实现代码&#xff1a; 4861. 构造数列 问题描述&#xff1a; 我们规定如果一个正整数满足除最高位外其它所有数位均为 00&#xff0c;则称该正整数为圆数。 例如&…...

进程的概念

进程的概念 程序的概念 这里说的是一个可执行文件&#xff0c;passive的意思可以理解为我们这个执行文件需要我们进行双击才会被被执行。 双击后&#xff0c;程序入口地址读入寄存器&#xff0c;程序加载入主存&#xff0c;成为一个进程 进程是主动去获取想要的资源&#xff0…...

自动化测试5年经验,分享一些心得

自动化测试介绍 自动化测试(Automated Testing)&#xff0c;是指把以人为驱动的测试行为转化为机器执行的过程。实际上自动化测试往往通过一些测试工具或框架&#xff0c;编写自动化测试用例&#xff0c;来模拟手工测试过程。比如说&#xff0c;在项目迭代过程中&#xff0c;持…...

independentsoft.de/MSG .NET Framework Crack

MSG .NET 是用于 .NET Framework / .NET Core 的 Microsoft Outlook .msg 文件 API。API 允许您轻松创建/读取/解析/转换 .msg 文件等。API 不需要在机器上安装 Microsoft Outlook 或任何其他第三方应用程序或库即可工作。 以下示例向您展示了如何打开现有文件并显示消息的某些…...

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…...

CTF show Web 红包题第六弹

提示 1.不是SQL注入 2.需要找关键源码 思路 进入页面发现是一个登录框&#xff0c;很难让人不联想到SQL注入&#xff0c;但提示都说了不是SQL注入&#xff0c;所以就不往这方面想了 ​ 先查看一下网页源码&#xff0c;发现一段JavaScript代码&#xff0c;有一个关键类ctfs…...

【JavaEE】-- HTTP

1. HTTP是什么&#xff1f; HTTP&#xff08;全称为"超文本传输协议"&#xff09;是一种应用非常广泛的应用层协议&#xff0c;HTTP是基于TCP协议的一种应用层协议。 应用层协议&#xff1a;是计算机网络协议栈中最高层的协议&#xff0c;它定义了运行在不同主机上…...

【机器视觉】单目测距——运动结构恢复

ps&#xff1a;图是随便找的&#xff0c;为了凑个封面 前言 在前面对光流法进行进一步改进&#xff0c;希望将2D光流推广至3D场景流时&#xff0c;发现2D转3D过程中存在尺度歧义问题&#xff0c;需要补全摄像头拍摄图像中缺失的深度信息&#xff0c;否则解空间不收敛&#xf…...

Linux云原生安全:零信任架构与机密计算

Linux云原生安全&#xff1a;零信任架构与机密计算 构建坚不可摧的云原生防御体系 引言&#xff1a;云原生安全的范式革命 随着云原生技术的普及&#xff0c;安全边界正在从传统的网络边界向工作负载内部转移。Gartner预测&#xff0c;到2025年&#xff0c;零信任架构将成为超…...

三体问题详解

从物理学角度&#xff0c;三体问题之所以不稳定&#xff0c;是因为三个天体在万有引力作用下相互作用&#xff0c;形成一个非线性耦合系统。我们可以从牛顿经典力学出发&#xff0c;列出具体的运动方程&#xff0c;并说明为何这个系统本质上是混沌的&#xff0c;无法得到一般解…...

动态 Web 开发技术入门篇

一、HTTP 协议核心 1.1 HTTP 基础 协议全称 &#xff1a;HyperText Transfer Protocol&#xff08;超文本传输协议&#xff09; 默认端口 &#xff1a;HTTP 使用 80 端口&#xff0c;HTTPS 使用 443 端口。 请求方法 &#xff1a; GET &#xff1a;用于获取资源&#xff0c;…...

4. TypeScript 类型推断与类型组合

一、类型推断 (一) 什么是类型推断 TypeScript 的类型推断会根据变量、函数返回值、对象和数组的赋值和使用方式&#xff0c;自动确定它们的类型。 这一特性减少了显式类型注解的需要&#xff0c;在保持类型安全的同时简化了代码。通过分析上下文和初始值&#xff0c;TypeSc…...

【Linux】自动化构建-Make/Makefile

前言 上文我们讲到了Linux中的编译器gcc/g 【Linux】编译器gcc/g及其库的详细介绍-CSDN博客 本来我们将一个对于编译来说很重要的工具&#xff1a;make/makfile 1.背景 在一个工程中源文件不计其数&#xff0c;其按类型、功能、模块分别放在若干个目录中&#xff0c;mak…...

Windows电脑能装鸿蒙吗_Windows电脑体验鸿蒙电脑操作系统教程

鸿蒙电脑版操作系统来了&#xff0c;很多小伙伴想体验鸿蒙电脑版操作系统&#xff0c;可惜&#xff0c;鸿蒙系统并不支持你正在使用的传统的电脑来安装。不过可以通过可以使用华为官方提供的虚拟机&#xff0c;来体验大家心心念念的鸿蒙系统啦&#xff01;注意&#xff1a;虚拟…...