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

news2025/3/12 19:02:49

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.coloradmin.cn/o/2299782.html

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

相关文章

再谈SpringCloud Gateway源码

再谈SpringCloud Gateway源码 一、整体请求流程二、前置对象准备1、实例化HandlerMapping2、实例化Route3、实例化WebHandler 三、实践业务扩展点1、定义扩展Route对象2、Filter能做什么3、定义扩展Filter对象4、定义父类Filter简化请求参数处理 前言&#xff1a; 之前有阅读过…

把 CSV 文件摄入到 Elasticsearch 中 - CSVES

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

C进阶 数据的存储

目录 前言 一&#xff0c;VS的知识储备 二&#xff0c;有趣的scanf()读取 三&#xff0c;数据的存储 引言 四&#xff0c;整数存储 五&#xff0c;小数存储 总结 前言 这里将深入计算机&#xff0c;看计算机是如何进行数据的存储的&#xff0c;怎么在计算机里面筑巢 为…

【c++】【Linux】【进程】线程终止/崩溃 会导致进程终止/崩溃 吗?

【c】【Linux】【进程】线程终止/崩溃 会导致进程终止/崩溃 吗&#xff1f; 1.线程终止会导致进程终止吗&#xff1f; 在操作系统中&#xff0c;线程是进程的基本执行单元&#xff0c;一个进程可以包含一个或多个线程。 当一个子线程终止时&#xff0c;进程并不会因此自动终…

springcloud集成gateway

本篇文章只介绍gateway模块的搭建步骤&#xff0c;并无gateway详细介绍 gateway详解请查看&#xff1a;SpringCloudGateway官方文档详解 前置处理 父模块中已指定版本 不知道如何选择版本看这篇&#xff1a; 手把手教你梳理springcloud与springboot与springcloudalibaba的版本…

pandas(13 Caveats Gotchas和SQL比较)

前面内容&#xff1a;pandas(12 IO工具和稀松数据) 目录 一、Caveats警告 & Gotchas预见 1.1 在Pandas中使用if/Truth语句 1.2 位运算布尔 1.3 isin操作 1.4 重新索引reindex和 loc&iloc 使用注意事项 1.5 loc和iloc 二、Python Pandas 与SQL的比较 2.1 数…

Android的Activity生命周期知识点总结,详情

一. Activity生命周期 1.1 返回栈知识点 二. Activity状态 2.1 启动状态 2.2 运行状态 2.3 暂停状态 2.4 停止状态 2.5 销毁状态 三. Activity生存期 3.1 回调方法 3.2 生存期 四. 体验Activity的生命周期 五. Activity被回收办法 引言&#xff1a; 掌握Acti…

基于Python的Flask微博话题舆情分析可视化系统

2024数据 ✅️标价源码 远程部署加 20 ✅️爬虫可用 有六月数据 ✅️修复bug不会突然打不开网页 系统稳定 系统的功能如下: 1.数据的爬取 2.用户的登录注册 3.热词统计&#xff0c;舆情统计 4.文章统计分析 5.发布地址统计 6.评论统计 7.情感分类统计 编程语言&#xff1a;py…

【油漆面积——线段树,扫描线,不用pushdown的特例,pushup兼有cal的性质】

题目 分析 不用pushdown是因为&#xff1a; 对于modify&#xff0c;操作是互逆过程&#xff0c;因此不会存在向下结算的pushdown过程 对于query&#xff0c;操作始终针对最上层的tr[1]&#xff0c;也不需要pushdown 对于pushdown&#xff0c;一则是怕不结算就标记&#xff0c;会…

深度学习(1)-简单神经网络示例

我们来看一个神经网络的具体实例&#xff1a;使用Python的Keras库来学习手写数字分类。在这个例子中&#xff0c;我们要解决的问题是&#xff0c;将手写数字的灰度图像&#xff08;28像素28像素&#xff09;划分到10个类别中&#xff08;从0到9&#xff09;​。我们将使用MNIST…

硬件学习笔记--42 电磁兼容试验-6 传导差模电流干扰试验介绍

目录 电磁兼容试验-传导差模电流试验 1.试验目的 2.试验方法 3.判定依据及意义 电磁兼容试验-传导差模电流干扰试验 驻留时间是在规定频率下影响量施加的持续时间。被试设备&#xff08;EUT&#xff09;在经受扫频频带的电磁影响量或电磁干扰的情况下&#xff0c;在每个步进…

Shader示例 6: 卡渲基础 - 描边 + 着色

0 、获取原神模型&#xff1a; 【游戏开发实战】下载原神模型&#xff0c;PMX转FBX&#xff0c;导入到Unity中&#xff0c;卡通渲染&#xff0c;绑定人形动画&#xff08;附Demo工程&#xff09;-CSDN博客 《原神》公测视频征集计划 一、描边pass&#xff1a;Outline 1. …

Cherno C++ P55 宏

这篇文章我们讲一下C当中的宏。其实接触过大型项目的朋友可能都被诡异的宏折磨过。 宏是在预处理当中&#xff0c;通过文本替换的方式来实现一些操作&#xff0c;这样可以不用反复的输入代码&#xff0c;帮助我们实现自动化。至于预处理的过程&#xff0c;其实就是文本编辑&am…

(20)从strlen到strtok:解码C语言字符函数的“生存指南1”

❤个人主页&#xff1a;折枝寄北的博客 ❤专栏位置&#xff1a;简单入手C语言专栏 目录 前言1. 求字符串长度函数1.1 strlen 2. 长度不受限制的字符串函数2.1 strcpy2.2 strcat2.3 strcmp 3. 长度受限制的字符串函数3.1 strncpy3.2 strncat3.3 strncmp 4. 字符串查找函数4.1 st…

Mongodb数据管理

Mongodb数据管理 1.登录数据库&#xff0c;查看默认的库 [rootdb51~]# mongo> show databases; admin 0.000GB config 0.000GB local 0.000GB> use admin switched to db admin > show tables system.version > admin库&#xff1a;admin 是 MongoDB 的管理…

从短片到长片:王琦携《Mountain》续作迈向新高度

在王琦(Qi Wang)的带领下,广受关注的短片《Mountain》迎来了成长篇续作《Rite of the Mountain》。这一全新长片不仅是她从短片迈向长篇叙事的重要一步,更是一次大胆的艺术挑战。作为制片人的她,将继续以敏锐的视觉风格和深刻的叙事洞察,拓展《Mountain》所触及的情感深度,并构…

DeepSeek应用——与PyCharm的配套使用

目录 一、配置方法 二、使用方法 三、注意事项 1、插件市场无continue插件 2、无结果返回&#xff0c;且在本地模型报错 记录自己学习应用DeepSeek的过程&#xff0c;使用的是自己电脑本地部署的私有化蒸馏模型...... &#xff08;举一反三&#xff0c;这个不单单是可以用…

如何画产品功能图、结构图

功能图的类型 常见的功能图包括数据流图、用例图、活动图、状态图、类图、组件图、部署图等等&#xff0c;不同的应用场景和目标下&#xff0c;需要确定不同的功能图类型。 数据流图 用例图 状态图 类图 组件图 组件图是由软件系统、组件和组件之间的关系组成的图形&#xf…

标准输入输出流,面向对象,构造函数

标准输入输出流 为什么不直接用printf和scanf? 不能输入/输出C新增的内容 std C的一些标识符&#xff0c;都是定义在std这个名字空间下面cout 是什么&#xff1f; 1.是一个ostream对象 output stream:输出流使用 <<&#xff1a;输出流运算符 作用&#xff1a;将右边…

PowerBI 矩阵 列标题分组显示(两行列标题)

先看效果 数据表如下&#xff1a; 我们在powerbi里新建一个矩阵&#xff0c;然后如图加入字段&#xff1a; 我们就会得到这样的矩阵&#xff1a; 我们在“可视化”->“列”&#xff0c;上双击&#xff0c;输入空格&#xff0c;就能消除左上角的"类别"两字 同理修…