进阶
# 扩展插件开发
如何开发一个拓展插件
# 对外提供开放式WebApi接口
本节中,我们将实现基于IoTCenter的开放式WebApi接口功能。
# 需求概述
1、需集成基础的鉴权功能,只允许携带指定认证请求头的调用,避免非法调用对数据造成了恶意修改,影响物联数据的准确性。
2、提供对外开放式WebApi,可通过GetValue接口获取指定设备的实时测点值,并通过SetValue方法修改指定设备的实时测点值。
3、提供回调接口,用以接收事件回调。
# 知识储备
在IoTCenter中扩展现有接口,可使用Asp .NET Core通用Web宿主技术实现,这种技术允许开发者在控制台或其他非Web应用中实现独立的Web能力。
同时,我们将采用Asp .NET Core提供的中间件,依赖注入等常见技术,限于篇幅,此处不赘述,开发者可通过微软官方文档获取相关知识。
接口的模拟调用,可通过Postman完成。
# 编码实现
新建ASP.NET Core Web API项目
操作步骤
点击下一步后,在配置新项目界面,将项目命名为
IoTCenter.ExprcWebApiSample
完成项目配置后,点击下一步,在其他信息界面选择框架为
.NET 6.0
。值得注意的是若不适应
.NET6.0
的顶级语句风格可以勾选不使用顶级语句选项;若不需要支持OpenAPI,取消选中启用OpenAPI支持。}
在NuGet包管理器中添加依赖项GwDataCenter,选择安装6.0.10及以上版本。
项目完成后的目录结构(删除了模板自带的WeatherForecast类以及对应的控制器)如下图所示:
创建
Infrastructure
的源码文件夹,用来存放基础设施(通用)代码,并在文件夹中新建以下四个类:Startup类:进行Web初始化启动项的配置
WebSelfHostService类:通过WebHostBuilder对象重写Web加载机制
SessionStateManager类:处理状态
JsonHelper类:处理Json序列化。
基础设施(通用)代码
StartUp
类的代码为:public class Startup { /// <summary> /// 配置服务 /// </summary> /// <param name="services"></param> public void ConfigureServices(IServiceCollection services) { Console.Out.WriteLine("--------服务启动配置"); services.AddScoped<AuthorizeMiddleware>(); services.AddSingleton<SessionStateManager>(); services.AddControllers(options => { options.Filters.Add(new WebSelfExecptionFilter()); }); services.AddMvcCore(options => { options.EnableEndpointRouting = false; }); services.AddRouting(); } /// <summary> /// 服务启动配置 /// </summary> /// <param name="app"></param> public void Configure(IApplicationBuilder app) { Console.Out.WriteLine("----------服务启动配置"); app.UseRouting(); app.UseMiddleware<AuthorizeMiddleware>(); app.UseMvc(s => { s.MapRoute("default", "api/{controller}/{action}"); }); app.UseEndpoints(endpoints => { endpoints.MapGet("/", async context => { Console.Out.WriteLine("终结点"); }); }); } }
值得注意的是Starup类中使用的WebSelfExecptionFilter类将在下一个步骤中进行创建。
WebSelfHostService
类的代码为:/// <summary> /// 创建自宿主web服务 /// </summary> public class WebSelfHostService { public async Task RunHostAsync(string hostUrl) { try { Console.Out.WriteLine("开始暴露WEBAPI"); var host = new WebHostBuilder() .UseKestrel() .UseUrls(hostUrl) .UseStartup<Startup>() .Build(); Task.WaitAll(host.RunAsync()); Console.Out.WriteLine("暴露WEBAPI成功!"); } catch (System.Exception ex) { Console.Out.WriteLine($"暴露WEBAPI异常:{ex}"); } } }
SessionStateManager
类的代码为:using System; using System.Collections.Generic; using System.Text; namespace IoTCenter.ExprcWebApiSample { /// <summary> /// 统一状态管理 /// </summary> public class SessionStateManager { /// <summary> /// 此处用字典来存储,真实场景下应使用 /// </summary> private Dictionary<string, string> _userTokens { get; set; } /// <summary> /// 新增token /// </summary> /// <param name="token"></param> /// <param name="userName"></param> public void Add(string token, string userName) { if (_userTokens == null) _userTokens = new Dictionary<string, string>(); if (!_userTokens.ContainsKey(token)) { _userTokens.Add(token, userName); } } /// <summary> /// 验证token是否存在 /// </summary> /// <param name="token"></param> /// <returns></returns> public bool CheckToken(string token) { return _userTokens == null ? false : _userTokens.ContainsKey(token); } /// <summary> /// 创建随机凭证 /// </summary> /// <returns></returns> public string CreateToken() { return Guid.NewGuid().ToString(); } } }
JsonHelper
类的代码为:using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using System; using System.Collections.Generic; using System.Text; namespace IoTCenter.ExprcWebApiSample.Infrastructure { /// <summary> /// Json序列化 /// </summary> public static class JsonHelper { /// <summary> /// 提供简单的对象Json字符串反序列化对象的方法 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="obj"></param>___ /// <returns></returns> public static T FromJson<T>(this string obj) where T : class { var jSetting = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, TypeNameHandling = TypeNameHandling.All, ContractResolver = new CamelCasePropertyNamesContractResolver() }; if (obj != null) { return Newtonsoft.Json.JsonConvert.DeserializeObject<T>(obj); } return null; } /// <summary> /// 提供简单的对象序列化Json字符串方法 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="obj"></param> /// <returns></returns> public static string ToJson<T>(this T obj) where T : class { var jSetting = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Include, ContractResolver = new CamelCasePropertyNamesContractResolver() }; return Newtonsoft.Json.JsonConvert.SerializeObject(obj, jSetting); } } }
创建一个源码文件夹,命名为
Middlewares
,并在该文件夹下创建AuthorizeMiddleware
的中间件,和WebSelfExecptionFilter
异常处理类。Middlewares源码文件夹
AuthorizeMiddleware
中间件代码为:/// <summary> /// 认证中间件 /// </summary> public class AuthorizeMiddleware : IMiddleware { /// <summary> /// 认证管理 /// </summary> private SessionStateManager _sessionStateManager; /// <summary> /// 执行中间件方法 /// </summary> /// <param name="context"></param> /// <param name="next"></param> /// <returns></returns> public async Task InvokeAsync(HttpContext context, RequestDelegate next) { Console.WriteLine("中间件"); _sessionStateManager = context.RequestServices.GetService<SessionStateManager>(); //登录方法不执行认证 if (context.Request.Path.Equals("/api/account/Authorize", StringComparison.Ordinal)) { await next(context); } //请求头携带token作为接口鉴权标识 else if (context.Request.Headers.ContainsKey("token") && _sessionStateManager.CheckToken(context.Request.Headers["token"])) { await context.Response.WriteAsync(new JsonResult(ResponseResult.Unauthorize("认证失败,请在Header中携带必备的Token参数")).ToJson()); } else { await next(context); } }
其中
WebSelfExecptionFilter
异常处理类代码为:/// <summary> /// 自定义异常过滤器 /// </summary> public class WebSelfExecptionFilter : IAsyncExceptionFilter { /// <summary> /// 重写OnExceptionAsync方法,定义自己的处理逻辑 /// </summary> /// <param name="context"></param> /// <returns></returns> public Task OnExceptionAsync(ExceptionContext context) { Console.WriteLine($"服务器内部异常,{context.Exception}"); // 如果异常没有被处理则进行处理 if (context.ExceptionHandled == false) { // 定义返回类型 var result = ResponseResult.InternalFail("内部服务器错误", ""); context.Result = new JsonResult(result); } // 设置为true,表示异常已经被处理了 context.ExceptionHandled = true; return Task.CompletedTask; } }
创建控制器
AccountController
,用来实现登录功能。控制器
AccountController
using IoTCenter.ExprcWebApiSample.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; using System.Text; namespace IoTCenter.ExprcWebApiSample { /// <summary> /// 认证方法 /// </summary> [Route("api/account/[action]")] public class AccountController : ControllerBase { /// <summary> /// 认证管理 /// </summary> private readonly SessionStateManager _sessionStateManager; public AccountController(SessionStateManager sessionStateManager) { _sessionStateManager = sessionStateManager; } /// <summary> /// 鉴权方法,此处仅提供示例,正式项目应使用jwt/oauth2等专业认证方法 /// </summary> /// <param name="account"></param> /// <returns></returns> [HttpPost] public JsonResult Authorize([FromBody] UserInfo account) { //此处仅供示例,真实场景下,应通过数据库集中管理用户名和密钥。 if (account.UserName == "admin" && account.Password == "123456") { string token = _sessionStateManager.CreateToken(); _sessionStateManager.Add(token, account.UserName); return new JsonResult(ResponseResult.Success("登录成功", token)); } else { return new JsonResult(ResponseResult.Fail("用户名或密码错误", "")); } } } }
新建一些模型类。
模型类
AlarmEventModel
类,回调事件的请求模型using System; using System.Collections.Generic; using System.Text; namespace IoTCenter.ExprcWebApiSample.Models { /// <summary> /// 回调事件模型 /// </summary> public class AlarmEventModel { /// <summary> /// 事件Id /// </summary> public string EventId { get; set; } /// <summary> /// 事件信息 /// </summary> public string EventInfo { get; set; } } }
EquipValueModel
类,物联属性查询的请求模型。using System; using System.Collections.Generic; using System.Text; namespace IoTCenter.ExprcWebApiSample.Models { /// <summary> /// 设备查询 /// </summary> public class EquipValueModel { /// <summary> /// 设备Id /// </summary> public int EquipId { get; set; } /// <summary> /// 遥测Id /// </summary> public int YcId { get; set; } /// <summary> /// 设置某设备的测点值 /// </summary> public double RealValue { get; set; } } }
ResponseResult
类,通用的响应模型。using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; using System.Net; using System.Text; namespace IoTCenter.ExprcWebApiSample { /// <summary> /// 响应结果 /// </summary> public class ResponseResult { /// <summary> /// http状态码 /// </summary> public HttpStatusCode StatusCode { get; set; } /// <summary> /// 响应结果 /// </summary> public string Result { get; set; } /// <summary> /// 错误提示 /// </summary> public string ErrorMsg { get; set; } /// <summary> /// 响应数据 /// </summary> public object Body { get; set; } /// <summary> /// 响应成功标识 /// </summary> /// <param name="result"></param> /// <returns></returns> public static ResponseResult Success(string result, object body) { return new ResponseResult { StatusCode = HttpStatusCode.OK, Body = body, ErrorMsg = "", Result = result }; } /// <summary> /// 请求失败 /// </summary> /// <param name="result"></param> /// <param name="errorMsg"></param> /// <returns></returns> public static ResponseResult Fail(string result, string errorMsg) { return new ResponseResult { StatusCode = HttpStatusCode.BadRequest, Result = result, ErrorMsg = errorMsg }; } /// <summary> /// 内部服务器错误 /// </summary> /// <param name="result"></param> /// <param name="errorMsg"></param> /// <returns></returns> public static ResponseResult InternalFail(string result, string errorMsg) { return new ResponseResult { StatusCode = HttpStatusCode.InternalServerError, Result = result, ErrorMsg = errorMsg }; } /// <summary> /// 身份认证失败 /// </summary> /// <returns></returns> public static ResponseResult Unauthorize(string result) { return new ResponseResult { StatusCode = HttpStatusCode.Unauthorized, Result = result, }; } } }
UserInfo
类,用户请求模型。using System; using System.Collections.Generic; using System.Text; namespace IoTCenter.ExprcWebApiSample.Models { /// <summary> /// 用户模型 /// </summary> public class UserInfo { /// <summary> /// 用户名 /// </summary> public string UserName { get; set; } /// <summary> /// 密码 /// </summary> public string Password { get; set; } } }
新建IoTCenter基础框架支持的扩展插件入口类
CExProc
,在该类中调用Web宿主服务,其代码如下:CExProc
using GWDataCenter; using GWDataCenter.Database; using IoTCenter.ExprcWebApiSample.Infrastructure; namespace IoTCenter.ExprcWebApiSample { public class CExProc : IExProcCmdHandle { public bool init(GWExProcTableRow Row) { var parm = Row.Proc_parm.ToString(); try { Console.Out.WriteLine("待加载web接口服务"); //调用Web宿主服务 new WebSelfHostService().RunHostAsync(parm).Wait(); Console.Out.WriteLine("Web接口已启动"); } catch (Exception ex) { Thread.Sleep(TimeSpan.FromSeconds(10)); Console.Out.WriteLine($"{ex}"); } return true; } public void SetParm(string main_instruction, string minor_instruction, string value) { Console.WriteLine($"调用事件:{main_instruction},{minor_instruction},{value}"); } } }
再新增一个控制器,用以从IoTCenter网关获取实时数据。在这个控制器中,我们将从网关的
GWDataCenter.StationItem
对象中,获取指定设备、指定测点的物联实时值。控制器
using IoTCenter.ExprcWebApiSample.Models; using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; using System.Text; namespace IoTCenter.ExprcWebApiSample { /// <summary> /// 示例接口 /// </summary> [Route("api/sample/[action]")] public class SampleController:ControllerBase { /// <summary> /// 获取指定测点的数值 /// </summary> /// <param name="equip"></param> /// <returns></returns> public JsonResult GetValue([FromBody] EquipValueModel equipModel) { var equip = GWDataCenter.StationItem.GetEquipItemFromEquipNo(equipModel.EquipId); if (equip != null) { var ycItem = equip.GetYCItem(equipModel.YcId); if (ycItem != null) { return new JsonResult(ResponseResult.Success("查询成功", ycItem.YCValue)); } return new JsonResult(ResponseResult.Fail("设置失败,测点不存在", "")); } else return new JsonResult(ResponseResult.Fail("查询失败,设备不存在", "")); } /// <summary> /// 设置设备数值 /// </summary> /// <param name="equipModel"></param> /// <returns></returns> public JsonResult SetValue([FromBody] EquipValueModel equipModel) { var equip = GWDataCenter.StationItem.GetEquipItemFromEquipNo(equipModel.EquipId); if (equip != null) { var ycItem = equip.GetYCItem(equipModel.YcId); if (ycItem != null) { ycItem.YCValue = equipModel.RealValue; return new JsonResult(ResponseResult.Success("设置成功", ycItem.YCValue)); } return new JsonResult(ResponseResult.Fail("设置失败,测点不存在", "")); } else return new JsonResult(ResponseResult.Fail("设置失败,设备不存在", "")); } /// <summary> /// Post回调方法 /// </summary> /// <param name="alarmEventModel"></param> /// <returns></returns> public JsonResult Callback([FromBody] AlarmEventModel alarmEventModel) { //此处应根据实际场景完善逻辑 Console.Out.WriteLine($"事件信息,{alarmEventModel.EventId},{alarmEventModel.EventInfo}"); return new JsonResult(ResponseResult.Success("回调成功", "")); } } }
加载调试
加载调试
使用IoTCenter网关主程序GWHost1.exe启动加载该插件,看起来跟普通Web应用区别不大。
注意
配置扩展插件参数为WebApI的URL,例如:http://localhost:5000
同样也支持Postman调试。
# 下载源码
可运行示例下载:
# FAQ
扩展插件没有加载?
检查数据库配置是否正确,并且确认生成的扩展插件已拷贝到IoTCenter\dll 目录中。值得注意的是,在本实例中采用的为Asp.NETCore网站形式开发,发布时,IoTCenter.ExprcWebApiSample.exe
和IoTCenter.ExprcWebApiSample.dll
,配置扩展插件时,只需配置后者为扩展插件即可。
# 事件订阅
IoTCenter中可以订阅的事件分为遥测值变化事件、遥信值变化事件、设置点事件、实时快照事件、设备状态变化事件
# 遥测值变化事件
遥测值变化事件适用于当遥测值发生变化时获取设备以及测点相关信息同时获取测点的实时值的场合。
事件订阅
int equipNo = int.Parse(Row.Proc_parm.Split(",")[0]);
int ycNo = int.Parse(Row.Proc_parm.Split(",")[1]);
var ycItem = DataCenter.EquipItemDict[equipNo].YCItemDict[ycNo];
//遥测值变化事件订阅
ycItem.ValueChanged -= YCYXValueChangedEvent;
ycItem.ValueChanged += YCYXValueChangedEvent;
说明
本示例中设备的编号和遥测编号配置在扩展插件表(GwExProc)的Proc_parm字段中,在具体开发过程中可以采用其它方式配置,例如:数据库
遥测值变化事件
private void YCYXValueChangedEvent(object sender, EventArgs e)
{
var iEquipno = 0;
try
{
if (sender is YCItem ycItem)
{
iEquipno = ycItem.Equip_no;
EquipValueChangedHandle(iEquipno ,ycItem, null);
}
else if (sender is YXItem yxItem)
{
iEquipno = yxItem.Equip_no;
EquipValueChangedHandle(iEquipno, null, yxItem);
}
}
catch (Exception ex)
{
DataCenter.Write2Log($"{MethodBase.GetCurrentMethod().Name}出现异常:{ex}", LogLevel.Error);
}
}
遥测值变化事件通知
private async Task EquipValueChangedHandle(int iEquipno, YCItem ycItem, YXItem yxItem)
{
try
{
if (ycItem != null)
{
EquipItem equipItem = DataCenter.GetEquipItem(iEquipno);
Console.WriteLine($"测点数据变化事件:设备{equipItem.Equip_nm}的测点{ycItem.Yc_nm}的值为{ycItem.YCValue}");
}
else if (yxItem != null)
{
EquipItem equipItem = DataCenter.GetEquipItem(iEquipno);
Console.WriteLine($"测点数据变化事件:设备{equipItem.Equip_nm}的测点{yxItem.Yx_nm}的值为{yxItem.YXValue}");
}
}
catch (Exception ex)
{
DataCenter.Write2Log($"{MethodBase.GetCurrentMethod().Name}出现异常:{ex}", LogLevel.Error);
}
}
# 遥信值变化事件
与遥测值变化类似,遥信值变化事件适用于当遥信值发生变化时获取设备以及测点相关信息同时获取测点的实时值的场合。
事件订阅
//遥信值变化事件订阅
int yxNo = int.Parse(Row.Proc_parm.Split(",")[1]);
var yxItem = DataCenter.EquipItemDict[equipNo].YCItemDict[yxNo];
yxItem.ValueChanged -= YCYXValueChangedEvent;
yxItem.ValueChanged += YCYXValueChangedEvent;
说明
遥信值变化事件与遥信值变化事件通知与遥测值变化事件相同,此处不在赘述。
# 设备状态变化事件
设备状态变化事件适用于当设备状态发生变化时获取设备相关信息同时获取设备的实时状态的场合。
事件订阅
//设备状态变化事件订阅
EquipItem equipItem = DataCenter.GetEquipItem(equipNo);
equipItem.EqpStateChanged -= EquipStateChangedEvent;
equipItem.EqpStateChanged += EquipStateChangedEvent;
设备状态变化事件
private void EquipStateChangedEvent(object sender, EventArgs e)
{
if (sender is EquipItem equipItem)
{
EquipStateChangedHandle(equipItem);
}
}
设备状态变化事件通知
private void EquipStateChangedHandle(EquipItem equipItem)
{
try
{
Console.WriteLine($"设备状态变化通知,设备{equipItem.Equip_nm}的状态为{equipItem.State}");
}
catch (Exception ex)
{
DataCenter.Write2Log($"{MethodBase.GetCurrentMethod().Name}出现异常:{ex}", LogLevel.Error);
}
}
# 设备设置点事件
设备设置点事件适用于当对设备下发指令后获取指令下发成功与否以及设备和设置点相关信息的场合。
//设备设置点事件订阅
StationItem.SetParmResultEvent -= StationItem_SetParmResultEvent;
StationItem.SetParmResultEvent += StationItem_SetParmResultEvent;
设备指令下发事件
private void StationItem_SetParmResultEvent(object sender, EventArgs e)
{
if (sender is SetItem setitem)
{
EquipSetParmResultHandle(setitem);
}
}
设备指令下发事件通知
private void EquipSetParmResultHandle(SetItem setitem)
{
try
{
Console.WriteLine($"指令下发执行结果事件,setitem信息:{JsonConvert.SerializeObject(setitem)}");
EquipItem equipItem = DataCenter.GetEquipItem(setitem.EquipNo);
var commandResult = setitem.WaitSetParmIsFinish.GetValueOrDefault(false) ? "下发成功" : "下发失败";
Console.WriteLine($"设备{equipItem.Equip_nm}的指令{setitem.m_SetNo}{commandResult}");
}
catch (Exception ex)
{
DataCenter.Write2Log($"{MethodBase.GetCurrentMethod().Name}出现异常:{ex}");
}
}
# 实时快照事件
实时快照事件适用于实时获取快照信息的场合。
事件订阅
//实时快照事件订阅
var eventLists = MessageService.GetEventList();
eventLists.CollectionChanged -= EventLists_CollectionChanged;
eventLists.CollectionChanged += EventLists_CollectionChanged;
实时快照消息事件
private void EventLists_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
try
{
if (sender == null) return;
var realTimeEventItems = e.NewItems;
var NewStartingIndex = e.NewStartingIndex;
if (realTimeEventItems != null && NewStartingIndex > 0)
{
foreach (RealTimeEventItem item in realTimeEventItems)
{
EquipEventAalarmHandle(item);
}
}
}
catch (Exception ex)
{
DataCenter.Write2Log($"{MethodBase.GetCurrentMethod().Name}出现异常:{ex}", LogLevel.Error);
}
}
实时快照消息通知
private void EquipEventAalarmHandle(RealTimeEventItem eventitem)
{
try
{
Console.WriteLine($"实时快照消息通知,eventitem信息:{JsonConvert.SerializeObject(eventitem)}");
EquipItem equipItem = DataCenter.GetEquipItem(eventitem.Equipno);
Console.WriteLine($"实时快照消息通知,Level:{eventitem.Level},equipName:{equipItem.Equip_nm},YCYXno:{eventitem.Ycyxno},Type:{eventitem.Type},ConTent:{eventitem.EventMsg}");
}
catch (Exception ex)
{
DataCenter.Write2Log($"{MethodBase.GetCurrentMethod().Name}出现异常:{ex}", LogLevel.Error);
}
}
# 下载源码
可运行示例下载: