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注册类
对出入的数据进行的业务操作进行初始化
目前通道支持连接协议 WebSocket
和 Socket
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 实现 通过微信小程序 在网页上进行电子签字的功能。
相关文章:
![](https://www.ngui.cc/images/no-images.jpg)
Springboot项目集成Netty组件
系列文章目录 Springboot项目集成Netty组件 Netty新增解析数据包指定长度功能 文章目录系列文章目录前言一、Netty是什么?二、使用步骤1. 项目引入依赖1.1 项目基础版本信息:1.2 Netty依赖2. 项目配置2.1 在 yml 配置文件中配置以下:2.2 创建…...
![](https://www.ngui.cc/images/no-images.jpg)
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才行...
![](https://img-blog.csdnimg.cn/img_convert/4e15f43aa93478ff245eef53f12c5d3a.jpeg)
[oeasy]python0088_字节_Byte_存储单位_KB_MB_GB_TB
编码进化 回忆上次内容 上次 回顾了 字符大战的结果 ibm 曾经的 EBCDIC 由于字符不连续的隐患 导致后续 出现 无数问题无法补救 7-bit 的 ASA X3.4-1963 字母序号连续 比较字符时 效率高判断字符 是否是字母 也很容易 获得了 IBM以外公司的 支持 为什么 ASA X3.4-1963 是 7…...
![](https://img-blog.csdnimg.cn/2e46244d9a674b6ca09398b761cdc38a.png#pic_center)
vue3.0 生命周期
目录前言:vue3.0生命周期图例1.beforeCreate2.created3.beforeMount/onBeforeMount4.mounted/onMounted5.beforeUpdate/onBeforeUpdate6.updated/onUpdated7.beforeUnmount/onBeforeUnmount8.unmounted/onUnmounted案例:总结前言: 每个Vue组…...
![](https://www.ngui.cc/images/no-images.jpg)
CGAL 数字类型
文章目录 一、简介二、内置数字类型三、CGAL中的数字类型参考资料一、简介 在CGAL汇总,数字类型必须满足特定的语法和语义要求,这样它们才能在CGAL代码中成功使用。一般来说,它们往往是代数结构概念的模型,如果它们对实数的子集模型,那么它们就也是RealEmbeddable模型。 二…...
![](https://img-blog.csdnimg.cn/img_convert/003c5fab8c1143230ebfb86b902bba60.png)
如何将Python打包后的exe还原成.py?
将python打包好的exe解压为py文件,步骤如下:下载pyinstxtractor.py文件下载地址:https://nchc.dl.sourceforge.net/project/pyinstallerextractor/dist/pyinstxtractor.py并将pyinstxtractor.py放到和exe相同的目录文件下打开命令控制台cd 进…...
![](https://www.ngui.cc/images/no-images.jpg)
CJSON简单介绍
json简介 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。 易于人阅读和编写。同时也易于机器解析和生成。 它基于JavaScript Programming Language, Standard ECMA-262 3rd Edition - December 1999的一个子集,最新的定义可以参考ECMA-404_2nd_ed…...
![](https://img-blog.csdnimg.cn/7247955a5a244f028decd9b02f548d8b.jpeg#pic_center)
算法训练营 day49 动态规划 爬楼梯 (进阶)零钱兑换 完全平方数
算法训练营 day49 动态规划 爬楼梯 (进阶)零钱兑换 完全平方数 爬楼梯 (进阶) 70. 爬楼梯 - 力扣(LeetCode) 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同…...
![](https://img-blog.csdnimg.cn/4c70ed74621642cfae2b18c3c973a952.png)
Vue:extends继承组件复用性
提到extends继承,最先想到的可能是ES6中的class、TS中的interface、面向对象编程语言中中的类和接口概念等等,但是我们今天的关注点在于:如何在Vue中使用extends继承特性。 目录 Vue:创建Vue实例的方式 构造函数方式࿱…...
![](https://www.ngui.cc/images/no-images.jpg)
ChatGPT 的一些思考
最近 ChatGPT3.5 在全世界范围内掀起了一次 AI 的潮流,ChatGPT1.0/ChatGPT2.0 当时也是比较火爆,但是那个当时感觉还是比较初级的应用,相当于是一个进阶版的微软小冰,给人的感觉是有一点智能,但不多。其实从早期版本开…...
![](https://img-blog.csdnimg.cn/046d15ec821d472695f245a2e5f413ca.png)
GEE学习笔记 六十九:【GEE之Python版教程三】Python基础编程一
环境配置完成后,那么可以开始正式讲解编程知识。之前我在文章中也讲过,GEE的python版接口它是依赖python语言的。目前很多小伙伴是刚开始学习GEE编程,之前或者没有编程基础,或者是没有学习过python。为了照顾这批小伙伴࿰…...
![](https://img-blog.csdnimg.cn/5d453ccd3c95447981103adbeaef2139.png)
大数据全系安装
内容版本号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 [依次选择]-&…...
![](https://img-blog.csdnimg.cn/83f0c953daca487f8932b6ff19542df0.png)
stable-diffusion-webui 安装使用
文章目录1.github 下载,按教程运行2.安装python 忘记勾选加入环境变量,自行加入(重启生效)3.环境变量添加后,清理tmp ,venv重新运行4.运行报错,无法升级pip,无法下载包,5…...
![](https://img-blog.csdnimg.cn/b1ae4b8875e24fe0a523c458752f7d45.png#pic_center)
3D点云处理:点云聚类--FEC: Fast Euclidean Clustering for Point Cloud Segmentation
文章目录 聚类结果一、论文内容1.1 Ground Surface Removal1.2 Fast Euclidean Clustering题外:欧几里得聚类Fast Euclidean Clustering二、参考聚类结果 原始代码中采用的是pcl中的搜索方式,替换为另外第三方库,速度得到进一步提升。 一、论文内容 论文中给出的结论:该…...
![](https://www.ngui.cc/images/no-images.jpg)
华为OD机试题 - 射击比赛(JavaScript)| 代码+思路+重要知识点
最近更新的博客 华为OD机试题 - 括号检查(JavaScript) 华为OD机试题 - 最小施肥机能效(JavaScript) 华为OD机试题 - 子序列长度(JavaScript) 华为OD机试题 - 众数和中位数(JavaScript) 华为OD机试题 - 服务依赖(JavaScript) 华为OD机试题 - 字符串加密(JavaScript)…...
![](https://img-blog.csdnimg.cn/img_convert/78165348f36cf13bfa6bdeb81d5ec8e6.png)
流程引擎之Flowable简介
背景Flowable 是一个流行的轻量级的采用 Java 开发的业务流程引擎,通过 Flowable 流程引擎,我们可以部署遵循 BPMN2.0 协议的流程定义(一般为XML文件)文件,并能创建流程实例,查询和访问流程相关的实例与数据…...
![](https://img-blog.csdnimg.cn/62535927ed0c4260943c75e7b157aca0.png)
AcWing:4861. 构造数列、4862. 浇花(C++)
目录 4861. 构造数列 问题描述: 实现代码: 4862. 浇花 问题描述: 实现代码: 4861. 构造数列 问题描述: 我们规定如果一个正整数满足除最高位外其它所有数位均为 00,则称该正整数为圆数。 例如&…...
![](https://img-blog.csdnimg.cn/80248648f0d6427ca8612f9d3bd0ce85.png)
进程的概念
进程的概念 程序的概念 这里说的是一个可执行文件,passive的意思可以理解为我们这个执行文件需要我们进行双击才会被被执行。 双击后,程序入口地址读入寄存器,程序加载入主存,成为一个进程 进程是主动去获取想要的资源࿰…...
![](https://img-blog.csdnimg.cn/img_convert/72e33f336ec66f5c1bd2985e65c3d3f4.png)
自动化测试5年经验,分享一些心得
自动化测试介绍 自动化测试(Automated Testing),是指把以人为驱动的测试行为转化为机器执行的过程。实际上自动化测试往往通过一些测试工具或框架,编写自动化测试用例,来模拟手工测试过程。比如说,在项目迭代过程中,持…...
![](https://img-blog.csdnimg.cn/20dd84fddfed464bb1ccbb4c194ca4c9.png)
independentsoft.de/MSG .NET Framework Crack
MSG .NET 是用于 .NET Framework / .NET Core 的 Microsoft Outlook .msg 文件 API。API 允许您轻松创建/读取/解析/转换 .msg 文件等。API 不需要在机器上安装 Microsoft Outlook 或任何其他第三方应用程序或库即可工作。 以下示例向您展示了如何打开现有文件并显示消息的某些…...
![](https://img-blog.csdnimg.cn/b90f295aded74e51a5fdf04d7cee80b7.png#pic_center)
基于Transformer的NLP处理管线
HuggingFace transformers 是一个整合了跨语言、视觉、音频和多模式模态与最先进的预训练模型并且提供用户友好的 API 的AI开发库。 它由 170 多个预训练模型组成,支持 PyTorch、TensorFlow 和 JAX 等框架,能够在代码之间进行互操作。 这个库还易于部署&…...
![](https://img-blog.csdnimg.cn/img_convert/4c03f9202bf6464bd56d56e952b1e1b4.png)
二叉树OJ(一)二叉树的最大深度 二叉搜索树与双向链表 对称的二叉树
二叉树的最大深度 二叉树中和为某一值的路径(一) 二叉搜索树与双向链表 对称的二叉树 二叉树的最大深度 描述 求给定二叉树的最大深度, 深度是指树的根节点到任一叶子节点路径上节点的数量。 最大深度是所有叶子节点的深度的最大值。 (注:…...
![](https://img-blog.csdnimg.cn/87e42c1fd83a45f7b038b43adfe6a936.png#pic_center)
使用Fairseq进行Bart预训练
文章目录前言环境流程介绍数据部分分词部分预处理部分训练部分遇到的问题问题1可能遇到的问题问题1问题2前言 本文是使用 fairseq 做 Bart 预训练任务的踩坑记录huggingface没有提供 Bart 预训练的代码 facebookresearch/fairseq: Facebook AI Research Sequence-to-Sequence…...
![](https://img-blog.csdnimg.cn/cdc079c75b85461dadf7fab7d95ef5f7.png)
n阶数字回转方阵 ← 模拟法
【问题描述】 请编程输出如下数字回旋方阵。 【算法代码】 #include <bits/stdc.h> using namespace std;const int maxn100; int z[maxn][maxn];void matrix(int n) {int num2;z[0][0]1;int i0,j1;while(i<n && j<n) {while(i<j) z[i][j]num;while(j&…...
![](https://www.ngui.cc/images/no-images.jpg)
【人工智能AI】二、NoSQL 基础知识《NoSQL 企业级基础入门与进阶实战》
写一篇介绍 NoSQL 基础知识的技术文章,分5个章节,每个章节细分到3级目录,重点介绍一下NoSQL 数据模型,NoSQL 数据库架构,NoSQL 数据库特性等,不少于2000字。 NoSQL 基础知识 NoSQL(Not Only SQ…...
![](https://www.ngui.cc/images/no-images.jpg)
Camera Rolling Shutter和Global Shutter的区别
卷帘快门(Rolling Shutter)与全局快门(Global Shutter)的区别 什么是快门 快门是照相机用来控制感光片有效曝光时间的机构。 快门是照相机的一个重要组成部分,它的结构、形式及功能是衡量照相机档次的一个重要因素。 …...
![](https://www.ngui.cc/images/no-images.jpg)
模版之AnyType
title: 模版之AnyType date: 2023-02-19 21:49:53 permalink: /pages/54a0bf/ categories: 通用领域编程语言C tags:C元编程 author: name: zhengzhibing link: https://azmddy.top/pages/54a0bf/ 模版之AnyType 在研究C的编译期反射时,发现了AnyType很有意思。 首…...
![](https://img-blog.csdnimg.cn/512afc759c5f474d924f3fcc21b23069.png)
【汇编】一、环境搭建(一只 Assember 的成长史)
嗨~你好呀! 我是一名初二学生,热爱计算机,码龄两年。最近开始学习汇编,希望通过 Blog 的形式记录下自己的学习过程,也和更多人分享。 这篇文章主要讲述汇编环境的搭建过程。 话不多说~我们开始吧! 系统环…...
![](https://www.ngui.cc/images/no-images.jpg)
【博客628】k8s pod访问集群外域名原理以及主机开启了systemd-resolved的不同情况
k8s pod访问集群外域名原理以及使用了systemd-resolved的不同情况 1、不同情况下的linux主机访问外部域名原理 没有使用systemd-resolved的linux主机上访问外部域名一般是按照以下步骤来的: 从dns缓存里查找域名与ip的映射关系 从/etc/hosts里查找域名与ip的映射…...
![](https://img-blog.csdnimg.cn/img_convert/8cd84387d3d994636d7e0d76bce97bbe.png)
测试3.测试方法的分类
3.测试分类 系统测试包括回归测试和冒烟测试 回归测试:修改了旧的代码后,重新测试功能是否正确,有没有引入新的错误或导致其它代码产生错误 冒烟测试:目的是确认软件基本功能正常,可以进行后续的正式测试工作 按是否…...
![](/images/no-images.jpg)
网站建设销售实习报告/网页代码模板
sidebar:frontia_menu概述Frontia的分享模块支持分享到新浪微博,腾讯微博,QQ空间,开心网,人人网,QQ好友,微信,短信,电子邮件等平台。(去下载SDK)使用前须知Frontia分享模块依赖于Fro…...
![](/images/no-images.jpg)
宁波网站建设科技有限公司/站长之家权重查询
了解如何针对评估、分析和性能来调整和调优 G1 GC。 2013 年 8 月发布 垃圾优先型垃圾回收器 (G1 GC) 是适用于 Java HotSpot VM 的低暂停、服务器风格的分代式垃圾回收器。G1 GC 使用并发和并行阶段实现其目标暂停时间,并保持良好的吞吐量。当 G1 GC 确定有必要进…...
![](/images/no-images.jpg)
做谷歌网站使用什么统计代码/网站推广公司大家好
导读:本文主要介绍 CentOS 系统二进制安装 MySQL 5.7.23 版本的安装步骤,其他版本安装过程相似。1.前置准备卸载旧版MySQL查看rpm包rpm -qa|grep mysql 若有可用rpm -e卸载查找mysql残留包,有则删除,没有则忽略find / -name mysql…...
![](https://yqfile.alicdn.com/img_82036adfdb951d5d0ff9be1e834b6bd0.png)
用vs与dw做网站/自媒体培训学校
内容介绍,为什么要使用前端路由? 在2005左右,兴起了一种叫做ajax的技术,有了ajax之后,我们向服务端提交数据的时候就不再需要使用from表单去提交了,因为from表单之间的提交会导致页面之间的切换,…...
![](/images/no-images.jpg)
做相册网站logo/网推怎么做
转载于:https://www.cnblogs.com/6DAN_HUST/archive/2013/06/02/3114323.html...
![](/images/no-images.jpg)
做女装的网站/怎么进行网络推广
[Mnsx_x]刷题笔记——顶端迭代器 请你设计一个迭代器,除了支持 hasNext 和 next 操作外,还支持 peek 操作。 实现 PeekingIterator 类: PeekingIterator(int[] nums) 使用指定整数数组 nums 初始化迭代器。 int next() 返回数组中的下一个元素…...