Nettyの网络聊天室扩展序列化算法

1、网络聊天室综合案例

        客户端初始代码:

@Slf4j
public class ChatClient {public static void main(String[] args) {NioEventLoopGroup group = new NioEventLoopGroup();LoggingHandler LOGGING_HANDLER = new LoggingHandler(LogLevel.DEBUG);MessageCodecSharable MESSAGE_CODEC = new MessageCodecSharable();try {Bootstrap bootstrap = new Bootstrap();bootstrap.channel(NioSocketChannel.class);bootstrap.group(group);bootstrap.handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ch.pipeline().addLast(new ProcotolFrameDecoder());ch.pipeline().addLast(LOGGING_HANDLER);ch.pipeline().addLast(MESSAGE_CODEC);}});Channel channel = bootstrap.connect("localhost", 8080).sync().channel();channel.closeFuture().sync();} catch (Exception e) {log.error("client error", e);} finally {group.shutdownGracefully();}}
}

        服务器初始代码:

@Slf4j
public class ChatServer {public static void main(String[] args) {NioEventLoopGroup boss = new NioEventLoopGroup();NioEventLoopGroup worker = new NioEventLoopGroup();LoggingHandler LOGGING_HANDLER = new LoggingHandler(LogLevel.DEBUG);MessageCodecSharable MESSAGE_CODEC = new MessageCodecSharable();try {ServerBootstrap serverBootstrap = new ServerBootstrap();serverBootstrap.channel(NioServerSocketChannel.class);serverBootstrap.group(boss, worker);serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ch.pipeline().addLast(new ProcotolFrameDecoder());ch.pipeline().addLast(LOGGING_HANDLER);ch.pipeline().addLast(MESSAGE_CODEC);}});Channel channel = serverBootstrap.bind(8080).sync().channel();channel.closeFuture().sync();} catch (InterruptedException e) {log.error("server error", e);} finally {boss.shutdownGracefully();worker.shutdownGracefully();}}
}
        1.1、登录业务

        业务流程:

  1. 客户端流水线上新增一个入站处理器,处理登录逻辑,有连接建立时触发的channelActive事件(处理登录逻辑)和channelRead事件(获取服务器返回登录的结果)。
  2. 入站处理器中异步操作,封装LoginRequestMessage消息请求对象,通过ctx.writeAndFlush发送给服务器,并且触发该入站处理器之前的所有出站处理器(消息编解码器,日志打印),然后陷入阻塞等待服务器返回结果
  3. 服务器创建一个自定义的Handle,专门监听客户端的LoginRequestMessage消息请求对象。
  4. 服务器对登录信息进行校验,如果登录信息正确则临时保存(将用户的channel和用户名绑定)。
  5. 服务器封装LoginResponseMessage消息响应对象,通过channelHandlerContext.writeAndFlush方法将消息发送给客户端,并且触发该入站处理器前的所有出站处理器(消息编解码器,日志打印)。
  6. 将自定义的Handle注册到服务器的流水线上。
  7. 客户端channelRead接收到服务器返回的结果,将结果记录,并且结束阻塞(无论是否登录成功)
  8. 客户端根据结果执行不同的业务逻辑,成功则让用户选择菜单,失败则断开连接。

        客户端,在流水线上新增一个入站处理器,专门处理登录相关逻辑:

        注意点:

  1. 使用channelActive,确保该入站处理器是在连接建立时触发。
  2. 并非在Netty的主线程中处理登录相关逻辑,而是新开启一个线程异步地处理,相应地,线程间的通信使用countDownLatch (判断是否拿到服务器端的返回结果)和 AtomicBoolean (判断服务器端返回的结果,是否登录成功)。

        成员位置:

CountDownLatch countDownLatch = new CountDownLatch(1);
AtomicBoolean loginResult = new AtomicBoolean(false);
 //编写登录逻辑ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {/*** 连接建立时触发,输入用户名和密码,传给后端校验* @param ctx* @throws Exception*/@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {new Thread(() -> {Scanner sc = new Scanner(System.in);System.out.println("请输入用户名");String username = sc.nextLine();System.out.println("请输入密码");String password = sc.nextLine();LoginRequestMessage requestMessage = new LoginRequestMessage(username, password, null);//发送给后端 后端有一个专门的处理器去处理请求信息并且返回结果ctx.writeAndFlush(requestMessage);try {countDownLatch.await();} catch (InterruptedException e) {e.printStackTrace();}boolean result = loginResult.get();//登录成功if (result) {while (true) {System.out.println("==================================");System.out.println("send [username] [content]");System.out.println("gsend [group name] [content]");System.out.println("gcreate [group name] [m1,m2,m3...]");System.out.println("gmembers [group name]");System.out.println("gjoin [group name]");System.out.println("gquit [group name]");System.out.println("quit");System.out.println("==================================");String command = sc.nextLine();String[] s = command.split(" ");switch (s[0]) {case "send":ctx.writeAndFlush(new ChatRequestMessage(username, s[1], s[2]));break;case "gsend":ctx.writeAndFlush(new GroupChatRequestMessage(username, s[1], s[2]));break;case "gcreate":Set<String> set = new HashSet<>(Arrays.asList(s[2].split(",")));set.add(username); // 加入自己ctx.writeAndFlush(new GroupCreateRequestMessage(s[1], set));break;case "gmembers":ctx.writeAndFlush(new GroupMembersRequestMessage(s[1]));break;case "gjoin":ctx.writeAndFlush(new GroupJoinRequestMessage(username, s[1]));break;case "gquit":ctx.writeAndFlush(new GroupQuitRequestMessage(username, s[1]));break;case "quit":ctx.channel().close();return;}}} else {//密码错误就关闭连接,触发 channel.closeFuture().sync();ctx.channel().close();}}, "login").start();}/*** 接受后端返回的登录校验结果* @param ctx* @param msg* @throws Exception*/@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {log.debug("登录结果:{}", msg);//记录状态if (msg instanceof LoginResponseMessage) {LoginResponseMessage responseMessage = (LoginResponseMessage) msg;if (responseMessage.isSuccess()) {loginResult.compareAndSet(false, true);}countDownLatch.countDown();}}});

        服务器端:

        注意点:

  1. 自定义一个Handler,继承SimpleChannelInboundHandler,只关注客户端发送的登录请求。
  2. 登录成功后,将当前会话信息临时进行保存。
@ChannelHandler.Sharable
@Slf4j
public class LoginRequesHandler extends SimpleChannelInboundHandler<LoginRequestMessage> {@Overrideprotected void channelRead0(ChannelHandlerContext channelHandlerContext, LoginRequestMessage loginRequestMessage) throws Exception {String username = loginRequestMessage.getUsername();String password = loginRequestMessage.getPassword();boolean loginSuccess = UserServiceFactory.getUserService().login(username, password);LoginResponseMessage responseMessage = null;if (loginSuccess) {//保存会话信息 key channel value 当前登录人 zhangsan lisiChannel channel = channelHandlerContext.channel();SessionFactory.getSession().bind(channel, loginRequestMessage.getUsername());responseMessage = new LoginResponseMessage(true, "登录成功!");log.info("账号:{}登录成功,绑定的交换机:{}",username,channel);} else {responseMessage = new LoginResponseMessage(false, "登录失败!");}//将结果返回给前端channelHandlerContext.writeAndFlush(responseMessage);}
}

        将自定义Handler注册到流水线上:

//接受前端传递的用户名和密码并校验,然后返回给前端登录结果
//指定关注的消息类型为LoginRequestMessage
ch.pipeline().addLast(new LoginRequesHandler());

    
        1.2、发送消息(单聊)

        客户端:

        如果用户在菜单中选择send,则触发单聊功能。

        

        通过ctx.writeAndFlush发送封装好的单聊消息请求,并且触发在这之前的所有出站消息。

 ctx.writeAndFlush(new ChatRequestMessage(username, s[1], s[2]));

        服务器端:

        注册一个ChatRequestHandler处理器,继承SimpleChannelInboundHandler,专门处理客户端传递的单聊请求。

        注意点:

  1. 发送消息之前需要检查收件人是否在线,通过用户名去查询对应的channel是否存在(如果该用户已登录,必定会将自己的用户名和channel绑定)
  2. 拿到收件人的channel后,利用收件人的channel向收件人的客户端发送消息。

         1.3、创建聊天群组

        客户端:

        如果用户在菜单中选择gcreate,则触发创建聊天群组功能:

        封装GroupCreateRequestMessage创建聊天群组请求对象,并且调用ctx.writeAndFlush触发之前所有的出站处理器。

ctx.writeAndFlush(new GroupCreateRequestMessage(s[1], set));

        服务器端:

        创建一个自定义的Handler,继承SimpleChannelInboundHandler,专门监听客户端的GroupCreateRequestMessage。

        注意点:

  1. 首先需要判断群聊是否存在,如果存在就不能重复创建。
  2. 创建成功后拿到所有群组成员的channel,向各自的客户端发送GroupChatResponseMessage消息响应对象。

        

        1.4、发送消息(群聊)

        客户端:

         如果用户在菜单中选择gsend,则触发创建聊天群组功能:

        封装GroupChatRequestMessage创建群聊请求对象,并且调用ctx.writeAndFlush触发之前所有的出站处理器。 

 ctx.writeAndFlush(new GroupChatRequestMessage(username, s[1], s[2]));

        服务器端:

        创建一个Handler继承SimpleChannelInboundHandler专门监听GroupChatRequestMessage群聊消息请求。

      
         1.5、心跳消息监测

        有时服务器长时间没有接收到客户端发出的消息,可能是因为网络设备出现故障, 网络不稳定,应用程序发生阻塞等原因,称之为连接假死。

        这时我们应该及时地去释放资源,那么如何去判定是否发生连接假死?如果通过常规的超时机制难以判定,因为连接本身并没有断开,但数据无法正常传输。

        可以通过心跳监测机制去实现。客户端和服务器之间定期互相发送心跳消息,对方在一定时间内收到心跳消息后,会发送一个确认消息,表示连接仍然正常。如果一方在指定时间内未收到对方的心跳消息,就认为连接可能已经中断或假死。

        心跳机制通常运用于分布式系统实时通信中,eureka运用的便是心跳检测机制。

        如果需要在Netty框架中使用心跳消息监测,需要在服务器端的流水线上加入:

  • IdleStateHandler:是 Netty 提供的一个处理器,用于检测连接的空闲状态,可以分为读空闲,写空闲和读写空闲
  • ChannelDuplexHandler:是一个入站/出站双向的处理器,在其中加入userEventTriggered,它是一个自定义的处理器,当IdleStateHandler检测到空闲事件后,会触发IdleStateEvent,被userEventTriggered捕获。

        服务器端关注的是读空闲

                    //空闲检测ch.pipeline().addLast(new IdleStateHandler(5, 0, 0));
//                    //双向监测 入站和出站ch.pipeline().addLast(new ChannelDuplexHandler() {/*** 用户自定义事件* @param ctx* @param evt* @throws Exception*/@Overridepublic void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {if (evt instanceof IdleStateEvent) {IdleStateEvent event = (IdleStateEvent) evt;if (event.state().equals(IdleState.READER_IDLE)) {log.debug("已经5s未读取到数据了");ctx.channel().close();}}}});

        同时在客户端中加入,客户端关注的是写空闲,如果一定时间内没有向客户端发送消息,就发送默认的心跳消息确认双方都是存活的。

//如果三秒内没有向服务器写出数据,就发送心跳消息ch.pipeline().addLast(new IdleStateHandler(0, 3, 0));
//                    双向监测 入站和出站ch.pipeline().addLast(new ChannelDuplexHandler() {/*** 用户自定义事件* @param ctx* @param evt* @throws Exception*/@Overridepublic void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {if (evt instanceof IdleStateEvent) {IdleStateEvent event = (IdleStateEvent) evt;if (event.state().equals(IdleState.WRITER_IDLE)) {log.debug("已经3s未写入数据了,发送默认消息");ctx.writeAndFlush(new PingMessage());}}}});

        如果超过一定的时间,客户端没有向服务器发送消息或心跳,则服务器默认客户端已经假死,就会断开连接释放资源。

        

        1.6、退出

        退出分为在客户端选择quit正常退出,以及异常退出的情况,服务器端为了处理这两种情况,需要在流水线上加入一个自定义的QuitHandler:

        创建一个自定义的QuitHandler,继承ChannelInboundHandlerAdapter接口,重写其中的

channelInactiveexceptionCaught方法

  // 当连接断开时触发 inactive 事件@Overridepublic void channelInactive(ChannelHandlerContext ctx) throws Exception {SessionFactory.getSession().unbind(ctx.channel());log.debug("{} 已经断开", ctx.channel());}// 当出现异常时触发@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {SessionFactory.getSession().unbind(ctx.channel());log.debug("{} 已经异常断开 异常是{}", ctx.channel(), cause.getMessage());}

2、扩展序列化算法

        在自定义通讯协议时,消息的传输使用到了序列化算法,当时使用的是JDK默认的序列化算法:

        序列化:

// 6. 获取内容的字节数组
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(msg);
byte[] bytes = bos.toByteArray();

        反序列化:

int length = in.readInt();
byte[] bytes = new byte[length];
in.readBytes(bytes, 0, length);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
Message message = (Message) ois.readObject();

        这里介绍一种不需要修改代码,只需要修改配置文件达成序列化方式切换的思路:

        application.properties

serializer.algorithm=JSON

        创建一个接口,定义序列化和反序列化方法的模版:

public interface Serialized {/*** 序列化** @param object 将要序列化的对象* @param <T>* @return 序列化后的byte数组*/<T> byte[] serialized(T object);/*** 反序列化** @param clazz 将要反序列化成的对象的类型* @param bytes 序列化的byte数组* @param <T>* @return 反序列化后的对象*/<T> T deSerialized(Class<T> clazz, byte[] bytes);}

        定义一个枚举类,实现接口,分别编写使用JDK自带的方式序列化以及使用JSON序列化的逻辑:

enum Algorithm implements Serialized {JAVA {@Overridepublic <T> byte[] serialized(T object) {try {ByteArrayOutputStream bos = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(bos);oos.writeObject(object);return bos.toByteArray();} catch (IOException e) {e.printStackTrace();throw new RuntimeException("序列化失败!");}}@Overridepublic <T> T deSerialized(Class<T> clazz, byte[] bytes) {try {ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));return (T) ois.readObject();} catch (IOException | ClassNotFoundException e) {e.printStackTrace();throw new RuntimeException("反序列化失败!");}}},JSON {@Overridepublic <T> byte[] serialized(T object) {Gson gson = new Gson();String str = gson.toJson(object);return str.getBytes(StandardCharsets.UTF_8);}@Overridepublic <T> T deSerialized(Class<T> clazz, byte[] bytes) {Gson gson = new Gson();return gson.fromJson(new String(bytes, StandardCharsets.UTF_8), clazz);}}}

        再定义一个读取 application.properties 文件的配置类,如果配置文件中未配置,就按照默认的JDK序列化方式实现:

/*** 序列化配置类*/
public class SerializedConfig {static Properties properties;static {//从application.properties配置文件中读取try (InputStream is = SerializedConfig.class.getResourceAsStream("/application.properties")) {properties = new Properties();properties.load(is);} catch (IOException e) {throw new ExceptionInInitializerError(e);}}public static int getServerPort() {//从配置文件中读取键为server.port的值String value = properties.getProperty("server.port");if (value == null) {return 8080;} else {return Integer.parseInt(value);}}public static Serialized.Algorithm getSerializedAlgorithm() {//从配置文件中读取键为serializer.algorithm的值String value = properties.getProperty("serializer.algorithm");if (value == null) {return Serialized.Algorithm.JAVA;} else {return Serialized.Algorithm.valueOf(value);}}}

        改造自定义协议类:

        编码主要有两处需要修改,一处是设定字节的序列化方式(获取的是序列化方式 java json 在枚举类中的位置 0,1):

out.writeByte(SerializedConfig.getSerializedAlgorithm().ordinal());

        另一处是将消息序列化的逻辑:

byte[] bytes = SerializedConfig.getSerializedAlgorithm().serialized(msg);

        解码也有两处需要修改:

        第一处是确定反序列化的算法:

Serialized.Algorithm[] values = Serialized.Algorithm.values();
//确定反序列化算法
Serialized.Algorithm algorithm = values[serializerType];

        第二处是确定消息类型,并且解码:

//确定消息类型
Class<? extends Message> messageClass = Message.getMessageClass(messageType);
Object message = algorithm.deSerialized(messageClass, bytes)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/40682.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

searchForm自适应布局 + 按钮插槽

收起 展开 代码&#xff1a; useResizeObserverHooks.js import { useEffect, useLayoutEffect } from "react";export const useResizeObserver (containerDom, domClass, callback) > {useLayoutEffect(() > {let resizeObserver null;let dom null;if …

Map Set(Java篇详解)

&#x1f341; 个人主页&#xff1a;爱编程的Tom&#x1f4ab; 本篇博文收录专栏&#xff1a;Java专栏&#x1f449; 目前其它专栏&#xff1a;c系列小游戏 c语言系列--万物的开始_ 等 &#x1f389; 欢迎 &#x1f44d;点赞✍评论⭐收藏&#x1f496;三连支持…

代谢组数据分析(十二):岭回归、Lasso回归、弹性网络回归构建预测模型

欢迎大家关注全网生信学习者系列: WX公zhong号:生信学习者Xiao hong书:生信学习者知hu:生信学习者CDSN:生信学习者2介绍 在代谢物预测模型的构建中,我们采用了三种主流的回归分析方法:岭回归、Lasso回归以及弹性网络回归。这三种方法各有其独特的原理和适用场景,因此在…

WPS操作技巧:制作可以打对勾的方框,只需简单几步!沈阳wps办公软件培训

日常工作中&#xff0c;我们经常需要在表格中添加复选框&#xff0c;比如【性别选择】、【任务完成状态】等等&#xff0c;通过打对勾来确定状态。今天就分别从WPS的Excel表格和Word文档2种场景&#xff0c;介绍制作可以打对勾的复选框的方法技巧&#xff0c;掌握技巧&#xff…

游戏AI的创造思路-技术基础-计算机视觉

让游戏的AI具备“眼睛”和“视觉”&#xff0c;就是通过计算机视觉的方法进行的。现在&#xff0c;越来越多的游戏&#xff0c;特别是动捕类游戏都在使用这个方法。当然&#xff0c;计算机视觉不仅仅用于游戏&#xff0c;越来越多的应用使用到这个技术 目录 1. 定义 2. 发展历…

腾讯混元文生图开源模型推出小显存版本,6G显存即可运行,并开源caption模型

7月4日&#xff0c;腾讯混元文生图大模型&#xff08;混元DiT&#xff09;宣布开源小显存版本&#xff0c;仅需6G显存即可运行&#xff0c;对使用个人电脑本地部署的开发者十分友好&#xff0c;该版本与LoRA、ControlNet等插件&#xff0c;都已适配至Diffusers库&#xff1b;并…

探索 Apache Paimon 在阿里智能引擎的应用场景

摘要&#xff1a;本文整理自Apache Yarn && Flink Contributor&#xff0c;阿里巴巴智能引擎事业部技术专家王伟骏&#xff08;鸿历&#xff09;老师在 5月16日 Streaming Lakehouse Meetup Online 上的分享。内容主要分为以下三个部分&#xff1a; 一、 阿里智能引擎…

LVS+Nginx高可用集群--基础篇

1.集群概述 单体部署&#xff1a; 可以将上面内容分别部署在不同的服务器上。 单体架构的优点&#xff1a; 小团队成型就可完成开发&#xff0c;测试&#xff0c;上线 迭代周期短&#xff0c;速度快 打包方便&#xff0c;运维简单 单体架构的挑战&#xff1a;单节点宕机造成…

DVWA sql手注学习(巨详细不含sqlmap)

这篇文章主要记录学习sql注入的过程中遇到的问题已经一点学习感悟&#xff0c;过程图片会比较多&#xff0c;比较基础和详细&#xff0c;不存在看不懂哪一步的过程 文章目录 靶场介绍SQL注入 lowSQL注入 MediumSQL注入 HighSQL注入 Impossible 靶场介绍 DVWA&#xff08;Damn…

必备的 Adobe XD 辅助工具

想要高效便捷的使用 Adobe XD&#xff0c; Adobe XD 插件是必不可少的&#xff0c; Adobe XD 的插件非常多&#xff0c;但 90%都是英文&#xff0c;并且良莠不齐。在这儿挑选 9 个好用的 Adobe XD 插件给大家&#xff0c;这里是我整理的一些实用 Adobe XD 插件&#xff0c;让你…

大屏开发系列——Echarts的基础使用

本文为个人近期学习总结&#xff0c;若有错误之处&#xff0c;欢迎指出&#xff01; Echarts在vue2中的基础使用 一、简单介绍二、基本使用&#xff08;vue2中&#xff09;1.npm安装2.main.js引入3.使用步骤(1)准备带有宽高的DOM容器&#xff1b;(2)初始化echarts实例&#xff…

PHP宜邦家政服务管理系统-计算机毕业设计源码04426

目 录 摘要 1 绪论 1.1 选题背景与意义 1.2开发现状 1.3论文结构与章节安排 2 宜邦家政服务管理系统系统分析 2.1 可行性分析 2.1.1 技术可行性分析 2.1.2 经济可行性分析 2.1.3 操作可行性分析 2.2 系统功能分析 2.2.1 功能性分析 2.2.2 非功能性分析 2.3 系统用…

国标GB28181视频汇聚平台LntonCVS视频监控安防平台与国标协议对接解决方案

应急管理部门以“以信息化推动应急管理能力现代化”为总体目标&#xff0c;加快现代信息技术与应急管理业务深度融合&#xff0c;全面支持现代应急管理体系建设&#xff0c;这不仅是国家加强和改进应急管理工作的关键举措&#xff0c;也是应对日益严峻的应急管理形势和满足公众…

微信小程序的运行机制与更新机制

1. 小程序运行机制 1.1. 冷启动与热启动 冷启动为用户第一次打开小程序时&#xff0c;因为之前没有打开过&#xff0c;这是第一种冷启动的情兑。第二种情况为虽然之前用户打开过&#xff0c;但是小程序被用户主动的销毁过&#xff0c;这种情况下我们再次打开小程序&#xff0…

【PALM、WRF-LES】微尺度气象数值模拟—大涡模拟技术

针对微尺度气象的复杂性&#xff0c;大涡模拟&#xff08;LES&#xff09;提供了一种无可比拟的解决方案。微尺度气象学涉及对小范围内的大气过程进行精确模拟&#xff0c;这些过程往往与天气模式、地形影响和人为因素如城市布局紧密相关。在这种规模上&#xff0c;传统的气象模…

doc文档下载

目录 下载 安装谷歌浏览器(chrome)Microsoft Edge浏览器 常见问题 下载 见邮件附件 安装 谷歌浏览器(chrome) 打开浏览器&#xff0c;地址栏输入&#xff1a;chrome://extensions/ 右上角打开开发者模式 点击如上图左上角的加载已解压的拓展程序&#xff0c;并选择刚刚解压…

安卓应用开发学习:通过腾讯地图SDK实现定位功能

一、引言 这几天有些忙&#xff0c;耽误了写日志&#xff0c;但我的学习始终没有落下&#xff0c;有空我就会研究《 Android App 开发进阶与项目实战》一书中定位导航方面的内容。在我的手机上先后实现了“获取经纬度及地理位置描述信息”和“获取导航卫星信息”功能后&#x…

afrog-漏洞扫描(挖洞)工具【了解安装使用详细】

★★免责声明★★ 文章中涉及的程序(方法)可能带有攻击性&#xff0c;仅供安全研究与学习之用&#xff0c;读者将信息做其他用途&#xff0c;由Ta承担全部法律及连带责任&#xff0c;文章作者不承担任何法律及连带责任。 1、afrog介绍 afrog 是一款性能卓越、快速稳定、PoC可定…

MySQL篇-SQL优化实战-减少子查询

回顾 上一篇了解了分析SQL使用的explain&#xff0c;可以点击查看MySQL篇-SQL优化实战了解我在写sql的注意事项还有explain的说明&#xff0c;这次拿一段生产使用的sql进行优化说明。从14s优化到2.6s 待优化的SQL SELECT DISTINCTswpe.tag_number,hca.ACCOUNT_NAME customer…

VBA中类的解读及应用第十三讲:限制复选选择,窗体模块的搭建

《VBA中类的解读及应用》教程【10165646】是我推出的第五套教程&#xff0c;目前已经是第一版修订了。这套教程定位于最高级&#xff0c;是学完初级&#xff0c;中级后的教程。 类&#xff0c;是非常抽象的&#xff0c;更具研究的价值。随着我们学习、应用VBA的深入&#xff0…