消息队列-RabbitMQ:延迟队列、rabbitmq 插件方式实现延迟队列、整合SpringBoot

十六、延迟队列

在这里插入图片描述

1、延迟队列概念

延时队列内部是有序的最重要的特性就体现在它的延时属性上,延时队列中的元素是希望在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。

延迟队列使用场景:

  • 订单在十分钟之内未支付则自动取消;
  • 新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒;
  • 用户注册成功后,如果三天内没有登陆则进行短信提醒;
  • 用户发起退款,如果三天内没有得到处理则通知相关运营人员;
  • 预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议。

这些场景都有一个特点,需要在某个事件发生之后或者之前的指定时间点完成某一项任务,如:发生订单生成事件,在十分钟之后检查该订单支付状态,然后将未支付的订单进行关闭。那我们一直轮询数据,每秒查一次,取出需要被处理的数据,然后处理不就完事了吗?

如果数据量比较少,确实可以这样做,比如:对于 “如果账单一周内未支付则进行自动结算” 这样的需求, 如果对于时间不是严格限制,而是宽松意义上的一周,那么每天晚上跑个定时任务检查一下所有未支付的账单,确实也是一个可行的方案。

但对于数据量比较大,并且时效性较强的场景,如:“订单十分钟内未支付则关闭 “,短期内未支付的订单数据可能会有很多,活动期间甚至会达到百万甚至千万级别,对这么庞大的数据量仍旧使用轮询的方式显然是不可取的很可能在一秒内无法完成所有订单的检查,同时会给数据库带来很大压力,无法满足业务要求而且性能低下。

在这里插入图片描述

2、整合SpringBoot

(1)创建模块项目

在这里插入图片描述

(2)添加依赖

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><!--RabbitMQ 依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.47</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><!--swagger--><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>3.0.0</version></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId><version>3.0.0</version></dependency><!--RabbitMQ 测试依赖--><dependency><groupId>org.springframework.amqp</groupId><artifactId>spring-rabbit-test</artifactId><scope>test</scope></dependency>
</dependencies>

(3)修改配置文件

在这里插入图片描述

spring.rabbitmq.host=42.192.149.71
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=123456

(4)添加Swagger配置类

在这里插入图片描述

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;@Configuration
@EnableSwagger2
public class SwaggerConfig {@Beanpublic Docket webApiConfig() {return new Docket(DocumentationType.SWAGGER_2).groupName("webApi").apiInfo(webApiInfo()).select().build();}private ApiInfo webApiInfo() {return new ApiInfoBuilder().title("rabbitmq 接口文档").description("本文档描述了 rabbitmq 微服务接口定义").version("1.0").contact(new Contact("zhiyuan", "http://oddfar.com", "test@qq.com")).build();}}

3、队列 TTL

代码架构图

创建两个队列 QA 和 QB,两者队列 TTL 分别设置为 10S 和 40S,然后在创建一个交换机 X 和死信交换机 Y,它们的类型都是 direct,创建一个死信队列 QD,它们的绑定关系如下:

在这里插入图片描述

在这里插入图片描述

(1)配置类

声明:普通交换机、死信交换机、普通队列X2、死信队列

绑定:XA、XB、DY

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.HashMap;
import java.util.Map;
@Configuration
public class TtlQueueConfig {public static final String X_EXCHANGE = "X";public static final String QUEUE_A = "QA";public static final String QUEUE_B = "QB";//死信交换机public static final String Y_DEAD_LETTER_EXCHANGE = "Y";//死信队列public static final String DEAD_LETTER_QUEUE = "QD";// 声明 xExchange@Bean("xExchange")public DirectExchange xExchange() {return new DirectExchange(X_EXCHANGE);}// 声明 死信队列交换机@Bean("yExchange")public DirectExchange yExchange() {return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);}//声明队列 A ttl 为 10s 并绑定到对应的死信交换机@Bean("queueA")public Queue queueA() {Map<String, Object> args = new HashMap<>(3);//声明当前队列绑定的死信交换机args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);//声明当前队列的死信路由 keyargs.put("x-dead-letter-routing-key", "YD");//声明队列的 TTLargs.put("x-message-ttl", 10000);return QueueBuilder.durable(QUEUE_A).withArguments(args).build();}// 声明队列 A 绑定 X 交换机@Beanpublic Binding queueaBindingX(@Qualifier("queueA") Queue queueA,@Qualifier("xExchange") DirectExchange xExchange) {return BindingBuilder.bind(queueA).to(xExchange).with("XA");}//声明队列 B ttl 为 40s 并绑定到对应的死信交换机@Bean("queueB")public Queue queueB() {Map<String, Object> args = new HashMap<>(3);//声明当前队列绑定的死信交换机args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);//声明当前队列的死信路由 keyargs.put("x-dead-letter-routing-key", "YD");//声明队列的 TTLargs.put("x-message-ttl", 40000);return QueueBuilder.durable(QUEUE_B).withArguments(args).build();}//声明队列 B 绑定 X 交换机@Beanpublic Binding queuebBindingX(@Qualifier("queueB") Queue queue1B,@Qualifier("xExchange") DirectExchange xExchange) {return BindingBuilder.bind(queue1B).to(xExchange).with("XB");}//声明死信队列 QD@Bean("queueD")public Queue queueD() {return new Queue(DEAD_LETTER_QUEUE);}//声明死信队列 QD 绑定关系@Beanpublic Binding deadLetterBindingQAD(@Qualifier("queueD") Queue queueD,@Qualifier("yExchange") DirectExchange yExchange) {return BindingBuilder.bind(queueD).to(yExchange).with("YD");}}

(2)消息生产者

在这里插入图片描述

使用RabbitTemplate发消息

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
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 java.util.Date;@Slf4j
@RequestMapping("ttl")
@RestController
public class SendMsgController {@Autowiredprivate RabbitTemplate rabbitTemplate;@GetMapping("sendMsg/{message}")public void sendMsg(@PathVariable String message) {log.info("当前时间:{},发送一条信息给两个 TTL 队列:{}", new Date(), message);rabbitTemplate.convertAndSend("X", "XA", "消息来自 ttl 为 10S 的队列: " + message);rabbitTemplate.convertAndSend("X", "XB", "消息来自 ttl 为 40S 的队列: " + message);}}

(3)消息消费者

在这里插入图片描述

在这里插入图片描述

package com.kdz.rabbitmq;import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Date;@Slf4j
@Component
public class DeadLetterQueueConsumer {@RabbitListener(queues = "QD")public void receiveD(Message message, Channel channel) throws IOException {String msg = new String(message.getBody());log.info("当前时间:{},收到死信队列信息{}", new Date().toString(), msg);}
}

测试效果:

访问:localhost:8080/ttl/sendMsg/LBJ

在这里插入图片描述

延迟消息发送成功

在这里插入图片描述

在这里插入图片描述

第一条消息在 10S 后变成了死信消息,然后被消费者消费掉,第二条消息在 40S 之后变成了死信消息, 然后被消费掉,这样一个延时队列就打造完成了。

不过,如果这样使用的话,岂不是每增加一个新的时间需求,就要新增一个队列,这里只有 10S 和 40S 两个时间选项,如果需要一个小时后处理,那么就需要增加 TTL 为一个小时的队列,如果是预定会议室然后提前通知这样的场景,岂不是要增加无数个队列才能满足需求?

4、延时队列 TTL 优化

在这里新增了一个队列 QC,绑定关系如下,该队列不设置 TTL 时间

在这里插入图片描述

(1)配置文件类

在这里插入图片描述

在这里插入图片描述

@Configuration
public class MsgTtlQueueConfig {public static final String Y_DEAD_LETTER_EXCHANGE = "Y";public static final String QUEUE_C = "QC";//声明队列 C 死信交换机@Bean("queueC")public Queue queueC() {Map<String, Object> args = new HashMap<>(3);//声明当前队列绑定的死信交换机args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);//声明当前队列的死信路由 keyargs.put("x-dead-letter-routing-key", "YD");//没有声明 TTL 属性return QueueBuilder.durable(QUEUE_C).withArguments(args).build();}//声明队列 B 绑定 X 交换机@Beanpublic Binding queuecBindingX(@Qualifier("queueC") Queue queueC,@Qualifier("xExchange") DirectExchange xExchange) {return BindingBuilder.bind(queueC).to(xExchange).with("XC");}
}

(2)生产者

在这里插入图片描述

查看源码,配置参数

在这里插入图片描述

/*** 延时队列优化* @param message 消息* @param ttlTime 延时的毫秒*/
@GetMapping("sendExpirationMsg/{message}/{ttlTime}")
public void sendMsg(@PathVariable String message, @PathVariable String ttlTime) {rabbitTemplate.convertAndSend("X", "XC", message, correlationData -> {correlationData.getMessageProperties().setExpiration(ttlTime);return correlationData;});log.info("当前时间:{},发送一条时长{}毫秒 TTL 信息给队列 C:{}", new Date(), ttlTime, message);
}

测试效果:

发起请求:

http://localhost:8080/ttl/sendExpirationMsg/ 你好 1/20000

http://localhost:8080/ttl/sendExpirationMsg/ 你好 2/2000

在这里插入图片描述

在这里插入图片描述

看起来似乎没什么问题,但是在最开始的时候,就介绍过如果使用在消息属性上设置 TTL 的方式,消息可能并不会按时 “死亡 “。

因为 RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到死信队列, 如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。这也就是为什么第二个延迟时间短,却后执行。

此外,我们还可以通过 Rabbitmq 插件实现延迟队列

17、 Rabbitmq 插件实现延迟队列

上文中提到的问题,确实是一个问题,如果不能实现在消息粒度上的 TTL,并使其在设置的 TTL 时间 及时死亡,就无法设计成一个通用的延时队列。那如何解决呢,接下来我们就去解决该问题

1、安装延时队列插件

在官网上下载 https://www.rabbitmq.com/community-plugins.html,下载 rabbitmq_delayed_message_exchange 插件,然后解压放置到 RabbitMQ 的插件目录。 进入 RabbitMQ 的安装目录下的 plugins 目录,执行下面命令让该插件生效,然后重启 RabbitMQ

/usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins rabbitmq-plugins enable rabbitmq_delayed_message_exchange

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

2、基于死信情况与基于插件的对比

在这里插入图片描述

在这里插入图片描述

3、代码架构图

在这里插入图片描述

4、代码实现

(1)配置文件

在我们自定义的交换机中,这是一种新的交换类型,该类型消息支持延迟投递机制 消息传递后并 不会立即投递到目标队列中,而是存储在 mnesia(一个分布式数据系统)表中,当达到投递时间时,才 投递到目标队列中。

①定义队列、交换机、RoutingKey

在这里插入图片描述

②声明交换机

在这里插入图片描述

查看源码

在这里插入图片描述

在这里插入图片描述

③声明延迟队列

在这里插入图片描述

④绑定交换机和队列

在这里插入图片描述

@Configuration
public class DelayedQueueConfig {public static final String DELAYED_QUEUE_NAME = "delayed.queue";public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";@Beanpublic Queue delayedQueue() {return new Queue(DELAYED_QUEUE_NAME);}//自定义交换机 我们在这里定义的是一个延迟交换机@Beanpublic CustomExchange delayedExchange() {Map<String, Object> args = new HashMap<>();//自定义交换机的类型args.put("x-delayed-type", "direct");return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", true, false, 
args);}@Beanpublic Binding bindingDelayedQueue(@Qualifier("delayedQueue") Queue queue,@Qualifier("delayedExchange") CustomExchange 
delayedExchange) {return 
BindingBuilder.bind(queue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();}
}

(2)生产者

在这里插入图片描述

public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
@GetMapping("sendDelayMsg/{message}/{delayTime}")
public void sendMsg(@PathVariable String message,@PathVariable Integer delayTime) {rabbitTemplate.convertAndSend(DELAYED_EXCHANGE_NAME, DELAYED_ROUTING_KEY, message, 
correlationData ->{correlationData.getMessageProperties().setDelay(delayTime);return correlationData;});log.info(" 当 前 时 间 : {}, 发送一条延迟 {} 毫秒的信息给队列 delayed.queue:{}", new 
Date(),delayTime, message);
}

(3) 消息消费者:

在这里插入图片描述

public static final String DELAYED_QUEUE_NAME = "delayed.queue";
@RabbitListener(queues = DELAYED_QUEUE_NAME)
public void receiveDelayedQueue(Message message){String msg = new String(message.getBody());log.info("当前时间:{},收到延时队列的消息:{}", new Date().toString(), msg);
}

测试效果:

发起请求:

http://localhost:8080/ttl/sendDelayMsg/come on baby1/20000

http://localhost:8080/ttl/sendDelayMsg/come on baby2/2000

在这里插入图片描述

第二个消息被先消费掉了,符合预期

5、延迟队列总结

延时队列在需要延时处理的场景下非常有用,使用 RabbitMQ 来实现延时队列可以很好的利用 RabbitMQ 的特性,如:消息可靠发送、消息可靠投递、死信队列来保障消息至少被消费一次以及未被正确处理的消息不会被丢弃。另外,通过 RabbitMQ 集群的特性,可以很好的解决单点故障问题,不会因为单个节点挂掉导致延时队列不可用或者消息丢失

当然,延时队列还有很多其它选择,比如利用 Java 的 DelayQueue,利用 Redis 的 zset利用 Quartz 或者利用 kafka 的时间轮,这些方式各有特点,看需要适用的场景。

消息队列-RabbitMQ:延迟队列、rabbitmq 插件方式实现延迟队列、整合SpringBoot 到此完结,笔者归纳、创作不易,大佬们给个3连再起飞吧

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

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

软考45-上午题-【数据库】-数据操纵语言DML

一、INSERT插入语句 向SQL的基本表中插入数据有两种方式&#xff1a; ①直接插入元组值 ②插入一个查询的结果值 1-1、直接插入元组值 【注意】&#xff1a; 列名序列是可选的&#xff0c;若是所有列都要插入数值&#xff0c;则可以不写列名序列。 示例&#xff1a; 1-2、插…

暑期宅家?计算机专业必看的8部电影!一定要安利给你们!

代码编程看上去枯燥乏味&#xff0c;但也是艺术的&#xff0c;感性的&#xff0c;计算机编程的许多概念被应用于电影中&#xff0c;其中有些非常之酷炫&#xff0c;它们甚至能帮助开发人员理解一些编程概念。 所以今天学姐来给大家推荐几部心中top级的编程人必看电影&#xff0…

matlab倒立摆小车LQR控制动画

1、内容简介 略 54-可以交流、咨询、答疑 2、内容说明 略 摆杆长度为 L&#xff0c;质量为 m 的单级倒立摆(摆杆的质心在杆的中心处)&#xff0c;小车的质量为 M。在水平方向施加控制力 u&#xff0c;相对参考系产生位移为 y。为了简化问题并且保其实质不变&#xff0c;忽…

数据结构:链表的冒泡排序

法一&#xff1a;修改指针指向 //法二 void maopao_link(link_p H){if(HNULL){printf("头节点为空\n");return;}if(link_empty(H)){printf("链表为空\n");return;}link_p tailNULL;while(H->next->next!tail){link_p pH;link_p qH->next;while(q…

抖音视频提取软件使用功能|抖音视频下载工具

我们的抖音视频提取软件是一款功能强大、易于操作的工具&#xff0c;旨在解决用户在获取抖音视频时需要逐个复制链接、下载的繁琐问题。我们的软件支持通过关键词搜索和分享链接两种方式获取抖音视频&#xff0c;方便用户快速找到自己感兴趣的内容。 主要功能模块&#xff1a;…

进程线程信号通道

4> 使用消息队列完成两个进程间相互通信 usr1代码&#xff1a; #include <myhead.h> //定义一个消息类型 struct msgbuf {long mtype;//消息类型char mtext[1024];//消息正文 }; #define MSGSIZE sizeof(struct msgbuf)-sizeof(long) int main(int argc, const char …

物奇ENC算法开关接口修改方法

物奇ENC算法开关接口修改 是否需要申请加入数字音频系统研究开发交流答疑群(课题组)&#xff1f;可加我微信hezkz17, 本群提供音频技术答疑服务&#xff0c;群赠送语音信号处理降噪算法&#xff0c;蓝牙耳机音频&#xff0c;DSP音频项目核心开发资料, 1 配置工具事件接口 2 代…

K线实战分析系列之十一:行情力量不足——平头形态

K线实战分析系列之十一&#xff1a;行情力量不足——平头形态 一、平头形态二、不同形态与平头形态的叠加三、总结平头形态 一、平头形态 前一根K线具有较长的实体&#xff0c;后一根K线的实体比较小&#xff0c;无论是多头还是空头的力量到第二根K线都被瓦解了多头上攻&#…

初识51单片机

##江科大51单片机学习 什么是单片机&#xff1f;&#xff1f;&#xff1f; 单片机&#xff0c;英文名&#xff0c;Micro Controller Unit&#xff0c;简称MCU&#xff08;tips&#xff1a;有人会简称它为CPU&#xff0c;但不是如此&#xff0c;CPU其实被集成在MCU中&#xff…

【牛客】【刷题节】美团2024届秋招笔试第二场编程真题

1.小美的加法【简单题】 题意理解&#xff1a; 给定一个数组做连加操作&#xff0c;其中只能将一个加号变成乘号 将哪个加号变成乘号&#xff0c;使式子最后的结果最大 解题思路&#xff1a; 只有将两个相邻且乘机最大的数之间变成乘号后&#xff0c;才能保证整个式子结果最大 …

微服务知识02

1、九大高并发解决方案 2、系统架构图​​​​​​​ 3、分布式事务 本地事务、分布式事务 操作不同服务器的数据库&#xff08;垂直分库&#xff09; 4、分布式事务解决方案&#xff08;没有seata之前&#xff09; &#xff08;1&#xff09;XA协议&#xff08;强一致性&a…

【Java程序员面试专栏 算法思维】三 高频面试算法题:搜索算法

一轮的算法训练完成后,对相关的题目有了一个初步理解了,接下来进行专题训练,以下这些题目就是汇总的高频题目,本篇主要聊聊搜索算法,以岛屿问题为切入点练习,所以放到一篇Blog中集中练习 题目关键字解题思路时间空间岛屿数量网格搜索分别向上下左右四个方向探索,遇到海…

1分钟带你学会Python的pass关键字和range函数

1.pass 关键字 pass关键字在 python 中没有任何实际意义&#xff0c;主要是用来完成占位的操作&#xff0c;保证语句的完整性 age int(input(请输入您的年龄&#xff1a;))if age > 18: pass # pass 在此处没有任何意义&#xff0c;只是占位 print(欢迎光临。。。…

Pillow:一个超实用的Python图像处理库

目录 Pillow 引言 Pillow库简介 Pillow的主要功能 1. 图像打开、保存和格式转换 2. 图像增强 3. 几何变换 4. 色彩空间转换 5. 图像分析和处理 Pillow的应用场景 1. 网页图像处理 2. 图像编辑工具 3. 计算机视觉和机器学习

个人IP的核心竞争力是什么?

做个人IP最重要的一点就是学会展示&#xff01; 可以利用网络“链接关系”&#xff0c;通过内容大方分享自己的观点、知识、经验等&#xff0c;以利他思维获取用户信任&#xff0c;从而链接到用户群体。 为什么要展示&#xff1f; 1、吸引同频用户 2、低成本高效率获客 3、…

conda 导出/导出配置好的虚拟环境

一. 导出环境配置&#xff08;yml文件&#xff09; 1. 在主目录下激活虚拟环境&#xff08;UE4是我的虚拟环境名称&#xff0c;请根据你自己的名称进行修改&#xff09; conda activate UE4 2. 运行此代码 conda env export > environment.yml 二. 导入环境配置&#xf…

WSL2配置Linux、Docker、VS Code、zsh、oh my zsh(附Docker开机自启设置)

0. 写在前面 本篇笔记来自于UP主麦兜搞IT的合集视频Windows10开发环境搭建中的部分内容 1. 安装WSL2 按照微软官方文档进行操作&#xff0c;当然也可以直接wsl --install 也可以按照 旧版手动安装的步骤 来进行操作 选择安装的是Ubuntu 20.04 LTS 注&#xff1a;WSL默认安装…

“曲线图揭示收支秘密:让你的支出开销一目了然!“

在繁忙的现代生活中&#xff0c;我们每个人的支出开销都如同一部复杂的交响乐&#xff0c;需要细致入微的管理和分析。而曲线图作为一种直观、易懂的视觉工具&#xff0c;正是我们透视收支明细、洞察支出开销规律的得力助手。 首先&#xff0c;第一步&#xff0c;我们要进入晨…

如何在本地电脑部署HadSky论坛并发布至公网可远程访问【内网穿透】

文章目录 前言1. 网站搭建1.1 网页下载和安装1.2 网页测试1.3 cpolar的安装和注册 2. 本地网页发布2.1 Cpolar临时数据隧道2.2 Cpolar稳定隧道&#xff08;云端设置&#xff09;2.3 Cpolar稳定隧道&#xff08;本地设置&#xff09;2.4 公网访问测试 总结 前言 经过多年的基础…

【黑客VS程序员】哪个更厉害?

对于绝大部分程序员来讲写代码是一种职业&#xff0c;而对于黑客来讲写代码算是人生的一种情怀&#xff0c;准确来讲算是一种事业。 要成为一个黑客首先成为一个程序员&#xff0c;首先需要让自己能够像写代码&#xff0c;黑客其实就是程序员职业方向里面的安全方向&#xff0…