RocketMQ 消费者源码解读:消费过程、负载原理、顺序消费原理

B站学习地址


上一遍学习了三种常见队列的消费原理,本次我们来从源码的角度来证明上篇中的理论。


1、准备


RocketMQ 版本

<!-- RocketMQ -->
<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <version>2.3.0</version>
</dependency>

消费者代码

import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;

@Component
@RocketMQMessageListener(consumerGroup = "my-consumer_asyn-topic", topic = "rocketmq-topic")
public class RocketmqConsumer1 implements RocketMQListener<MessageExt> {

    @Override
    public void onMessage(MessageExt messageExt) {
        byte[] body = messageExt.getBody();
        System.out.println("RocketMQ 001" + new String(body));
    }
}

2、源码阅读


2-1、对使用@RocketMQMessageListener的类进行增强,生成监听器ListenerContainer,并启动


在 RocketMQMessageListener 包下面有一个 Bean后置处理器,会对每个使用了 @RocketMQMessageListener 的类进行增强,生成 监听器,并启动这个监听器

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

在这里插入图片描述

在这里插入图片描述

2-2、基于顺序消费和并发消费创建对应的Service,创建处理消息的的线程池


org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl#start 这个方法很长,这里简化一下来看看我们比较关注的几个点,详情看源码

// 默认就是 CREATE_JUST
private volatile ServiceState serviceState = ServiceState.CREATE_JUST;

public synchronized void start() throws MQClientException {
    switch (this.serviceState) {
        case CREATE_JUST:
        
            // ... 省略 ...
            
            // 如果是顺序消费就创建顺序消费的 监听器 ConsumeMessageOrderlyService
            if (this.getMessageListenerInner() instanceof MessageListenerOrderly) {
                this.consumeOrderly = true;
                this.consumeMessageService = new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner());
            } 
            // 创建并发消费的监听器 ConsumeMessageConcurrentlyService
            else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) {
                this.consumeOrderly = false;
                this.consumeMessageService = new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner());
            }

            // ... 省略 ...
             
            // 把当前的消费者组和消费者存入本地的 ConcurrentHashMap
            boolean registerOK = mQClientFactory.registerConsumer(this.defaultMQPushConsumer.getConsumerGroup(), this);

            // ... 省略 ...
            
            // 进行下一步的启动
            mQClientFactory.start();
            log.info("the consumer [{}] start OK.", this.defaultMQPushConsumer.getConsumerGroup());
            this.serviceState = ServiceState.RUNNING;
            break;
            
            // ... 省略 ...
    }

    // ... 省略 ...
}

上篇讲到RocketMQ消费的模式是一个线程去不停的拉消息,然后丢到一个线程池里面去消费,刚刚我们看到根据是否是顺序消费,创建不同的 service,这个线程池就是在这个地方创建的。

在这里插入图片描述

在这里插入图片描述

注: 默认情况下,consumeThreadMin = 20 、consumeThreadMax = 64


2-3、拉消息和负载均衡的开始


在这里插入图片描述


在这里插入图片描述


2-4、队列和消费者之间的负载均衡


虽然拉消息的代码在前面,但没有关系,负载和拉消息都是新开启线程去执行,我个人觉得负载均衡放在前面讲更合适一些

一个topic中的queue数量大多数时候是固定的,但消费者却不是,很多时候我们会动态的去调整消费者的数量,而在上一期的理论中得知消费组中的消费者数量如果大于queue的数量是没用的,下面通过源码来看它是如何实现的


this.rebalanceService.start();

在这里插入图片描述


循环遍历每一个消费者去负载均衡


在这里插入图片描述


consumerTable 数据的由来参看【2-4-1、consumerTable 数据的由来】


负载的核心代码 rebalanceByTopic

消费模式有集群消费和广播消费,负载均衡肯定是基于:集群消费

private boolean rebalanceByTopic(final String topic, final boolean isOrder) {
    boolean balanced = true;
    switch (messageModel) {
        case BROADCASTING: {
           // ... 省略 ...
        }
        case CLUSTERING: {
            // 获取当前 topic 的 queue
            Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
            
            // 发起 netty请求,获取当前组下面的消费者
            List<String> cidAll = this.mQClientFactory.findConsumerIdList(topic, consumerGroup);
           
             // ... 省略 ... 参数校验
             
            if (mqSet != null && cidAll != null) {
                List<MessageQueue> mqAll = new ArrayList<>();
                mqAll.addAll(mqSet);

                Collections.sort(mqAll);
                Collections.sort(cidAll);

                AllocateMessageQueueStrategy strategy = this.allocateMessageQueueStrategy;

                List<MessageQueue> allocateResult = null;
                try {
                    // 使用策略进行分配,默认的策略是平均分配
                    allocateResult = strategy.allocate(this.consumerGroup,this.mQClientFactory.getClientId(), mqAll, cidAll);
                } catch (Throwable e) {
                    log.error("allocate message queue exception. strategy name: {}, ex: {}", strategy.getName(), e);
                    return false;
                }

                Set<MessageQueue> allocateResultSet = new HashSet<>();
                if (allocateResult != null) {
                    allocateResultSet.addAll(allocateResult);
                }
                
                // 对分配的结果进行 设置
                boolean changed = this.updateProcessQueueTableInRebalance(topic, allocateResultSet, isOrder);
                if (changed) {
                    log.info(
                        "client rebalanced result changed. allocateMessageQueueStrategyName={}, group={}, topic={}, clientId={}, mqAllSize={}, cidAllSize={}, rebalanceResultSize={}, rebalanceResultSet={}",
                        strategy.getName(), consumerGroup, topic, this.mQClientFactory.getClientId(), mqSet.size(), cidAll.size(),
                        allocateResultSet.size(), allocateResultSet);
                    this.messageQueueChanged(topic, mqSet, allocateResultSet);
                }

                balanced = allocateResultSet.equals(getWorkingMessageQueue(topic));
            }
            break;
        }
        default:
            break;
    }

    return balanced;
}

  1. 如何进入rebalanceByTopic,参看【2-4-2、进入 rebalanceByTopic】
  2. 默认的平均分配策略如何执行的,参看【2-4-3、平均分配策略原理】
  3. 分配结果参看【2-4-4、重置队列和消费者之间的关系】

2-4-1、consumerTable 数据的由来

在【2-3、拉消息和负载均衡的开始】开始的第一张图中 start开始之前执行了一个 registerConsumer 方法,这个方法就是把当前消费者和其组 consumerTable

在这里插入图片描述

2-4-2、进入 rebalanceByTopic

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


2-4-3、平均分配策略原理

  1. 这里假设当前queue只有 1个,消费者有 2个,当前消费者是第一个
  2. 下面的 index 、mod 等其它参数都是基于这个假设来计算的
public class AllocateMessageQueueAveragely extends AbstractAllocateMessageQueueStrategy {

    @Override
    public List<MessageQueue> allocate(String consumerGroup, String currentCID, List<MessageQueue> mqAll,
        List<String> cidAll) {

        List<MessageQueue> result = new ArrayList<>();
        if (!check(consumerGroup, currentCID, mqAll, cidAll)) {
            return result;
        }
        // index = 1
        int index = cidAll.indexOf(currentCID);
        // mod = 2
        int mod = mqAll.size() % cidAll.size();
        // averageSize = 1
        int averageSize =
            mqAll.size() <= cidAll.size() ? 1 : (mod > 0 && index < mod ? mqAll.size() / cidAll.size()
                + 1 : mqAll.size() / cidAll.size());
        // startIndex = 1
        int startIndex = (mod > 0 && index < mod) ? index * averageSize : index * averageSize + mod;
        // range = 0
        int range = Math.min(averageSize, mqAll.size() - startIndex);
        for (int i = 0; i < range; i++) {
            result.add(mqAll.get((startIndex + i) % mqAll.size()));
        }
        // result 为空数组
        return result;
    }

    @Override
    public String getName() {
        return "AVG";
    }
}

通过上面的计算可以得出,当消费者的数量大于队列数量的时候,返回值是 空数组


2-4-4、重置队列和消费者之间的关系

重置的操作分三步

  1. 删除进程中与当前消费者绑定的队列
  2. 删除broker中的绑定的关系
  3. 建立新的关系
private boolean updateProcessQueueTableInRebalance(final String topic, final Set<MessageQueue> mqSet,
    final boolean isOrder) {
    boolean changed = false;

    // 删除进程中与当前消费者绑定的队列
    HashMap<MessageQueue, ProcessQueue> removeQueueMap = new HashMap<>(this.processQueueTable.size());
    Iterator<Entry<MessageQueue, ProcessQueue>> it = this.processQueueTable.entrySet().iterator();
    while (it.hasNext()) {
        Entry<MessageQueue, ProcessQueue> next = it.next();
        MessageQueue mq = next.getKey();
        ProcessQueue pq = next.getValue();
        removeQueueMap.put(mq, pq);
        // ... 删除操作 ...
    }

    // 删除broker中的绑定的关系
    for (Entry<MessageQueue, ProcessQueue> entry : removeQueueMap.entrySet()) {
        MessageQueue mq = entry.getKey();
        ProcessQueue pq = entry.getValue();

        if (this.removeUnnecessaryMessageQueue(mq, pq)) {
            this.processQueueTable.remove(mq);
            changed = true;
            log.info("doRebalance, {}, remove unnecessary mq, {}", consumerGroup, mq);
        }
    }

    // 建立新的关系
    boolean allMQLocked = true;
    List<PullRequest> pullRequestList = new ArrayList<>();
    for (MessageQueue mq : mqSet) {
        // ... 建立新的关系 ...
        
        // 并把新的结果存入 pullRequestList 这很重要
    }

    if (!allMQLocked) {
        mQClientFactory.rebalanceLater(500);
    }
    
    // 基于新的绑定关系去获取消息
    this.dispatchPullRequest(pullRequestList, 500);

    return changed;
}

如果上一步的平均分配的结果为 空数组,那在这里就会删除所有的绑定关系,并且无法建立新的关系,也就说明当消费组中的消费者的数量大于queue的数量是无用的


dispatchPullRequest,这个方法的实现类只有如下代码

@Override
public void dispatchPullRequest(final List<PullRequest> pullRequestList, final long delay) {
    for (PullRequest pullRequest : pullRequestList) {
        if (delay <= 0) {
            this.defaultMQPushConsumerImpl.executePullRequestImmediately(pullRequest);
        } else {
            this.defaultMQPushConsumerImpl.executePullRequestLater(pullRequest, delay);
        }
    }
}

这个操作的最终结果就是把 pullRequest,放入 messageRequestQueue 中,delay不为空的时候,会开启一个定时认为每隔delay时间往messageRequestQueue里面塞一次, 这个点很重要


如果分配给当前消费者处理的 queue有2个,那这里就会生成两个 pullRequest


2-5、拉消息


解释完负载均衡,让我们再次回到【2-3】,现在来看看2-3提到的拉消息逻辑

在这里插入图片描述


2-5-1、固定一个线程去拉消息

在这里插入图片描述


  1. 在【2-4-4】中得出,分配给当前消费者的queue会生成一个 PullRequest,然后以500ms一次塞进 messageRequestQueue里面去
  2. take 方法是一个阻塞的方法,如果队列中没有数据,它会阻塞一直等待有数据为止
  3. public class PullRequest implements MessageRequest

2-5-2、拉消息的过程

在这里插入图片描述


pullMessage 是拉消息的核心代码,简单来说就是各种判断,组装参数去请求broker获取消息,这里要关注的几个参数

  1. CommunicationMode.ASYNC 使用异步拉取参数
  2. pullCallback 拉到数据后的回调方法

在这里插入图片描述


在真实发起netty请求之前也是一些参数的处理,流程参看下面的截图

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


成功拉到消息后就调用回调的 onSuccess 方法


2-5-3、消息回调方法 onSuccess

org.apache.rocketmq.client.consumer.PullCallback#onSuccess
PullCallback 有onSuccess和onException,在 onSuccess 中 有个 switch语句,对于正常拉到消息的状态为 FOUND,所以来着重看这个部分的代码块

public void onSuccess(PullResult pullResult) {
    if (pullResult != null) {
        pullResult = DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(pullRequest.getMessageQueue(), pullResult, subscriptionData);
        switch(pullResult.getPullStatus()) {
        case FOUND:
            long prevRequestOffset = pullRequest.getNextOffset();
            pullRequest.setNextOffset(pullResult.getNextBeginOffset());
            long pullRT = System.currentTimeMillis() - beginTimestamp;
            DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullRT(pullRequest.getConsumerGroup(), pullRequest.getMessageQueue().getTopic(), pullRT);
            long firstMsgOffset = 9223372036854775807L;
            if (pullResult.getMsgFoundList() != null && !pullResult.getMsgFoundList().isEmpty()) {
                firstMsgOffset = ((MessageExt)pullResult.getMsgFoundList().get(0)).getQueueOffset();
                DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullTPS(pullRequest.getConsumerGroup(), pullRequest.getMessageQueue().getTopic(), (long)pullResult.getMsgFoundList().size());
                boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList());
                // 消息丢入线程池消费,分并发消费和顺序消费
                DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(pullResult.getMsgFoundList(), processQueue, pullRequest.getMessageQueue(), dispatchToConsume);
                // 继续把请求放入队列,由单线程继续去拉取消息 默认 pullInterval = 0
                if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0L) {
                    DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest, DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval());
                } else {
                    DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
                }
            } else {
                DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
            }

            if (pullResult.getNextBeginOffset() < prevRequestOffset || firstMsgOffset < prevRequestOffset) {
                DefaultMQPushConsumerImpl.log.warn("[BUG] pull message result maybe data wrong, nextBeginOffset: {} firstMsgOffset: {} prevRequestOffset: {}", new Object[]{pullResult.getNextBeginOffset(), firstMsgOffset, prevRequestOffset});
            }
            break;
            
        // ... 省略 ...
    }

}

DefaultMQPushConsumerImpl.this.executePullRequestImmediately 这个方法在负载均衡的最后一步已经讲到了,其实就是把 pullRequest 存入 messageRequestQueue 中


2-5-4、并发消费

@Override
public void submitConsumeRequest(
    final List<MessageExt> msgs,
    final ProcessQueue processQueue,
    final MessageQueue messageQueue,
    final boolean dispatchToConsume) {
    final int consumeBatchSize = this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize();
    if (msgs.size() <= consumeBatchSize) {
        ConsumeRequest consumeRequest = new ConsumeRequest(msgs, processQueue, messageQueue);
        try {
            // 丢进线程池去消费
            this.consumeExecutor.submit(consumeRequest);
        } catch (RejectedExecutionException e) {
            this.submitConsumeRequestLater(consumeRequest);
        }
    } else {
        // 消息量过大,分批消费,逻辑一样
    }
}

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

这里可以看到并发消息,只是直接就把消息组装成一个可执行的 Runnable,然后交给线程池去执行


2-5-5、顺序消费

顺序消息可不一样,顺序消息必须要求同一个队列的消息只能单线程去消费才可以保证绝对的顺序

在这里插入图片描述


可以看到顺序消息也是直接把消息丢进了线程池,但是在进行消息处理的时候,使用队列进行加锁了,相当于这个队列只能单线程消费了,后续逻辑就都一样了,最终走到我们自己重写的 onMessage 里面

在这里插入图片描述


3、总结

看完上面的源码你最少可以回答下面几个问题

  1. RocketMQ消费的流程是怎么样的
  2. 为什么消费者大于queue的时候,消费者就没用了
  3. 顺序消费如何保证顺序的
  4. 添加消费者的时候,如何重新分配的

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

yolov5关键点检测-实现溺水检测与警报提示(代码+原理)

基于YOLOv5的关键点检测应用于溺水检测与警报提示是一种结合深度学习与计算机视觉技术的安全监控解决方案。该项目通常会利用YOLOv5强大的实时目标检测能力&#xff0c;并通过扩展或修改网络结构以支持人体关键点检测&#xff0c;来识别游泳池或其他水域中人们的行为姿态。 项…

常关型p-GaN栅AlGaN/GaN HEMT作为片上电容器的建模与分析

来源&#xff1a;Modeling and Analysis of Normally-OFF p-GaN Gate AlGaN/GaN HEMT as an ON-Chip Capacitor&#xff08;TED 20年&#xff09; 摘要 提出了一种精确基于物理的解析模型&#xff0c;用于描述p-GaN栅AlGaN/GaN高电子迁移率晶体管&#xff08;HEMT&#xff09…

【Linux】Vim编辑器

专栏文章索引&#xff1a;Linux 目录 在Vim编辑器中&#xff0c;一个Tab键相当于几个空格&#xff1f; 在Vim编辑器中&#xff0c;一个Tab键相当于几个空格&#xff1f; 在Vim编辑器中&#xff0c;默认情况下&#xff0c;一个Tab键相当于8个空格。 这是Vim的默认设置&#x…

【C++】哈希之位图

目录 一、位图概念二、海量数据面试题 一、位图概念 假如有40亿个无重复且没有排序的无符号整数&#xff0c;给一个无符号整数&#xff0c;如何判断这个整数是否在这40亿个数中&#xff1f; 我们用以前的思路有这些&#xff1a; 把这40亿个数遍历一遍&#xff0c;直到找到为…

鸿蒙OS元服务开发:【(Stage模型)设置悬浮窗】

一、设置悬浮窗说明 悬浮窗可以在已有的任务基础上&#xff0c;创建一个始终在前台显示的窗口。即使创建悬浮窗的任务退至后台&#xff0c;悬浮窗仍然可以在前台显示。通常悬浮窗位于所有应用窗口之上&#xff1b;开发者可以创建悬浮窗&#xff0c;并对悬浮窗进行属性设置等操…

frp内网穿透之(反向代理nginx)

通过公网 https 连接访问内网&#xff08;局域网&#xff09;本地http服务如下&#xff1a; 1.准备工作 ​ 想要实现内网穿透功能首先我们需要准备&#xff1a; 一台公网服务器&#xff08;用作frps的服务端&#xff09;一台需要做转发的内网服务器&#xff08;用作frpc的客…

D-迷恋网游(遇到过的题,做个笔记)

我的代码&#xff1a; #include <iostream> using namespace std; int main() {int a, b, c; //a表示内向&#xff0c;b表示外向&#xff0c;c表示无所谓cin >> a >> b >> c; //读入数 if (b % 3 0 || 3-b % 3 < c) //如果外向的人能够3人组成…

Golang Channel底层实现原理

1、本文讨论Channel的底层实现原理 首先&#xff0c;我们看Channel的结构体 简要介绍管道结构体中&#xff0c;几个关键字段 在Golang中&#xff0c;管道是分为有缓冲区的管道和无缓冲区的管道。 这里简单提一下&#xff0c;缓冲区大小为1的管道和无缓冲区的管道的区别&…

Android14之BpBinder构造函数Handle拆解(二百零四)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…

详解人工智能(概念、发展、机遇与挑战)

前言 人工智能&#xff08;Artificial Intelligence&#xff0c;简称AI&#xff09;是一门新兴的技术科学&#xff0c;是指通过模拟、延伸和扩展人类智能的理论、方法、技术和应用系统&#xff0c;以实现对人类认知、决策、规划、学习、交流、创造等智能行为的模拟、延伸和扩展…

Linux 线程互斥、互斥量、可重入与线程安全

目录 一、线程互斥 1、回顾相关概念 2、抢票场景分析代码 多个线程同时操作全局变量 产生原因 如何解决 二、互斥量 1、概念 2、初始化互斥量&#xff1a; 方法1&#xff1a;静态分配 方法2&#xff1a;动态分配 3、销毁互斥量&#xff1a; 4、加锁和解锁 示例抢…

MySQL 8.0.13安装配置教程

写个博客记录一下&#xff0c;省得下次换设备换系统还要到处翻教程&#xff0c;直接匹配自己常用的8.0.13版本 1.MySQL包解压到某个路径 2.将bin的路径加到系统环境变量Path下 3.在安装根目录下新建my.ini配置文件&#xff0c;并用编辑器写入如下数据 [mysqld] [client] port…

基于jsp网上教师点评系统

基于jsp网上教师点评系统 关键词&#xff1a;教师点评 信息技术 JSP技术 系统实现 首页 评分规则 教室信息 后台首页 相关技术介绍 B/S架构 对于架构&#xff0c;听起来说我们可能比较陌生&#xff0c;但对于通俗的语法讲。他的访问方式是通过网址还是说通过点图标这…

YoloV8改进策略:Neck改进|GCNet(独家原创)|附结构图

摘要 本文使用GCNet注意力改进YoloV8,在YoloV8的Neck中加入GCNet实现涨点。改进方法简单易用&#xff0c;欢迎大家使用&#xff01; 论文:《GCNet: Non-local Networks Meet Squeeze-Excitation Networks and Beyond》 非局部网络&#xff08;NLNet&#xff09;通过为每个查…

Flex布局:打造灵动、响应式网页设计的利器

Flex布局&#xff08;Flexible Box Layout&#xff09;&#xff0c;也称为弹性盒布局&#xff0c;是一种现代CSS布局模式&#xff0c;旨在为复杂、响应式的网页设计提供更加灵活、简洁的解决方案。Flex布局通过定义一个弹性容器&#xff08;flex container&#xff09;及其内部…

49岁前港姐退圈出嫁「南丫岛王子」,打排卵针高龄连生两女。

现年49岁的吴忻熹&#xff08;原名吴文忻&#xff09;1998年参选香港小姐夺得季军入行&#xff0c;在TVB签约发展平平&#xff0c;继而转战影坛&#xff0c;凭性感演出而为人熟悉。其后她在2011年嫁给有「南丫岛王子」之称的金融才俊&#xff0c;并在近40岁开始诞下两名女儿。吴…

Set a Light 3D Studio:探索光影艺术的全新维度mac/win中文版

Set a Light 3D Studio 是一款领先的三维建模和渲染软件&#xff0c;它将设计师、艺术家和摄影师的创意想法转化为生动逼真的三维场景。这款软件以其强大的功能和直观的界面&#xff0c;成为行业内众多专业人士的首 选工具。 set.a.light 3D STUDIO中文版软件获取 在Set a Lig…

最简单的 AAC 音频码流解析程序

最简单的 AAC 音频码流解析程序 最简单的 AAC 音频码流解析程序原理源程序运行结果下载链接参考 最简单的 AAC 音频码流解析程序 参考雷霄骅博士的文章&#xff1a;视音频数据处理入门&#xff1a;AAC音频码流解析 本文中的程序是一个AAC码流解析程序。该程序可以从AAC码流中…

信息系统项目管理师——第17章项目干系人管理

本章节内容属于10大管理知识领域&#xff0c;选择、案例、论文都会考。 选择题&#xff0c;稳定考1-2分左右&#xff0c;新教材基本考课本原话&#xff0c;这个分不能丢。 案例题&#xff0c;本期考的概率一般。 论文题&#xff0c;202205期考过。 1管理基础 管理的重要性 为…

QT5-qmediaplayer播放视频及进度条控制实例

qmediaplayer是QT5的播放视频的一个模块。它在很多时候还是要基于第三方的解码器。这里以Ubuntu系统为例&#xff0c;记录其用法及进度条qslider的控制。 首先&#xff0c;制作一个简单的界面文件mainwindow.ui&#xff1a; 然后&#xff0c;下载一个mp4或其他格式视频&#x…