最近一段时间由于要做一套智能设备系统,而有幸了解到Surging中的Mqtt broker,学习了很多东西本篇文章基于Surging开源的.netcore项目有兴趣的朋友可点击此处进行了解。话不多说我们来基于Surging 中的WS与MqttClient结合来开发服务端MqttClient的使用。
准备工作
开发环境: Visual Studio 2017 15.9.5
.netCore版本:2.2.102(目前Surging已经升级至netcore 2.2版本)
开始工作
接口部分
新建类库Surging.IModuleServices.MqttWithWS
添加引用Surging.Core.Protocol.WS
新建文件夹Model 并创建类MqttClientOption.cs 此类为读取surgingSettings.json中配置的MqttClient的相关参数MqttClientOption的代码如下
public class MqttClientOption
{
public string ClientID { get; set; }
public string MqttClientConnection { get; set; } = "";
public string MqttClientUserName { get; set; }
public string MqttClientPassword { get; set; }
public int Port { get; set; }
public int KeepAlivedTime { get; set; }
public List<string> Topics { get; set; } = new List<string>();
public bool CleanSession { get; set; }
}
surgingSettings.json的MqttClient配置代码参见底部Surging.MqttClientWithWsServices.Server中的配置
根据Surging作者创建的例子来创建一个接口IChatService.cs
using Surging.Core.CPlatform.Ioc;
using Surging.Core.CPlatform.Runtime.Client.Address.Resolvers.Implementation.Selectors.Implementation;
using Surging.Core.CPlatform.Runtime.Server.Implementation.ServiceDiscovery.Attributes;
using Surging.Core.CPlatform.Support.Attributes;
using Surging.Core.Protocol.WS.Attributes;
using Surging.IModuleServices.MqttWithWS.Models;
using System;
using System.Threading.Tasks;
namespace Surging.IModuleServices.MqttWithWS
{
[ServiceBundle("Api/{Service}")]
[BehaviorContract(IgnoreExtensions = true)]
public interface IChatService: IServiceKey
{
[Command(ShuntStrategy = AddressSelectorMode.HashAlgorithm)]
Task RunMqttClient(MqttClientOption mqttClientOption);
}
}
结构如图所示
接口实现部分
新建类库Surging.Modules.WSWithMqtt
添加引用Surging.IModuleServices.MqttWithWS
添加依赖项:MQTTnet
由于要获取surgingSettings.json中的配置项,因此需要创建一个文件夹Configurations并在文件夹下创建SurgingMqttBuilderExtendsions.cs类代码如下:
using Autofac;
using Microsoft.Extensions.Configuration;
using Surging.Core.CPlatform;
using Surging.Core.ServiceHosting.Internal;
using Surging.IModuleServices.MqttWithWS;
using Surging.IModuleServices.MqttWithWS.Models;
using System;
using System.Collections.Generic;
using System.Text;
namespace Surging.Modules.WSWithMqtt.Configurations
{
public static class SurgingMqttBuilderExtendsions
{
public static IServiceHostBuilder UseMqttClient(this IServiceHostBuilder builder)
{
builder.MapServices(collection =>
{
MqttClientOption mqttClientOption = new MqttClientOption();
var section = AppConfig.GetSection("MqttClient");
if (section.Exists())
mqttClientOption = section.Get<MqttClientOption>();
collection.Resolve<IChatService>().RunMqttClient(mqttClientOption);
});
return builder;
}
}
}
由于此服务基于WSServiceBase 因此我创建了MqttWSServieBase继承自WSServiceBase用于创建MqttClient客户端,代码如下:
using MQTTnet;
using MQTTnet.Client;
using Surging.Core.Protocol.WS;
using Surging.IModuleServices.MqttWithWS.Models;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace Surging.Modules.WSWithMqtt
{
public abstract class MqttWSServieBase : WSServiceBase
{
protected IMqttClientOptions _mqttClientOptions;
protected IMqttClient _mqttClient;
protected IEnumerable<TopicFilter> _topicFilters;
public Task RunMqttClientBase(MqttClientOption mqttClientOption)
{
var clientOptions = new MqttClientOptionsBuilder();
if (mqttClientOption != null)
{
clientOptions.WithClientId(mqttClientOption.ClientID + Guid.NewGuid().ToString("N"))
.WithCleanSession(mqttClientOption.CleanSession);
clientOptions.WithTcpServer(mqttClientOption.MqttClientConnection, mqttClientOption.Port);
if (!string.IsNullOrWhiteSpace(mqttClientOption.MqttClientUserName))
clientOptions.WithCredentials(mqttClientOption.MqttClientUserName, mqttClientOption.MqttClientPassword);
clientOptions.WithKeepAlivePeriod(TimeSpan.FromSeconds(mqttClientOption.KeepAlivedTime));
}
_mqttClientOptions = clientOptions.Build();
IList<TopicFilter> filters = new List<TopicFilter>();
if (mqttClientOption != null)
{
foreach (var item in mqttClientOption.Topics)
{
var topicFilerbuilder = new TopicFilterBuilder();
topicFilerbuilder.WithTopic(item);
filters.Add(topicFilerbuilder.Build());
}
}
_topicFilters = filters;
_mqttClient = new MqttFactory().CreateMqttClient();
return Task.CompletedTask;
}
}
}
接口继承类ChatService,此类用于连接WS,并通过此连接对Surging的 Mqtt Broker进行发布订阅。 代码如下:
using MQTTnet;
using MQTTnet.Client;
using Surging.IModuleServices.MqttWithWS;
using Surging.IModuleServices.MqttWithWS.Models;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;
using WebSocketCore;
namespace Surging.Modules.WSWithMqtt
{
public class ChatService : MqttWSServieBase, IChatService
{
private static readonly ConcurrentDictionary<string, string> _users = new ConcurrentDictionary<string, string>();
private static readonly ConcurrentDictionary<string, string> _clients = new ConcurrentDictionary<string, string>();
private string _name;
private string _to;
protected override void OnMessage(MessageEventArgs e)
{
if (_clients.ContainsKey(ID))
{
Dictionary<string, object> model = new Dictionary<string, object>();
model.Add("name", _to);
model.Add("data", e.Data);
//var result = ServiceLocator.GetService<IServiceProxyProvider>()
// .Invoke<object>(model, "api/chat/SendMessage").Result;
}
}
protected override void OnOpen()
{
_name = Context.QueryString["name"];
_to = Context.QueryString["to"];
if (!string.IsNullOrEmpty(_name))
{
_clients[ID] = _name;
_users[_name] = ID;
}
}
public Task SendMessage(string name, string data)
{
if (_users.ContainsKey(name))
{
this.GetClient().SendTo($"hello,{name},{data}", _users[name]);
}
return Task.CompletedTask;
}
public async Task RunMqttClient(MqttClientOption mqttClientOption)
{
await base.RunMqttClientBase(mqttClientOption);
_mqttClient.ApplicationMessageReceived += _mqttClient_ApplicationMessageReceived;
_mqttClient.Connected += _mqttClient_Connected;
_mqttClient.Disconnected += _mqttClient_Disconnected;
await _mqttClient.ConnectAsync(_mqttClientOptions);
}
#region mqttClient
/// <summary>
/// 接收消息
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void _mqttClient_ApplicationMessageReceived(object sender, MqttApplicationMessageReceivedEventArgs e)
{
if (e.ApplicationMessage != null)
{
}
}
/// <summary>
/// 断开连接
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void _mqttClient_Disconnected(object sender, MqttClientDisconnectedEventArgs e)
{
Console.WriteLine("mqtt客户端与服务端断开连接!");
await Task.Delay(TimeSpan.FromSeconds(5));
try
{
await _mqttClient.ConnectAsync(_mqttClientOptions);
}
catch
{
Console.WriteLine("mqtt客户端与服务端尝试连接失败!");
}
}
/// <summary>
/// 连接成功
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void _mqttClient_Connected(object sender, MqttClientConnectedEventArgs e)
{
_mqttClient.SubscribeAsync(_topicFilters);
}
#endregion
}
}
结构如图
创建控制台应用程序,用于项目启动
新建控制台程序Surging.MqttClientWithWsServices.Server将Surging.Services.Server中的配置文件cacheSettings.json、eventBusSettings.json、log4net.config、NLog.config、以及surgingSettings.json文件都拷贝到新建的这个控制台程序中。
添加依赖项:Autofac Autofac.Extensions.DependencyInjection System.Text.Encoding.CodePages Microsoft.Extensions.Logging Microsoft.Extensions.Logging.Console
添加引用
修改Program.cs
using Autofac;
using Microsoft.Extensions.Logging;
using Surging.Core.Caching.Configurations;
using Surging.Core.CPlatform;
using Surging.Core.CPlatform.Configurations;
using Surging.Core.CPlatform.Utilities;
using Surging.Core.Nlog;
using Surging.Core.ProxyGenerator;
using Surging.Core.ServiceHosting;
using Surging.Core.ServiceHosting.Internal.Implementation;
using Surging.Modules.WSWithMqtt.Configurations;
using System;
using System.Text;
namespace Surging.MqttClientWithWsServices.Server
{
class Program
{
static void Main(string[] args)
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var host = new ServiceHostBuilder()
.RegisterServices(builder =>
{
builder.AddMicroService(option =>
{
option.AddServiceRuntime()
.AddClientProxy()
.AddRelateService()
.AddConfigurationWatch();
builder.Register(p => new CPlatformContainer(ServiceLocator.Current));
});
})
.ConfigureLogging(logger =>
{
logger.AddConfiguration(
Surging.Core.CPlatform.AppConfig.GetSection("Logging"));
})
.UseNLog(LogLevel.Debug, "NLog.config")
.UseServer(options => { })
.UseConsoleLifetime()
.UseMqttClient()
.UseProxy()
.Configure(build =>
build.AddCacheFile("${cachepath}|cacheSettings.json", optional: false, reloadOnChange: true))
.Configure(build =>
build.AddCPlatformFile("${surgingpath}|surgingSettings.json", optional: false, reloadOnChange: true))
.UseStartup<Startup>()
.Build();
using (host.Run())
{
Console.WriteLine($"服务端启动成功,{DateTime.Now}。");
}
}
}
}
注意添加是需要加一段代码.UseMqttClient()用于启动MqttClient。
添加Startup.cs 此类与Surging.Services.Server中的Startup.cs一致。
using Autofac;
using Autofac.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Surging.Core.Caching.Configurations;
using Surging.Core.CPlatform.Utilities;
using System.IO;
namespace Surging.MqttClientWithWsServices.Server
{
public class Startup
{
public Startup(IConfigurationBuilder config)
{
config.SetBasePath(Directory.GetCurrentDirectory());
ConfigureCache(config);
}
public IContainer ConfigureServices(ContainerBuilder builder)
{
var services = new ServiceCollection();
ConfigureLogging(services);
builder.Populate(services);
ServiceLocator.Current = builder.Build();
return ServiceLocator.Current;
}
public void Configure(IContainer app)
{
}
#region 私有方法
/// <summary>
/// 配置日志服务
/// </summary>
/// <param name="services"></param>
private void ConfigureLogging(IServiceCollection services)
{
services.AddLogging();
}
/// <summary>
/// 配置缓存服务
/// </summary>
private void ConfigureCache(IConfigurationBuilder build)
{
build
.AddCacheFile("cacheSettings.json", optional: false);
}
#endregion
}
}
现在基本上已经完事了,下面再说下surgingSettings.json配置信息为了避免与Surging.Services.Server的端口重复因此配置如下:
{
"Surging": {
"Ip": "${Surging_Server_IP}",
"WatchInterval": 30,
"Port": "${Surging_Server_Port}|100",
"MappingIp": "${Mapping_ip}",
"MappingPort": "${Mapping_Port}",
"Token": "true",
"WanIp": "${Mapping_WanIp}",
"Libuv": true,
"MaxConcurrentRequests": 20,
"ExecutionTimeoutInMilliseconds": 30000,
"Protocol": "${Protocol}|None", //Http、Tcp、None
"RootPath": "${RootPath}|",
"Ports": {
"HttpPort": "${HttpPort}|2801",
"MQTTPort": "${MQTTPort}|971",
"WSPort": "${WSPort}|961"
},
"RequestCacheEnabled": false,
"Packages": [
{
"TypeName": "EnginePartModule",
"Using": "${UseEngineParts}|DotNettyModule;NLogModule;MessagePackModule;ServiceProxyModule;ConsulModule;EventBusRabbitMQModule;CachingModule;"
}
]
}, //如果引用多个同类型的组件,需要配置Packages,如果是自定义按需引用,无需配置Packages
"Consul": {
"ConnectionString": "${Register_Conn}|127.0.0.1:8500", // "127.0.0.1:8500",
"SessionTimeout": "${Register_SessionTimeout}|50",
"RoutePath": "${Register_RoutePath}",
"ReloadOnChange": true,
"EnableChildrenMonitor": false
},
"Swagger": {
"Version": "${SwaggerVersion}|V1", // "127.0.0.1:8500",
"Title": "${SwaggerTitle}|Surging Demo",
"Description": "${SwaggerDes}|surging demo",
"Contact": {
"Name": "API Support",
"Url": "https://github.com/dotnetcore/surging",
"Email": "fanliang1@hotmail.com"
},
"License": {
"Name": "MIT",
"Url": "https://github.com/dotnetcore/surging/blob/master/LICENSE"
}
},
"EventBus_Kafka": {
"Servers": "${EventBusConnection}|localhost:9092",
"MaxQueueBuffering": "${MaxQueueBuffering}|10",
"MaxSocketBlocking": "${MaxSocketBlocking}|10",
"EnableAutoCommit": "${EnableAutoCommit}|false",
"LogConnectionClose": "${LogConnectionClose}|false",
"OffsetReset": "${OffsetReset}|earliest",
"GroupID": "${EventBusGroupID}|surgingdemo"
},
"WebSocket": {
"WaitTime": 2,
"KeepClean": false,
"Behavior": {
"IgnoreExtensions": true,
"EmitOnPing": false
}
},
"EventBus": {
"EventBusConnection": "${EventBusConnection}|192.168.1.127",
"EventBusUserName": "${EventBusUserName}|guest",
"EventBusPassword": "${EventBusPassword}|guest",
"VirtualHost": "${VirtualHost}|/",
"MessageTTL": "${MessageTTL}|30000",
"RetryCount": "${RetryCount}|1",
"FailCount": "${FailCount}|3",
"prefetchCount": "${PrefetchCount}|0",
"BrokerName": "${BrokerName}|surging_demo",
"Port": "${EventBusPort}|32671"
},
"Zookeeper": {
"ConnectionString": "${Zookeeper_ConnectionString}|127.0.0.1:2181",
"SessionTimeout": 50,
"ReloadOnChange": true
},
"Logging": {
"Debug": {
"LogLevel": {
"Default": "Information"
}
},
"Console": {
"IncludeScopes": true,
"LogLevel": {
"Default": "${LogLevel}|Debug"
}
},
"LogLevel": {
"Default": "${LogLevel}|Debug"
}
},
"MqttClient": {
"ClientID": "${MqttClientID}|serverclientid",
"MqttClientConnection": "${MqttClientConnection}|127.0.0.1",
"MqttClientUserName": "admin",
"MqttClientPassword": "123456",
"Port": 97,
"KeepAlivedTime": 60,
"CleanSession": true,
"Topics": [ "test1","test2" ]
}
}
总的Server代码如图所示:
此时代码开发阶段结束。我们可以设置多项目启动,启动项目前确定你的 Consul能够正常使用。本地启动Consul 可以通过控制台来做测试就OK
1、Surging.Services.Server 2、Surging.MqttClientWithWsServices.Server即(红框标注内容):
说到此处,想必大家都知道怎么使用SurgingWS、MqttClient 与MqttBroker进行连接了。写出这段代码主要是针对于Surging不了解,或者摄入不深的人,能直接快速的使用本代码让用户与设备间可以正常通讯。
注意如果使用Docker编排,或者Rancher编排Surging Broker 或者 WSMqttClient 时如果涉及到多个编排我们需要进行相应的逻辑判断。
近期由于个人需求,需要把设备在线状态通知给各个DBMqttClient端,用于保存现有设备状态。在不处理设备连接时,我们就可以知道设备是否在线,是否有异常。异常时长等。如有此需求可在下方留言。此代码近期可能会贡献给Surging,让Surging更加强大。写这篇文章呢,主要目的是再没有看懂作者代码的情况下,可以尽情使用MqttBroker的功能。次处只是引用了WS与MqttClient,其实可以以此为参考部署更多的MqttClient,比如数据保存与服务通讯分开等。在此感谢Surging作者在业余时间为我们做了这么好的开源项目,愿Surging越来越好。