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

项目实战 — 消息队列(7){虚拟主机设计(2)}

目录

一、消费消息的规则

二、消费消息的具体实现方法

🍅 1、编写消费者类(ConsumerEnv)

🍅 2、编写Consumer函数式接口(回调函数)

🍅 3、编写ConsumeerManager类

        🎄定义成员变量

        🎄notifyConsume()方法

        🎄添加构造方法       

 🎄 addConsumer()方法

🎄 完善consumeMessage()方法

 🍅 4、完成VirtualHost类编写

        🎄  basicConsume()方法编写

        🎄 编写basicAck类(手动应答)

 三、测试VirtualHost

🍅 1、准备工作和收尾工作

🍅 2、测试交换机的创建和删除

🍅3、测试队列的创建和删除

🍅 4、测试绑定的创建和删除

🍅 5、测试发布消息

🍅6、测试消费消息

🎄 先订阅队列,再发送消息

🎄 先发送消息,再订阅队列

🍅 测试basicAck


一、消费消息的规则

前面主要讲了basicPublish,发布消息这一块,同时写了Router类,实现了bindingKey和routingKey的命名规则和匹配规则,主要就是讲的是生产消息。

那么接下来就实现消费者消费消息。

🎊 推送给消费者消息的基本思路:

        1、broker server管理者哪些消费者

        2、收到了对应的消息,把消息推送给消费者

已知,一个broker server中是包含了很多个队列的:

 

🎊 消费者调用basicConsume,就是订阅某个队列的消息:

        1、消费者是以队列的维度订阅消息

        2、一个队列可以有多个消费者

 

 此处,只需要约定消费者如何消费即可。

这里使用“轮询”的方式消费消息:轮询,举例子,如上图,有123三个消费者,让他们分别轮流消费一条消息,依次轮流来,一次消费一个。

具体实现:

1、定义一个类,描述一个消费者

2、然后给每个队列对象(MSGQueue对象)加上属性,相当于一个List,包含若干个消费者对象。


二、消费消息的具体实现方法

在VirtualHost类中实现一个订阅消息的方法basicConsume()

添加一个队列的订阅者,当队列收到消息以后,就要把消息推送给对应的订阅者。

consumerTag:消费者的身份标识

aotoAck:消息被消费完成后,应答的方式,为true自动应答,为false就手动应答。

Consumer:一个回调函数,也就是一个函数式接口(lambda函数底层实现),这样在后面调用basicConsume的时候,并且传实参的时候,就可以写作lambda样子

🍅 1、编写消费者类(ConsumerEnv)

/** 表示一个消费者* */
@Data
public class ConsumerEnv {private String consumerTag; //消费者身份标识private String queueName;private boolean autoAck;
//    通过回调处理收到的消息private Consumer consumer;
}

然后再MSGQueue.java类中,进行相应的扩充。

    private List<ConsumerEnv> consumerEnvList = new ArrayList<>();
//    记录取到了第几个消费者,方便实现轮询策略
//    AtomicInteger是一个原子性类型,因为consumerSeq再消费信息的时候会被修改,
//    如果使用int可能造成线程不安全,于是这里就使用AtomicIntegerpublic AtomicInteger consumerSeq = new AtomicInteger();
//      添加一个新的订阅者public void addConsumerEnv(ConsumerEnv consumerEnv){synchronized (this){consumerEnvList.add(consumerEnv);}}
//    挑选一个订阅者,处理当前的消息(轮询)public ConsumerEnv chooseConsumer(){if (consumerEnvList.size() == 0){
//           该队列没有人订阅return null;}
//        计算当前要取的元素的下标int index = consumerSeq.get() % consumerEnvList.size();
//      getAndIncrement()先获取当前值,再加1。相当于 getAndAdd(1).consumerSeq.getAndIncrement();   //进行自增return consumerEnvList.get(index);}


🍅 2、编写Consumer函数式接口(回调函数)

 创建一个Consumer接口。

/*
* 只是一个函数式接口
* 收到消息之后要处理消息时调用的方法
* */
@FunctionalInterface
public interface Consumer {
//    处理投递
//    每次服务器收到消息之后,调用消息,通过这个方法把消息推送给对应的消费者void handleDelivery(String consumerTag, BasicProperties basicProperties, byte[] body) throws MqException, IOException;
}


🍅 3、编写ConsumeerManager类

这个类主要就是用来实现消费者消费消息的核心逻辑。主要有以下几块。

消费消息:就是让线程池,执行对应消费者中的回调函数。在调用回调函数的时候,就把消息的内容通过参数,传进去。消费者在最初订阅消息的时候,就把回调注册给broker server。回调函数的内容时消费者确定的,取决于消费者的业务逻辑。

扫描线程:能够感知到哪个队列里面收到了新的消息,扫描线程会取出该消息,找出对应的消费者,将该内容打包成一个任务,丢给线程池去调用

 

为什么需要线程池?

 一些消费者给出的回调函数,处理起来可能会比较耗时,如果只有一个扫描线程,那么可能会导致处理不及时,导致队列中消息越来越多。所以这里引入的扫描线程就轻量的取消息和获取回调,而线程池就用来执行处理的回调函数。

扫描线程如何明白哪个队列中有了新消息?

引入一个阻塞队列。该队列中的元素是有消息的队列的名字,哪一个队列有消息了,就把队列名放到该阻塞队列中。扫描线程就可以从阻塞队列中获取到新增消息的队列的名字。

如何保证消息不被丢失?

使用消息确认(ACK)。在消息确认就是为了避免,消费者的回调方法在执行过程中出错,导致消息丢失这种情况。

为了保证消息不丢失:

(1)在真正执行回调之前,把该消息放到“待确认集合”中,也就是前面MemoryDataCenter中的queueMessageWaitAckMap集合中;

(2)执行回调

(3)当前消费者采取的是autoAck == true,也就是回调执行完毕不抛异常,就算消费成功;消费成功以后,删除消息(硬盘,内存哈希表,待确认集合)

(4)当前消息采取的是autoAck == false,手动应答。也就是消费者这边,在回调方法内部,显示调用basicAck这个核心API。

        🎄定义成员变量

也就是上面提到过的,阻塞队列,扫描线程,线程池。

public class ConsumerManager {
//    持有上层VirtualHostprivate VirtualHost parent;
//    指定一个线程池,负责去执行具体的回调任务private ExecutorService workerPool = Executors.newFixedThreadPool(4);
//      引入一个阻塞队列,存放队列名的private BlockingQueue<String > tokenQueue = new LinkedBlockingDeque<>();
//   扫描线程private Thread scannerThread = null;}

        🎄notifyConsume()方法

这个方法主要就是为了通知什么时候消费,这里主要就是在发送消息的时候,通知消费,将含有该消息的队列名放在阻塞队列中:

//    通知消费
//    调用时机:发送消息的时候,就调用(sendMessage)public void notifyConsume(String queueName) throws InterruptedException {tokenQueue.put(queueName);}

所以,我们就需要在前面VirtualHost类中的sendMessage方法中再调用一个通知消费的方法:

异常大家自己向上抛一下。

//        通知消费者进行消费consumerManager.notifyConsume(queue.getName());

        🎄添加构造方法       

添加构造方法,构造一个线程,编写从队列中取出消息的过程,

其中的consumeMessage(queue)是消费消息的具体实现方法,先列在这里,不实现

 public ConsumerManager(VirtualHost p){parent = p;scannerThread = new Thread(()->{
//            持续运行while (true){try {
//                    1、从阻塞队列中拿到队列名String queueName = tokenQueue.take();
//                   2、根据队列名找到队列MSGQueue queue = parent.getMemoryDataCenter().getQueue(queueName);if (queue == null){throw new MqException("[ConsumerManager]取出令牌后发现,该队列名不存在!queuName = " + queueName);}
//                    3、从队列中消费一个消息synchronized (queue){consumeMessage(queue);}} catch (InterruptedException  | MqException e) {e.printStackTrace();}}});
//        把线程设为后台线程scannerThread.setDaemon(true);scannerThread.start();}private void consumeMessage(MSGQueue queue) {//TODO
}

 🎄 addConsumer()方法

该方法主要是为了新增一个Consumer对象到指定的队列中。

//    新增一个Consumer对象到指定的队列中public void addConsumer(String consumerTag, String queueName, boolean autoAck, Consumer consumer) throws MqException {
//        找到对应的队列MSGQueue queue = parent.getMemoryDataCenter().getQueue(queueName);if (queue == null){throw new MqException("[ConsumerManager]队列不存在!queueName = " + queueName);}ConsumerEnv consumerEnv = new ConsumerEnv(consumerTag,queueName,autoAck,consumer);synchronized (queue){queue.addConsumerEnv(consumerEnv);
//            如果当前队列中已经有了一些消息,需要立即消费掉int n = parent.getMemoryDataCenter().getMessageCount(queueName);for (int i = 0; i < n; i++) {
//                调用一次就消费一条消息consumeMessage(queue);}}}

🎄 完善consumeMessage()方法

这个方法前面只列了一下,没有实现,这里具体实现一下。

主要有以下几步:

        (1)按照轮询的方式,找出一个消费者

        (2)从队列中取出一个消息 

        (3)把消息丢给回调函数,给线程池处理。

              a. 把消息放到待确认集合中

              b. 真正的执行回调操作

              c. 如果是自动应答,直接删除消息;手动应答,先不处理,交给后续消费者调用                        basicAck()。

private void consumeMessage(MSGQueue queue) {
//        1、按照轮询的方式,找出一个消费者来ConsumerEnv luckyDog = queue.chooseConsumer();if (luckyDog == null){
//            当前没有消费者,暂时不消费return;}
//        2、从队列中取出一个消息
//        pollMessage是为了从队列中取出消息Message message = parent.getMemoryDataCenter().pollMessage(queue.getName());if (message == null) {
//            当前队列没有消息return;}
//        3、把消息丢给回调函数中,给线程池处理workerPool.submit(() -> {try {
//                1、把消息放到待确认集合中parent.getMemoryDataCenter().addMessageWaitAck(queue.getName(),message);
//            2、真正执行回调操作luckyDog.getConsumer().handleDelivery(luckyDog.getConsumerTag(),message.getBasicProperties(),message.getBody());
//            3、如果当前是自动应答,就可以直接删除消息
//            如果是手动应答,就需要调用basicAck()if (luckyDog.isAutoAck()){
//                1).删除硬盘,先看是不是持久化消息if (message.getDeliverMode() == 2){parent.getDiskDataCenter().deleteMessage(queue,message);}
//                    2)、待确认集合中的消息parent.getMemoryDataCenter().removeMessageWaitAck(queue.getName(),message.getMessageId());
//                    3)、删除内存中消息中心的消息parent.getMemoryDataCenter().removeMessage(message.getMessageId());System.out.println("[ConsumerManager]消息被成功消费!queueName = " + queue.getName());}} catch (Exception e) {e.printStackTrace();}});}


 🍅 4、完成VirtualHost类编写

        🎄  basicConsume()方法编写

该方法主要作用是订阅消息(消费消息)。在VirtualHost中实现。其中调用了ConsumerManager中的方法。

首先在VirtualHost添加consumerManager的实例。

    private ConsumerManager consumerManager = new ConsumerManager(this);

然后写订阅消的方法。 

//    订阅消息public boolean basicConsume(String consumerTag, String queueName, boolean autoAck, Consumer consumer){
//        构造一个ConsumerEnv对象,也就是消费者对象,把对应的队列找到,然后将Consumer对象添加到该队列中。queueName = virtualHostName + queueName;try {consumerManager.addConsumer(consumerTag,queueName,autoAck,consumer);System.out.println("[VirtualHost]basicConsume成功! queueName = " + queueName);return true;} catch (Exception e){System.out.println("[VirtualHost]basicConsume失败! queueName = " + queueName);e.printStackTrace();return false;}}

        🎄 编写basicAck类(手动应答)

public boolean basicAck(String queueName,String messageId){queueName = virtualHostName + queueName;try{
//            1、获取到消息和队列Message message = memoryDataCenter.getMessage(messageId);if (message == null){throw new MqException("[VirtualHost] 消息不存在!messgeId = " + messageId);}MSGQueue queue = memoryDataCenter.getQueue(queueName);if (queue == null){throw new MqException("[VirtualHost] 要确认的队列不存在!queueName = " + queueName);}
//            2、删除硬盘上的数据if (message.getDeliverMode() == 2){diskDataCenter.deleteMessage(queue,message);}
//            3、、删除内存中的数据memoryDataCenter.removeMessage(messageId);
//            4、删除待确认集合中的数据memoryDataCenter.removeMessageWaitAck(queueName,messageId);System.out.println("[VirtualHost]basicAck成功!消息被成功确认!queueName = " + queueName);return true;
//}catch (Exception e){System.out.println("[VirtualHost]basicAck失败!消息被成功失败!queueName = " + queueName);e.printStackTrace();return false;}}

到这里,我们的虚拟主机VirtualHost类,就算全部写完了。


 三、测试VirtualHost

🍅 1、准备工作和收尾工作

@SpringBootTest
public class VirtualHostTests {private VirtualHost virtualHost = null;@BeforeEachpublic void setUp(){TigerMqApplication.context = SpringApplication.run(TigerMqApplication.class);virtualHost = new VirtualHost("default");}public void tearDown() throws IOException {TigerMqApplication.context.close();virtualHost = null;
//        把硬盘目录删除File dataDir = new File("./data");FileUtils.deleteDirectory(dataDir);}
}

🍅 2、测试交换机的创建和删除

//    测试创建和删除交换机@Testpublic void testExchange(){boolean ok = virtualHost.exchangeDeclare("testExchange", ExchangeType.DIRECT,true);Assertions.assertTrue(ok);ok = virtualHost.exchangeDelete("testExchange");Assertions.assertTrue(ok);}

🍅3、测试队列的创建和删除

//测试创建队列和删除队列@Testpublic void testQueue(){boolean ok = virtualHost.queueDeclare("testQueue", true);Assertions.assertTrue(ok);ok = virtualHost.queueDelete("testQueue");Assertions.assertTrue(ok);}

🍅 4、测试绑定的创建和删除

//    测试创建绑定和删除绑定@Testpublic void testQueueBind(){boolean ok = virtualHost.exchangeDeclare("testExchange", ExchangeType.DIRECT,true);Assertions.assertTrue(ok);ok = virtualHost.queueDeclare("testQueue", true);Assertions.assertTrue(ok);ok = virtualHost.queueBind("testQueue","testExchange","testBindingKey");Assertions.assertTrue(ok);ok = virtualHost.queueUnbind("testQueue","testExchange");Assertions.assertTrue(ok);}

🍅 5、测试发布消息

//    测试发布消息
@Test
public void testBasicPublish() {boolean ok = virtualHost.queueDeclare("testQueue", true);Assertions.assertTrue(ok);ok = virtualHost.exchangeDeclare("testExchange", ExchangeType.DIRECT,true);Assertions.assertTrue(ok);ok = virtualHost.basicPublish("testExchange", "testQueue", null,"hello".getBytes());Assertions.assertTrue(ok);
}

🍅6、测试消费消息

🎄 先订阅队列,再发送消息

    //    消费消息// 先订阅队列, 后发送消息@Testpublic void testBasicConsume1() throws InterruptedException {boolean ok = virtualHost.queueDeclare("testQueue", true);Assertions.assertTrue(ok);ok = virtualHost.exchangeDeclare("testExchange", ExchangeType.DIRECT,true);Assertions.assertTrue(ok);// 先订阅队列ok = virtualHost.basicConsume("testConsumerTag", "testQueue", true, new Consumer() {@Overridepublic void handleDelivery(String consumerTag, BasicProperties basicProperties, byte[] body) {// 消费者自身设定的回调方法.System.out.println("messageId=" + basicProperties.getMessageId());System.out.println("body=" + new String(body, 0, body.length));Assertions.assertEquals("testQueue", basicProperties.getRoutingKey());Assertions.assertEquals(1, basicProperties.getDeliverMode());Assertions.assertArrayEquals("hello".getBytes(), body);}});Assertions.assertTrue(ok);Thread.sleep(500);// 再发送消息ok = virtualHost.basicPublish("testExchange", "testQueue", null,"hello".getBytes());Assertions.assertTrue(ok);}

打印的日志如下:

[DataBaseManger]创建表完成
[DataBaseManger]创建初始数据已经完成
[DataBaseManger]数据库初始化完成
[MemoryDataCenter]队列删除成功!queueName = defaulttestQueue
[VirtualHost]队列创建成功!queueName = defaulttestQueue
[MemoryDataCenter]新交换机添加成功!exchangeName = defaulttestExchange
[VirtualHost] 交换机创建完成!exchangeName = defaulttestExchange
[VirtualHost]basicConsume成功! queueName = defaulttestQueue
[MemoryDataCenter]新消息添加成功!messageId = M-a500879e-5461-4550-8d56-5bef00571ab3
[MemoryDataCenter]消息被投递到到队列中! messageId = M-a500879e-5461-4550-8d56-5bef00571ab3
[MemoryDataCenter]消息从队列中取出!messageId = M-a500879e-5461-4550-8d56-5bef00571ab3
[MemoryDataCenter]消息进入待确认队列!messageId = M-a500879e-5461-4550-8d56-5bef00571ab3
messageId=M-a500879e-5461-4550-8d56-5bef00571ab3
body=hello
[MemoryDataCenter]消息从待确认队列删除!messageId = M-a500879e-5461-4550-8d56-5bef00571ab3
[MemoryDataCenter]消息被移除!messageId = M-a500879e-5461-4550-8d56-5bef00571ab3
[ConsumerManager]消费被成功消费!queueName = defaulttestQueue

🎄 先发送消息,再订阅队列

 @Testpublic void testBasicConsume2() throws InterruptedException {boolean ok = virtualHost.queueDeclare("testQueue", true);Assertions.assertTrue(ok);ok = virtualHost.exchangeDeclare("testExchange", ExchangeType.DIRECT,true);Assertions.assertTrue(ok);// 先发送消息ok = virtualHost.basicPublish("testExchange", "testQueue", null,"hello".getBytes());Assertions.assertTrue(ok);Thread.sleep(500);// 再订阅队列ok = virtualHost.basicConsume("testConsumerTag", "testQueue", true, new Consumer() {@Overridepublic void handleDelivery(String consumerTag, BasicProperties basicProperties, byte[] body) {// 消费者自身设定的回调方法.System.out.println("messageId=" + basicProperties.getMessageId());System.out.println("body=" + new String(body, 0, body.length));Assertions.assertEquals("testQueue", basicProperties.getRoutingKey());Assertions.assertEquals(1, basicProperties.getDeliverMode());Assertions.assertArrayEquals("hello".getBytes(), body);}});Assertions.assertTrue(ok);}
[MessageFileManager]恢复Message数据完成
[VirtualHost]队列已经存在!queueName = defaulttestQueue
[VirtualHost]交换机已经存在!exchangeName = defaulttestExchange
[MemoryDataCenter]新消息添加成功!messageId = M-b765df78-6997-4ce2-b87e-a6d37e3ee3c8
[MemoryDataCenter]消息被投递到到队列中! messageId = M-b765df78-6997-4ce2-b87e-a6d37e3ee3c8
[MemoryDataCenter]消息从队列中取出!messageId = M-b765df78-6997-4ce2-b87e-a6d37e3ee3c8
[VirtualHost]basicConsume成功! queueName = defaulttestQueue
[MemoryDataCenter]消息进入待确认队列!messageId = M-b765df78-6997-4ce2-b87e-a6d37e3ee3c8
messageId=M-b765df78-6997-4ce2-b87e-a6d37e3ee3c8
body=hello
[MemoryDataCenter]消息从待确认队列删除!messageId = M-b765df78-6997-4ce2-b87e-a6d37e3ee3c8
[MemoryDataCenter]消息被移除!messageId = M-b765df78-6997-4ce2-b87e-a6d37e3ee3c8
[ConsumerManager]消费被成功消费!queueName = defaulttestQueue

🍅 测试basicAck

@Testpublic void testBasicAck() throws InterruptedException {boolean ok = virtualHost.queueDeclare("testQueue", true);Assertions.assertTrue(ok);ok = virtualHost.exchangeDeclare("testExchange", ExchangeType.DIRECT,true);Assertions.assertTrue(ok);// 先发送消息ok = virtualHost.basicPublish("testExchange", "testQueue", null,"hello".getBytes());Assertions.assertTrue(ok);// 再订阅队列 [把 autoAck 改成 false]ok = virtualHost.basicConsume("testConsumerTag", "testQueue", false, new Consumer() {@Overridepublic void handleDelivery(String consumerTag, BasicProperties basicProperties, byte[] body) {// 消费者自身设定的回调方法.System.out.println("messageId=" + basicProperties.getMessageId());System.out.println("body=" + new String(body, 0, body.length));Assertions.assertEquals("testQueue", basicProperties.getRoutingKey());Assertions.assertEquals(1, basicProperties.getDeliverMode());Assertions.assertArrayEquals("hello".getBytes(), body);// [新增手动调用 basicAck]boolean ok = virtualHost.basicAck("testQueue", basicProperties.getMessageId());Assertions.assertTrue(ok);}});Assertions.assertTrue(ok);Thread.sleep(500);}
[DataBaseManger]创建表完成
[DataBaseManger]创建初始数据已经完成
[DataBaseManger]数据库初始化完成
[MemoryDataCenter]队列删除成功!queueName = defaulttestQueue
[VirtualHost]队列创建成功!queueName = defaulttestQueue
[MemoryDataCenter]新交换机添加成功!exchangeName = defaulttestExchange
[VirtualHost] 交换机创建完成!exchangeName = defaulttestExchange
[MemoryDataCenter]新消息添加成功!messageId = M-72d857bf-fea8-4cf3-a94b-2c87c5226107
[MemoryDataCenter]消息被投递到到队列中! messageId = M-72d857bf-fea8-4cf3-a94b-2c87c5226107
[MemoryDataCenter]消息从队列中取出!messageId = M-72d857bf-fea8-4cf3-a94b-2c87c5226107
[VirtualHost]basicConsume成功! queueName = defaulttestQueue
[MemoryDataCenter]消息进入待确认队列!messageId = M-72d857bf-fea8-4cf3-a94b-2c87c5226107
messageId=M-72d857bf-fea8-4cf3-a94b-2c87c5226107
body=hello
[MemoryDataCenter]消息被移除!messageId = M-72d857bf-fea8-4cf3-a94b-2c87c5226107
[MemoryDataCenter]消息从待确认队列删除!messageId = M-72d857bf-fea8-4cf3-a94b-2c87c5226107
[VirtualHost]basicAck成功!消息被成功确认!queueName = defaulttestQueue

相关文章:

项目实战 — 消息队列(7){虚拟主机设计(2)}

目录 一、消费消息的规则 二、消费消息的具体实现方法 &#x1f345; 1、编写消费者类&#xff08;ConsumerEnv&#xff09; &#x1f345; 2、编写Consumer函数式接口&#xff08;回调函数&#xff09; &#x1f345; 3、编写ConsumeerManager类 &#x1f384;定义成员变…...

手把手教你快速实现内网穿透

快速内网穿透教程 文章目录 快速内网穿透教程前言*cpolar内网穿透使用教程*1. 安装cpolar内网穿透工具1.1 Windows系统1.2 Linux系统1.2.1 安装1.2.2 向系统添加服务1.2.3 启动服务1.2.4 查看服务状态 2. 创建隧道映射内网端口3. 获取公网地址 前言 要想实现在公网访问到本地的…...

【Linux取经路】揭秘进程的父与子

文章目录 1、进程PID1.1 通过系统调用接口查看进程PID1.2 父进程与子进程 2、通过系统调用创建进程-fork初始2.1 调用fork函数后的现象2.2 为什么fork给子进程返回0&#xff0c;给父进程返回pid&#xff1f;2.3 fork函数是如何做到返回两次的&#xff1f;2.4 一个变量怎么会有不…...

iOS链式编程风格 -- 富文本字符串

一、概念 链式编程风格是一种将多个函数调用连接起来&#xff0c;形成一条函数调用链的编程风格。这种风格的代码可以通过返回 self 或某个适当的对象来实现。 1.优点 代码简洁、连贯、易于阅读。可以将一个方法的输出直接作为下一个方法的输入&#xff0c;降低中间变量的使…...

后端开发5.Redis的搭建

使用docker安装 Redis【redis】(6379) 拉取Redis镜像 docker pull redis:6.2.6 启动Redis容器 docker run -di --name=redis -p 6379:6379 redis:6.2.6 启动Redis容器并设置密码 docker run -di --name=redis -p 6379:6379 redis:6.2.6 --requirepass "密码" 测…...

推特群推王构建你的流量池

随着社交媒体的兴起&#xff0c;推特已成为了一个信息传播、交流、互动的重要平台。在这个充满了各种声音和观点的数字世界里&#xff0c;如何有效地将自己的声音传达出去&#xff0c;吸引更多的关注和互动&#xff0c;已经成为了一个备受关注的话题。而在这个过程中&#xff0…...

【从零学习python 】12.Python字符串操作与应用

文章目录 学习目标字符串介绍字符串表示方式小总结转义字符 下标和切片一、 下标/索引1. 如果想取出部分字符&#xff0c;那么可以通过下标的方法&#xff0c;&#xff08;注意在计算机中&#xff0c;下标从 0 开始&#xff09;2. 遍历3. 切片 进阶案例 学习目标 字符串的表示…...

MongoDB创建用户 、数据库、索引等基础操作

MongoDB的权限认证是相对来说比较复杂的&#xff0c;不同的库创建后需要创建用户来管理。 本机中的MongoDB是docker 启动的&#xff0c;所以先进入docker的镜像中 docker exec -it mongodb bash 这样就进入到了镜像MongoDB中&#xff0c;然后输入命令连接MongoDB数据库 注…...

Docker容器监控(Cadvisor +Prometheus+Grafana)

环境部署&#xff0c;接着上一篇文章Docker容器部署&#xff08;Cadvisor InfluxDBGrafana&#xff09;开始 目录 1、先清理一下容器 2、部署Cadvisor 3、访问Cadvisor页面 4、部署Prometheus 5、准备配置 6、运行prometheus容器 7、访问prometheus页面 8、部署Grafan…...

家电用PCM板:市场现状研究分析与发展前景预测

家电PCM板属于一种兴起不久的功能性复合材料。属于家电复合外观材料中占比较大的一种。家电复合外观材料主要分为覆膜板&#xff08;VCM&#xff09;系列和有机涂层板&#xff08;PCM&#xff09;系列两大类&#xff1a;VCM系列表面复合各类功能性薄膜&#xff0c;可根据需要实…...

详解lambda表达式(一):表达式定义与异常处理

目录 一、lambda表达式 二、lambda表达式实现函数接口 2.1 函数式接口 2.2 lambda表达式实现无参抽象方法 2.3 lambda表达式实现有参抽象方法 2.4 lambad表达式使用代码块 三、lambda表达式调用外部变量 3.1 lambda表达式可以更改类成员变量 3.2 lambda表达式无法更改…...

UE5、CesiumForUnreal接入WMTS格式地图瓦片,如ArcGIS、Mapbox、天地图

文章目录 1.实现目标2.实现过程2.1 WMTS与TMS2.2 cesium-native改造2.3 CesiumForUnreal插件改造2.4 WMTS瓦片加载测试2.5 EPSG:3857与43263.参考资料1.实现目标 通过改造cesium-native和CesiumForUnreal插件,参考tms的栅格瓦片地图加载逻辑,实现在UE5中通过CesiumForUnreal…...

AI模型公司如何定位 ?

AI模型公司如何定位 ? 企业与消费者&#xff1f; 和 多用途与利基市场&#xff1f; 文本将分解每个象限。 消费类和多用途 最有价值的象限并引发了人工智能热潮。 顶级公司&#xff1a; Open AI - 通过 ChatGPT 为消费者构建&#xff0c;并通过其旗舰 GPT 模型为企…...

C#,OpenCV开发指南(01)

C#&#xff0c;OpenCV开发指南&#xff08;01&#xff09; 一、OpenCV的安装1、需要安装两个拓展包&#xff1a;OpenCvSharp4和OpenCvSharp4.runtime.win 二、C#使用OpenCV的一些代码1、需要加头文件2、读取图片3、在图片上画矩形框4、 在图片上画直线 一、OpenCV的安装 1、需…...

windows永久关闭更新

不要去services.msc 服务里面关闭windowUpdata了&#xff0c;对win11和部分win10根本不管用&#xff0c;下面在教你一招永久关闭&#xff08;原理不是关闭&#xff0c;只是延长更新时间&#xff0c;时间可以设置百年后&#xff0c;所以和关闭差不多&#xff09; windows图形化…...

python类型转换笔记.python运算符笔记

进制 现代的计算机和依赖计算机的设备里都用到二进制(即0和1)来保存和表示数据&#xff0c;一个二进制表示一个比特(Bit)。 在二进制的基础上&#xff0c;计算机还支持八进制和十六进制这两种进制。 除了计算机里的进制以外&#xff0c;我们生活中经常用到的是十进制。 Pyt…...

【CSS】背景图定位问题适配不同机型

需求 如图, 实现一个带有飘带的渐变背景 其中头像必须显示飘带凹下去那里 , 需要适配不同的机型, 一不下心容易错位 实现 因为飘带背景是版本迭代中更新的, 所以飘带和渐变背景实则两个div 飘带切图如下 , 圆形部分需要契合头像 <view class"box-bg"><…...

20 个实例玩转 Java 8 Stream

先贴上几个案例&#xff0c;水平高超的同学可以挑战一下&#xff1a; 从员工集合中筛选出salary大于8000的员工&#xff0c;并放置到新的集合里。统计员工的最高薪资、平均薪资、薪资之和。将员工按薪资从高到低排序&#xff0c;同样薪资者年龄小者在前。将员工按性别分类&…...

局部变量数组和malloc申请的指针使用区别和注意事项

函数内定义的局部变量的大数组和通过malloc申请的指针在使用时有几个主要的区别和注意事项&#xff1a; 内存位置&#xff1a;函数内定义的局部变量的大数组通常在栈上分配内存&#xff0c;而通过malloc申请的指针分配的内存位于堆上。 生命周期&#xff1a;局部变量的大数组的…...

Springboot2.5.6整合Elasticsearch7.12.1完整示例

Springboot2.5.6整合Elasticsearch7.12.1完整示例 SpringBoot是2.5.6&#xff0c;elasticsearch是7.12.1 使用依赖elasticsearch-rest-high-level-client 使用RestHighLevelClient操作 1、引入Pom文件的依赖 <?xml version"1.0" encoding"UTF-8"?…...

Vim 调用外部命令学习笔记

Vim 外部命令集成完全指南 文章目录 Vim 外部命令集成完全指南核心概念理解命令语法解析语法对比 常用外部命令详解文本排序与去重文本筛选与搜索高级 grep 搜索技巧文本替换与编辑字符处理高级文本处理编程语言处理其他实用命令 范围操作示例指定行范围处理复合命令示例 实用技…...

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…...

手游刚开服就被攻击怎么办?如何防御DDoS?

开服初期是手游最脆弱的阶段&#xff0c;极易成为DDoS攻击的目标。一旦遭遇攻击&#xff0c;可能导致服务器瘫痪、玩家流失&#xff0c;甚至造成巨大经济损失。本文为开发者提供一套简洁有效的应急与防御方案&#xff0c;帮助快速应对并构建长期防护体系。 一、遭遇攻击的紧急应…...

内存分配函数malloc kmalloc vmalloc

内存分配函数malloc kmalloc vmalloc malloc实现步骤: 1)请求大小调整:首先,malloc 需要调整用户请求的大小,以适应内部数据结构(例如,可能需要存储额外的元数据)。通常,这包括对齐调整,确保分配的内存地址满足特定硬件要求(如对齐到8字节或16字节边界)。 2)空闲…...

应用升级/灾备测试时使用guarantee 闪回点迅速回退

1.场景 应用要升级,当升级失败时,数据库回退到升级前. 要测试系统,测试完成后,数据库要回退到测试前。 相对于RMAN恢复需要很长时间&#xff0c; 数据库闪回只需要几分钟。 2.技术实现 数据库设置 2个db_recovery参数 创建guarantee闪回点&#xff0c;不需要开启数据库闪回。…...

基于FPGA的PID算法学习———实现PID比例控制算法

基于FPGA的PID算法学习 前言一、PID算法分析二、PID仿真分析1. PID代码2.PI代码3.P代码4.顶层5.测试文件6.仿真波形 总结 前言 学习内容&#xff1a;参考网站&#xff1a; PID算法控制 PID即&#xff1a;Proportional&#xff08;比例&#xff09;、Integral&#xff08;积分&…...

脑机新手指南(八):OpenBCI_GUI:从环境搭建到数据可视化(下)

一、数据处理与分析实战 &#xff08;一&#xff09;实时滤波与参数调整 基础滤波操作 60Hz 工频滤波&#xff1a;勾选界面右侧 “60Hz” 复选框&#xff0c;可有效抑制电网干扰&#xff08;适用于北美地区&#xff0c;欧洲用户可调整为 50Hz&#xff09;。 平滑处理&…...

微软PowerBI考试 PL300-选择 Power BI 模型框架【附练习数据】

微软PowerBI考试 PL300-选择 Power BI 模型框架 20 多年来&#xff0c;Microsoft 持续对企业商业智能 (BI) 进行大量投资。 Azure Analysis Services (AAS) 和 SQL Server Analysis Services (SSAS) 基于无数企业使用的成熟的 BI 数据建模技术。 同样的技术也是 Power BI 数据…...

UDP(Echoserver)

网络命令 Ping 命令 检测网络是否连通 使用方法: ping -c 次数 网址ping -c 3 www.baidu.comnetstat 命令 netstat 是一个用来查看网络状态的重要工具. 语法&#xff1a;netstat [选项] 功能&#xff1a;查看网络状态 常用选项&#xff1a; n 拒绝显示别名&#…...

《基于Apache Flink的流处理》笔记

思维导图 1-3 章 4-7章 8-11 章 参考资料 源码&#xff1a; https://github.com/streaming-with-flink 博客 https://flink.apache.org/bloghttps://www.ververica.com/blog 聚会及会议 https://flink-forward.orghttps://www.meetup.com/topics/apache-flink https://n…...