ABP - 事件总线之分布式事件总线

ABP - 事件总线之分布式事件总线

  • 1. 分布式事件总线的集成
    • 1.2 基于 RabbitMQ 的分布式事件总线
  • 2. 分布式事件总线的使用
    • 2.1 发布
    • 2.2 订阅
    • 2.3 事务和异常处理
  • 3. 自己扩展的分布式事件总线实现

事件总线可以实现代码逻辑的解耦,使代码模块之间功能职责更清晰。而分布式事件总线的功能不止这些,它允许我们通过消息队列进行中转,发布和订阅跨应用/服务边界传输的事件,经常被用作微服务或不同应用程序之间异步发送和接收消息的手段,属于分布式应用通讯的方式之一。

分布式事件总线依赖于 消息队列 中间件,ABP 框架中提供了4种开箱即用的提供程序,我们也可以基于抽象的接口自行实现分布式事件总线提供程序,已有的四种分别适用于 RabbitMQ、Kafka、Rebus 等消息队列,还有一种是默认实现:进程内的分布式事件总线,它允许我们在没有接入消息队列时,也能够编写与分布式体系结构兼容的代码,方便日后可能的微服务拆分,这时它的工作方式与本地事件总线一样,整体的设计思想就和微软的分布式缓存一样。

1. 分布式事件总线的集成

以下的演示还是基于控制台程序,分布式事件总线不会默认集成在 ABP 启动模板之中,需要我们自行集成,Web 应用的集成方式也是一样的。

通过以下命令创建一个控制台程序启动模板:

abp new AbpDistributeEventBusSample  -t console

之后再打开解决方案,由于分布式事件总线是在不同应用程序之间进行通讯的,所以还需要再创建一个控制台项目进行演示,将解决方案中的项目复制一份即可。

在这里插入图片描述
本地分布式事件总线

首先讲一下进程内的事件总线的集成,这个还是有些必要的,如果有考虑后续进行微服务拆分的情况下,前期对于事件总线的使用可以基于这个进行开发。当然并不是说本地事件总线就不推荐使用,从我自己的日常工作经验中,很多时候还是分布式事件总线和本地事件总线搭配使用的。

首先,分布式事件总线的核心依赖包为 Volo.Abp.EventBus,和本地事件总线一样。我们在需要集成的项目的根目录下,通过以下命令进行集成:

abp add-package Volo.Abp.EventBus

由于是本地分布式事件总线,所以这种方式下是没办法跨进程通讯的,使用方式和上一篇讲的本地事件总线类似,不过使用的时候不再通过 ILocalEventBus接口,而是通过 IDistributedEventBus 接口,主要是用于业务逻辑的解耦,同时也为后续可能的分布式拆分做好准备。具体的使用方式下面细讲。

1.2 基于 RabbitMQ 的分布式事件总线

ABP 框架提供了三种开箱即用的分布式事件总线提供程序,分别对应 RabbitMQ、Kafka、Rebus,通过结合第三方消息队列实现真正基于消息跨进程通讯的分布式事件总线,这里主要讲一下基于 RabbitMQ 的方式,其他方式用法类似。

首先,基于 RabbitMQ 的分布式事件总线需要安装 Volo.Abp.EventBus.RabbitMQ 驱动程序包。可通过一下方式安装:

abp add-package Volo.Abp.EventBus.RabbitMQ

上面创建的两个工程都要安装,因为我们要演示两个进程间的通讯。

在这里插入图片描述
之后,需要部署 RabbitMQ 消息队列,这里我通过 docker 快速启动一个带有管理平台的 RabbitMQ,命令如下:

docker run -d -p 5672:5672 -p 15672:15672 --name myrabbitmq rabbitmq:management

RabbitMQ 默认用户密码为:guest / guest,这里只是用于测试就直接使用了。生产环境中大家最好将默认用户禁用,另行创建自己的用户。RabbitMQ 相关的更详细的使用和配置这里就不细讲了,详细内容可见 [[2.1 RabbitMQ基本概念]] 系列文章。

在这里插入图片描述
然后,添加分布式事件总线相应的配置,我们可以在 appsettings.json 文件中添加以下配置节点:

"RabbitMQ": {"Connections": {// 这里的配置支持 RabbitMQ 官方 sdk 中的 ConnectionFactory 的任意属性的配置"Default": {"HostName": "localhost", // rabbitmq 地址,集群环境下多个ip用逗号分隔"Port": "5672", // rabbbitmq 端口,默认5672"UserName": "guest","Password": "guest"}// 允许配置多个 rabbitmq 连接,但只能有一个用于事件总线//"Second": {//  "HostName": "xxx.xxx.xxx.xxx", // rabbitmq 地址,集群环境下多个ip用逗号分隔//  "Port": "5672" // rabbbitmq 端口,默认5672//}},"EventBus": {"ClientName": "MyClientName", // 用于事件总线的队列名"ExchangeName": "MyExchangeName", // 用于事件总线的交换机名称// "ConnectionName": "Default" // 配置多个连接时,指定用于事件总线的 RabbitMQ 连接,默认是 Default}}

以上配置,最后都会被转换为 AbpRabbitMqOptions 和 AbpRabbitMqEventBusOptions,所以我们也可以直接在代码中对这两个选项进行配置:

Configure<AbpRabbitMqOptions>(options =>
{options.Connections.Default.UserName = "guest";options.Connections.Default.Password = "guest";options.Connections.Default.HostName = "localhost";options.Connections.Default.Port = 5672;
});Configure<AbpRabbitMqEventBusOptions>(options =>
{options.ClientName = "TestApp1";options.ExchangeName = "TestMessages";
});

两种方式选择一种即可,如果两种方式同时使用,代码配置优先于配置文件。解决方案的两个项目都需要进行配置,进行通讯的两个项目需要连接到同一个队列。

完成上面的配置之后,启动应用,即可看到 RabbitMQ 中创建了我们配置的交换机和队列:

在这里插入图片描述

2. 分布式事件总线的使用

2.1 发布

事件发布需要一个事件对象,官方将之称为 Eto(事件传输对象),这是一个普通类,用于保存和事件相关的数据,一般以 Eto 作为后缀。就算一个事件不需要传输任何数据,也必须创建一个空类,这和上一章的本地事件总线是一样的,由于在分布式事件触发之后,事件对象会被序列化传输到消息队列中,所以事件对象应避免循环引用、多态、私有setter,并提供默认(空)构造函数,如果你有其他的构造函数(虽然某些序列化器可能会正常工作)。 下面是一个用于测试的 Eto 对象的定义。

[EventName("helloEvent")]
public class HelloEto
{public string Who { get; set; }public DateTime When { get; set; }public string ToWho { get; set; }
}

默认情况下,事件名将事件名称将是事件类的全名,我们可以通过 EventNameattribute 特性指定事件名称。

分布式事件的发布通过 IDistributedEventBus 接口,只需将其注入到相应的类中使用即可,使用方式和本地事件总线一样。

public class HelloWorldService : ITransientDependency
{private readonly IDistributedEventBus _distributedEventBus;public HelloWorldService(IDistributedEventBus distributedEventBus){_distributedEventBus = distributedEventBus;}public async Task SayHelloAsync(){await _distributedEventBus.PublishAsync(new HelloEto{Who = "Jerry",When = DateTime.Now,ToWho = "Jack"});}
}

以上代码写在 AbpDistributeSample 项目中,这里是事件发布的进程。

2.2 订阅

事件的订阅也和本地事件总线类似,这里通过实现了 IDistributedEventHandler<TEvent> 接口的处理器来处理事件,当前像上一章本地事件总线中讲到的,我们也可以通过 IDistributedEventBus 来自己订阅事件。

public class HelloDistribuedEventHandler : IDistributedEventHandler<HelloEto>, ITransientDependency
{public Task HandleEventAsync(HelloEto eventData){Console.WriteLine($"{eventData.Who} Say Hello To {eventData.ToWho} at { eventData.When.ToString("yyyy-MM-dd HH:mm:ss") }");return Task.CompletedTask;}
}

一个事件处理程序可以同时处理多种事件,只需要实现多个针对不同 ETO 的 IDistributedEventHandler 泛型接口即可。

之后通过 vs 设置多项目启动:

在这里插入图片描述
应用启动之后,可以看到控制台的输出如下,一个简单的基于消息队列的跨进程通讯已经完成:

在这里插入图片描述
同时在 RabbitMQ 上可以看到已经连接上来了2个消费者,每个进程既作为生产者,也作为消费者:

在这里插入图片描述
通过源码可以看到,分布式事件总线中会进行初始化,其实就是根据配置连接了 RabbitMQ 队列,并创建了一个消费者自动订阅消息队列上的事件,当接受到消息之后会从消息中获取消息的类型和具体的数据,触发消息执行处理程序,如果事件处理程序成功执行(没有抛出任何异常),它将向消息代理发送确认(ACK)。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这里的 EventTypes 其实就是维护消息类型和它的类名的对应关系的一个字典,在 SubscribeHandlers 中初始化消息类型和执行器对应关系的时候维护的,这个就和前一篇的本地事件总线大同小异了。

而消息的发布过程就更简单了,也就是将消息序列化,发送到 RabbitMQ 队列中。

在这里插入图片描述
在这里插入图片描述
ABP 分布式事件总线从 5.0 版本开始,还加入了 Inbox 、Outbox 机制,我们可以通过 AbpDistributedEventBusOptions 选项进行配置,例如:

Configure<AbpDistributedEventBusOptions>(option =>
{option.Inboxes.Configure(config => config.UseDbContext<TDbContext>());option.Outboxes.Configure(config => config.UseDbContext<TDbContext>());
});

从配置方式也可以看出,这里是借助了数据库的,实际上 Inbox 就是从消息队列接收到消息之后,将消息先存入数据库中,没有直接执行。

在这里插入图片描述
在这里插入图片描述
而在应用启动的时候,消息队列模块会注册一个 InboxProcessManager,实际上这就是一个后台工作者,这里面又用到 IInboxProcessor

在这里插入图片描述
在这里插入图片描述
在 InboxProcessor 中再通过定时器,每个一段时间从数据库中读取消息真正地去执行,并且将数据库中的记录删除。

在这里插入图片描述
在这里插入图片描述
OutBox 的机制也是一样的,这样做大概就是起到缓冲的作用,避免短时间内大量的消息对消息队列或者我们的消费者造成冲击导致应用崩溃,而是将这些消息的发送、执行均匀地处理。

2.3 事务和异常处理

分布式事件总线默认实现是 LocalDistributedEventBus,在没有集成 RabbitMQ 等第三方消息队列中间件,并且没有使用发件箱/收件箱模式的情况,事件发布和事件订阅是在同一个进程的。如果当前事件发布的模块使用了工作单元,事件总线是在和发布事件的同一工作单元范围内执行事件处理程序,如果事件处理程序抛出异常,那么相关的工作单元(数据库事务)将被回滚。这样,我们的应用程序逻辑和事件处理逻辑就具有事务性(原子)。如果想忽略事件处理程序中的错误,则必须在处理程序中使用try-catch块,并且不应该重新抛出异常。

当我们切换到真正的分布式事件总线提供程序时,事件处理程序将在不同的进程/应用程序中执行。在这种情况下,实现事务性事件发布的唯一方法是使用发件箱/收件箱模式。这篇文章上面的内容简单地提了一下发件箱/收件箱,之后还会有文章详细梳理它的工作模式。

如果在未真正使用分布式事件总线的情况下,想在工作单元中立即发布事件,而不是等到工作单元中的逻辑执行完成之后再发布,可以在使用IDistributedEventBus.PublishAsync方法时将onUnitOfWorkComplete设置为false。如果接入 RabbitMQ 等第三方消息队列实现了分布式事件总线,想要立即发布事件,则还得将 useOutbox 设置为 false。

3. 自己扩展的分布式事件总线实现

要注意的一点是,ABP 框架的分布式事件总线只能配置使用一个队列,这就意味着如果一个进程需要和多个进程进行通讯时,是无法通过不同的队列进行区分的。当多个进程都连接到同一个队列中时,我们无法控制一个进程发送的消息会被哪个进程所消费,无法单独通知某一个消费者,这是需要注意的。

有一种折中的方式,那就是如果一个事件只想发送给某一个进程,那就只在这个进程中实现其处理程序,其他没有实现处理程序的进程接收到消息之后,因为没有处理会抛出异常,消息重新返回消息队列中。这种方式依旧是存在一些问题,只是将工作中的一点经验供大家参考一下。

public interface IWantDistributedEventBus
{Task PublishAsync(Type eventType, object eventData, string queueName);
}public class RabbitMqWantDistributedEventBus : IWantDistributedEventBus, ISingletonDependency
{protected ILocalEventBus LocalEventBus { get; }protected IConnectionPool ConnectionPool { get; }protected WantRabbitMqEventBusOptions WantRabbitMqEventBusOptions { get; }protected AbpLocalEventBusOptions AbpLocalEventBusOptions { get; }protected IRabbitMqSerializer Serializer { get; }protected IRabbitMqMessageConsumer Consumer { get; private set; }protected IRabbitMqMessageConsumerFactory MessageConsumerFactory { get; }protected ConcurrentDictionary<string, Type> EventTypes { get; }public RabbitMqWantDistributedEventBus(IConnectionPool connectionPool,IOptions<WantRabbitMqEventBusOptions> options,IOptions<AbpLocalEventBusOptions> localEventBusOptions,IRabbitMqSerializer serializer,IRabbitMqMessageConsumerFactory rabbitMqMessageConsumerFactory,ILocalEventBus localEventBus){ConnectionPool = connectionPool;WantRabbitMqEventBusOptions = options.Value;Serializer = serializer;MessageConsumerFactory = rabbitMqMessageConsumerFactory;LocalEventBus = localEventBus;AbpLocalEventBusOptions = localEventBusOptions.Value;EventTypes = new ConcurrentDictionary<string, Type>();}/// <summary>/// 队列消费者初始化/// </summary>public void Initialize(){foreach (var queueName in WantRabbitMqEventBusOptions.ConsumeQueueNames){Consumer = MessageConsumerFactory.Create(new ExchangeDeclareConfiguration(WantRabbitMqEventBusOptions.ExchangeName,type: "direct",durable: true),new QueueDeclareConfiguration(queueName,durable: true,exclusive: false,autoDelete: false),WantRabbitMqEventBusOptions.ConnectionName);Consumer.BindAsync(queueName);Consumer.OnMessageReceived(ProcessEventAsync);}LoadEventTypes(AbpLocalEventBusOptions.Handlers);}/// <summary>/// 消息消费逻辑/// </summary>/// <param name="message">从队列接收到的消息</param>/// <returns></returns>private async Task ProcessEventAsync(IModel channel, BasicDeliverEventArgs ea){var msgData = (MessageData)Serializer.Deserialize(ea.Body.ToArray(), typeof(MessageData));var eventName = msgData.Type;var eventType = EventTypes.GetOrDefault(eventName);// eventType为空, 即不存在类型对应的Handler,消息不应该被消费if (eventType == null){// return;throw new NotFoundHandlerException($"不存在{eventName}消息Handler!");}// 通过消息类型转发LocalHandler进行具体逻辑处理var eventData = Serializer.Deserialize(Serializer.Serialize(msgData.Data), eventType);await LocalEventBus.PublishAsync(eventType, eventData);}/// <summary>/// 根据现有注册的Handler构建消息类型集合/// </summary>/// <param name="handlers">当前应用中Handler类型集合</param>/// <returns></returns>public Task LoadEventTypes(ITypeList<IEventHandler> handlers){foreach (var handler in handlers){var interfaces = handler.GetInterfaces();foreach (var @interface in interfaces){if (!typeof(IEventHandler).GetTypeInfo().IsAssignableFrom(@interface)){continue;}var genericArgs = @interface.GetGenericArguments();if (genericArgs.Length == 1){var eventTypeName = genericArgs[0].FullName;if (!EventTypes.ContainsKey(eventTypeName)){EventTypes[eventTypeName] = genericArgs[0];}}}}return Task.CompletedTask;}/// <summary>/// 发布消息/// </summary>/// <param name="eventType">消息类型</param>/// <param name="eventData">消息内容</param>/// <param name="queueName">队列名称</param>/// <returns></returns>public Task PublishAsync(Type eventType, object eventData, string queueName){var msg = new MessageData { Type = eventType.FullName, Data = eventData };var body = Serializer.Serialize(msg);using (var channel = ConnectionPool.Get(WantRabbitMqEventBusOptions.ConnectionName).CreateModel()){channel.ExchangeDeclare(WantRabbitMqEventBusOptions.ExchangeName,"direct",durable: true);var properties = channel.CreateBasicProperties();properties.DeliveryMode = RabbitMqConsts.DeliveryModes.Persistent;var queue = channel.QueueDeclare(queueName, durable: true, exclusive: false, autoDelete: false);channel.QueueBind(queueName, WantRabbitMqEventBusOptions.ExchangeName, queueName);channel.BasicPublish(exchange: WantRabbitMqEventBusOptions.ExchangeName,routingKey: queueName,mandatory: true,basicProperties: properties,body: body);}return Task.CompletedTask;}
}public class WantRabbitMqEventBusOptions
{public string ConnectionName { get; set; }public string ClientName { get; set; }public string ExchangeName { get; set; }public IList<string> ConsumeQueueNames { get; }public WantRabbitMqEventBusOptions(){ConsumeQueueNames = new List<string>();}
}public class MessageData
{public string Type { get; set; }public object Data { get; set; }
}[DependsOn(typeof(AbpRabbitMqModule))]
[DependsOn(typeof(AbpEventBusModule))]
public class WantAbpEventBusRabbitMqModule : AbpModule
{public override void ConfigureServices(ServiceConfigurationContext context){var configuration = context.Services.GetConfiguration();Configure<WantRabbitMqEventBusOptions(configuration.GetSection("RabbitMQ:EventBus"));}public override void OnApplicationInitialization(ApplicationInitializationContext context){context.ServiceProvider.GetRequiredService<RabbitMqWantDistributedEventBus>().Initialize();}
}

以上就是 ABP 框架下分布式事件总线的基本知识点,其中也提到了 发件箱/收件箱 等新特性,ABP 框架经过长时间的发展,基本与 .NET 版本同步更新,到现在 9.0 版本,各种组件的特性也在逐步迭代更新。分布式事件总线除了这篇文章中提到的基本内容之外,还有一些新特性,后续会有一篇文章专门讲一下这些特性。

参考文档:
ABP 官方文档 - 分布式事件总线

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

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

相关文章

Zotero7 从下载到安装

Zotero7 从下载到安装 目录 Zotero7 从下载到安装下载UPDATE2025.2.16 解决翻译api异常的问题 下载 首先贴一下可用的链接 github官方仓库&#xff1a;https://github.com/zotero/zotero中文社区&#xff1a;https://zotero-chinese.com/官网下载页&#xff1a;https://www.z…

typecho快速发布文章

typecho_Pytools typecho_Pytools工具由python编写&#xff0c;可以快速批量的在本地发布文章&#xff0c;不需要登陆后台粘贴md文件内容&#xff0c;同时此工具还能查看最新的评论消息。… 开源地址: GitHub Gitee 使用教学&#xff1a;B站 一、主要功能 所有操作不用登陆博…

Redis7——基础篇(一)

前言&#xff1a;此篇文章系本人学习过程中记录下来的笔记&#xff0c;里面难免会有不少欠缺的地方&#xff0c;诚心期待大家多多给予指教。 基础篇&#xff1a; Redis&#xff08;一&#xff09; 一、Redis定义 官网地址&#xff1a;Redis - The Real-time Data Platform R…

K8s组件

一、Kubernetes 集群架构组件 K8S 是属于主从设备模型&#xff08;Master-Slave 架构&#xff09;&#xff0c;即有 Master 节点负责集群的调度、管理和运维&#xff0c;Slave 节点是集群中的运算工作负载节点。 主节点一般被称为 Master 节点&#xff0c;master节点上有 apis…

草图绘制技巧

1、点击菜单栏文件–》新建–》左下角高级新手切换–》零件&#xff1b; 2、槽口&#xff1a;直槽口&#xff0c;中心点槽口&#xff0c;三点源槽口&#xff0c;中心点圆弧槽口&#xff1b; 3、草图的约束&#xff1a;需要按住ctrl键&#xff0c;选中两个草图&#xff0c;然后…

一款基于若依的wms系统

Wms-Ruoyi-仓库库存管理 若依wms是一套基于若依的wms仓库管理系统&#xff0c;支持lodop和网页打印入库单、出库单。毫无保留给个人及企业免费使用。 前端采用Vue、Element UI。后端采用Spring Boot、Spring Security、Redis & Jwt。权限认证使用Jwt&#xff0c;支持多终…

AWS transit gateway 的作用

说白了,就是根据需要,来起到桥梁的作用,内部沟通,或者面向internet. 先看一下diagram 图: 最中间的就是transit gateway, 要达到不同vpc 直接通讯的目的: The following is an example of a default transit gateway route table for the attachments shown in the previ…

把 CSV 文件摄入到 Elasticsearch 中 - CSVES

在我们之前的很多文章里&#xff0c;我有讲到这个话题。在今天的文章中&#xff0c;我们就提重谈。我们使用一种新的方法来实现。这是一个基于 golang 的开源项目。项目的源码在 https://github.com/githubesson/csves/。由于这个原始的代码并不支持 basic security 及带有安全…

[操作系统] 基础 IO:理解“文件”与 C 接口

在 Linux 操作系统中&#xff0c;“一切皆文件”这一哲学思想贯穿始终。从基础 IO 学习角度来看&#xff0c;理解“文件”不仅仅意味着了解磁盘上存储的数据&#xff0c;还包括对内核如何管理各种资源的认识。本文将从狭义与广义两个层面对“文件”进行解读&#xff0c;归纳文件…

国产编辑器EverEdit - 二进制模式下观察Window/Linux/MacOs换行符差异

1 换行符格式 1.1 应用场景 稍微了解计算机历史的人都知道&#xff0c; 计算机3大操作系统&#xff1a; Windows、Linux/Unix、MacOS&#xff0c;这3大系统对文本换行的定义各不相同&#xff0c;且互不相让&#xff0c;导致在文件的兼容性方面存在一些问题&#xff0c;比如它们…

设计模式Python版 命令模式(下)

文章目录 前言一、命令队列的实现二、撤销操作的实现三、请求日志四、宏命令 前言 GOF设计模式分三大类&#xff1a; 创建型模式&#xff1a;关注对象的创建过程&#xff0c;包括单例模式、简单工厂模式、工厂方法模式、抽象工厂模式、原型模式和建造者模式。结构型模式&…

Linux:进程概念详解

​ 进程概念详解 一、进程的基本概念 进程在书本上的定义是&#xff1a;计算机中正在运行的程序实例。仅此描述可能让很多人感到困惑。 我们磁盘上存储着.exe文件&#xff0c;启动文件时&#xff0c;文件会从磁盘加载到内存&#xff0c;由CPU对文件的数据和代码进行运算。但…

04性能监控与调优篇(D1_学习前言)

目录 一、引言 二、基本介绍 三、JVM基础 1. java堆 2. 垃圾回收 3. STW 四、调优层次 五、调优指标 六、JVM调优原则 1. 优先原则 2. 堆设置 3. 垃圾回收器设置 1> GC 发展阶段 2> G1的适用场景 3> 其他收集器适⽤场景 4. 年轻代设置 5. 年⽼代设置 …

系统思考—慢就是快

“所有成长&#xff0c;都是一个缓慢渗透的过程&#xff0c;回头看&#xff0c;才发现自己已经走了很远。” —— 余秋雨 这让我想起一个最近做的项目。和一家公司合作&#xff0c;他们的管理模式一直陷入困境&#xff0c;员工积极性低&#xff0c;领导层的决策效率也不高。刚…

String常量池(2)

大家好&#xff0c;今天我们继续学习String常量池&#xff0c;昨天我们已经做了一个介绍&#xff0c;相信大家✓String常量池有了一定了解&#xff0c;那么就来看看它的应用。 字符串常量地(String Table). 字常量她在IVM中是StringTable类,实际是一个固定大小的 HashTable(一…

LabVIEW显微镜成像偏差校准

在高精度显微镜成像中&#xff0c;用户常常需要通过点击图像的不同位置&#xff0c;让电机驱动探针移动到指定点进行观察。然而&#xff0c;在实际操作中&#xff0c;经常会遇到一个问题&#xff1a;当点击位于图像中心附近的点时&#xff0c;探针能够相对准确地定位&#xff1…

Typora“使用”教程

文章目录 零、Typora简介一、下载并安装Typora二、修改License文件三、每次启动第一个Typora时&#xff0c;总弹出Activate窗口四、去除软件左下角未Activate提示五、参考文章 零、Typora简介 Typora 是一款由 Abner Lee 开发的轻量级 Markdown 编辑器&#xff0c;与其他 Mark…

【scikit-multiflow】使用 scikit-multiflow 的流数据生成器生成概念漂移数据流

说在前面 scikit-multiflow 是一个专注于多流学习&#xff08;multi-stream learning&#xff09;的Python库&#xff0c;它为数据流挖掘和在线学习提供了丰富的工具集。这个库的设计灵感来源于著名的scikit-learn&#xff0c;旨在为研究人员和从业者提供一个易于使用且功能强…

计算机视觉-局部特征

一、局部特征 1.1全景拼接 先用RANSAC估计出变换&#xff0c;就可以拼接两张图片 ①提取特征 ②匹配特征 ③拼接图像 1.2 点的特征 怎么找到对应点&#xff1f;&#xff08;才能做点对应关系RANSAC&#xff09; &#xff1a;特征检测 我们希望找到的点具有的特征有什么特…

matlab下载安装图文教程

【matlab介绍】 MATLAB是一款由美国MathWorks公司开发的专业计算软件&#xff0c;主要应用于数值计算、可视化程序设计、交互式程序设计等高科技计算环境。以下是关于MATLAB的简要介绍&#xff1a; MATLAB是MATrix LABoratory&#xff08;矩阵实验室&#xff09;的缩写&#…