Abp vnext 集成 MQTTnet-v4.1.4
前言
阅读前,先了解下 MQTTnet 如何集成 AspNetCore:
https://github.com/dotnet/MQTTnet/wiki/Server#validating-mqtt-clients
之前写得Abp vNext 集成 MQTTnet-v3.1.2
集成MQTTnet的版本是v3.1.2, 然而 MQTTnet v4.x 版本对 MqttServer 类进行了颠覆式重构,故更新下写法。
源码
本课程源码下载:https://gitee.com/Artisan-k/artizan-iot-hub-mqtt-demo
Artizan.Iot.Hub.Mqtt.Application.Contracts
新建类库项目 【Artizan.Iot.Hub.Mqtt.Application.Contracts】,添加对包MQTTnet的引用
<PackageReference Include="MQTTnet" Version="4.1.4.563" />
IMqttServiceBase
在项目【Artizan.Iot.Hub.Mqtt.Application.Contracts】中新建接口 IMqttServiceBase ,用于配置 MQTTnet 框架的 MqttServer 对象
代码清单:Artizan.Iot.Hub.Mqtt.Application.Contracts/Server/IMqttServiceBase.cs
using MQTTnet.Server;
namespace Artizan.Iot.Hub.Mqtt.Server
{
public interface IMqttServiceBase
{
protected MqttServer MqttServer { get; }
/// <summary>
/// 配置MqttServer
/// </summary>
/// <param name="mqttServer"></param>
void ConfigureMqttServer(MqttServer mqttServer);
}
}
其中,MqttServer 保留对 MqttServer 的对象的引用,以便在接口方法
void ConfigureMqttServer(MqttServer mqttServer)
中对其进行配置。
IMqttConnectionService :负责 MqttServer 与连接相关初始化
代码清单:Artizan.Iot.Hub.Mqtt.Application.Contracts/Server/IMqttConnectionService.cs
namespace Artizan.Iot.Hub.Mqtt.Server
{
public interface IMqttConnectionService : IMqttServiceBase
{
}
}
IMqttPublishingService:负责 MqttServer 的发布相关初始化
代码清单:Artizan.Iot.Hub.Mqtt.Application.Contracts/Server/IMqttPublishingService .cs
namespace Artizan.Iot.Hub.Mqtt.Server
{
public interface IMqttPublishingService : IMqttServiceBase
{
}
}
IMqttSubscriptionService:负责 MqttServer 的订阅相关初始化
代码清单:Artizan.Iot.Hub.Mqtt.Application.Contracts/Server/IMqttPublishingService .cs
namespace Artizan.Iot.Hub.Mqtt.Server
{
public interface IMqttSubscriptionService : IMqttServiceBase
{
}
}
Artizan.Iot.Hub.Mqtt.Application
新建类库项目 【Artizan.Iot.Hub.Mqtt.Application】,添加对项目 【Artizan.Iot.Hub.Mqtt.Application.Contracts的引用
MqttServiceBase
在项目【Artizan.Iot.Hub.Mqtt.Application】中新建接口 IMqttServiceBase 的实现类IMqttServiceBase:
代码清单:Artizan.Iot.Hub.Mqtt.Application/Server/MqttServiceBase.cs
using MQTTnet;
using MQTTnet.Server;
namespace Artizan.Iot.Hub.Mqtt.Server
{
public class MqttServiceBase : IMqttServiceBase
{
public MqttServer MqttServer { get; private set; }
public MqttServiceBase() { }
public virtual void ConfigureMqttServer(MqttServer mqttServer)
{
MqttServer = mqttServer;
}
protected virtual async Task<MqttClientStatus?> GetClientStatusAsync(string clientId)
{
var allClientStatuses = await MqttServer.GetClientsAsync();
return allClientStatuses.FirstOrDefault(cs => cs.Id == clientId);
}
protected virtual string GetClientIdFromPayload(MqttApplicationMessage message)
{
var payload = System.Text.Encoding.UTF8.GetString(message.Payload);
// TODO: for JSON type data transfer get clientId from json payload
return payload;
}
protected virtual async Task DisconnectClientAsync(string clientId)
{
var clientStatus = await GetClientStatusAsync(clientId);
if (clientStatus != null)
{
await clientStatus.DisconnectAsync();
}
}
}
}
MqttConnectionService :负责 MqttServer 与连接相关初始化
代码清单:Artizan.Iot.Hub.Mqtt.Application/Server/MqttConnectionService.cs
using Artizan.Iot.Hub.Mqtt.Topics;
using Artizan.Iot.Hub.Mqtt.Server.Etos;
using MQTTnet;
using MQTTnet.Server;
using System;
using System.Collections.Generic;
using System.Diagnostics.Tracing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Distributed;
using MQTTnet.Protocol;
using Microsoft.Extensions.Logging;
using Artizan.Iot.Hub.Mqtt.Server.Extensions;
namespace Artizan.Iot.Hub.Mqtt.Server
{
public class MqttConnectionService : MqttServiceBase, IMqttConnectionService, ITransientDependency
{
private readonly ILogger<MqttConnectionService> _logger;
private readonly IDistributedEventBus _distributedEventBus;
public MqttConnectionService(
ILogger<MqttConnectionService> logger,
IDistributedEventBus distributedEventBus)
: base()
{
_logger = logger;
_distributedEventBus = distributedEventBus;
}
public override void ConfigureMqttServer(MqttServer mqttServer)
{
base.ConfigureMqttServer(mqttServer);
MqttServer.ValidatingConnectionAsync += ValidatingConnectionHandlerAsync; ;
MqttServer.ClientConnectedAsync += ClientConnectedHandlerAsync;
MqttServer.ClientDisconnectedAsync += ClientDisconnectedHandlerAsync;
}
private Task ValidatingConnectionHandlerAsync(ValidatingConnectionEventArgs eventArgs)
{
// TODO:检查设备
// 无效客户端:if(context.ClientId != deviceId) context.ReasonCode = MQTTnet.Protocol.MqttConnectReasonCode.ClientIdentifierNotValid; // id不对
// 用户名和密码不对:MQTTnet.Protocol.MqttConnectReasonCode.BadUserNameOrPassword;
// 没有权限: MQTTnet.Protocol.MqttConnectReasonCode.NotAuthorized;
// ???: MqttConnectReasonCode.UnspecifiedError;
if (eventArgs.ClientId == "SpecialClient")
{
if (eventArgs.UserName != "USER" || eventArgs.Password != "PASS")
{
eventArgs.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword;
}
}
return Task.CompletedTask;
}
private async Task ClientConnectedHandlerAsync(ClientConnectedEventArgs eventArgs)
{
MqttServer.PublishByHub(BrokerTopics.Event.NewClientConnected, eventArgs.ClientId);
await _distributedEventBus.PublishAsync(
new ClientConnectedEto
{
ClientId = eventArgs.ClientId
}
);
}
private async Task ClientDisconnectedHandlerAsync(ClientDisconnectedEventArgs eventArgs)
{
MqttServer.PublishByHub(BrokerTopics.Event.NewClientDisconnected, eventArgs.ClientId);
await _distributedEventBus.PublishAsync(
new ClientDisconnectedEto
{
ClientId = eventArgs.ClientId
}
);
}
}
}
其中,
- MQTTnet的MqttServer对象的初始化,
MqttServer.ValidatingConnectionAsync += ValidatingConnectionHandlerAsync; // 处理连接验证
MqttServer.ClientConnectedAsync += ClientConnectedHandlerAsync; // 处理连接
MqttServer.ClientDisconnectedAsync += ClientDisconnectedHandlerAsync; // 处理客户端断开
- IotHub 发布内部主题
// MQtt 主题:/sys/broker/event/client-connected/new
MqttServer.PublishByHub(BrokerTopics.Event.NewClientConnected, eventArgs.ClientId);
其中使用的是自定义扩展方法:
代码清单:Artizan.Iot.Hub.Mqtt.Application/Server/Extensions/MqttServerExtensions
using Artizan.Iot.Hub.Mqtt.Topics;
using MQTTnet;
using MQTTnet.Server;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Artizan.Iot.Hub.Mqtt.Server.Extensions
{
public static class MqttServerExtensions
{
/// <summary>
//// -----------------------------------------------------------------
/// MQTTnet v3.x :
/// MQTTnet v3.x 中的MqttServer 类中的方法PublishAsync() 已被删除,故以下代码不能再使用:
///
/// await MqttServer.PublishAsync(BrokerEventTopics.NewClientConnected, arg.ClientId);
/// ,其源码: https://github.com/dotnet/MQTTnet/blob/release/3.1.x/Source/MQTTnet/Server/MqttServer.cs
///
/// -----------------------------------------------------------------
/// MQTTnet v4.1 :
/// 该版本中可以调用:MqttServer.InjectApplicationMessage() 方法注入消息
///
/// </summary>
/// <param name="mqttServer"></param>
/// <param name="mqttApplicationMessage"></param>
/// <exception cref="ArgumentNullException"></exception>
public static void PublishByHub(this MqttServer mqttServer, string topic, string payload)
{
if (topic == null) throw new ArgumentNullException(nameof(topic));
mqttServer.PublishByHub(new MqttApplicationMessageBuilder()
.WithTopic(topic)
.WithPayload(payload)
.Build());
}
public static void PublishByHub(this MqttServer mqttServer, MqttApplicationMessage mqttApplicationMessage)
{
if (mqttServer == null) throw new ArgumentNullException(nameof(mqttServer));
if (mqttApplicationMessage == null) throw new ArgumentNullException(nameof(mqttApplicationMessage));
mqttServer.InjectApplicationMessage(new InjectedMqttApplicationMessage(mqttApplicationMessage)
{
SenderClientId = MqttServiceConsts.IotHubMqttClientId
});
}
}
}
- 通过分布式事件总线,
IDistributedEventBus _distributedEventBus向其它模块或者微服务发送事件,以降低耦合度。
事件参数Eto的定义如下:
namespace Artizan.Iot.Hub.Mqtt.Server.Etos
{
/// <summary>
/// EventName 属性是可选的,但是建议使用。
/// 如果没有为事件类型(ETO 类)声明它,
/// 则事件名称将是事件类的全名,
/// 参见:Abp:Distributed Event Bus: https://docs.abp.io/en/abp/latest/Distributed-Event-Bus
///
/// 客户端上线
///
/// </summary>
[EventName(MqttServerEventConsts.ClientConnected)]
public class ClientConnectedEto
{
public string ClientId { get; set; }
}
/// <summary>
/// Eto: Client下线
/// </summary>
[EventName(MqttServerEventConsts.ClientDisconnected)]
public class ClientDisconnectedEto
{
public string ClientId { get; set; }
}
}
IMqttPublishingService:负责 MqttServer 的发布相关初始化
代码清单:Artizan.Iot.Hub.Mqtt.Application/Server/MqttPublishingService.cs
using Artizan.Iot.Hub.Mqtt.Server.Etos;
using MQTTnet.Server;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Distributed;
using Artizan.Iot.Hub.Mqtt.Topics;
using MQTTnet;
using Microsoft.Extensions.Logging;
namespace Artizan.Iot.Hub.Mqtt.Server
{
public class MqttPublishingService : MqttServiceBase, IMqttPublishingService, ITransientDependency
{
private readonly ILogger<MqttPublishingService> _logger;
private readonly IDistributedEventBus _distributedEventBus;
public MqttPublishingService(
ILogger<MqttPublishingService> logger,
IDistributedEventBus distributedEventBus)
: base()
{
_logger = logger;
_distributedEventBus = distributedEventBus;
}
public override void ConfigureMqttServer(MqttServer mqttServer)
{
base.ConfigureMqttServer(mqttServer);
MqttServer.InterceptingPublishAsync += InterceptingPublishHandlerAsync;
}
private async Task InterceptingPublishHandlerAsync(InterceptingPublishEventArgs eventArgs)
{
var topic = eventArgs.ApplicationMessage.Topic;
_logger.LogDebug($"'{eventArgs.ClientId}' published '{eventArgs.ApplicationMessage.Topic}' > '{Encoding.UTF8.GetString(eventArgs.ApplicationMessage.Payload ?? new byte[0])}'");
#region TODO:根据ClientId,用户等判断是能发布这种主题 等具体的业务逻辑,慢慢规划
/// TDOD
if (topic == "not_allowed_topic") // TODO:
{
eventArgs.ProcessPublish = false;
eventArgs.CloseConnection = true;
}
if (MqttTopicFilterComparer.Compare(eventArgs.ApplicationMessage.Topic, "/myTopic/WithTimestamp/#") == MqttTopicFilterCompareResult.IsMatch)
{
// Replace the payload with the timestamp. But also extending a JSON
// based payload with the timestamp is a suitable use case.
eventArgs.ApplicationMessage.Payload = Encoding.UTF8.GetBytes(DateTime.Now.ToString("O"));
}
#endregion
// TODO:根据ClientId, 判断是否有权限去发布主题
if (MqttTopicHelper.IsSystemTopic(topic))
{
if (!MqttTopicHelper.IsBrokerItself(eventArgs.ClientId))
{
// 客户端要发布系统主题,不接受,而是由系统执行系统主题的发布
// await _mqttInternalService.ExecuteSystemCommandAsync(context);
eventArgs.ProcessPublish = false;
return;
}
}
else
{
if (!eventArgs.SessionItems.Contains(topic))
{
var payloadString = Encoding.UTF8.GetString(eventArgs.ApplicationMessage.Payload);
eventArgs.SessionItems.Add(eventArgs.ApplicationMessage.Topic, eventArgs.ApplicationMessage.Payload);
}
else
{
var retainPayload = (byte[])eventArgs.SessionItems[eventArgs.ApplicationMessage.Topic];
if (!retainPayload.SequenceEqual(eventArgs.ApplicationMessage.Payload))
{
}
}
}
if (eventArgs.ProcessPublish)
{
await _distributedEventBus.PublishAsync(
new ClientPublishTopicEto
{
ClientId = eventArgs.ClientId,
Topic = eventArgs.ApplicationMessage.Topic,
Payload = eventArgs.ApplicationMessage.Payload
}
);
}
}
}
}
IMqttSubscriptionService:负责 MqttServer 的订阅相关初始化
代码清单:Artizan.Iot.Hub.Mqtt.Application/Server/MqttPublishingService.cs
using Artizan.Iot.Hub.Mqtt.Topics;
using Microsoft.Extensions.Logging;
using MQTTnet.Protocol;
using MQTTnet.Server;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Distributed;
namespace Artizan.Iot.Hub.Mqtt.Server
{
/// <summary>
/// TODO: 加入权限系统
/// </summary>
public class MqttSubscriptionService : MqttServiceBase, IMqttSubscriptionService, ITransientDependency
{
private readonly ILogger<MqttPublishingService> _logger;
private readonly IDistributedEventBus _distributedEventBus;
public MqttSubscriptionService(
ILogger<MqttPublishingService> logger,
IDistributedEventBus distributedEventBus)
: base()
{
_logger= logger;
_distributedEventBus = distributedEventBus;
}
public override void ConfigureMqttServer(MqttServer mqttServer)
{
base.ConfigureMqttServer(mqttServer);
MqttServer.ClientSubscribedTopicAsync += ClientSubscribedTopicHandlerAsync;
MqttServer.InterceptingSubscriptionAsync += InterceptingSubscriptionHandlerAsync;
MqttServer.ClientUnsubscribedTopicAsync += ClientUnsubscribedTopicHandlerAsync;
MqttServer.InterceptingUnsubscriptionAsync += InterceptingUnsubscriptionHandlerAsync;
}
/// <summary>
/// 处理客户端订阅
/// </summary>
/// <param name="eventArgs"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
private Task ClientSubscribedTopicHandlerAsync(ClientSubscribedTopicEventArgs eventArgs)
{
_logger.LogDebug($"'{eventArgs.ClientId}' subscribed '{eventArgs.TopicFilter.Topic}'");
return Task.CompletedTask;
}
/// <summary>
/// 拦截客户端订阅
/// </summary>
/// <param name="eventArgs"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
private Task InterceptingSubscriptionHandlerAsync(InterceptingSubscriptionEventArgs eventArgs)
{
/// TODO: 使用数据库+缓存 Client 的订阅权限
if (MqttTopicHelper.IsSystemTopic(eventArgs.TopicFilter.Topic) && eventArgs.ClientId != "Administrator") // TODO:后续支持可配置是否可订阅指定的主题
{
eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.ImplementationSpecificError;
}
if (eventArgs.TopicFilter.Topic.StartsWith(BrokerTopics.Secret.Base) && eventArgs.ClientId != "Imperator")
{
eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.ImplementationSpecificError;
eventArgs.CloseConnection = true;
}
return Task.CompletedTask;
}
/// <summary>
/// 处理客户端取消订阅
/// </summary>
/// <param name="eventArgs "></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
private Task ClientUnsubscribedTopicHandlerAsync(ClientUnsubscribedTopicEventArgs eventArgs )
{
//_logger.LogDebug($"'{eventArgs.ClientId}' unsubscribed topic '{eventArgs.xxx}'");
return Task.CompletedTask;
}
/// <summary>
/// 拦截取消订阅
/// </summary>
/// <param name="eventArgs"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
private Task InterceptingUnsubscriptionHandlerAsync(InterceptingUnsubscriptionEventArgs eventArgs)
{
_logger.LogDebug($"'{eventArgs.ClientId}' unsubscribed topic '{eventArgs.Topic}'");
return Task.CompletedTask;
}
}
}
MqttServer 初始化汇总类
定义一个接口IMqttServerService,用于汇总并一次性调用用于初始化 MQTTnet的 MqttServer对象的所有服务接口:
IMqttConnectionServiceIMqttPublishingServiceIMqttSubscriptionService,
目的是实现内部封装,调用者就不用关心了解 MQTTnet的 MqttServer对象初始化细节。
在项目【Artizan.Iot.Hub.Mqtt.Application.Contracts】中新建接口 IMqttServerService,
代码清单:Artizan.Iot.Hub.Mqtt.Application.Contracts/Server/IMqttServerService.cs
using MQTTnet.Server;
namespace Artizan.Iot.Hub.Mqtt.Server
{
public interface IMqttServerService
{
MqttServer MqttServer { get; }
/// <summary>
/// 配置MqttServer
/// </summary>
/// <param name="mqttServer"></param>
void ConfigureMqttService(MqttServer mqttServer);
}
}
在项目【Artizan.Iot.Hub.Mqtt.Application】中实现接口 IMqttServerService,
代码清单:Artizan.Iot.Hub.Mqtt.ApplicationServer/MqttServerService.cs
using MQTTnet.Server;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
using Volo.Abp.DependencyInjection;
namespace Artizan.Iot.Hub.Mqtt.Server
{
public class MqttServerService: IMqttServerService, ITransientDependency
{
private readonly IMqttConnectionService _mqttConnectionService;
private readonly IMqttPublishingService _mqttPublishingService;
private readonly IMqttSubscriptionService _mqttSubscriptionService;
private readonly IMqttInternalService _mqttInternalService;
public MqttServer MqttServer { get; private set; }
public MqttServerService(
IMqttConnectionService mqttConnectionService,
IMqttPublishingService mqttPublishingService,
IMqttSubscriptionService mqttSubscriptionService,
IMqttInternalService mqttInternalService)
{
_mqttConnectionService = mqttConnectionService;
_mqttPublishingService = mqttPublishingService;
_mqttSubscriptionService = mqttSubscriptionService;
_mqttInternalService = mqttInternalService;
}
public void ConfigureMqttService(MqttServer mqttServer)
{
MqttServer = mqttServer;
_mqttConnectionService.ConfigureMqttServer(mqttServer);
_mqttPublishingService.ConfigureMqttServer(mqttServer);
_mqttSubscriptionService.ConfigureMqttServer(mqttServer);
_mqttInternalService.ConfigureMqttServer(mqttServer);
}
}
}
ConfigureMqttService()内对 MQTTnet的 MqttServer对象相关初始化进行调用。
MQTTnet 集成 AspNet Core 扩展类库
MQTTnet集成 AspNet Core需要做一些额外的代码编写,单独新建一个类库,以便使用者不需要了结 MQTTnet 内部的初始化步骤,使用起来更加丝滑,参见:
https://github.com/dotnet/MQTTnet/wiki/Server#validating-mqtt-clients
下面编写一个类库【Artizan.Iot.Hub.Mqtt.AspNetCore】,封装这些初始化工作,
新建类库项目【Artizan.Iot.Hub.Mqtt.AspNetCore】,添加如下包引用:
<ItemGroup>
<PackageReference Include="MQTTnet.AspNetCore" Version="4.1.4.563" />
<PackageReference Include="Volo.Abp.AspNetCore.Mvc" Version="7.0.1" />
</ItemGroup>
接口:IIotHubMqttServer
新建接口IIotHubMqttServer,注意这里继承接口:ISingletonDependency,使其实现类成为单例
代码清单:Artizan.Iot.Hub.Mqtt.AspNetCore/Server/IIotHubMqttServer
using MQTTnet.Server;
namespace Artizan.Iot.Hub.Mqtt.AspNetCore.Servser
{
public interface IIotHubMqttServer : ISingletonDependency
{
MqttServer MqttServer { get; }
void ConfigureMqttServer(MqttServer mqttServer);
}
}
实现类:IotHubMqttServer
接口IIotHubMqttServer的实现类:IotHubMqttServer,
特别注意:这里使用的是单例接口 ISingletonDependency,其目的是确保 MQTTnet中的MqttServer对象是唯一的(即:单例)。
代码清单:Artizan.Iot.Hub.Mqtt.AspNetCore/Server/IotHubMqttServer
using Artizan.Iot.Hub.Mqtt.Server;
using MQTTnet.Server;
using Volo.Abp.DependencyInjection;
namespace Artizan.Iot.Hub.Mqtt.AspNetCore.Servser
{
/// <summary>
/// MQTTnet 集成 AspNetCore:
/// https://github.com/dotnet/MQTTnet/wiki/Server#validating-mqtt-clients
///
/// 特别注意:这里使用的是单例接口 ISingletonDependency
///
/// ----------
/// 使用MQTTnet部署MQTT服务:
/// https://www.cnblogs.com/wx881208/p/14325011.html--+
/// C# MQTTnet 3.1升级到MQTTnet 4.0 Client编程变化:
/// https://blog.csdn.net/yuming/article/details/125834921?spm=1001.2101.3001.6650.7&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-7-125834921-blog-127175694.pc_relevant_3mothn_strategy_recovery&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-7-125834921-blog-127175694.pc_relevant_3mothn_strategy_recovery&utm_relevant_index=12
/// </summary>
public class IotHubMqttServer : IIotHubMqttServer, ISingletonDependency
{
private readonly IMqttServerService _mqttServerService;
public IotHubMqttServer(IMqttServerService mqttServerService)
{
_mqttServerService = mqttServerService;
}
public void ConfigureMqttServer(MqttServer mqttServer)
{
MqttServer = mqttServer;
_mqttServerService.ConfigureMqttService(mqttServer);
}
}
}
其中,
_mqttServerService.ConfigureMqttService(mqttServer);
调用IMqttServerService 初始化汇总类的ConfigureMqttServer()的方法,对 MQTTnet的 MqttServer所有的初始化服务一次性调用。
MQTTnet 与 AspNetCore 集成
新建 【AspNet Core WebApi】项目,名为【Artizan.Iot.Hub.Mqtt.HttpApi.Host】,添加如下包的引用
<ItemGroup>
<PackageReference Include="Serilog.AspNetCore" Version="6.1.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Volo.Abp.AspNetCore.Serilog" Version="7.0.1" />
<PackageReference Include="Volo.Abp.Autofac" Version="7.0.1" />
<PackageReference Include="Volo.Abp.Swashbuckle" Version="7.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Artizan.Iot.Hub.Mqtt.Application\Artizan.Iot.Hub.Mqtt.Application.csproj" />
<ProjectReference Include="..\Artizan.Iot.Hub.Mqtt.AspNetCore\Artizan.Iot.Hub.Mqtt.AspNetCore.csproj" />
<ProjectReference Include="..\Artizan.Iot.Hub.Mqtt.HttpApi\Artizan.Iot.Hub.Mqtt.HttpApi.csproj" />
</ItemGroup>
配置 MqttServer
代码清单:Artizan.Iot.Hub.Mqtt.HttpApi.Host/ArtizanIotHubMqttHttpApiHostModule.cs
namespace Artizan.Iot.Hub.Mqtt.HttpApi.Host
{
[DependsOn(
typeof(AbpAutofacModule),
typeof(AbpAspNetCoreSerilogModule),
typeof(AbpSwashbuckleModule),
typeof(ArtizanIotHubMqttApplicationModule),
typeof(ArtizanIotHubMqttHttpApiModule),
typeof(ArtizanIotHubMqttAspNetCoreModule)
)]
public class ArtizanIotHubMqttHttpApiHostModule : AbpModule
{
...
public override void ConfigureServices(ServiceConfigurationContext context)
{
...
ConfigureIotHubMqttServer(context);
}
private void ConfigureIotHubMqttServer(ServiceConfigurationContext context)
{
context.Services.AddIotHubMqttServer(builder =>
{
builder.WithDefaultEndpoint();
});
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
app.UseRouting();
...
app.UseIotHubMqttServer();
...
}
}
其中,
扩展方法:AddIotHubMqttServer
AddIotHubMqttServer()是自定义扩展方法:
代码清单:Artizan.Iot.Hub.Mqtt.AspNetCore.Extensions/IotHubMqttServerExtensions.cs
using Artizan.Iot.Hub.Mqtt.AspNetCore.Servser;
using MQTTnet.AspNetCore;
namespace Artizan.Iot.Hub.Mqtt.HttpApi.Host.Extensions
{
public static class IotHubMqttServerExtensions
{
/// <summary>
/// MQTTnet 集成 AspNetCore:
/// https://github.com/dotnet/MQTTnet/wiki/Server#validating-mqtt-clients
/// </summary>
public static void AddIotHubMqttServer(this IServiceCollection services,
Action<AspNetMqttServerOptionsBuilder> configure)
{
services.AddHostedMqttServerWithServices(builder =>
{
configure(builder);
});
services.AddMqttConnectionHandler();
services.AddConnections();
}
}
}
其中,
services.AddHostedMqttServerWithServices的 MQTTnet框架内置扩展方法。
扩展方法:UseIotHubMqttServer()
在模块ArtizanIotHubMqttHttpApiHostModule的如下方法中调用自定义扩展方法app.UseIotHubMqttServer()
代码清单:Artizan.Iot.Hub.Mqtt.HttpApi.Host/ArtizanIotHubMqttHttpApiHostModule.cs
public class ArtizanIotHubMqttHttpApiHostModule : AbpModule
{
...
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
app.UseRouting();
...
app.UseIotHubMqttServer();
...
}
自定义扩展方法 app.UseIotHubMqttServer() 代码如下:
代码清单:Artizan.Iot.Hub.Mqtt.AspNetCore.Extensions/IotHubMqttServerExtensions.cs
public static void UseIotHubMqttServer(this IApplicationBuilder app)
{
app.UseMqttServer(mqttServer =>
{
app.ApplicationServices.GetRequiredService<IIotHubMqttServer>()
.ConfigureMqttServer(mqttServer);
});
}
其中,app.UseMqttServer(mqttServer =>{})方法 MQTTnet类库内置的扩展方法。
初始化 MQTTnet的 MqttServer对象
其中方法 MQTTnet 库的方法UseMqttServer(mqttServer =>{...})
app.UseMqttServer(mqttServer =>
{
app.ApplicationServices.GetRequiredService<IIotHubMqttServer>()
.ConfigureMqttServer(mqttServer);
});
内部调用 IotHubMqttServer的 ConfigureMqttServer()方法:
public class IotHubMqttServer : IIotHubMqttServer, ISingletonDependency
{
private readonly IMqttServerService _mqttServerService;
public void ConfigureMqttServer(MqttServer mqttServer)
{
MqttServer = mqttServer;
_mqttServerService.ConfigureMqttService(mqttServer);
}
}
然后调用:MqttServerService 的 ConfigureMqttService()方法
public class MqttServerService: IMqttServerService, ITransientDependency
{
public void ConfigureMqttService(MqttServer mqttServer)
{
MqttServer = mqttServer;
_mqttConnectionService.ConfigureMqttServer(mqttServer);
_mqttPublishingService.ConfigureMqttServer(mqttServer);
_mqttSubscriptionService.ConfigureMqttServer(mqttServer);
_mqttInternalService.ConfigureMqttServer(mqttServer);
}
}
最终调用
MqttConnectionService
public override void ConfigureMqttServer(MqttServer mqttServer)
{
base.ConfigureMqttServer(mqttServer);
MqttServer.ValidatingConnectionAsync += ValidatingConnectionHandlerAsync; ;
MqttServer.ClientConnectedAsync += ClientConnectedHandlerAsync;
MqttServer.ClientDisconnectedAsync += ClientDisconnectedHandlerAsync;
}
MqttPublishingService
public override void ConfigureMqttServer(MqttServer mqttServer)
{
base.ConfigureMqttServer(mqttServer);
MqttServer.InterceptingPublishAsync += InterceptingPublishHandlerAsync;
}
MqttSubscriptionService
public override void ConfigureMqttServer(MqttServer mqttServer)
{
base.ConfigureMqttServer(mqttServer);
MqttServer.ClientSubscribedTopicAsync += ClientSubscribedTopicHandlerAsync;
MqttServer.InterceptingSubscriptionAsync += InterceptingSubscriptionHandlerAsync;
MqttServer.ClientUnsubscribedTopicAsync += ClientUnsubscribedTopicHandlerAsync;
MqttServer.InterceptingUnsubscriptionAsync += InterceptingUnsubscriptionHandlerAsync;
}
这样就把MQTTnet的 MqttServer对象的各个事件处理器注册成功了
设置 MQTT终结点
代码清单:Artizan.Iot.Hub.Mqtt.HttpApi.Host/Extensions/IotHubMqttServerExtensions.cs
public static void UseIotHubMqttServer(this IApplicationBuilder app)
{
...
app.UseEndpoints(endpoint =>
{
// endpoint.MapMqtt("/mqtt");
endpoint.MapConnectionHandler<MqttConnectionHandler>(
"/mqtt", // 设置MQTT的访问地址: localhost:端口/mqtt
httpConnectionDispatcherOptions => httpConnectionDispatcherOptions.WebSockets.SubProtocolSelector =
protocolList => protocolList.FirstOrDefault() ?? string.Empty); // MQTT 支持 HTTP WebSockets
});
}
设置 MQTT的终结点,并让其 MQTT 支持 HTTP WebSockets 支持。
Program.cs
代码清单:Artizan.Iot.Hub.Mqtt.HttpApi.Host/Program.cs
using Artizan.Iot.Hub.Mqtt.HttpApi.Host;
using MQTTnet.AspNetCore;
using Serilog;
using Serilog.Events;
namespace AbpManual.BookStore;
public class Program
{
public async static Task<int> Main(string[] args)
{
Log.Logger = new LoggerConfiguration()
#if DEBUG
.MinimumLevel.Debug()
#else
.MinimumLevel.Information()
#endif
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning)
.Enrich.FromLogContext()
.WriteTo.Async(c => c.File("Logs/logs.txt"))
.WriteTo.Async(c => c.Console())
.CreateLogger();
try
{
Log.Information("Starting Artizan-IotHub HttpApi Host.");
var builder = WebApplication.CreateBuilder(args);
builder.Host.AddAppSettingsSecretsJson()
.UseAutofac()
.UseSerilog();
builder.WebHost.ConfigureKestrel(serverOptions =>
{
// This will allow MQTT connections based on TCP port 1883.
serverOptions.ListenAnyIP(2883, opts => opts.UseMqtt());
// This will allow MQTT connections based on HTTP WebSockets with URI "localhost:5883/mqtt"
// See code below for URI configuration.
serverOptions.ListenAnyIP(5883); // Default HTTP pipeline
});
await builder.AddApplicationAsync<ArtizanIotHubMqttHttpApiHostModule>();
var app = builder.Build();
await app.InitializeApplicationAsync();
await app.RunAsync();
return 0;
}
catch (Exception ex)
{
Log.Fatal(ex, "Host terminated unexpectedly!");
return 1;
}
finally
{
Log.CloseAndFlush();
}
}
}
其中,设置 MQTT的监听端口
builder.WebHost.ConfigureKestrel(serverOptions =>
{
// This will allow MQTT connections based on TCP port 2883.
serverOptions.ListenAnyIP(2883, opts => opts.UseMqtt());
// This will allow MQTT connections based on HTTP WebSockets with URI "localhost:5883/mqtt"
// See code below for URI configuration.
serverOptions.ListenAnyIP(5883); // Default HTTP pipeline
});
普通的TCP端口使用:2883,访问URL:localhost:2883/mqtt,
HTTP WebSockets端口:5883,访问URL:localhost:5883/mqtt
调试
启动 MqttServer
设置项目【Artizan.Iot.Hub.Mqtt.HttpApi.Host】为启动项目,

查看输出日志:
[22:49:40 WRN] Overriding address(es) 'https://localhost:44397, http://localhost:9397'. Binding to endpoints defined via IConfiguration and/or UseKestrel() instead.
[22:49:40 INF] Now listening on: http://[::]:2883
[22:49:40 INF] Now listening on: http://[::]:5883
[22:49:40 INF] Application started. Press Ctrl+C to shut down.
[22:49:40 INF] Hosting environment: Development
[22:49:40 INF] Content root path: F:\05-workspace\dev\01-lab\iot\learning\artizan-iot-hub-mqtt-demo\modules\mqtt\src\Artizan.Iot.Hub.Mqtt.HttpApi.Host
监听了端口:2883和5883
修复 Http和Https 的监听端口
特别注意:
输出的日志中,也明确警告:
[22:49:40 WRN] Overriding address(es) 'https://localhost:44397, http://localhost:9397'. Binding to endpoints defined via IConfiguration and/or UseKestrel() instead.
网站用于发布 API 的监听的端口:https://localhost:44397, http://localhost:9397 已经被覆盖,需要重新设置监听。
原因:
通过 ConfigureKestrel() 方法在 Kestrel 上设置的 Http 和 Https 监听端口后,其它地方设置(例如:在launchSettings.json 文件)的监听方式会被覆盖而失效!而我们又没有在ConfigureKestrel()的方法中重新设置 Http 和 Https 的监听端口。如何修复呢?
可通过修改配置文件appsettings.json来设置Http和Https的监听端口,具体操作如下:
在配置文件appsettings.json中添加如下 Kestrel 配置节点:
{
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://localhost:9397",
"Protocols": "Http1AndHttp2"
},
"Https": {
"Url": "https://localhost:44397",
"Protocols": "Http1AndHttp2"
},
}
}
// .....
}
这样就通过配置文件的方式为Kestrel 设置了 Http 和 Https的监听端口,如下图所示:

MQTT客户:MQTTx
MQTT客户端我们使用MQTTx,下载地址:https://mqttx.app,下载安装后,MQTT X 界面如下图所示:

订阅 Hub的系统主题
创建 client-01,点击左侧菜单中的【+】按钮,如下图所示:

在弹出框中填写相关参数,比如:Mqtt broker地址等,如下图所示

点击【连接】按钮,连接成功后,在 *.Mqtt.HttpApi.Host 中将有如下输出日志:

这是因为下面的代码:
public class MqttConnectionService : MqttServiceBase, IMqttConnectionService, ISingletonDependency
{
public override void ConfigureMqttServer(MqttServer mqttServer)
{
...
MqttServer.ClientConnectedAsync += ClientConnectedHandlerAsync;
}
private async Task ClientConnectedHandlerAsync(ClientConnectedEventArgs eventArgs)
{
MqttServer.PublishByHub(BrokerTopics.Event.NewClientConnected, eventArgs.ClientId);
...
}
...
Client 连接成功后,调用方法 MqttServer.PublishByHub(BrokerTopics.Event.NewClientConnected, eventArgs.ClientId); 发布客户端上线主题:BrokerTopics.Event.NewClientConnected,该主题的值为:
/sys/broker/clients/connected/new
TIPS
更多系统主题的定义,参见 类Artizan.Iot.Hub.Mqtt.Topics.BrokerTopics
点击【添加订阅】按钮,

为 client-01 订阅客户端下线主题:
/sys/broker/clients/disconnected/new
该主题用于发布下线客户端的 ClientId。
TIPS
更多系统主题的定义,参见 类Artizan.Iot.Hub.Mqtt.Topics.BrokerTopics

然后会看到如下报错:

这是因为在类MqttSubscriptionService中对以/sys 开头的系统主题进行了拦截,代码如下:
/// <summary>
/// 拦截客户端订阅
/// </summary>
/// <param name="eventArgs"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
private Task InterceptingSubscriptionHandlerAsync(InterceptingSubscriptionEventArgs eventArgs)
{
/// TODO: 使用数据库+缓存 Client 的订阅权限
if (MqttTopicHelper.IsSystemTopic(eventArgs.TopicFilter.Topic) && eventArgs.ClientId != "Administrator") // TODO:后续支持可配置是否可订阅指定的主题
{
eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.ImplementationSpecificError;
}
....
return Task.CompletedTask;
}
如果eventArgs.ClientId != "Administrator",返回错误,不给订阅,这只是为演示订阅拦截,具体判断逻辑根据实际业务而定。
为了能成功订阅该系统主题,得将【client-01】的Client ID修改为: Administrator,如下图所示:

这样就能成功订阅了,如下图所示:

创建 client-02,操作步骤如下图所示

然后选择【Client-02】,点击关闭按钮,如下图所示

由于如下代码:
public class MqttConnectionService : MqttServiceBase, IMqttConnectionService, ISingletonDependency
{
public override void ConfigureMqttServer(MqttServer mqttServer)
{
...
MqttServer.ClientDisconnectedAsync += ClientDisconnectedHandlerAsync;
}
private async Task ClientDisconnectedHandlerAsync(ClientDisconnectedEventArgs eventArgs)
{
MqttServer.PublishByHub(BrokerTopics.Event.NewClientDisconnected, eventArgs.ClientId);
...
}
...
Client 下线后,调用方法 MqttServer.PublishByHub(BrokerTopics.Event.NewClientDisconnected, eventArgs.ClientId); 发布客户端下线主题:BrokerTopics.Event.NewClientDisconnected,该主题的值为:
/sys/broker/clients/disconnected/new
这时,由于 client-01 订阅了主题 /sys/broker/clients/disconnected/new,故收到了该主题的消息,如下图所示

Client之间相互订阅
订阅主题
选择【client-02】,添加订阅主题:testtopic/client01/hello, 如下图所示:

订阅成功后,可以在 【client-02】的订阅主题列表中查看,如下图所示:

发布主题
选择【client-01】,让其在主题 testtopic/client01/hello 上发布消息,如下图进行操作:

点击【发布】按钮后,【client-02】收到了该主题消息,如下图所示:

点击【client-02】,查看消息内容,如下图所示:

IIS部署的问题
问题1
将集成了MQTTnet 的 AspNetCore 部署到IIS,AspNetCore的API能正常调用,但是无法连接 MQTT broker,
可以参考这个MQTTnet/issues/1471
最终的解决方案是:我这里使用的是 AspNetCore 6, MQTTnet-v4.1.4
在 ConfigureServices() 方法中,添加如下代码
services.AddHostedMqttServerWithServices(builder =>
{
builder.WithDefaultEndpoint();
builder.WithDefaultEndpointPort(2883); // 端口自己定
});
services.AddMqttConnectionHandler();
services.AddConnections();
services.AddMqttTcpServerAdapter();
然后在 Program.cs 中,添加如下代码
builder.WebHost.UseIIS();
builder.WebHost.UseIIS() 方法用于配置 ASP.NET Core 应用程序以在 IIS (Internet Information Services)中托管。具体作用包括配置一些 IIS 集成方面的功能,例如处理反向代理请求、获取应用程序的根地址等。在部署 ASP.NET Core 应用程序到 IIS 中时,通常需要使用该方法进行相应的配置。
如果你准备使用 Windows Service 来部署 ASP.NET Core 网站,而不是依赖于 IIS,那么可以考虑不使用builder.WebHost.UseIIS()方法。Windows Service 可以独立地托管 ASP.NET Core 应用程序,而无需依赖于 IIS。
如果你不删除builder.WebHost.UseIIS()这行代码,当应用程序运行时,不会直接影响应用程序的行为,因为当应用程序在非 IIS 托管的环境下运行时,对该方法的调用会被忽略。不过,为了代码的清晰性和避免不必要的混淆,最好根据实际部署环境的需求,合理选择是否调用这个方法。
这里我们使用了 builder.WebHost.UseIIS(),就不再使用如下方法: ConfigureKestrel() 或者 UseKestrel():
builder.WebHost
.ConfigureKestrel(serverOptions =>
{
// This will allow MQTT connections based on TCP port 2883.
serverOptions.ListenAnyIP(2883, opts => opts.UseMqtt());
// This will allow MQTT connections based on HTTP WebSockets with URI "localhost:5883/mqtt"
// See code below for URI configuration.
serverOptions.ListenAnyIP(5883); // Default HTTP pipeline
});
如果要部署到IIS,可以将上述代码删除掉,在配置文件appsettings.json中的 Kestrel 配置节点也可以删除掉,因为部署到 IIS 如下配置不生效:
{
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://localhost:9397",
"Protocols": "Http1AndHttp2"
},
"Https": {
"Url": "https://localhost:44397",
"Protocols": "Http1AndHttp2"
},
}
}
// .....
}
然后右键项目,点击【发布】,

在IIS新建站点,

端口绑定,只需要为 AspNetCore Web/API 绑定端口即可,不需要为MQTT broker 绑定端口(2883),
最终站点的端口绑定如下图所示:

发布后的文件复制到IIS站点文件夹,然后启动网站,
打开网站:http://localhost:44339/swagger/index.html

使用 MQTTx 客户端连接部署在IIS上的MQTT broker,如下图所示:

然后点击【连接】按钮,连接成功

发消息也是没问题的,如下图所示:

问题2
IIS 网站无请求,闲置一定时间后进程会被回收,导致 MQTT broker 无法连接。
查看IIS网站的应用程序池

如上图所示,当网站在20分钟内无任何请求,网站的进程会被回收,导致 MQTT broker 无法连接失败。
Tips
可以将网站部署为 WindowsServer,无上述问题
将 AspNetCore 部署为 WinddowsServer,参见:https://www.cnblogs.com/easy5weikai/archive/2022/04.html
参考:https://www.cnblogs.com/zswto999/p/3565105.html
建议将网站对应的应用程序池按如下图做修改(没有做过严格测试,请自行测试是否有效),

在 “进程模型” 选项卡中,将 “闲置超时(分钟)” 的值设置为 “0”。将此值设置为 “0” 将禁用闲置超时。
经测试,此法无效
其它方法
在 IIS 中,可以使用以下机制来保持应用程序活跃:
IIS Application Initialization 模块:这个模块允许您在 IIS 启动时预先加载应用程序,并在后台保持活跃状态。通过配置应用程序的初始化,您可以确保应用程序始终保持可用,并处理来自客户端的请求。
配置步骤如下:
在 IIS 管理器中选择您的站点,然后双击“Application Initialization”模块
在右侧窗格中,选中“Enable application initialization”复选框,然后可以选择以下配置:
Preload Enabled:将该选项设置为 True 可以启用预加载功能,并在 IIS 启动时加载应用程序。
Start Mode:此选项允许您选择如何启动应用程序。例如,您可以选择在 IIS 启动时启动应用程序,或者只是在第一个请求到达时启动。
Initialization Page:可以选择设置一个初始化页面,当应用程序启动时,IIS 会发送请求到该页面,来预热应用程序。
健康检查(Health Checks):健康检查是一种定期检测应用程序状态的机制。通过配置健康检查终结点,IIS 可以在预定的时间间隔内发送 HTTP 请求到应用程序,并根据响应状态判断应用程序是否运行正常。这将防止应用程序因长时间无人访问而被 IIS 关闭。
配置步骤如下:
在 IIS 管理器中选择您的站点,然后双击“Health and Diagnostics”模块
在右侧窗格中,找到“HTTP/HTTPS Ping”选项卡,选中“Enable pinging”复选框,然后可以选择以下配置:
Ping Enabled:将该选项设置为 True 可以启用 Ping 功能,定期检查应用程序是否处于活动状态。
Ping Frequency:此选项允许您选择 Ping 的时间间隔。
Ping Maximum Response Time:此选项允许您定义 Ping 最大响应时间,超过该时间就会被判断为失效。
Always On 功能(仅适用于 IIS 8.5+):Always On 功能允许您配置应用程序池在长时间无操作时保持活跃状态。通过将 startMode 属性设置为 AlwaysRunning,应用程序池将始终保持运行状态,不会随着时间的推移而关闭。
配置步骤如下:
在 IIS 管理器中选择您的应用程序池,然后双击“Advanced Settings”选项
在右侧窗格中,找到“Process Model”选项卡,将“Start Mode”设置为“AlwaysRunning”。
请注意,这些机制可能会对服务器资源产生一定的压力,因此请确保服务器资源足够以支持这些机制并避免过度使用。根据您的具体需求和环境,选择适合的机制来保持应用程序的活跃状态。
简单的自定义定期访问
在 ASP.NET Core 中,您可以使用后台任务和定时任务来周期性地发送请求以保持连接活跃。下面是一些基本的步骤:
首先,可以使用 HttpClient 类来发送请求。确保在 ConfigureServices 方法中注册 HttpClient 服务:
public void ConfigureServices(IServiceCollection services)
{
// 其他服务的注册
services.AddHttpClient();
}
创建一个后台任务类,该类将负责发送请求。您可以使用 IHttpClientFactory 从 DI 容器中获取 HttpClient:
public class KeepAliveTask : BackgroundService
{
private readonly IHttpClientFactory _httpClientFactory;
public KeepAliveTask(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// 发送请求
using (var httpClient = _httpClientFactory.CreateClient())
{
// 发送请求的逻辑,例如:
// var response = await httpClient.GetAsync("http://your-website.com");
// 处理响应逻辑
}
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); // 每隔一分钟发送一次请求
}
}
}
在 Startup.cs 文件 Configure 方法中启动后台任务:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider)
{
// 其他配置
// 启动后台任务
var keepAliveTask = serviceProvider.GetService<KeepAliveTask>();
Task.Run(() => keepAliveTask.Start());
}
请注意,上述示例中的 KeepAliveTask 是一个简单的后台任务,主要目的是发送请求以保持连接活跃。您可以根据自己的需求进行自定义。另外,也可以使用类似 Hangfire、Quartz.NET 等第三方库来管理定时任务,更加灵活和强大。
定期调用心跳接口
方案:在 ASP.NET Core 程序中写个定时任务,定时调用ASP.NET Core的心跳接口。
使用 .NET Core 中的 Hosted Services 来实现定时任务,并在定时任务中调用 ASP.NET Core 的心跳接口。下面是一个简单的示例:
第一步:创建心跳接口:
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
namespace YourApp.Controllers
{
[ApiController]
[Route("[controller]")]
public class HeartbeatController : ControllerBase
{
private readonly ILogger<HeartbeatController> _logger;
public HeartbeatController(ILogger<HeartbeatController> logger)
{
_logger = logger;
}
[HttpGet]
[Route("heartbeat")]
public IActionResult Heartbeat()
{
_logger.LogInformation("Heartbeat received at: {time}", DateTimeOffset.Now);
return Ok("Heartbeat received");
}
}
}
第二步:创建一个实现 IHostedService 接口的服务类,用于执行定时任务:
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
public class HeartbeatService : IHostedService, IDisposable
{
private readonly ILogger<HeartbeatService> _logger;
private readonly IHttpClientFactory _httpClientFactory;
private Timer _timer;
public HeartbeatService(ILogger<HeartbeatService> logger, IHttpClientFactory httpClientFactory)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Heartbeat service is starting.");
_timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(30)); // 每30秒执行一次
return Task.CompletedTask;
}
private async void DoWork(object state)
{
var httpClient = _httpClientFactory.CreateClient();
try
{
// 发送 GET 请求到心跳接口
var response = await httpClient.GetAsync("http://yourdomain.com/heartbeat");
// 检查响应是否成功
if (response.IsSuccessStatusCode)
{
_logger.LogInformation("Heartbeat sent successfully at: {time}", DateTimeOffset.Now);
}
else
{
_logger.LogError("Failed to send heartbeat. Status code: {statusCode}", response.StatusCode);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while sending heartbeat.");
}
finally
{
httpClient.Dispose();
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Heartbeat service is stopping.");
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
}
第三步:在 Startup.cs 文件的 ConfigureServices 方法中注册该服务:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// 添加日志记录
services.AddLogging();
// 注册定时任务服务
services.AddSingleton<IHostedService, HeartbeatService>();
// 注册 IHttpClientFactory 和 HttpClient 到依赖注入容器中,并配置一些默认行为,例如处理超时、重试策略等
services.AddHttpClient();
}
}
HeartbeatService 类实现了 IHostedService 接口,其中 StartAsync 方法用于启动定时任务,DoWork 方法是定时任务的具体实现,可以在其中调用心跳接口。在 Startup.cs 文件中通过 ConfigureServices 方法注册了该服务,使其在应用程序启动时启动定时任务。
通过这样的方式,你就可以在 ASP.NET Core 程序中实现定时调用心跳接口的定时任务了。
Abp BackgroundWorker
在 ASP.NET Core 程序中使用 Abp BackgroundWorker 定时调用心跳接口:
https://gitee.com/Artisan-k/artizan-iot-hub-mqtt-demo/commit/ba54c48d13043103e16fc26177f4070422293a16
参考
Quartz.Net 整合NetCore3.1,部署到IIS服务器上后台定时Job不被调度的解决方案
几种方案总结
实测证明,不能将定时心跳任务(如:Abp BackgroundWorker)放在部署在IIS中的ASP.NET Core 程序中,因为有很多因素会导致部署在IIS的ASP.NET Core 程序被回收,
故稳妥的方案是,定时心跳任务另行部署在其它不会被回收的应用程序中,比如单独部署在另一个 Windows Service。
或者 ASP.NET Core 程序本就部署为 Windows Service。

浙公网安备 33010602011771号