
开源工作流引擎 Workflow Core 的研究和使用教程 [TOC] 一,工作流对象和使用前说明 为了避免歧义,事先约定。 工作流有很多节点组成,一个节点成为步骤点(Step)。 1,IWorkflow / IWorkflowBuilder Workflow Core 中,用于构建工作流的类继承 IWorkflow,代表一条有任务规则的工作流,可以表示工作流任务的开始或者 Do() 方法,或工作流分支获取其它方法。 IWorkflow 有两个同名接口: public interface IWorkflow<TData> where TData : new() { string Id { get; } int Version { get; } void Build(IWorkflowBuilder<TData> builder); } public interface IWorkflow : IWorkflow<object> { } Id:此工作流的唯一标识符; Version:此工作流的版本。 void Build:在此方法内构建工作流。 工作流运作过程中,可以传递数据。有两种传递方法:使用泛型,从运行工作流时就要传入;使用 object 简单类型,由单独的步骤产生并且传递给下一个节点。 IWorkflowBuilder 是工作流对象,构建一个具有逻辑规则的工作流。可以构建复杂的、具有循环、判断的工作流规则,或者并行或者异步处理工作流任务。 一个简单的工作流规则: public class DeferSampleWorkflow : IWorkflow { public string Id => "DeferSampleWorkflow"; public int Version => 1; public void Build(IWorkflowBuilder<object> builder) { builder .StartWith(context => { // 开始工作流任务 Console.WriteLine("Workflow started"); return ExecutionResult.Next(); }) .Then<SleepStep>() .Input(step => step.Period, data => TimeSpan.FromSeconds(20)) .Then(context => { Console.WriteLine("workflow complete"); return ExecutionResult.Next(); }); } } 2,EndWorkflow 此对象表示当前工作流任务已经结束,可以表示主工作流或者工作流分支任务的完成。 /// Ends the workflow and marks it as complete IStepBuilder<TData, TStepBody> EndWorkflow(); 因为工作流是可以出现分支的,每个工作流各自独立工作,每个分支都有其生命周期。 3,容器 ForEach、While、If、When、Schedule、Recur 是步骤容器。都返回IContainerStepBuilder<TData, Schedule, TStepBody> Parallel、Saga是步骤的容器,都返回 IStepBuilder<TData, Sequence>。 ForEach、While、If、When、Schedule、Recur 返回类型的接口: public interface IContainerStepBuilder<TData, TStepBody, TReturnStep> where TStepBody : IStepBody where TReturnStep : IStepBody { /// The block of steps to execute IStepBuilder<TData, TReturnStep> Do(Action<IWorkflowBuilder<TData>> builder); Parallel、Saga : /// Execute multiple blocks of steps in parallel IParallelStepBuilder<TData, Sequence> Parallel(); /// Execute a sequence of steps in a container IStepBuilder<TData, Sequence> Saga(Action<IWorkflowBuilder<TData>> builder); 也就是说,ForEach、While、If、When、Schedule、Recur 是真正的容器。 按照我的理解,继承了 IContainerStepBuilder的,是一个容器,一个流程下的一个步骤/容器;因为 Workflow Core 作者对接口的命名很明显表达了 This a container。 因为里面包含了一组操作,可以说是一个步骤里面包含了一个流程,这个流程由一系列操作组成,它是线性的,是顺序的。里面是一条工作流(Workflow)。 而 Parllel、Saga,相当于步骤点的容器。 更直观的理解是电路,继承 IContainerStepBuilder 的是串联设备的容器,是顺序的; Parllel 是并联电路/设备的一个容器,它既是一个开关,使得一条电路变成多条并流的电路,又包含了这些电路的电器。里面可以产生多条工作流,是多分支的、不同步的、独立的。 从实现接口上看,ForEach、While、If、When、Schedule、Recur、Parllel 都实现了 Do() 方法,而 Saga 没有实现。 关于 Saga,后面说明。 4,工作流的步骤点 实现接口如下: IStepBuilder<TData, TStep> StartWith<TStep>(Action<IStepBuilder<TData, TStep>> stepSetup = null) where TStep : IStepBody; IStepBuilder<TData, InlineStepBody> StartWith(Func<IStepExecutionContext, ExecutionResult> body); IStepBuilder<TData, ActionStepBody> StartWith(Action<IStepExecutionContext> body); IEnumerable<WorkflowStep> GetUpstreamSteps(int id); IWorkflowBuilder<TData> UseDefaultErrorBehavior(WorkflowErrorHandling behavior, TimeSpan? retryInterval = null); 方法名称 说明 StartWith 任务的开始,必须调用此方法 GetUpstreamSteps 获取上一个步骤(StepBody)的ID UseDefaultErrorBehavior 不详 StepBody 是一个节点,IStepBuilder 构建一个节点,只有通过 StartWith,才能开始一个工作流、一个分支、异步任务等。 UseDefaultErrorBehavior笔者没有使用到,不敢瞎说。貌似与事务有关,当一个步骤点发生异常时,可以终止、重试等。 二,IStepBuilder 节点 IStepBuilder 表示一个节点,或者说一个容器,里面可以含有其它操作,例如并行、异步、循环等。 1,设置属性的方法 Name:设置此步骤点的名称;id:步骤点的唯一标识符。 /// Specifies a display name for the step IStepBuilder<TData, TStepBody> Name(string name); /// Specifies a custom Id to reference this step IStepBuilder<TData, TStepBody> Id(string id); 2,设置数据 前面说到,工作流每个步骤点传递数据有两种方式。 TData(泛型) 是工作流中,随着流传的数据,这个对象会在整个工作流程生存。 例如 Mydata class RecurSampleWorkflow : IWorkflow<MyData> { public string Id => "recur-sample"; public int Version => 1; public void Build(IWorkflowBuilder<MyData> builder) { ... } } public class MyData { public int Counter { get; set; } } 3,Input / Output 为当前步骤点(StepBody)设置数据,亦可为 TData 设置数据。 两类数据:每个步骤点都可以拥有很多字段、属性和方法等;工作流流转 TData。 Input、Output 是设置这些数据的具体方法。 IStepBuilder<TData, TStepBody> Input<TInput>(Expression<Func<TStepBody, TInput>> stepProperty, Expression<Func<TData, TInput>> value); IStepBuilder<TData, TStepBody> Input<TInput>(Expression<Func<TStepBody, TInput>> stepProperty, Expression<Func<TData, IStepExecutionContext, TInput>> value); IStepBuilder<TData, TStepBody> Input(Action<TStepBody, TData> action); IStepBuilder<TData, TStepBody> Output<TOutput>(Expression<Func<TData, TOutput>> dataProperty, Expression<Func<TStepBody, object>> value); 三,工作流节点的逻辑和操作 容器操作 1,Saga 用于在容器中执行一系列操作。 /// Execute a sequence of steps in a container IStepBuilder<TData, Sequence> Saga(Action<IWorkflowBuilder<TData>> builder); 虽然注释说明 “用于在容器中执行一系列操作”,但实际上它不是一个真正的”容器“。 因为它没有继承 IContainerStepBuilder,也没有实现 Do() 。 但是它返回的 Sequence 实现了ContainerStepBody。 如果说真正的容器相当于一条长河流中的一个湖泊(可以容纳和储水),而 Saga 可能只是某一段河流的命名,而不是具体的湖泊。 或者说 static void Main(string[] args)里面的代码太多了,新建一个方法体,把部分代码放进去。总不能把所有代码写在一个方法里吧?那么创建一个类,把代码分成多个部分,放到不同方法中,增强可读性。本质还是没有变。 Saga 可以用来处理事务,进行重试或回滚等操作。后面说明。 普通节点 1,Then 用于创建下一个节点,创建一个普通节点。可以是主工作流的节点(最外层)、或者作为循环、条件节点里的节点、作为节点中节点的节点。 IStepBuilder<TData, TStep> Then<TStep>(Action<IStepBuilder<TData, TStep>> stepSetup = null) where TStep : IStepBody; IStepBuilder<TData, TStep> Then<TStep>(IStepBuilder<TData, TStep> newStep) where TStep : IStepBody; IStepBuilder<TData, InlineStepBody> Then(Func<IStepExecutionContext, ExecutionResult> body); IStepBuilder<TData, ActionStepBody> Then(Action<IStepExecutionContext> body); 2,Attach Then 作为普通节点,按顺序执行。操作对象是类型、StepBody。 Attach 也是普通节点,无特殊意义,通过 id 来指定要执行 StepBody 。可以作为流程控制的跳转。 相当于 goto 语句。 /// Specify the next step in the workflow by Id IStepBuilder<TData, TStepBody> Attach(string id); 事件 1,WaitFor 用于定义事件,将当前节点作为事件节点,然后在后台挂起,工作流会接着执行下一个节点。在工作流停止前,可以通过指定 标识符(Id) 触发事件。在一个工作流中,每个事件的标识符都是唯一的。 IStepBuilder<TData, WaitFor> WaitFor(string eventName, Expression<Func<TData, string>> eventKey, Expression<Func<TData, DateTime>> effectiveDate = null, Expression<Func<TData, bool>> cancelCondition = null); IStepBuilder<TData, WaitFor> WaitFor(string eventName, Expression<Func<TData, IStepExecutionContext, string>> eventKey, Expression<Func<TData, DateTime>> effectiveDate = null, Expression<Func<TData, bool>> cancelCondition = null); 条件体和循环体 1,End 意思应该是结束一个节点的运行。 如果在 When 中使用,相当于 break。 IStepBuilder<TData, TStep> End<TStep>(string name) where TStep : IStepBody; 使用例子 builder .StartWith<RandomOutput>(x => x.Name("Random Step")) .When(0) .Then<TaskA>() .Then<TaskB>() .End<RandomOutput>("Random Step") .When(1) .Then<TaskC>() .Then<TaskD>() .End<RandomOutput>("Random Step"); 2,CancelCondition 在一个条件下过早地取消此步骤的执行。 应该相当于 contiune。 /// Prematurely cancel the execution of this step on a condition IStepBuilder<TData, TStepBody> CancelCondition(Expression<Func<TData, bool>> cancelCondition, bool proceedAfterCancel = false); 节点的异步或多线程 1,Delay 延迟执行,使得当前节点延时执行。并非是阻塞当前的工作流运行。Delay 跟在节点后面,使得这个节点延时运行。可以理解成异步,工作流不会等待此节点执行完毕,会直接执行下一个节点/步骤。 /// Wait for a specified period IStepBuilder<TData, Delay> Delay(Expression<Func<TData, TimeSpan>> period); 2,Schedule 预定执行。将当前节点设置一个时间,将在一段时间后执行。Schedule 不会阻塞工作流。 Schedule 是非阻塞的,工作流不会等待Schedule执行完毕,会直接执行下一个节点/步骤。 /// Schedule a block of steps to execute in parallel sometime in the future IContainerStepBuilder<TData, Schedule, TStepBody> Schedule(Expression<Func<TData, TimeSpan>> time); 例子 builder .StartWith(context => Console.WriteLine("Hello")) .Schedule(data => TimeSpan.FromSeconds(5)).Do(schedule => schedule .StartWith(context => Console.WriteLine("Doing scheduled tasks")) ) .Then(context => Console.WriteLine("Doing normal tasks")); 3,Recur 用于重复执行某个节点,直至条件不符。 Recur 是非阻塞的,工作流不会等待 Rezur 执行完毕,会直接执行下一个节点/步骤。 /// Schedule a block of steps to execute in parallel sometime in the future at a recurring interval IContainerStepBuilder<TData, Recur, TStepBody> Recur(Expression<Func<TData, TimeSpan>> interval, Expression<Func<TData, bool>> until); 用于事务的操作 相当于数据库中的事务,流程中某些步骤发生异常时的时候执行某些操作。 例如: builder .StartWith(context => Console.WriteLine("Begin")) .Saga(saga => saga .StartWith<Task1>() .CompensateWith<UndoTask1>() .Then<Task2>() .CompensateWith<UndoTask2>() .Then<Task3>() .CompensateWith<UndoTask3>() ) .OnError(Models.WorkflowErrorHandling.Retry, TimeSpan.FromSeconds(5)) .Then(context => Console.WriteLine("End")); 1,CompensateWith 如果此步骤引发未处理的异常,则撤消步骤;如果发生异常,则执行。 可以作为节点的 B计划。当节点执行任务没有问题时, CompensateWith 不会运行;如果节点发生错误,就会按一定要求执行 CompensateWith 。 /// Undo step if unhandled exception is thrown by this step IStepBuilder<TData, TStepBody> CompensateWith<TStep>(Action<IStepBuilder<TData, TStep>> stepSetup = null) where TStep : IStepBody; IStepBuilder<TData, TStepBody> CompensateWith(Func<IStepExecutionContext, ExecutionResult> body); IStepBuilder<TData, TStepBody> CompensateWith(Action<IStepExecutionContext> body); 2,CompensateWithSequence 如果此步骤引发未处理的异常,则撤消步骤;如果发生异常,则执行。与 CompensateWith 的区别是,传入参数前者是 Func,后者是 Action。 CompensateWith 的内部实现了 CompensateWith,是对 CompensateWith 的封装。 /// Undo step if unhandled exception is thrown by this step IStepBuilder<TData, TStepBody> CompensateWithSequence(Action<IWorkflowBuilder<TData>> builder); 3,OnError 用于事务操作,表示发生错误时如果回滚、设置时间等。一般与 Saga 一起使用。 OnError 是阻塞的。 /// Configure the behavior when this step throws an unhandled exception IStepBuilder<TData, TStepBody> OnError(WorkflowErrorHandling behavior, TimeSpan? retryInterval = null); OnError 可以捕获一个容器内,某个节点的异常,并执行回滚操作。如果直接在节点上使用而不是容器,可以发生回滚,然后执行下个节点。如果作用于容器,那么可以让容器进行重新运行,等一系列操作。 OnError 可以与 When、While 等节点容器一起使用,但他们本身带有循环功能,使用事务会让代码逻辑变得奇怪。 Saga 没有条件判断、没有循环,本身就是一个简单的袋子,是节点的容器。因此使用 Saga 作为事务操作的容器,十分适合,进行回滚、重试等一系列操作。 四,条件或开关 迭代 1,ForEach 迭代,也可以说是循环。内部使用 IEnumerable 来实现。 与 C# 中 Foreach 的区别是,C# 中是用来迭代数据; 而工作流中 ForEach 用来判断元素个数,标识应该循环多少次。 ForEach 是阻塞的。 /// Execute a block of steps, once for each item in a collection in a parallel foreach IContainerStepBuilder<TData, Foreach, Foreach> ForEach(Expression<Func<TData, IEnumerable>> collection); 示例 builder .StartWith<SayHello>() .ForEach(data => new List<int>() { 1, 2, 3, 4 }) .Do(x => x .StartWith<DisplayContext>() .Input(step => step.Item, (data, context) => context.Item) .Then<DoSomething>()) .Then<SayGoodbye>(); 最终会循环5次。 条件判断 1,When 条件判断,条件是否真。 When 是阻塞的。 When 可以捕获上一个节点流转的数据(非 TData)。 /// Configure an outcome for this step, then wire it to another step [Obsolete] IStepOutcomeBuilder<TData> When(object outcomeValue, string label = null); /// Configure an outcome for this step, then wire it to a sequence IContainerStepBuilder<TData, When, OutcomeSwitch> When(Expression<Func<TData, object>> outcomeValue, string label = null); 前一个方法例如 When(0),会捕获 return ExecutionResult.Outcome(value); 的值,判断是否相等。但是这种方式已经过时。 需要使用表达式来判断。例如 .When(data => 1) .When(data => data.value==1) 2,While 条件判断,条件是否真。与When有区别,When可以捕获 ExecutionResult.Outcome(value); 。 While 是阻塞的。 /// Repeat a block of steps until a condition becomes true IContainerStepBuilder<TData, While, While> While(Expression<Func<TData, bool>> condition); 示例 builder .StartWith<SayHello>() .While(data => data.Counter < 3) .Do(x => x .StartWith<DoSomething>() .Then<IncrementStep>() .Input(step => step.Value1, data => data.Counter) .Output(data => data.Counter, step => step.Value2)) .Then<SayGoodbye>(); 3,If 条件判断,是否符合条件。 If是阻塞的。 /// Execute a block of steps if a condition is true IContainerStepBuilder<TData, If, If> If(Expression<Func<TData, bool>> condition); When、While、If的区别是,When、While 是条件是否为真,If是表达式是否为真。 实质上,是语言上的区别,与代码逻辑无关。 真假用 When/While,条件判断、表达式判断用 If 。 节点并发 1,Parallel 并行任务。作为容器,可以在里面设置多组任务,这些任务将会同时、并发运行。 Parallel 是阻塞的。 /// Execute multiple blocks of steps in parallel IParallelStepBuilder<TData, Sequence> Parallel(); 示例: .StartWith<SayHello>() .Parallel() .Do(then => then.StartWith<PrintMessage>() .Input(step => step.Message, data => "Item 1.1") .Then<PrintMessage>() .Input(step => step.Message, data => "Item 1.2")) .Do(then => then.StartWith<PrintMessage>() .Input(step => step.Message, data => "Item 2.1") .Then<PrintMessage>() .Input(step => step.Message, data => "Item 2.2") .Then<PrintMessage>() .Input(step => step.Message, data => "Item 2.3")) .Do(then => then.StartWith<PrintMessage>() .Input(step => step.Message, data => "Item 3.1") .Then<PrintMessage>() .Input(step => step.Message, data => "Item 3.2")) .Join() .Then<SayGoodbye>(); 有三个 Do,代表三个并行任务。三个 Do 是并行的,Do 内的代码,会按顺序执行。 Paeallel 的 Do: public interface IParallelStepBuilder<TData, TStepBody> where TStepBody : IStepBody { IParallelStepBuilder<TData, TStepBody> Do(Action<IWorkflowBuilder<TData>> builder); IStepBuilder<TData, Sequence> Join(); } 比起 ForEach、When、While、If,除了有 Do,还有 Join 方法。 对于其它节点类型来说,Do直接构建节点。 对于Parallel来说,Do收集任务,最终需要Join来构建节点和运行任务。 五,其它 写得长不好看,其它内容压缩一下。 数据传递和依赖注入 Workflow Core 支持对每个步骤点进行依赖注入。 .png) 支持数据持久化 Workflow Core 支持将构建的工作流存储到数据库中,以便以后再次调用。 支持 Sql Server、Mysql、SQLite、PostgreSQL、Redis、MongoDB、AWS、Azure、 Elasticsearch、RabbitMQ... .... 支持动态调用和动态生成工作流 你可以通过 C# 代码构建工作流,或者通过 Json、Yaml 动态构建工作流。 可以利用可视化设计器,将逻辑和任务生成配置文件,然后动态传递,使用 Workflow Core 动态创建工作流。 篇幅有限,不再赘述。 有兴趣请关注 Workflow Core:https://github.com/danielgerlag/workflow-core
代码下载地址 https://github.com/whuanle/txypx20190809 前提 创建子账号 打开 https://console.cloud.tencent.com/cam 创建子用户,设置子账号策略为 AdministratorAccess ,或者参考https://cloud.tencent.com/document/product/436/11714 ,添加访问 COS 的权限 记录子用户的 账号ID。 切换子用户登录。 添加 appid 密钥 打开 https://console.cloud.tencent.com/cam/capi 新建 API 密钥,记录下 appid,记录 SecretId 和 SecretKey。 记录 Region 打开 https://cloud.tencent.com/document/product/436/6224 可以查询可用区/地域的 region。 本教程使用 C# 开发。 一,SDK 和使用 腾讯云官网提供了 .NET 版本的 对象存储(COS) SDK,并提供使用教程,教程链接: https://cloud.tencent.com/document/product/436/32819 Nuget 搜索 Tencent.QCloud.Cos.Sdk 安装即可。 using 需引入 using COSXML; using COSXML.Auth; using COSXML.Model.Object; using COSXML.Model.Bucket; using COSXML.CosException; using COSXML.Utils; using COSXML.Model.Service; using COSXML.Transfer; using COSXML.Model; 根据官方的教程,很容易编写自己的软件: Ctrl + C ,然后 Ctrl + V 拷贝完毕,大概是这样的 using System; using System.Collections.Generic; using System.Text; using COSXML; using COSXML.Auth; using COSXML.Model.Object; using COSXML.Model.Bucket; using COSXML.CosException; using COSXML.Utils; using COSXML.Model.Service; using COSXML.Transfer; using COSXML.Model; namespace CosTest { public class CosClient { CosXmlServer cosXml; private readonly string _appid; private readonly string _region; public CosClient(string appid, string region) { _appid = appid; _region = region; //初始化 CosXmlConfig //string appid = "100011070645"; //string region = "ap-guangzhou"; CosXmlConfig config = new CosXmlConfig.Builder() .SetConnectionTimeoutMs(60000) .SetReadWriteTimeoutMs(40000) .IsHttps(true) .SetAppid(appid) .SetRegion(region) .SetDebugLog(true) .Build(); QCloudCredentialProvider cosCredentialProvider = null; string secretId = "AKID62jALHsVmpfHentPs9E6lBMJ2XnnsTzH"; //"云 API 密钥 SecretId"; string secretKey = "CC0c1DAtNdfS0IPIvISRFtIUSCUYTAgy"; //"云 API 密钥 SecretKey"; long durationSecond = 600; //secretKey 有效时长,单位为 秒 cosCredentialProvider = new DefaultQCloudCredentialProvider(secretId, secretKey, durationSecond); //初始化 CosXmlServer cosXml = new CosXmlServer(config, cosCredentialProvider); } public bool CreateBucket(string buketName) { try { string bucket = buketName + "-" + _appid; //存储桶名称 格式:BucketName-APPID PutBucketRequest request = new PutBucketRequest(buketName); //设置签名有效时长 request.SetSign(TimeUtils.GetCurrentTime(TimeUnit.SECONDS), 600); //执行请求 PutBucketResult result = cosXml.PutBucket(request); //请求成功 Console.WriteLine(result.GetResultInfo()); return true; } catch (COSXML.CosException.CosClientException clientEx) { //请求失败 Console.WriteLine("CosClientException: " + clientEx.Message); return false; } catch (COSXML.CosException.CosServerException serverEx) { //请求失败 Console.WriteLine("CosServerException: " + serverEx.GetInfo()); return false; } } public bool SelectBucket() { try { GetServiceRequest request = new GetServiceRequest(); //设置签名有效时长 request.SetSign(TimeUtils.GetCurrentTime(TimeUnit.SECONDS), 600); //执行请求 GetServiceResult result = cosXml.GetService(request); //请求成功 Console.WriteLine(result.GetResultInfo()); return true; } catch (COSXML.CosException.CosClientException clientEx) { //请求失败 Console.WriteLine("CosClientException: " + clientEx.Message); return false; } catch (COSXML.CosException.CosServerException serverEx) { //请求失败 Console.WriteLine("CosServerException: " + serverEx.GetInfo()); return false; } } public bool Upfile(string buketName, string key, string srcPath) { try { string bucket = buketName + "-" + _appid; //存储桶名称 格式:BucketName-APPID PutObjectRequest request = new PutObjectRequest(bucket, key, srcPath); //设置签名有效时长 request.SetSign(TimeUtils.GetCurrentTime(TimeUnit.SECONDS), 600); //设置进度回调 request.SetCosProgressCallback(delegate (long completed, long total) { Console.WriteLine(String.Format("progress = {0:##.##}%", completed * 100.0 / total)); }); //执行请求 PutObjectResult result = cosXml.PutObject(request); //请求成功 Console.WriteLine(result.GetResultInfo()); return true; } catch (COSXML.CosException.CosClientException clientEx) { //请求失败 Console.WriteLine("CosClientException: " + clientEx.Message); return false; } catch (COSXML.CosException.CosServerException serverEx) { //请求失败 Console.WriteLine("CosServerException: " + serverEx.GetInfo()); return false; } } public void UpBigFile(string buketName, string key, string srcPath) { string bucket = buketName + "-" + _appid; //存储桶名称 格式:BucketName-APPID TransferManager transferManager = new TransferManager(cosXml, new TransferConfig()); COSXMLUploadTask uploadTask = new COSXMLUploadTask(bucket, null, key); uploadTask.SetSrcPath(srcPath); uploadTask.progressCallback = delegate (long completed, long total) { Console.WriteLine(String.Format("progress = {0:##.##}%", completed * 100.0 / total)); }; uploadTask.successCallback = delegate (CosResult cosResult) { COSXML.Transfer.COSXMLUploadTask.UploadTaskResult result = cosResult as COSXML.Transfer.COSXMLUploadTask.UploadTaskResult; Console.WriteLine(result.GetResultInfo()); }; uploadTask.failCallback = delegate (CosClientException clientEx, CosServerException serverEx) { if (clientEx != null) { Console.WriteLine("CosClientException: " + clientEx.Message); } if (serverEx != null) { Console.WriteLine("CosServerException: " + serverEx.GetInfo()); } }; transferManager.Upload(uploadTask); } public class ResponseModel { public int Code { get; set; } public string Message { get; set; } public dynamic Data { get; set; } } public ResponseModel SelectObjectList(string buketName) { try { string bucket = buketName + "-" + _appid; //存储桶名称 格式:BucketName-APPID GetBucketRequest request = new GetBucketRequest(bucket); //设置签名有效时长 request.SetSign(TimeUtils.GetCurrentTime(TimeUnit.SECONDS), 600); //执行请求 GetBucketResult result = cosXml.GetBucket(request); //请求成功 Console.WriteLine(result.GetResultInfo()); return new ResponseModel { Code = 200, Data = result.GetResultInfo() }; } catch (COSXML.CosException.CosClientException clientEx) { //请求失败 Console.WriteLine("CosClientException: " + clientEx.Message); return new ResponseModel { Code = 200, Data = clientEx.Message }; } catch (COSXML.CosException.CosServerException serverEx) { //请求失败 Console.WriteLine("CosServerException: " + serverEx.GetInfo()); return new ResponseModel { Code = 200, Data = serverEx.Message }; } } public bool DownObject(string buketName, string key, string localDir, string localFileName) { try { string bucket = buketName + "-" + _appid; //存储桶名称 格式:BucketName-APPID GetObjectRequest request = new GetObjectRequest(bucket, key, localDir, localFileName); //设置签名有效时长 request.SetSign(TimeUtils.GetCurrentTime(TimeUnit.SECONDS), 600); //设置进度回调 request.SetCosProgressCallback(delegate (long completed, long total) { Console.WriteLine(String.Format("progress = {0:##.##}%", completed * 100.0 / total)); }); //执行请求 GetObjectResult result = cosXml.GetObject(request); //请求成功 Console.WriteLine(result.GetResultInfo()); return true; } catch (COSXML.CosException.CosClientException clientEx) { //请求失败 Console.WriteLine("CosClientException: " + clientEx.Message); return false; } catch (COSXML.CosException.CosServerException serverEx) { //请求失败 Console.WriteLine("CosServerException: " + serverEx.GetInfo()); return false; } } public bool DeleteObject(string buketName) { try { string bucket = buketName + "-" + _appid; //存储桶名称 格式:BucketName-APPID string key = "exampleobject"; //对象在存储桶中的位置,即称对象键. DeleteObjectRequest request = new DeleteObjectRequest(bucket, key); //设置签名有效时长 request.SetSign(TimeUtils.GetCurrentTime(TimeUnit.SECONDS), 600); //执行请求 DeleteObjectResult result = cosXml.DeleteObject(request); //请求成功 Console.WriteLine(result.GetResultInfo()); return true; } catch (COSXML.CosException.CosClientException clientEx) { //请求失败 Console.WriteLine("CosClientException: " + clientEx.Message); return false; } catch (COSXML.CosException.CosServerException serverEx) { //请求失败 Console.WriteLine("CosServerException: " + serverEx.GetInfo()); return false; } } } } 概览: 但是大神说,这样不好,程序要高内聚、低耦合,依赖与抽象而不依赖于具体实现。 那怎么办?只能修改一下。 二,优化 1,对象构建器 对象存储的 SDK 中,有三个重要的对象: CosXmlConfig 提供配置 SDK 接口。 QCloudCredentialProvider 提供设置密钥信息接口。 CosXmlServer 提供各种 COS API 服务接口。 但是,初始化和配置对象,过于麻烦,那么我们做一个对象构建器,实现函数式编程和链式语法。 /// <summary> /// 生成Cos客户端工具类 /// </summary> public class CosBuilder { private CosXmlServer cosXml; private string _appid; private string _region; private CosXmlConfig cosXmlConfig; private QCloudCredentialProvider cosCredentialProvider; public CosBuilder() { } public CosBuilder SetAccount(string appid, string region) { _appid = appid; _region = region; return this; } public CosBuilder SetCosXmlServer(int ConnectionTimeoutMs = 60000, int ReadWriteTimeoutMs = 40000, bool IsHttps = true, bool SetDebugLog = true) { cosXmlConfig = new CosXmlConfig.Builder() .SetConnectionTimeoutMs(ConnectionTimeoutMs) .SetReadWriteTimeoutMs(ReadWriteTimeoutMs) .IsHttps(true) .SetAppid(_appid) .SetRegion(_region) .SetDebugLog(true) .Build(); return this; } public CosBuilder SetSecret(string secretId, string secretKey, long durationSecond = 600) { cosCredentialProvider = new DefaultQCloudCredentialProvider(secretId, secretKey, durationSecond); return this; } public CosXmlServer Builder() { //初始化 CosXmlServer cosXml = new CosXmlServer(cosXmlConfig, cosCredentialProvider); return cosXml; } } 2,消息响应对象 为了统一返回消息,创建一个 Response Model 的类。 /// <summary> /// 消息响应 /// </summary> public class ResponseModel { public int Code { get; set; } public string Message { get; set; } public dynamic Data { get; set; } } 3,接口 实际上,访问 COS和控制,和存储桶内的操作,是可以分开的。 访问 COS 可以控制对象存储内的所有东西,但是每个存储桶又是一个独立的对象。 为了松耦合,我们拆分两个客户端,一个用来管理连接、存储桶等,一个用来管理存储桶内的操作。 接口如下: public interface ICosClient { // 创建存储桶 Task<ResponseModel> CreateBucket(string buketName); // 获取存储桶列表 Task<ResponseModel> SelectBucket(int tokenTome = 600); } public interface IBucketClient { // 上传文件 Task<ResponseModel> UpFile(string key, string srcPath); // 分块上传大文件 Task<ResponseModel> UpBigFile(string key, string srcPath, Action<long, long> progressCallback, Action<CosResult> successCallback); // 查询存储桶的文件列表 Task<ResponseModel> SelectObjectList(); // 下载文件 Task<ResponseModel> DownObject(string key, string localDir, string localFileName); // 删除文件 Task<ResponseModel> DeleteObject(string buketName); } 所有接口功能都使用异步实现。 4,COS 客户端 基架代码: public class CosClient : ICosClient { CosXmlServer _cosXml; private readonly string _appid; private readonly string _region; public CosClient(CosXmlServer cosXml) { _cosXml = cosXml; } } 创建存储桶: public async Task<ResponseModel> CreateBucket(string buketName) { try { string bucket = buketName + "-" + _appid; PutBucketRequest request = new PutBucketRequest(bucket); //设置签名有效时长 request.SetSign(TimeUtils.GetCurrentTime(TimeUnit.SECONDS), 600); //执行请求 PutBucketResult result = await Task.FromResult(_cosXml.PutBucket(request)); return new ResponseModel { Code = 200, Message = result.GetResultInfo() }; } catch (COSXML.CosException.CosClientException clientEx) { //请求失败 Console.WriteLine(); return new ResponseModel { Code = 0, Message = "CosClientException: " + clientEx.Message }; } catch (COSXML.CosException.CosServerException serverEx) { return new ResponseModel { Code = 200, Message = "CosServerException: " + serverEx.GetInfo() }; } } 查询存储桶列表 public async Task<ResponseModel> SelectBucket(int tokenTome = 600) { try { GetServiceRequest request = new GetServiceRequest(); //设置签名有效时长 request.SetSign(TimeUtils.GetCurrentTime(TimeUnit.SECONDS), tokenTome); //执行请求 GetServiceResult result = await Task.FromResult(_cosXml.GetService(request)); return new ResponseModel { Code = 200, Message = "Success", Data = result.GetResultInfo() }; } catch (COSXML.CosException.CosClientException clientEx) { return new ResponseModel { Code = 0, Message = "CosClientException: " + clientEx.Message }; } catch (CosServerException serverEx) { return new ResponseModel { Code = 0, Message = "CosServerException: " + serverEx.GetInfo() }; } } 5,存储桶操作客户端 基架代码如下: /// <summary> /// 存储桶客户端 /// </summary> public class BucketClient : IBucketClient { private readonly CosXmlServer _cosXml; private readonly string _buketName; private readonly string _appid; public BucketClient(CosXmlServer cosXml, string buketName, string appid) { _cosXml = cosXml; _buketName = buketName; _appid = appid; } } 上传对象 public async Task<ResponseModel> UpFile(string key, string srcPath) { try { string bucket = _buketName + "-" + _appid; //存储桶名称 格式:BucketName-APPID PutObjectRequest request = new PutObjectRequest(bucket, key, srcPath); //设置签名有效时长 request.SetSign(TimeUtils.GetCurrentTime(TimeUnit.SECONDS), 600); //设置进度回调 request.SetCosProgressCallback(delegate (long completed, long total) { Console.WriteLine(String.Format("progress = {0:##.##}%", completed * 100.0 / total)); }); //执行请求 PutObjectResult result = await Task.FromResult(_cosXml.PutObject(request)); return new ResponseModel { Code = 200, Message = result.GetResultInfo() }; } catch (CosClientException clientEx) { return new ResponseModel { Code = 0, Message = "CosClientException: " + clientEx.Message }; } catch (CosServerException serverEx) { return new ResponseModel { Code = 0, Message = "CosServerException: " + serverEx.GetInfo() }; } } 大文件分块上传 /// <summary> /// 上传大文件、分块上传 /// </summary> /// <param name="key"></param> /// <param name="srcPath"></param> /// <param name="progressCallback">委托,可用于显示分块信息</param> /// <param name="successCallback">委托,当任务成功时回调</param> /// <returns></returns> public async Task<ResponseModel> UpBigFile(string key, string srcPath, Action<long, long> progressCallback, Action<CosResult> successCallback) { ResponseModel responseModel = new ResponseModel(); string bucket = _buketName + "-" + _appid; //存储桶名称 格式:BucketName-APPID TransferManager transferManager = new TransferManager(_cosXml, new TransferConfig()); COSXMLUploadTask uploadTask = new COSXMLUploadTask(bucket, null, key); uploadTask.SetSrcPath(srcPath); uploadTask.progressCallback = delegate (long completed, long total) { progressCallback(completed, total); //Console.WriteLine(String.Format("progress = {0:##.##}%", completed * 100.0 / total)); }; uploadTask.successCallback = delegate (CosResult cosResult) { COSXMLUploadTask.UploadTaskResult result = cosResult as COSXMLUploadTask.UploadTaskResult; successCallback(cosResult); responseModel.Code = 200; responseModel.Message = result.GetResultInfo(); }; uploadTask.failCallback = delegate (CosClientException clientEx, CosServerException serverEx) { if (clientEx != null) { responseModel.Code = 0; responseModel.Message = clientEx.Message; } if (serverEx != null) { responseModel.Code = 0; responseModel.Message = "CosServerException: " + serverEx.GetInfo(); } }; await Task.Run(() => { transferManager.Upload(uploadTask); }); return responseModel; } 查询对象列表 public async Task<ResponseModel> SelectObjectList() { try { string bucket = _buketName + "-" + _appid; //存储桶名称 格式:BucketName-APPID GetBucketRequest request = new GetBucketRequest(bucket); //设置签名有效时长 request.SetSign(TimeUtils.GetCurrentTime(TimeUnit.SECONDS), 600); //执行请求 GetBucketResult result = await Task.FromResult(_cosXml.GetBucket(request)); return new ResponseModel { Code = 200, Data = result.GetResultInfo() }; } catch (CosClientException clientEx) { return new ResponseModel { Code = 0, Data = "CosClientException: " + clientEx.Message }; } catch (CosServerException serverEx) { return new ResponseModel { Code = 0, Data = "CosServerException: " + serverEx.GetInfo() }; } } 下载对象 、删除对象 public async Task<ResponseModel> DownObject(string key, string localDir, string localFileName) { try { string bucket = _buketName + "-" + _appid; //存储桶名称 格式:BucketName-APPID GetObjectRequest request = new GetObjectRequest(bucket, key, localDir, localFileName); //设置签名有效时长 request.SetSign(TimeUtils.GetCurrentTime(TimeUnit.SECONDS), 600); //设置进度回调 request.SetCosProgressCallback(delegate (long completed, long total) { Console.WriteLine(String.Format("progress = {0:##.##}%", completed * 100.0 / total)); }); //执行请求 GetObjectResult result = await Task.FromResult(_cosXml.GetObject(request)); return new ResponseModel { Code = 200, Message = result.GetResultInfo() }; } catch (CosClientException clientEx) { return new ResponseModel { Code = 0, Message = "CosClientException: " + clientEx.Message }; } catch (CosServerException serverEx) { return new ResponseModel { Code = 0, Message = serverEx.GetInfo() }; } } public async Task<ResponseModel> DeleteObject(string buketName) { try { string bucket = _buketName + "-" + _appid; //存储桶名称 格式:BucketName-APPID string key = "exampleobject"; //对象在存储桶中的位置,即称对象键. DeleteObjectRequest request = new DeleteObjectRequest(bucket, key); //设置签名有效时长 request.SetSign(TimeUtils.GetCurrentTime(TimeUnit.SECONDS), 600); //执行请求 DeleteObjectResult result = await Task.FromResult(_cosXml.DeleteObject(request)); return new ResponseModel { Code = 200, Message = result.GetResultInfo() }; } catch (CosClientException clientEx) { return new ResponseModel { Code = 0, Message = "CosClientException: " + clientEx.Message }; } catch (CosServerException serverEx) { return new ResponseModel { Code = 0, Message = "CosServerException: " + serverEx.GetInfo() }; } } 以上代码将官方示例作了优化。 依赖于抽象、实现接口; 松耦合; 异步网络流、异步文件流; 统一返回信息; 增加匿名委托作方法参数; 增加灵活性。 三,使用封装好的代码 1,初始化 官网示例文档: .png) 使用修改后的代码,你可以这样初始化: var cosClient = new CosBuilder() .SetAccount("1252707544", " ap-guangzhou") .SetCosXmlServer() .SetSecret("AKIDEZohU6AmkeNTVPmedw65Ws462rVxLIpG", "Sn1iFi182jMARcheQ1gYIsGSROE5rSwG") .Builder(); 简单测试代码 static async Task Main(string[] args) { // 构建一个 CoxXmlServer 对象 var cosClient = new CosBuilder() .SetAccount("125x707xx4", "ap-guangzhou") .SetCosXmlServer() .SetSecret("AKIxxxxxxedw65Ws462rVxLIpG", "Sn1iFi1xxxxxwG") .Builder(); // 创建Cos连接客户端 ICosClient client = new CosClient(cosClient, "125xx0xx44"); // 创建一个存储桶 var result = await client.CreateBucket("fsdgerer"); Console.WriteLine("处理结果:" + result.Message); // 查询存储桶列表 var c = await client.SelectBucket(); Console.WriteLine(c.Message + c.Data); Console.ReadKey(); } 运行结果(部分重要信息使用xx屏蔽): 处理结果:200 OK Connection: keep-alive Date: Fri, 09 Aug 2019 14:15:00 GMT Server: tencent-cos x-cos-request-id: xxxxxxxx= Content-Length: 0 Success200 OK Connection: keep-alive Date: Fri, 09 Aug 2019 14:15:01 GMT Server: tencent-cos x-cos-request-id: xxxxxxx= Content-Type: application/xml Content-Length: 479 {ListAllMyBuckets: {Owner: ID:qcs::cam::uin/1586xx146:uin/158xxx2146 DisPlayName:158x2146 } Buckets: {Bucket: Name:fsdgerer-125xxx7544 Location:ap-guangzhou CreateDate: } {Bucket: Name:work-1252xxx7544 Location:ap-guangzhou CreateDate: } } } 其它不再赘述
腾讯云-ASP.NET Core+Mysql+Jexus+CDN上云实践.md 请先开通云服务器 知识点: ASP.NET Core和 Entity Framework Core的使用 Linux 下 安装和配置 Mysql 数据库 通过实体生成数据库 简单 Linux 命令和 Shell 工具的使用 反向代理 腾讯云CDN的使用、配置服务器 SSL 证书 一,创建 CVM 服务器 云服务器 CVM 简介 云服务器(Cloud Virtual Machine,CVM)为您提供安全可靠的弹性计算服务。 只需几分钟,您就可以在云端获取和启用 CVM,来实现您的计算需求。随着业务需求的变化,您可以实时扩展或缩减计算资源。 CVM 支持按实际使用的资源计费,可以为您节约计算成本。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。 云数据库 MySQL 简介 腾讯云数据库 MySQL(TencentDB for MySQL)让用户可以轻松在云端部署、使用 MySQL 数据库。MySQL 是世界上最流行的开源关系数据库,通过云数据库 MySQL,您在几分钟内即可部署可扩展的 MySQL 数据库实例。不仅经济实惠,而且可以弹性调整硬件容量的大小而无需停机。云数据库 MySQL 提供备份回档、监控、快速扩容、数据传输等数据库运维全套解决方案,为您简化 IT 运维工作,让您能更加专注于业务发展。 在教程开始之前,你需要创建一台腾讯云CVM服务器,并到安全组开发全部端口(或者只增加开发3306端口)。 请选择 Ubuntu 18.04 或 Centos 7.5。 Ubuntu 比较适合初学者,建议服务器安装 Ubuntu。 Shell 工具: 免费正版的 XShell 软件下载地址: https://www.netsarang.com/zh/free-for-home-school/ 连接 Linux : 需要注意的是,Ubuntu 默认的账户名是 ubuntu。 在 XShell 界面你可以直接使用 Shell 命令连接 Linux: ssh ubuntu:7t@DfP3Ym3FwDoLM@129.204.104.20 格式 ssh [用户账号]:[密码]@[主机IP] 登录后,需要手动添加一个 root 用户: sudo passwd root 然后按要求输入两次密码即可。 使用 su 命令可以切换用户,例如 su root。 二,服务器安装 Mysql 由于云数据库只能在内网访问,如需公网访问需要购买弹性公网IP,同时为了学习在 Linux 下安装 Mysql ,这里先不使用云数据库,而是手动搭建一个 Mysql数据库。 1,安装、配置、使用Mysql数据库 下面操作需要使用 root 权限,请先切换 root 用户。 安装 Mysql apt install mysql-server 或 apt-get install mysql-server apt-get 与 apt 的区别是,apt-get 可以输出详细信息。 如果你的是 Centos 系统,则使用 yum install mysql-server 允许远程登录 Mysql vim /etc/mysql/mysql.conf.d/mysqld.cnf 找到 bind-address 然后按下 i 键,即可修改内容。请修改成 bind-address = 0.0.0.0 0.0.0.0 的意思是允许任何 IP 登录到此服务器的 Mysql。 修改完毕,按下 Esc 键,输入 :wq! 回车,即可保存并退出。 重启 Mysql 一次 service mysql restart 配置远程登录权限 mysql -u root -p 然后就会登录到 Mysql 中。 在 Mysql 数据库中创建一个 root 用户并设置密码为 123456: RANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY "123456" WITH GRANT OPTION; 创建数据库 create database testmvc; 然后退出 Mysql 管理 exit; 再重启一次 service mysql restart 管理 Mysql Navicat for MySQL 是一个用于管理 Mysql 数据库的商业软件。 下载地址 https://navicatformysql.en.softonic.com。 网上有很多在线管理 Mysql 的工具,请自行搜索。 三,创建用于测试的ASP.NET Core应用 请在 Visual Studio 2017/2019 上创建一个 ASP.NET Core 应用,选择 MVC(模型视图控制器)。 1,添加 Nuget 包 依次搜索并安装以下 Nuget 包 Microsoft.EntityFrameworkCore Microsoft.EntityFrameworkCore.Tools MySql.Data.EntityFrameworkCore MySql.Data.EntityFrameworkCore.Design 在网站应用的 Models 目录中,新建一个 Users.cs 的类,这个类被成为 模型类,因为这个类用于通过 EF Core 生成数据库表、映射数据模型, 因此又被称为 实体类(Entity)。 Users 类的代码: 这将生成一个用户信息的表(你可以随意增加属性) public class Users { public int Id { get; set; } public string UserName { get; set; } public int YearsOld { get; set; } [Phone] public string PhoneNumber{get;set;} [EmailAddress] public string Email { get; set; } } 2,创建数据库访问上下文 在 Models 目录创建一个 DatabaseContext.cs public class DatabaseContext : DbContext { public DatabaseContext(DbContextOptions<DatabaseContext> options) : base(options) { } public DbSet<Users> Users { get; set; } } 数据库上下文用于访问数据库、依赖注入。 3,配置服务 在 Startup.cs 文件找到 ConfigureServices方法,在里面增加 services.AddDbContext<DatabaseContext>(options => options.UseMySQL("server=129.204.104.20;user id=root;password=123456;database=test;charset=utf8;sslMode=None")); 请修改上面的连接字符串,改成自己的。 4,添加修改数据库表的控制器 使用自带的代码生成器,生成页面和数据库操作代码。 右键点击项目的 Controllers 文件夹 .png) .png) 配置网站端口 由于用于测试,因此只使用 http 访问。 public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseUrls("http://*:5001") .UseStartup<Startup>(); 在 Startup.cs 文件删除 app.UseHttpsRedirection(); 生成实体映射 Entity Framework Core 是一个 ORM 框架,通过 EF Core,我们可以直接通过代码即可操作数据库,而不必编写复杂的 SQL 语句。 打开 程序包管理控制台 生成映射: Add-Migration Migrations 生成数据库表: Update-Database 然后你会发现数据库多了一个 Users 表。 准备迁移到云服务器运行 修改 services.AddDbContext中的数据库ip,改成 127.0.0.1。因为一旦我们迁移到服务器,访问数据库就变成了本地访问,不需要填写公网 IP。 发布网站项目,把 publish 的内容打包,直接打包成 publish.zip。 四,服务器搭建环境 1,安装.NET Core SDK 全版本的 .NET Core SDK 下载地址 https://dotnet.microsoft.com/download 直接下载 ubuntu 版本的: https://dotnet.microsoft.com/download/linux-package-manager/ubuntu18-04/sdk-current 根据页面提示复制命令到服务器执行即可。 2,安装lrzsz 为了方便上传文件,安装 lrzsz apt install lrzsz 然后就可以直接拖文件上传到 Linux 了。 这里我们在临时目录存放网站。 mkdir /tmp/www cd /tmp/www 然后把网站压缩包上传到此目录。 解压文件 unzip publish.zip cd publish 3,安装 jexus 这里使用 jexus 作为Web服务器,托管应用、进行反向代理。 原因是 Jexus 轻量、简单。当然也可以用 Nginx 等。 安装: curl https://jexus.org/release/x64/install.sh|sudo sh 配置 jexus 我们配置 jexus,使得网站能够被外界访问。 /usr/jexus/siteconf touch testmvc vim testmvc 复制 以下内容粘贴上去 ###################### # Web Site: Default ######################################## port=80 root=/ /tmp/www/publish hosts=129.204.104.20 #OR your.com,*.your.com reproxy= / http://127.0.0.1:5001 # User=www-data # AspNet.Workers=2 # Set the number of asp.net worker processes. Defauit is 1. # addr=0.0.0.0 # CheckQuery=false NoLog=true AppHost={cmd=dotnet /tmp/www/publish/WebApplication2.dll; root=/tmp/www/publish; port=5001} 80 是外界访问网站的端口, 129.204.104.20 是公网 IP, reproxy= / http://127.0.0.1:5001 是反向代理 AppHost={cmd=dotnet /tmp/www/publish/WebApplication2.dll; root=/tmp/www/publish; port=5001} ,cmd 指要运行的命令,root 是目录位置,port是 网站端口。 配置说明: 通过 Jexus 来托管 ASP.NET Core ,使用 Web 服务器 启动应用。 配置反向代理,使得外界可以访问 ASP.NET Core 应用。 4,重启 Jexus、启动网站 cd /usr/jexus/ ./jws restart 5,打开网站、体验数据库操作 如果你需要使用 Nginx 配置反向代理请参考笔者的另一篇文章 https://www.cnblogs.com/whuanle/p/10228893.html 五,腾讯云 CDN 和 网站 SSL 配置 这里就不进行实际代码操作,大概演示一下思路。 你可以在 https://console.cloud.tencent.com/ssl 为你的网站申请免费的 SSL 证书。 在 https://console.cloud.tencent.com/cdn 为你的网站配置加速功能。 .png) 将你的域名使用 CNAME 解析到腾讯云 CDN 加速域名上吗,即可配置加速、缓存功能。 .png) 配置网站SSL的思路和解决方案 配置网站 SSL,有个问题是强制跳转到 https。 用户访问 http 时,如何强制跳转到 https ? 肯定不能在网站或服务器上配置,一是配置免费、流量大容易影响性能,二是会降低访问速度。 我们可以在腾讯云 CDN 里面配置 强制 HTTPS 功能。 这样可以在域名解析后直接强制跳转 https,而不必对 Web 服务器或 网站做任何修改。 .png) 但是hi,这样也带来了问题。 因为使用 CDN 加速和缓存功能,需要使用 CNAME。 大多数域名,会把主域名设置企业邮箱,那么,无法在使用 CNAME 解析,但可以使用 A 记录。 因此,假如你有个 域名为 qq.com,并且配置了邮箱系统 xxx@.qq.com,那么你将无法使用 qq.com 去配置 CDN 记录。 通用解决方法: qq.com 不用了,qq.com 用来配置邮箱。 www.qq.com 以及其它前缀作为网站的域名访问,访问www.qq.com,会强制跳转https。 但是不能浪费 qq.com,我们可以把任何访问qq.com的客户端,重定向到 www.qq.com。 也就是说, qq.com 不会作为网站域名被访问,访问 qq.com 会被跳转到 www.qq.com。 可以验证。 浏览器输入 qq.com,访问。发现跳转到 https://www.qq.com 再输入 www.qq.com,访问,发现跳转到 https://www.qq.com 使用命令进行测试: root@VM-14-73-ubuntu:/tmp# curl qq.com <html> <head><title>302 Found</title></head> <body bgcolor="white"> <center><h1>302 Found</h1></center> <hr><center>nginx/1.6.0</center> </body> </html> root@VM-14-73-ubuntu:/tmp# curl www.qq.com <html> <head><title>302 Found</title></head> <body bgcolor="white"> <center><h1>302 Found</h1></center> <hr><center>nginx</center> </body> </html> 解释: 访问qq.com和www.qq.com都会进行302重定向。 问题 有个问题是, 你试试访问:https://qq.com 你会发现,访问不了。不信你试试。 腾讯的 qq.com 域名竟然不能使用 https:// 访问。。。 当然不清楚 qq.com 的解析是怎么设置的。 我这里只是举例。很多网站访问 xx.com,都会跳转到 www.xx.com。
跟同事合作前后端分离项目,自己对 WebApi 的很多知识不够全,虽说不必要学全栈,可是也要了解基础知识,才能合理设计接口、API,方便与前端交接。 晚上回到宿舍后,对 WebApi 的知识查漏补缺,主要补充了 WebAPi 的一些方法、特性等如何与前端契合,如何利用工具测试 API 、Axios 请求接口。 本文主要写 WebApi 前端请求数据到 API 、后端返回处理结果,不涉及登录、跨域请求、前端 UI 等。(难一点我不会了。。。看张队的公众号,篇篇都看不懂。。。) 前提:会一点点 VUE、会一点 Axios、会一点点 Asp.net Core。 工具:Visual Studio 2019(或者其它版本) + Visual Studio Code + Swagger +Postman 由于 Visual Studio 2019 写 ASP.NET Core 页面时,没有 Vue 的智能提示,所以需要使用 VSCode 来写前端页面。 一. 微软WebApi特性 绑定源[FromBody] 请求正文[FromForm] 请求正文中的表单数据[FromHeader] 请求标头[FromQuery] 请求查询字符串参数[FromRoute] 当前请求中的路由数据[FromServices] 作为操作参数插入的请求服务来一张 Postman 的图片: HTTP 请求中,会携带很多参数,这些参数可以在前端设置,例如表单、Header、文件、Cookie、Session、Token等。 那么,上面的表格正是用来从 HTTP 请求中获取数据的 “方法” 或者说 “手段”。HttpCentext 等对象不在本文讨论范围。 Microsoft.AspNetCore.Mvc 命名空间提供很多用于配置Web API 控制器的行为和操作方法的属性: 特性 说明[Route] 指定控制器或操作的 URL 模式。[Bind] 指定要包含的前缀和属性,以进行模型绑定。[Consumes] 指定某个操作接受的数据类型。[Produces] 指定某个操作返回的数据类型。[HttpGet] 标识支持 HTTP GET 方法的操作。[HttpPost] 标识支持 HTTP POST 方法的操作。... ... ... ... ... ...WebApi 应用 首先创建一个 Asp.Net Core MVC 应用,然后在 Controllers 目录添加一个 API 控制器 DefaultController.cs。(这里不创建 WebApi 而是 创建 MVC,通过 MVC 创建 API 控制器)。 创建后默认代码: [Route("api/[controller]")][ApiController]public class DefaultController : ControllerBase{} 安装 Swagger在 Nuget 中搜索 Swashbuckle.AspNetCore,或打开 程序包管理器控制台 -> 程序包管理器控制台 ,输入以下命令进行安装 Install-Package Swashbuckle.AspNetCore -Version 5.0.0-rc2打开 Startup 文件,添加引用 using Microsoft.OpenApi.Models;在 ConfigureServices 中添加服务,双引号文字内容随便改。 services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); }); 添加中间件 app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseCookiePolicy(); // 添加下面的内容 app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); }); 访问 /swagger 可以访问到 Swagger 的 UI 界面。 为了便于查看输出和固定端口,打开 Progarm,cs ,修改内容 public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseUrls("https://*:5123") .UseStartup<Startup>(); 1562163847(1) 不要使用 IIS 托管运行。 注意:本文全部使用 [HttpPost] ;全局使用 JsonResult 作为返回类型。 二. 数据绑定与获取1,默认不加直接写 action,不使用特性 [HttpPost("aaa")] public async Task<JsonResult> AAA(int? a, int? b) { if (a == null || b == null) return new JsonResult(new { code = 0, result = "aaaaaaaa" }); return new JsonResult(new { code = 2000, result = a + "|" + b }); } 打开 https://localhost:5123/swagger/index.html 查看 UI 界面 1562138960(1) 也就是说,创建一个 action ,什么都不加,默认是 query。 通过 Postman 提交数据、测试接口 1562139085(1) 对于 Query 的 action 来说, axios 的写法 postaaa: function () { axios.post('/api/default/aaa?a=111&b=222' ) .then(res => { console.log(res.data) console.log(res.data.code) console.log(res.data.result) }) .catch(err => { console.error(err); }) } 在网上查找资料时,发现有人说通过 params 添加数据也可以,不过笔者测试,貌似不行。 讲道理,别人可以,为啥我不行。。。 axios 代码: postaaa: function () { axios.post('/api/default/aaa', { params: { a: 123, b: 234 } } ) .then(res => { console.log(res.data) console.log(res.data.code) console.log(res.data.result) }) .catch(err => { console.error(err); }) } 包括下面的,都试过了,不行。 axios.post('/api/default/aaa', { a:1234, b:1122 } axios.post('/api/default/aaa', { data:{ a:1234, b:1122 } } 把 [HttpPost] 改成 [HttpGet] ,则可以使用 axios.post('/api/default/aaa', { params: { a: 123, b: 234 } } ... ... 提示: ... ... .then(res => { console.log(res.data) console.log(res.data.code) console.log(res.data.result) }) .catch(err => { console.error(err); }) .then 当请求成功时触发,请求失败时触发 catch 。res 是请求成功后返回的信息,res.data 是请求成功后服务器返回的信息。即是 action 处理数据后返回的信息。 在浏览器,按下 F12 打开控制台,点击 Console ,每次请求后,这里会打印请求结果和数据。 2, [FromBody]官方文档解释:请求正文。[FromBody] 针对复杂类型参数进行推断。 [FromBody] 不适用于具有特殊含义的任何复杂的内置类型,如 IFormCollection 和 CancellationToken。 绑定源推理代码将忽略这些特殊类型。 算了,看得一头雾水,手动实际试试。 刚刚开始的时候,我这样使用: public async Task<JsonResult> BBB([FromBody]int? a, [FromBody]int? b) 结果编译时就报错,提示只能使用一个 [FromBody],于是改成 [HttpPost("bbb")] public async Task<JsonResult> BBB([FromBody]int? a, int? b) { if (a == null || b == null) return new JsonResult(new { code = 0, result = "aaaaaaaa" }); return new JsonResult(new { code = 2000, result = a + "|" + b }); } 打开 Swagger UI 界面,刷新一下 1562139375(1) 从图片中发现,只有 b,没有 a,而且右上角有下拉框,说明了加 [FromBody] 是 json 上传。 那么说明 [FromBody] 修饰得应当是对象,而不是 字段。 修改程序如下: // 增加一个类型 public class AppJson { public int? a { get; set; } public int? b { get; set; } } [HttpPost("bbb")] public async Task<JsonResult> BBB([FromBody]AppJson ss) { if (ss.a == null || ss.b == null) return new JsonResult(new { code = 0, result = "aaaaaaaa" }); return new JsonResult(new { code = 2000, result = ss.a + "|" + ss.b }); } 再看看微软的文档:[FromBody] 针对复杂类型参数进行推断。,这下可理解了。。。 即是不应该对 int、string 等类型使用 [FromBody] ,而应该使用一个 复杂类型。 而且,一个 action 中,应该只能使用一个 [FromBody] 。 打开 Swagger 界面(有修改需要刷新下界面,下面不再赘述)。 1562139627(1) 这样才是我们要的结果嘛,前端提交的是 Json 对象。 用 Postman 测试下 1562139749(1) 证实了猜想,嘿嘿,嘿嘿嘿。 前端提交的是 Json 对象,遵循 Json 的格式规范,那么 [FromBody] 把它转为 Object 对象。 前端 axios 写法: methods: { postaaa: function () { axios.post('/api/default/bbb', { "a": 4444, "b": 5555 }) .then(res => { console.log(res.data) console.log(res.data.code) console.log(res.data.result) }) .catch(err => { console.error(err); }) } } 3, [FromForm] [HttpPost("ccc")] public async Task<JsonResult> CCC([FromForm]int? a, [FromForm]int? b) { if (a == null || b == null) return new JsonResult(new { code = 0, result = "aaaaaaaa" }); return new JsonResult(new { code = 200, result = a + "|" + b }); } 当然,这样写也行,多个字段或者对象都可以 [HttpPost("ccc")] public async Task<JsonResult> CCC([FromForm]AppJson ss) { if (ss.a == null || ss.b == null) return new JsonResult(new { code = 0, result = "aaaaaaaa" }); return new JsonResult(new { code = 200, result = ss.a + "|" + ss.b }); } 1562141896(1) 根据提示,使用 Postman 进行测试 0187f3234bb69a6eea144a3a16ee5d8 事实上,这样也行 ↓ form-data 和 x-www.form-urlencoded 都是键值形式,文件 form-data 可以用来上传文件。具体的区别请自行查询。 df8a45f6c95af394ae2fdbb269f9ae2 axios 写法(把 Content-Type 字段修改成 form-data 或 x-www.form-urlencoded ) postccc: function () { let fromData = new FormData() fromData.append('a', 111) fromData.append('b', 222) axios.post('/api/default/ccc', fromData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }) .then(res => { console.log(res.data) console.log(res.data.code) console.log(res.data.result) }) .catch(err => { console.error(err); }) } 4, [FromHeader][FromHeader] 不以表单形式上传,而是跟随 Header 传递参数。 [HttpPost("ddd")] public async Task<JsonResult> DDD([FromHeader]int? a, [FromHeader]int? b) { if (a == null || b == null) return new JsonResult(new { code = 0, result = "aaaaaaaa" }); return new JsonResult(new { code = 200, result = a + "|" + b }); } 1562144122(1) axios 写法 postddd: function () { axios.post('/api/default/ddd', {}, { headers: { a: 123, b: 133 } }) .then(res => { console.log(res.data) console.log(res.data.code) console.log(res.data.result) }) .catch(err => { console.error(err); }) } 需要注意的是,headers 的参数,必须放在第三位。没有要提交的表单数据,第二位就使用 {} 代替。 params 跟随 url 一起在第一位,json 或表单数据等参数放在第二位,headers 放在第三位。 由于笔者对前端不太熟,这里有说错,麻烦大神评论指出啦。 5, [FromQuery]前面已经说了,Action 参数不加修饰,默认就是 [FromQuery] ,参考第一小节。 有个地方需要记住, Action 参数不加修饰。默认就是 [FromQuery] ,有时几种参数并在一起放到 Action 里,会忽略掉,调试时忘记了,造成麻烦。 6, [FromRoute]获取路由规则,这个跟前端上传的参数无关;跟 URL 可以说有关,又可以说无关。 [HttpPost("fff")] public async Task<JsonResult> FFFxxx(int a,int b, [FromRoute]string controller, [FromRoute]string action) { // 这里就不处理 a和 b了 return new JsonResult(new { code = 200, result = controller+"|"+action }); } 1562147096 [FromRoute] 是根据路由模板获取的,上面 API 的两个参数和路由模板的名称是对应的: [FromRoute]string controller, [FromRoute]string action app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); 当然,还可以加个 [FromRoute]int? id [FromRoute] 和 [FromQuery] 区别 以此 URL 为例 https://localhost:5123/api/Default/fff?a=111&b=22 Route 会查到 controller = Default ,action = FFFxxx 。查询到的是代码里的真实名称。 Query 会查询到 a = 111 和 b = 22 那么,如果路由规则里,不在 URL 里出现呢? [HttpPost("/ooo")] public async Task<JsonResult> FFFooo(int a, int b, [FromRoute]string controller, [FromRoute]string action) { // 这里就不处理 a和 b了 return new JsonResult(new { code = 200, result = controller + "|" + action }); } 那么,访问地址变成 https://localhost:5123/ooo 通过 Postman ,测试 df8a45f6c95af394ae2fdbb269f9ae2 说明了 [FromRoute] 获取的是代码里的 Controller 和 Action 名称,跟 URL 无关,根据测试结果推断跟路由表规则也无关。 7, [FromService]参考 https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/dependency-injection?view=aspnetcore-2.2 这个是与依赖注入容器有关,跟 URL 、路由等无关。 新建一个接口、一个类 public interface ITest { string GGG { get; } } public class Test : ITest { public string GGG { get { return DateTime.Now.ToLongDateString(); } } } 在 ConfigureServices 中 注入 services.AddSingleton<ITest, Test>(); 在 DefaultController 中,创建构造函数,然后 private readonly ITest ggg; public DefaultController(ITest ttt) { ggg = ttt; } 添加一个 API [HttpPost("ggg")] public async Task<JsonResult> GGG([FromServices]ITest t) { return new JsonResult(new { code = 200, result = t.GGG }); } 访问时,什么参数都不需要加,直接访问此 API 即可。 1562148774(1) [FromService] 跟后端的代码有关,跟 Controller 、Action 、URL、表单数据等无关。 小结: 特性可以几种放在一起用,不过尽量每个 API 的参数只使用一种特性。 优先取值 Form > Route > Query。 IFromFile 由于文件的上传,本文就不谈这个了。 关于数据绑定,更详细的内容请参考: https://docs.microsoft.com/zh-cn/aspnet/core/mvc/models/model-binding?view=aspnetcore-2.2 三. action 特性方法Microsoft.AspNetCore.Mvc 命名空间提供可用于配置 Web API 控制器的行为和操作方法的属性。 下表是针对于 Controller 或 Action 的特性. 特性 说明[Route] 指定控制器或操作的 URL 模式。[Bind] 指定要包含的前缀和属性,以进行模型绑定。[Consumes] 指定某个操作接受的数据类型。[Produces] 指定某个操作返回的数据类型。[HttpGet] 标识支持 HTTP GET 方法的操作。... ...下面使用这些属性来指定 Controller 或 Action 接受的 HTTP 方法、返回的数据类型或状态代码。 1, [Route]在微软文档中,把这个特性称为 属性路由 ,定义:属性路由使用一组属性将操作直接映射到路由模板。 请教了大神,大神解释说,ASP.NET Core 有路由规则表,路由表是全局性、唯一性的,在程序运行时,会把所有路由规则收集起来。 MVC 应用中设置路由的方法有多种,例如 app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); [Route("Home/Index")] public IActionResult Index() { return View(); } [Route("api/[controller]")] [ApiController] public class DefaultController : ControllerBase { } 路由是全局唯一的,可以通过不同形式使用,但是规则不能发生冲突,程序会在编译时把路由表收集起来。 根据笔者经验,发生冲突,应该就是在编译阶段直接报错了。(注:笔者不敢确定) 关于路由,请参考 : https://docs.microsoft.com/zh-cn/aspnet/core/mvc/controllers/routing?view=aspnetcore-2.2#token-replacement-in-route-templates-controller-action-area 2, [Bind]笔者知道这个是绑定模型的,但是对原理不太清楚。ASP.NET Core 自动生成的可读写的 Controller ,默认都是使用 [Bind] 来绑定数据。 文档定义:用于对复杂类型的模型绑定。 有下面几种相近的特性: [BindRequired][BindNever][Bind]微软文档提示:如果发布的表单数据是值的源,则这些属性会影响模型绑定。 就是说,上面的特性是针对类、接口等复杂类型(下面统称模型),对于 int、string 这些类型,可能出毛病。 [BindRequired] 、[BindNever] 只能应用于模型的属性,如 public class TestB { [BindNever] public int ID { get; set; } [BindRequired] public string Name { get; set; } } 但是 [BindRequired] 、[BindNever] 不在讨论范围内,这里只说 [Bind]。 [Bind] 用于类或方法(Controller、Action),指定模型绑定中应包含的模型属性。 在微软官方文档,对于[Bind] 的解释: [Bind] 属性可用于防止“创建”方案中的过多发布情况 。 由于排除的属性设置为 NULL 或默认值,而不是保持不变,因此它在编辑方案中无法很好地工作;因为 Bind 特性将清除未在 某个 参数中列出的字段中的任何以前存在的数据。一脸懵逼。 下面是我的踩坑过程,不感兴趣的话直接跳过吧。笔记笔记,记得当然是自己觉得要记的哈哈哈。 新建一个类 public class TestBind { public string A { get; set; } public string B { get; set; } public string C { get; set; } public string D { get; set; } public string E { get; set; } public string F { get; set; } public string G { get; set; } } 新建 API [HttpPost("hhh")] public async Task<JsonResult> HHH([Bind("A,B,C")] TestBind test) { if (ModelState.IsValid == true) return new JsonResult(test); return new JsonResult(new { Code = 0, Result = "验证不通过" }); } 15622028717670 使用 Postman 进行,测试,发现必须使用 Json 形式,才能访问到这个 Action ,其它方式会直接 返回 错误。 { "errors": { "": [ "A non-empty request body is required." ] }, "title": "One or more validation errors occurred.", "status": 400, "traceId": "0HLO03IFQFTQU:00000007" }通过两次 Postman 进行测试 15622032271015 15622037112944 经过测试,我猜想 ModelState.IsValid 跟模型里的验证规则有关系,跟 [Bind] 没关系(尽管用于测试的 TestB 类中没有写验证规则),因此不能使用 ModelState.IsValid 验证 [Bind] 是否符合规则。 Action 的参数:[Bind("A,B,C")] TestBind test,刚开始的时候我以为请求的数据中必须包含 A、B、C。 测试后发现不是。。。再认真看了文档 :因为 Bind 特性将清除未在 某个 参数中列出的字段中的任何以前存在的数据。 我修改一下: [HttpPost("hhh")] public async Task<JsonResult> HHH( string D, string E,[Bind("A,B,C")] TestBind test) { if (ModelState.IsValid == true) return new JsonResult(new { data1 = test, data2 = D, data3 = E }); return new JsonResult(new { Code = 0, Result = "验证不通过" }); } 参数变成了 string D, string E,[Bind("A,B,C")] TestBind test 使用 Swagger 进行测试: 15622043721294返回结果 { "data1": { "a": "string", "b": "string", "c": "string", "d": "string", "e": "string", "f": "string", "g": "string" }, "data2": null, "data3": null}改成 [HttpPost("hhh")] public async Task<JsonResult> HHH([Bind("A,B,C")] TestBind test, string J, string Q) { if (ModelState.IsValid == true) return new JsonResult(new { data1 = test, data2 = J, data3 = Q }); return new JsonResult(new { Code = 0, Result = "验证不通过" }); } 返回结果 { "data1": { "a": "string", "b": "string", "c": "string", "d": "string", "e": "string", "f": "string", "g": "string" }, "data2": null, "data3": null}文档中对 [Bind] 描述最多的是:防止过多发布。 通过上面的测试,首先肯定的是一个 Action 里,有多个参数 如 [Bind("A,B,C")] TestBind test, string D, string E string J, string Q。 注意,下面的结论是错的! 那么 D、E 因为于 除了 Test, J、Q就会无效,通过百度,[Bind] 修饰的 Action ,前端请求的数据只有 Test 里面的数据有效,其它 Query等形式一并上传的数据都会失效,防止黑客在提交数据时掺杂其它特殊参数。应该就是这样理解吧。 上面是一开始我的结论,直到多次测试,我发现是错的。 可是有一个地方不明白, [Bind("A,B,C")][Bind("A,B,C,D,E,F,G")]这两者的区别是是什么。还是没搞清楚。 突然想到 Query,当字段没有使用特性修饰时,默认为 Query 。 最终踩坑测试代码 模型类 public class TestBind { public string A { get; set; } public string B { get; set; } public string C { get; set; } public string D { get; set; } public string E { get; set; } public string F { get; set; } public string G { get; set; } } Action [HttpPost("hhh")] public async Task<JsonResult> HHH( string A, string B, string E, string F, string G, [Bind("A,B,C,D")] TestBind test, string C, string D, string J, string Q) { if (ModelState.IsValid == true) return new JsonResult(new { data1 = test, dataA = A, dataB = B, dataC = C, dataD = D, dataE = E, dataF = F, dataG = G, dataJ = J, dataQ = Q }); return new JsonResult(new { Code = 0, Result = "验证不通过" }); } Swagger 测试 15622129564070 Postman 测试 15622126298494 15622126493775 { "data1": { "a": "111", "b": "111", "c": "111", "d": "111", "e": "111", "f": "111", "g": "111" }, "dataA": "222", "dataB": "222", "dataC": "222", "dataD": "222", "dataE": "222", "dataF": "222", "dataG": "222", "dataJ": "222", "dataQ": "222" }再在 Swagger 或 Postman ,换着法子尝试各种不同组合的输入。 我懵逼了。试了半天试不出什么。 实在不理解 [Bind] 里,“防止过多发布” 是什么意思 [Bind("A,B,C")][Bind("A,B,C,D,E,F,G")]这两者的区别是是什么。还是没搞清楚。算了,不踩了。 我再到 stackoverflow 提问题,地址 https://stackoverflow.com/questions/56884876/asp-net-core-bind-how-to-use-it/56885153#56885153 获得一个回答: What's the difference between [Bind("A,B,C")] and [Bind("A,B,C,D,E,F,G")]? The former tells the model binder to include only the properties of TestBind named A, B and C. The latter tells the model binder to include those same properties plus D, E, F and G. Are you testing by posting data for all properties of your model? You should notice that the values you post for the excluded properties are not bound. 算了,嘿嘿,测试不出来,放弃。 3, [Consumes]、[Produces] [Consumes("application/json")] [Produces("application/json")] [Produces("application/xml")] [Produces("text/html")] ... ... 目前只了解到 [Consumes]、[Produces] 是筛选器,用来表示 Controller 或 Action 所能接受的数据类型。大概就是像下面这样使用: [Consumes("application/json")] [Produces("application/json")] public class DefaultTestController : ControllerBase { } 但是如何实际应用呢?我找了很久,都没有找到什么结果。在 stackoverflow 找到一个回答: https://stackoverflow.com/questions/41462509/adding-the-produces-filter-globally-in-asp-net-core 4, [HttpGet]、[HttpPost]、[HttpDelete]、[HttpPut]修饰 Action ,用来标识这个 Action 能够通过什么方式访问、访问名称。 例如: [Route("api/[controller]")] [ApiController] public class DefaultController : ControllerBase { [HttpPost("aaa")] public async Task<JsonResult> AAA(int? a, int? b) { if (a == null | b == null) return new JsonResult(new { code = 0, result = "aaaaaaaa" }); return new JsonResult(new { code = 200, result = a + "|" + b }); } } 访问地址 https://localhost:5123/api/Default/aaa 使用时,会受到 Controller 和 Action 路由的影响。 但 本身亦可控制路由。以上面的控制器为例 [HttpPost("aaa")] //相对路径访问地址 xxx:xxx/api/Default/aaa [HttpPost("/aaa")] //绝对路径访问地址 xxx:xxx/aaa 四,返回类型1, 查询备忘表Microsoft.AspNetCore.Mvc 命名空间中,包含控制 MVC 的各种操作方法和类型,笔者从命名空间中抽出与 MVC 或 API 返回类型有关的类型,生成表格: 类型 描述AcceptedAtActionResult An ActionResult that returns a Accepted (202) response with a Location header.AcceptedAtRouteResult An ActionResult that returns a Accepted (202) response with a Location header.AcceptedResult An ActionResult that returns an Accepted (202) response with a Location header.AcceptVerbsAttribute Specifies what HTTP methods an action supports.ActionResult A default implementation of IActionResult.ActionResult A type that wraps either an TValue instance or an ActionResult.BadRequestObjectResult An ObjectResult that when executed will produce a Bad Request (400) response.BadRequestResult A StatusCodeResult that when executed will produce a Bad Request (400) response.ChallengeResult An ActionResult that on execution invokes AuthenticationManager.ChallengeAsync.ConflictObjectResult An ObjectResult that when executed will produce a Conflict (409) response.ConflictResult A StatusCodeResult that when executed will produce a Conflict (409) response.ContentResult CreatedAtActionResult An ActionResult that returns a Created (201) response with a Location header.CreatedAtRouteResult An ActionResult that returns a Created (201) response with a Location header.CreatedResult An ActionResult that returns a Created (201) response with a Location header.EmptyResult Represents an ActionResult that when executed will do nothing.FileContentResult Represents an ActionResult that when executed will write a binary file to the response.FileResult Represents an ActionResult that when executed will write a file as the response.FileStreamResult Represents an ActionResult that when executed will write a file from a stream to the response.ForbidResult An ActionResult that on execution invokes AuthenticationManager.ForbidAsync.JsonResult An action result which formats the given object as JSON.LocalRedirectResult An ActionResult that returns a Found (302), Moved Permanently (301), Temporary Redirect (307), or Permanent Redirect (308) response with a Location header to the supplied local URL.NotFoundObjectResult An ObjectResult that when executed will produce a Not Found (404) response.NotFoundResult Represents an StatusCodeResult that when executed will produce a Not Found (404) response.OkObjectResult An ObjectResult that when executed performs content negotiation, formats the entity body, and will produce a Status200OK response if negotiation and formatting succeed.OkResult An StatusCodeResult that when executed will produce an empty Status200OK response.PartialViewResult Represents an ActionResult that renders a partial view to the response.PhysicalFileResult A FileResult on execution will write a file from disk to the response using mechanisms provided by the host.RedirectResult An ActionResult that returns a Found (302), Moved Permanently (301), Temporary Redirect (307), or Permanent Redirect (308) response with a Location header to the supplied URL.RedirectToActionResult An ActionResult that returns a Found (302), Moved Permanently (301), Temporary Redirect (307), or Permanent Redirect (308) response with a Location header. Targets a controller action.RedirectToPageResult An ActionResult that returns a Found (302) or Moved Permanently (301) response with a Location header. Targets a registered route.RedirectToRouteResult An ActionResult that returns a Found (302), Moved Permanently (301), Temporary Redirect (307), or Permanent Redirect (308) response with a Location header. Targets a registered route.SignInResult An ActionResult that on execution invokes AuthenticationManager.SignInAsync.SignOutResult An ActionResult that on execution invokes AuthenticationManager.SignOutAsync.StatusCodeResult Represents an ActionResult that when executed will produce an HTTP response with the given response status code.UnauthorizedObjectResult An ObjectResult that when executed will produce a Unauthorized (401) response.UnauthorizedResult Represents an UnauthorizedResult that when executed will produce an Unauthorized (401) response.UnprocessableEntityObjectResult An ObjectResult that when executed will produce a Unprocessable Entity (422) response.UnprocessableEntityResult A StatusCodeResult that when executed will produce a Unprocessable Entity (422) response.UnsupportedMediaTypeResult A StatusCodeResult that when executed will produce a UnsupportedMediaType (415) response.ViewComponentResult An IActionResult which renders a view component to the response.ViewResult Represents an ActionResult that renders a view to the response.VirtualFileResult A FileResult that on execution writes the file specified using a virtual path to the response using mechanisms provided by the host.留着写 WebApi 时查询备忘嘿嘿。 那些类型主要继承的两个接口: 类型 描述IActionResult Defines a contract that represents the result of an action method.IViewComponentResult Result type of a ViewComponent.注意的是,上面有些是抽象类,例如 FileResult,而 FileStreamResult 实现了 FileResult 。有些类是继承关系。 2, 返回的数据类型特定类型IActionResult 类型ActionResult 类型Action 的 return ,返回的数据类型必定是上面三种。 3, 直接返回基元或复杂数据类型[HttpGet]public IEnumerable Get(){ return _repository.GetProducts(); }4, IActionResult 类型响应状态码、Json、重定向、URL 跳转等,属于 IActionResult。 MVC 的 Controller 与 API 的 Controller 有很多相同的地方,亦有很多不同的地方。 API 的 Controller 继承 ControllerBase MVC 的 Controller 继承 Controller而 Controller 继承 Controller : ControllerBase, IActionFilter, IFilterMetadata, IAsyncActionFilter, IDisposableAPI 里的 Controller 是最原始的。 API 里的 返回类型需要实例化, new 一下; MVC 里的返回类型,“不需要实例化”。 当然,有些例如 FileResult 是抽象类,不能被实例化。 API: [HttpGet("returnaaa")] public async Task<IActionResult> ReturnAAA() { return new ViewResult(); return new JsonResult(new { code="test"}); return new RedirectToActionResult("DefaultController","ReturnAAA",""); return new NoContentResult("666"); return new NotFoundResult(); ... } MVC public async Task<IActionResult> Test() { return View(); return Json(new { code = "test" }); return RedirectToAction("DefaultController", "ReturnAAA", ""); return NoContent("666"); return NotFound(); ... } MVC 中,Action 默认是 [HttpGet],不加也可以被访问到; 而 API 的Action,不加 [Httpxxx],则默认不能被访问到。
CZGL.AliIoTClient 有7个委托事件,设置了默认的方法。你可以通过下面的方法使用默认的方法绑定到委托事件中。 public void UseDefaultEventHandler() 1)默认的方法 收到服务器下发属性设置时: public void Default_PubPropertyEventHandler(object sender, MqttMsgPublishEventArgs e) 收到服务器调用服务命令时: public void Default_PubServiceEventHandler(object sender, MqttMsgPublishEventArgs e) 收到普通Topic、上传数据的响应等其它情况: public void Default_PubCommonEventHandler(object sender, MqttMsgPublishEventArgs e) 收到服务器QOS为1的推送 public void Default_PubedEventHandler(object sender, MqttMsgPublishedEventArgs e) 当向服务器发送消息成功时: public void Default_SubedEventHandler(object sender, MqttMsgSubscribedEventArgs e) 向服务器推送消息失败时: public void Default_UnSubedEventHandler(object sender, MqttMsgUnsubscribedEventArgs e) 连接断开时 public void Default_ConnectionClosedEventHandler(object sender, System.EventArgs e) 2)方法的写法 不同的委托参数不同,有好几种类型,参考笔者的方法使用参数。 /// 一般的推送 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> public void Default_PubCommonEventHandler(object sender, MqttMsgPublishEventArgs e) { // handle message received string topic = e.Topic; string message = Encoding.ASCII.GetString(e.Message); Console.WriteLine("- - - - - - - - - - "); Console.WriteLine("get topic message,Date: " + DateTime.Now.ToLongTimeString()); Console.WriteLine("topic: " + topic); Console.WriteLine("get messgae :\n" + message); } /// <summary> /// 收到属性设置 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> public void Default_PubPropertyEventHandler(object sender, MqttMsgPublishEventArgs e) { // handle message received string topic = e.Topic; string message = Encoding.ASCII.GetString(e.Message); Console.WriteLine("- - - - - - - - - - "); Console.WriteLine("get topic message,Date: " + DateTime.Now.ToLongTimeString()); Console.WriteLine("topic: " + topic); Console.WriteLine("get messgae :\n" + message); } /// <summary> /// 收到服务调用 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> public void Default_PubServiceEventHandler(object sender, MqttMsgPublishEventArgs e) { // handle message received string topic = e.Topic; string message = Encoding.ASCII.GetString(e.Message); Console.WriteLine("- - - - - - - - - - "); Console.WriteLine("get topic message,Date: " + DateTime.Now.ToLongTimeString()); Console.WriteLine("topic: " + topic); Console.WriteLine("get messgae :\n" + message); } /// <summary> /// 收到服务器QOS为1的推送 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> public void Default_PubedEventHandler(object sender, MqttMsgPublishedEventArgs e) { Console.WriteLine("- - - - - - - - - - "); Console.WriteLine("published,Date: " + DateTime.Now.ToLongTimeString()); Console.WriteLine("MessageId: " + e.MessageId + " Is Published: " + e.IsPublished); } /// <summary> /// 向服务器推送成功 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> public void Default_SubedEventHandler(object sender, MqttMsgSubscribedEventArgs e) { Console.WriteLine("- - - - - - - - - - "); Console.WriteLine("Sub topic,Date: " + DateTime.Now.ToLongTimeString()); Console.WriteLine("MessageId: " + e.MessageId); Console.WriteLine("List of granted QOS Levels: " + Encoding.UTF8.GetString(e.GrantedQoSLevels)); } /// <summary> /// 推送失败 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> public void Default_UnSubedEventHandler(object sender, MqttMsgUnsubscribedEventArgs e) { Console.WriteLine("- - - - - - - - - - "); Console.WriteLine("Sub topic error,Date: " + DateTime.Now.ToLongTimeString()); Console.WriteLine("MessageId: " + e.MessageId); } /// <summary> /// 连接发生异常,断网等 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> public void Default_ConnectionClosedEventHandler(object sender, EventArgs e) { Console.WriteLine("- - - - - - - - - - "); Console.WriteLine("Connect Closed error,Date: " + DateTime.Now.ToLongTimeString()); }
CZGL.AliIoTClient 里设置了 7 个委托事件,在程序的不不同生命周期触发。 1)7个委托事件 /// <summary> /// 服务器属性设置 /// </summary> public PublishPropertyEventHandler PubPropertyEventHandler; /// <summary> /// 服务调用 /// </summary> public PublishServiceEventHandler PubServiceEventHandler; /// <summary> /// 收到其它Topic时触发 /// </summary> public PublishCommonEventHandler PubCommonEventHandler; /// <summary> /// 当 QOS=1或2时,收到订阅触发 /// </summary> public uPLibrary.Networking.M2Mqtt.MqttClient.MqttMsgPublishedEventHandler PubedEventHandler; /// <summary> /// 向服务器发布 Topic 时 /// </summary> public uPLibrary.Networking.M2Mqtt.MqttClient.MqttMsgSubscribedEventHandler SubedEventHandler; /// <summary> /// 向服务器发布 Topic 失败时 /// </summary> public uPLibrary.Networking.M2Mqtt.MqttClient.MqttMsgUnsubscribedEventHandler UnSubedEventHandler; /// <summary> /// 断开连接时 /// </summary> public uPLibrary.Networking.M2Mqtt.MqttClient.ConnectionClosedEventHandler ConnectionClosedEventHandler; 使用方法 public static void TestEvent(object sender, MqttMsgPublishEventArgs e) { { // handle message received string topic = e.Topic; string message = Encoding.ASCII.GetString(e.Message); Console.WriteLine("- - - - - - - - - - "); Console.WriteLine("get topic message,Date: " + DateTime.Now.ToLongTimeString()); Console.WriteLine("topic: " + topic); Console.WriteLine("get messgae :\n" + message); } client.PubPropertyEventHandler += TestEvent; 这里是旧版本的文档,可以参考一下。 https://www.cnblogs.com/whuanle/p/10786991.html
预先在设备编写好相应的代码,这些代码能够处理一个或多个任务,即为 服务 ,一个服务包含一个或多个任务。 CZGL.AliIoTClient 中,服务器下发服务调用指令不需要设置,默认服务器可以直接下发命令而不需要经过客户端同意。 虽然客户端能够直接接收服务器的服务调用指令,但是必须要设置相应的方法绑定到委托中,方能处理指令。 下面会举例说明如何使用服务调用: 设备定义一个服务,这个服务是定时爆炸。 当收到服务器下发的爆炸命令和定时爆炸时间,就会开始任务 爆炸后,返回爆炸结果 服务器下发命令给设备,让它爆炸 服务器不管设备怎么爆炸,也不等你爆炸 设备爆炸完了,去告诉服务器设备以及完成爆炸 1)设置服务调用 打开阿里云物联网控制台,点击自定义功能,按以下定义。 服务定义: 输入参数: .png "设置输入参数") 输出参数: 2)定义服务说明 定义的服务,有输入参数、输出参数,是指服务器向设备下发命令调用服务,这个服务需要的输入参数、调用这个服务后返回的参数。 这个是相对设备来说的,服务器调用设备的服务,给设备传入数据(输入参数),然后设备处理完毕,返回结果(输出参数)。 里面有异步、同步方法,使用异步方法,服务器不需要等待设备响应结果,可以直接返回。 同步方法,服务器必须等待响应结果,一直没有获得结果则会超时报错。 使用的基础测试代码如下(请替换 DeviceOptions 的值): static AliIoTClientJson client; static void Main(string[] args) { // 创建客户端 client = new AliIoTClientJson(new DeviceOptions { ProductKey = "a1A6VVt72pD", DeviceName = "json", DeviceSecret = "7QrjTptQYCdepjbQvSoqkuygic2051zM", RegionId = "cn-shanghai" }); client.OpenPropertyDownPost(); // 设置要订阅的Topic、运行接收内容的Topic string[] topics = new string[] { client.CombineHeadTopic("get") }; // 使用默认事件 client.UseDefaultEventHandler(); // 连接服务器 client.ConnectIoT(topics, null, 60); Console.ReadKey(); } 运行控制台程序,打开阿里云物联网控制台,在线调试,找到服务,选择 机器自动爆炸 在输入框输入以下内容: { "timee":10 } 点击发送,再查看控制台输出。 {"method":"thing.service.bom","id":"670534570","params":{"timee":10},"version":"1.0.0"} 根据定义和要求,实际上收到服务调用命令后,需要进行处理并且返回响应结果。 3)编写接收模型和响应模型 收到的消息是 Alink json ,你可以通过 CZGL.AliIoTClient 转换成相应的对象。 同样,也需要将相应的对象转成 Alink json 上传到服务器中,作为响应。 编写接收模型:里面只有一个很简单的参数 timee ,这个就是在控制台定义的 传入参数。 public class Bom { public string method { get { return "thing.service.bom"; } set { } } public string id { get; set; } public string version { get { return "1.0.0"; } set { } } public Params @params { get; set; } public class Params { public int timee { get; set; } } public Bom() { @params = new Params(); } } 编写响应模型: public class ReBom { public string id { get; set; } public int code { get; set; } public Data data { get; set; } public class Data { public int isbom { get; set; } } public ReBom() { data = new Data(); } } 4)定义委托方法 CZGL.AliIoTClient 中,有个 PubServiceEventHandler 委托,当收到服务器的服务调用命令时,这个委托就会触发响应的事件。 所以,我们编写一个处理命令的方法,另外自定义一个委托方法。 服务调用方法: /// <summary> /// 服务调用方法 /// </summary> /// <param name="timee"></param> /// <returns></returns> public static bool BomServer(int timee) { Console.WriteLine($"我将在 {timee} 秒后爆炸"); /* * 其它代码 * */ // 返回处理结果,已经爆炸 return true; } 编写委托方法: 当收到服务调用命令时,应当如何处理。 /// <summary> /// 收到服务调用 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> public static void Service_Bom_EventHandler(object sender, MqttMsgPublishEventArgs e) { // handle message received string topic = e.Topic; string message = Encoding.ASCII.GetString(e.Message); if (topic.Contains("bom")) { // 将收到的服务调用数据转为对象 var model = client.Thing_Service_JsonToObject<Bom>(message); // 获取里面的timee参数,将这个参数传入给方法进行处理 var re = BomServer(model.@params.timee); // 设置要返回的信息 var reModel = new ReBom() { code = 200, id = model.id }; reModel.data.isbom = 1; // 对服务器做出响应,返回处理结果 client.Thing_Service_Identifier_Reply<ReBom>(reModel, "bom", false); } } 如果你有多个服务,那么在上面给出的示例方法 Service_Bom_EventHandler 中,加个判断即可。总之,这些是自定义的,灵活性很高,CZGL.AliIoTClient 负责将你的数据处理以及进行上传下达,但是如何处理指令,需要你编写相应的处理方法。 5)绑定到委托中 在连接服务器前,绑定到委托中 client.PubServiceEventHandler += Service_Bom_EventHandler; // 连接服务器 client.ConnectIoT(topics, null, 60); 就这么一句代码而已。 当然, CZGL.AliIoTClient 默认有一些方法,在收到服务器消息时触发,这些不会影响到你的委托方法。 如果你不需要,去除即可。 // 使用默认事件 // client.UseDefaultEventHandler(); client.PubServiceEventHandler += Service_Bom_EventHandler; // 连接服务器 client.ConnectIoT(topics, null, 60); 至此,完整的代码如下: class Program { static AliIoTClientJson client; static void Main(string[] args) { // 创建客户端 client = new AliIoTClientJson(new DeviceOptions { ProductKey = "a1A6VVt72pD", DeviceName = "json", DeviceSecret = "7QrjTptQYCdepjbQvSoqkuygic2051zM", RegionId = "cn-shanghai" }); client.OpenPropertyDownPost(); // 设置要订阅的Topic、运行接收内容的Topic string[] topics = new string[] { client.CombineHeadTopic("get") }; // 使用默认事件 client.UseDefaultEventHandler(); client.PubServiceEventHandler += Service_Bom_EventHandler; // 连接服务器 client.ConnectIoT(topics, null, 60); Console.ReadKey(); } /// <summary> /// 收到服务调用 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> public static void Service_Bom_EventHandler(object sender, MqttMsgPublishEventArgs e) { // handle message received string topic = e.Topic; string message = Encoding.ASCII.GetString(e.Message); if (topic.Contains("bom")) { // 将收到的服务调用数据转为对象 var model = client.Thing_Service_JsonToObject<Bom>(message); // 获取里面的timee参数,将这个参数传入给方法进行处理 var re = BomServer(model.@params.timee); // 设置要返回的信息 var reModel = new ReBom() { code = 200, id = model.id }; reModel.data.isbom = 1; // 对服务器做出响应,返回处理结果 client.Thing_Service_Identifier_Reply<ReBom>(reModel, "bom", false); } } public class Bom { public string method { get { return "thing.service.bom"; } set { } } public string id { get; set; } public string version { get { return "1.0.0"; } set { } } public Params @params { get; set; } public class Params { public int timee { get; set; } } public Bom() { @params = new Params(); } } public class ReBom { public string id { get; set; } public int code { get; set; } public Data data { get; set; } public class Data { public int isbom { get; set; } } public ReBom() { data = new Data(); } } /// <summary> /// 服务调用方法 /// </summary> /// <param name="timee"></param> /// <returns></returns> public static bool BomServer(int timee) { Console.WriteLine($"我将在 {timee} 秒后爆炸"); /* * 其它代码 * */ // 返回处理结果,已经爆炸 return true; } } 5)服务器下发服务调用指令 运行上面设置的程序,打开阿里云物联网控制台,进入 在线调试。 选择演示的产品、设备,选择上面定义的机器自动爆炸服务。 在文本框输入以下内容 { "timee":10 } 点击 发送指令 ,然后点一下 刷新。可以看到右侧出现了 设备上报数据、云端下发数据 再到设备中,在导航栏点击 服务调用,即可看到调用的服务、传入参数、输出参数等信息。 6)后续说明 上传响应时,响应的 id 必须与收到的指令 id 一致。
根据阿里云物联网普通的定义,事件上报有 信息、告警、故障三种类型,事件是设备上传的消息通知,应当及时处理。 1)定义事件 打开阿里云物联网控制台,进入产品,点击 自定义功能 ,添加一个事件。 2)上传事件的方法 CZGL.AliIoTClient 中,有四个上传事件的方法 public int Thing_Event_Post(string eventName, string content, [bool isToLower = True]) public int Thing_Event_Post(string eventName, string content, [bool isToLower = True], [System.Text.Encoding encoding = null]) public int Thing_Event_Post<TModel>(TModel model, string eventName, [bool isToLower = True]) public int Thing_Event_Post<TModel>(TModel model, string eventName, [bool isToLower = True], [System.Text.Encoding encoding = null]) eventName: 事件的名称,即标识符。 content: Alink json 内容isToLower:是否转为小写encoding: 自定义上传 Alink json 的编码model: 事件的模型 第一种方法需要手动编写好 json,然后通过方法上传。第二种方法在第一种方法的基础上允许自定义字符编码。第三种、第四种是传入模型,由 CZGL.AliIoTClient 处理好再上传。 3)编写事件模型 每次只能上传一个事件,一个事件对应一个 模型 或 Alink json。 在 CZGL.AliIoTClient 中,你每次上传一个事件时,都需要设置此事件的名称。 根据上面在阿里云物联网控制台定义的事件,编写模型。 预览要生成的 Alink json : { "id": "123", "version": "1.0", "params": { "value": { "temperature":100.1 }, "time": 1524448722000 }, "method": "thing.event.cpuerror.post" } 对应模型如下: public class Cpuerror { public Cpuerror() { @params = new Params(); } public string id { get { return DateTime.Now.Ticks.ToString(); } set { } } public string version { get { return "1.0"; } set { } } public Params @params { get; set; } public class Params { public Params() { value = new Value(); } public Value value { get; set; } public long time { get { return AliIoTClientJson.GetUnixTime(); } set { } } public class Value { public float temperature { get; set; } } } public string @method { get { return "thing.event.cpuerror.post"; } set { } } } 一个事件对应一个类,如果事件里有多个输出参数,则在 Value 里定义好。 { ... ... public class Value { public float temperature { get; set; } /* *定义多个输出参数 */ } ... ... } 上报事件: Cpuerror cpuerror = new Cpuerror(); cpuerror.@params.value.temperature = 100.1F; client.Thing_Event_Post<Cpuerror>(cpuerror, "cpuerror", false); 4)容错上传事件的 Alink json 可以 容错 ,这给我们编写代码时带来了方便。、 例如将上面上传事件的代码改一下: public class Cpuerror { public string name = "cpuerror"; public Cpuerror() { @params = new Params(); } public string id { get { return DateTime.Now.Ticks.ToString(); } set { } } public string version { get { return "1.0"; } set { } } public Params @params { get; set; } public class Params { public Params() { value = new Value(); } public Value value { get; set; } public long time { get { return AliIoTClientJson.GetUnixTime(); } set { } } public class Value { public float temperature { get; set; } } } public string @method { get { return $"thing.event.{name}.post"; } set { } } } Cpuerror cpuerror = new Cpuerror(); cpuerror.@params.value.temperature = 100.2F; client.Thing_Event_Post<Cpuerror>(cpuerror, cpuerror.name, false); 对于 消息ID 等是必不可少的,“可多不可少”,其它无关字段可以增加上去,不会影响到上传和使用,例如上面的例子增加了一个 name 属性。 5)补充说明
CZGL.AliIoTClientCZGL.AliIoTClient 是一个阿里云物联网 SDK,基于 .NET Standard 2.0 开发, 提供了对接阿里云物联网平台的类库,采用 MQTT 协议(M2MQTT.DotNetCore),可以快速开发物联网设备对接阿里云物联网的程序。包括通讯连接与加密、Topic推送和订阅、设备属性上传和设置、事件上报、服务调用、位置上传,支持透传和 Alink json 两种数据方式。 CZGL.AliIoTClient 支持跨平台跨CPU,能够在市场上多种嵌入式开发板上运行,做过大量测试,兼容性稳定性没问题。 documentation address:https://www.cnblogs.com/whuanle/ 笔者即将毕业,再有一个月就不是大学生啦~从此就是社会人。 干嘛用的阿里云物联网文档:https://help.aliyun.com/product/30520.html?spm=a2c4g.750001.list.208.54f37b13JmMfSk 阿里云提供的物联网 SDK 中,是没有 .NET Core 客户端的,所以笔者写了这个SDK。 SDK里面是按照文档来完成开发,里面也写了很多文档没有的功能。 那些一机一密、Topic 订阅和推送、属性上传、设置属性等一系列功能,SDK里面都给予支持。 文档也写得很详细了,有不懂的地方可以加笔者微信,注明 博客园 即可。 应该没有比我更详细的了吧~哈哈哈 文档里面的教程写得很详细了,把阿里云物联网怎么使用也说了~ 如何使用需要使用 CZGL.AliIoTClient ,在 Nuget 中搜索 CZGL.AliIoTClient 即可。 版本V1.1 相对于 V1.0 差异很大,基本是断层更新,此文档只适用于 v1.1 版本。 v1.0 可以查看 https://www.cnblogs.com/whuanle/p/10780220.html 一个逗逗的大学生
在上一章,格力空调温度 gree_temperature 设置了 读写 权限,因为空调的输出温度是可以被设置的。 CPU 温度是根据实际情况进行采集,而 空调温度 是遥控器设置的,服务器可以或者这个温度数据,同时也可以设置这个数据。 读写权限表示服务器有权限下发指令设置设备的属性。 注意的是,只有 读/读写 两种情况,没有 写 。 1)允许服务器设置设备属性 通讯就是 订阅/推送 、数据上下传输,本身没有这么复杂,无论 是属性、事件、服务,本质还是 Topic。 CZGL.AliIoTClient 作了细致的划分(快夸笔者),里面设置了很多参数,这样更自由、方便。 第3章已经说到如何打开和取消响应和其它功能,这里就不再解释。 以下为初始代码,将会在此基础上进行说明(请修改 DeviceOptions 的信息): static AliIoTClientJson client; static void Main(string[] args) { // 创建客户端 client = new AliIoTClientJson(new DeviceOptions { ProductKey = "a1A6VVt72pD", DeviceName = "json", DeviceSecret = "7QrjTptQYCdepjbQvSoqkuygic2051zM", RegionId = "cn-shanghai" }); client.OpenPropertyDownPost(); // 设置要订阅的Topic、运行接收内容的Topic string[] topics = new string[] { client.CombineHeadTopic("get") }; // 使用默认事件 client.UseDefaultEventHandler(); // 连接服务器 client.ConnectIoT(topics, null, 60); Console.ReadKey(); } 在 Console.ReadKey() 前加一行代码,运行服务器下发属性设置命令: client.OpenPropertyDownPost(); 运行程序。 2)下发命令 打开阿里云物联网控制台,进入 在线调试 ,然后选择前面已经创建的产品、设备。 你也可以直接打开: https://iot.console.aliyun.com/lk/monitor/debug进行如下设置: 调试设备:调试真实设备 功能: 格力空调温度(gree_temperature) 方法: 设置 然后将输入框里的数值改为 20.0 ,点击 发送指令 { "gree_temperature": 20 } 然后你可以看到控制台程序收到了指令: get topic message,Date: 16:52:55 topic: /sys/a1A6VVt72pD/json/thing/service/property/set get messgae : {"method":"thing.service.property.set","id":"666237842","params":{"gree_temperature":20},"version":"1.0.0"} 由于使用 Alink json,所以服务器下发的指令实际上是这样的: {"method":"thing.service.property.set","id":"666237842","params":{"gree_temperature":20},"version":"1.0.0"} 那为什么会输出其它东西呢?这是因为 CZGL.AliIoTClient 种设置了多个默认的事件方法, 它会输出收到的消息内容(message)等信息,你可以自定义方法来处理。 将 字符串格式化后: { "method": "thing.service.property.set", "id": "666237842", "params": { "gree_temperature": 20 }, "version": "1.0.0" } 但是目前只能收到服务器下发的命令,还没有写如何处理这些命令的方法,这需要你编写相应的方法绑定到委托事件中。 当收到属性消息时,会触发这些方法。如何设置,请参考后面的章节。 3)做出响应 当你收到服务器下发的指令时,你可以对这条 Topic 做出响应。 // 返回消息ID public int Thing_Property_set(CZGL.AliIoTClient.PropertyDownModel model, [bool isToLower = True]) public int Thing_Property_set(CZGL.AliIoTClient.PropertyDownModel model, [bool isToLower = True], [System.Text.Encoding encoding = null]) public int Thing_Property_set<TModel>(TModel model, [bool isToLower = True]) 实际上,不需要做出响应。。。如果有需要的话,可以自定义方法,在方法里面加上响应,绑定到委托里,自动响应。 如何设置,请参考后面的章节。
阿里云物联网的位置服务,并不是完全独立的功能。位置信息包含 二维、三维,位置数据来源于属性的上传。 1)添加二维位置数据 打开 数据分析 -> 空间数据可视化 -> 二维数据 -> 添加,为上面演示的设备添加位置,刷新时间为1秒。在产品功能中,打开功能定义 ,在 标准功能 里,添加功能。 选择 其它类型 ,里面搜索 位置 ,在出现的列表中选一个(前面那几个都可以)。 笔者选择的是: 标识符:GeoLocation 适用类别:CuttingMachine 位置上传要设置的信息: 注意注意,如果选择的标准属性跟上图的类型定义不一样,需要手动修改。要把上面的位置属性按上图来改,有一个地方不同,都会失败。 当然,也可以一开始就按图手动创建。 2)基础代码 上传位置数据,不需要做什么大操作,按照属性的上传方法上传即可。模型代码参考: public class TestModel { public string id { get { return DateTime.Now.Ticks.ToString(); } set { } } public string version { get { return "1.0"; } set { } } public Params @params { get; set; } public TestModel() { @params = new Params(); } public class Params { public geoLocation GeoLocation { get; set; } public class geoLocation { public Value value { get; set; } public long time { get { return AliIoTClientJson.GetUnixTime(); } set { } } public geoLocation() { value = new Value(); } public class Value { public double Longitude { get; set; } public double Latitude { get; set; } public double Altitude { get; set; } public int CoordinateSystem { get; set; } } } public Params() { GeoLocation = new geoLocation(); } } public string method { get { return "thing.event.property.post"; } set { } } } 整体代码参考:定义位置模型 -> 设置位置数据 -> 上传位置数据 class Program { static AliIoTClientJson client; static void Main(string[] args) { // 创建客户端 client = new AliIoTClientJson(new DeviceOptions { ProductKey = "a1A6VVt72pD", DeviceName = "json", DeviceSecret = "7QrjTptQYCdepjbQvSoqkuygic2051zM", RegionId = "cn-shanghai" }); client.OpenPropertyDownPost(); // 设置要订阅的Topic、运行接收内容的Topic string[] topics = new string[] { client.CombineHeadTopic("get") }; // 使用默认事件 client.UseDefaultEventHandler(); // 连接服务器 client.ConnectIoT(topics, null, 60); while (true) { ToServer(); Thread.Sleep(1000); } Console.ReadKey(); } public static void ToServer() { // 实例化模型 TestModel model = new TestModel(); // 设置属性值 // 经度 model.@params.GeoLocation.value.Longitude = 113.952981; // 纬度 model.@params.GeoLocation.value.Latitude = 22.539843; // 海拔 model.@params.GeoLocation.value.Altitude = 56; // 坐标系类型 model.@params.GeoLocation.value.CoordinateSystem = 2; // 上传属性数据 client.Thing_Property_Post<TestModel>(model, false); } public class TestModel { public string id { get { return DateTime.Now.Ticks.ToString(); } set { } } public string version { get { return "1.0"; } set { } } public Params @params { get; set; } public TestModel() { @params = new Params(); } public class Params { public geoLocation GeoLocation { get; set; } public class geoLocation { public Value value { get; set; } public long time { get { return AliIoTClientJson.GetUnixTime(); } set { } } public geoLocation() { value = new Value(); } public class Value { public double Longitude { get; set; } public double Latitude { get; set; } public double Altitude { get; set; } public int CoordinateSystem { get; set; } } } public Params() { GeoLocation = new geoLocation(); } } public string method { get { return "thing.event.property.post"; } set { } } } 上面使用的是模拟位置数据,请实际情况设置位置数据。 打开阿里云物联网控制台 -> 数据分析 -> 空间数据可视化 -> 二维数据 -> 演示产品 会看到定位到了深圳阿里云大厦(高新园地铁站附近)~~~
设备自身 CPU 温度、电源输入电压、内存使用率等,以及接入到设备的传感器如温度传感器、光敏传感器等,这些硬件的数据输出即是 属性 。 设备将这些硬件的数据上传到阿里云物联网平台,实时显示这些设备的状态和实测数据,这个过程是 上传设备属性 。 1)定义物模型 在阿里云物联网控制台,点击 产品 -> 功能定义 -> 添加自定义功能 填入一下内容: 功能类型:属性 功能名称: CPU温度 标识符: cpu_temperature 数据类型: float (单精度浮点型) 取值范围:0-120 步长: 0.1 单位: 摄氏度 / °C 读写类型:只读 再定义一个属性: 功能类型:属性 功能名称: 格力空调温度 标识符: gree_temperature 数据类型: float (单精度浮点型) 取值范围:0-35 步长: 0.1 单位: 摄氏度 / °C 读写类型:读写 注意的是,表示符是区分大小写的,相当于 C# 中的变量,笔者这里建议统一使用小写,具体原因后面说明。 注意:读写类型,一个只读、一个读写。 2)编写模型 前面说过, Alink json 是阿里云定义具有一定格式的 Json , 因此这些属性数据是以 Json 形式上传。在 C# 中,可以通过 类 快速生成 Json 。 | 参数 | 类型 | 说明 | |---|---|---| |id |string |消息ID号,在这个设备的生涯中,ID应当是唯一的。可以使用时间戳或guid||version| string| 协议版本号,目前协议版本号为1.0。固定 "1.0" 即可||params| Object| 属性数据,里面包含多个属性对象,每个属性对象包含上报时间(time)和上报的值(value)。||time |long| 属性上报时间。||value |object| 上报的属性值。||method |string| 固定取值 thing.event.property.post| 那么,我们要编写一个类,存储信息,然后转为 Alink json 上传到阿里云物联网服务器。在编写这个模型前,预览要生成的 Alink json : { "id": "123456789", "version": "1.0", "params": { "cpu_temperature": { "value": 58.6, "time": 1524448722000 }, "gree_temperature": { "value": 26.6, "time": 1524448722000 } }, "method": "thing.event.property.post" } 我们只需关注 params 部分的编写即可。 在控制台程序中,新建一个类 TestModel。 public class TestModel { public string id { get { return DateTime.Now.Ticks.ToString(); } set { } } public string version { get { return "1.0"; } set { } } public Params @params { get; set; } public class Params { /* * */ } public string @method { get { return "thing.event.property.post"; } set { } } } 这样定义后,我们使用时,只需定义 params 部分即可, id、version等,不需要自己动态取值,做重复劳动。 上面有个 @params ,这是因为 params 是 C# 的关键字,命名字段时为了取消冲突所以加个 @ 。 根据我们在阿里云物联网控制台定义的 属性 ,继续补充内容: public class TestModel { public string id { get { return DateTime.Now.Ticks.ToString(); } set { } } public string version { get { return "1.0"; } set { } } public Params @params { get; set; } public class Params { public Cpu_temperature cpu_temperature { get; set; } public Gree_temperature gree_temperature { get; set; } public class Cpu_temperature { public float value{ get; set; } public long time { get; set; } } public class Gree_temperature { public float value { get; set; } public long time { get; set; } } } public string @method { get { return "thing.event.property.post"; } set { } } } 问题是,这样写还不行,因为还没有给 TestModel 里的类进行实例化。 我们可以利用 构造函数 对里面的引用类型进行实例化,当然亦可编写依赖注入容器。。 public class TestModel { public string id { get { return DateTime.Now.Ticks.ToString(); } set { } } public string version { get { return "1.0"; } set { } } public Params @params { get; set; } public TestModel() { @params = new Params(); } public class Params { public Cpu_temperature cpu_temperature { get; set; } public Gree_temperature gree_temperature { get; set; } public Params() { cpu_temperature = new Cpu_temperature(); gree_temperature = new Gree_temperature(); } public class Cpu_temperature { public float value{ get; set; } public long time { get; set; } } public class Gree_temperature { public float value { get; set; } public long time { get; set; } } } public string method { get { return "thing.event.property.post"; } set { } } } 3)上传设备属性数据 编写控制台程序,引入 CZGL.AliIoTClient ,编写基础代码(请替换 DeviceOptions 的信息): static AliIoTClientJson client; static void Main(string[] args) { // 创建客户端 client = new AliIoTClientJson(new DeviceOptions { ProductKey = "a1A6VVt72pD", DeviceName = "json", DeviceSecret = "7QrjTptQYCdepjbQvSoqkuygic2051zM", RegionId = "cn-shanghai" }); // 设置要订阅的Topic、运行接收内容的Topic string[] topics = new string[] { client.CombineHeadTopic("get") }; // 使用默认事件 client.UseDefaultEventHandler(); // 连接服务器 client.ConnectIoT(topics,null,60); ToServer(); // 自定义方法,后面说明 Console.ReadKey(); } 再 Program 类中,编写一个方法用来收集属性数据、上传属性数据: public static void ToServer() { // 实例化模型 TestModel model = new TestModel(); // 设置属性值 model.@params.cpu_temperature.value = 56.5F; model.@params.cpu_temperature.time =AliIoTClientJson.GetUnixTime(); // 低碳环境、节约资源,从你我做起,夏天空调不低于 26° model.@params.gree_temperature.value=26.0F; model.@params.gree_temperature.time=AliIoTClientJson.GetUnixTime(); // 上传属性数据 client.Thing_Property_Post<TestModel>(model,false); } 启动控制台应用,在阿里云物联网控制台,打开设备,点击 运行状态 ,即可看到上传的属性数据。文章后面会详细说明 CZGL.AliIoTClient 关于属性上传的具体情况。 当然,这样的数据只是固定赋值的,这里只是演示,具体数据需要开发者采集。下面给出一些模拟数据的方法。 4)模拟数据 笔者编写了三个数据模拟方法: 不需要理会里面是怎么写的,仅是个模拟数据的工具而已,你也可以自己编写相应的模拟数据方法。 里面有四个参数,对应:原始值、最小值、最大值、波动范围。 /// <summary> /// 模拟数据 /// </summary> public static class DeviceSimulate { /// <summary> /// /// </summary> /// <param name="original">原始数据</param> /// <param name="range">波动范围</param> /// <param name="min">最小值</param> /// <param name="max">最大值</param> /// <returns></returns> public static int Property(ref int original, int min, int max, int range) { int num = (new Random()).Next(0, range + 1); bool addorrm; if (original + num > max || original > max) addorrm = false; else if (original < min || original - num < min) addorrm = true; else addorrm = ((new Random()).Next(1, 3) > 1) ? true : false; if (addorrm == true) original += num; else original -= num; return original; } public static float Property(ref float original, float min, float max, int range = 8) { original = float.Parse(original.ToString("#0.00")); float num = float.Parse(((new Random()).NextDouble() / range).ToString("#0.00")); bool addorrm; if (original + num > max || original > max) addorrm = false; else if (original < min || original - num < min) addorrm = true; else addorrm = ((new Random()).Next(1, 3) > 1) ? true : false; if (addorrm == true) original += num; else original -= num; original = float.Parse(original.ToString("#0.00")); return original; } public static double Property(ref double original, double min, double max, int range = 8) { original = double.Parse(original.ToString("#0.0000")); double num = double.Parse(((new Random()).NextDouble() / range).ToString("#0.0000")); bool addorrm; if (original + num > max || original > max) addorrm = false; else if (original < min || original - num < min) addorrm = true; else addorrm = ((new Random()).Next(1, 3) > 1) ? true : false; if (addorrm == true) original += num; else original -= num; original = double.Parse(original.ToString("#0.0000")); return original; } } int 模拟数据 range 是指每次生成 [0,range] 范围的增/减量, 例如 初始值 56 , range = 2 ,那么可能 56±0 或 56±1 或 56±2 ,是增还是减,是随机的。但是设置 min 、 max 后,最后生成的值会在此范围内波动。 float、double 模拟数据 对应 float、double,range 的值越大,波动范围越小。默认 range = 8,大概就是每次 0.1 的波动范围。 其中,float 小数保留两位, double 小数保留 4 位, 需要更高或减少小数位数,修改一下 ...ToString("#0.0000") 模拟属性数据 接下来我们模拟一下两个属性的数据。 在 Program 中定义两个变量存储 cpu 和 空调 数据。 static float cpu_temperature = 50.0F; static float gree_temperature = 26.0F; 修改 ToServer() 方法 public static void ToServer() { // 实例化模型 TestModel model = new TestModel(); // 设置属性值 model.@params.cpu_temperature.value = DeviceSimulate.Property(ref cpu_temperature, 40, 60, 8); model.@params.cpu_temperature.time = AliIoTClientJson.GetUnixTime(); // 低碳环境、节约资源,从你我做起,夏天空调不低于 26° model.@params.gree_temperature.value = DeviceSimulate.Property(ref gree_temperature, 40, 60, 8); ; model.@params.gree_temperature.time = AliIoTClientJson.GetUnixTime(); // 上传属性数据 client.Thing_Property_Post<TestModel>(model, false); } 在 Main() 方法里增加代码: // 定时上传数据 while (true) { ToServer(); Thread.Sleep(1000); } 至此,已经基本完成。 完整代码如下: class Program { static AliIoTClientJson client; static void Main(string[] args) { // 创建客户端 client = new AliIoTClientJson(new DeviceOptions { ProductKey = "a1A6VVt72pD", DeviceName = "json", DeviceSecret = "7QrjTptQYCdepjbQvSoqkuygic2051zM", RegionId = "cn-shanghai" }); // 设置要订阅的Topic、运行接收内容的Topic string[] topics = new string[] { client.CombineHeadTopic("get") }; // 使用默认事件 client.UseDefaultEventHandler(); // 连接服务器 client.ConnectIoT(topics, null, 60); // 定时上传数据 while (true) { ToServer(); Thread.Sleep(1000); } Console.ReadKey(); } static float cpu_temperature = 50.0F; static float gree_temperature = 26.0F; public static void ToServer() { // 实例化模型 TestModel model = new TestModel(); // 设置属性值 model.@params.cpu_temperature.value = DeviceSimulate.Property(ref cpu_temperature, 40, 60, 8); model.@params.cpu_temperature.time = AliIoTClientJson.GetUnixTime(); // 低碳环境、节约资源,从你我做起,夏天空调不低于 26° model.@params.gree_temperature.value = DeviceSimulate.Property(ref gree_temperature, 40, 60, 8); ; model.@params.gree_temperature.time = AliIoTClientJson.GetUnixTime(); // 上传属性数据 client.Thing_Property_Post<TestModel>(model, false); } /// <summary> /// 模拟数据 /// </summary> public static class DeviceSimulate { /// <summary> /// /// </summary> /// <param name="original">原始数据</param> /// <param name="range">波动范围</param> /// <param name="min">最小值</param> /// <param name="max">最大值</param> /// <returns></returns> public static int Property(ref int original, int min, int max, int range) { int num = (new Random()).Next(0, range + 1); bool addorrm; if (original + num > max || original > max) addorrm = false; else if (original < min || original - num < min) addorrm = true; else addorrm = ((new Random()).Next(1, 3) > 1) ? true : false; if (addorrm == true) original += num; else original -= num; return original; } public static float Property(ref float original, float min, float max, int range = 8) { original = float.Parse(original.ToString("#0.00")); float num = float.Parse(((new Random()).NextDouble() / range).ToString("#0.00")); bool addorrm; if (original + num > max || original > max) addorrm = false; else if (original < min || original - num < min) addorrm = true; else addorrm = ((new Random()).Next(1, 3) > 1) ? true : false; if (addorrm == true) original += num; else original -= num; original = float.Parse(original.ToString("#0.00")); return original; } public static double Property(ref double original, double min, double max, int range = 8) { original = double.Parse(original.ToString("#0.0000")); double num = double.Parse(((new Random()).NextDouble() / range).ToString("#0.0000")); bool addorrm; if (original + num > max || original > max) addorrm = false; else if (original < min || original - num < min) addorrm = true; else addorrm = ((new Random()).Next(1, 3) > 1) ? true : false; if (addorrm == true) original += num; else original -= num; original = double.Parse(original.ToString("#0.0000")); return original; } } } 运行控制台程序,然后打开阿里云物联网控制台,查看设备的运行状态,打开 自动刷新 ,查看数据变化。 如果你觉得每次波动得范围太大,可以把 range 改大一些,如果你觉得数据不稳定, 可以把 min - max 的范围改小一些,模拟的数据值将在此范围波动。 5)设备属性 - CZGL.AliIoTClient 首先要说明,产品创建前,需要设置为 Alinkjson/透传 产品, 因此 CZGL.AliIoTClient 设置了两个客户端类。 类名 说明 AliIoTClientJson 以Alink json形式上传数据 AliIoTClientBinary 以透传形式上传数据 这两个类,仅在 属性、事件、服务 三个功能中数据上传形式有差别,连接服务器、普通Topic等其它数据的使用是完全一致的。 一个产品只能定义一种上传数据的形式。 CZGL.AliIoTClient 中上传属性的方法(Alink json): // 不需要SDK处理任何中间过程,直接把数据上传。 // 那你需要先将数据存储到json中,在转成byte[],由SDK发送。 public int Thing_Property_Post(byte[] json) // 由SDK帮你发送原始的json,是否需要将json转为小写再发送,默认 true public int Thing_Property_Post(string json, [bool isToLwer = True]) // 设置要发送的json;是否转为小写;设置编码格式,为空则为UTF8 public int Thing_Property_Post(string json, [bool isToLwer = True], [System.Text.Encoding encoding = null]) // 直接传入模型,什么都不需要管,SDK转换后上传 public int Thing_Property_Post<TModel>(TModel model, [bool isToLower = True]) 获取 UNIX 时间: 由于阿里云要求上传的属性数据等,要带上 Unix 时间,所以笔者一并写在 CZGL.AliIoTClient 了。 public static long GetUnixTime() 使用示例参考上面的过程。 透传 如果你想使用透传,则使用 AliIoTClientBinary 类, // 设备上传属性--透传 public int Thing_Property_UpRaw(byte[] bytes) // 设备上传属性--透传,转为 Base 64位加密后上传 public int Thing_Property_UpRawToBase64(byte[] bytes, [System.Text.Encoding encoding = null]) 6)关于透传 透传以二进制报文形式上传,例如 0x020000007b00 ,这里是 16 进制,每两位一个字节。 如果是 2进制 ,则是 8位 一个字节。 透传需要在阿里云物联网控制台创建 透传 产品后,设置脚本,将透传数据 转为 Alink json。 透传数据是自定义的,以字节为单位,其中有5个字节为特定字节,以字节位进行拆分的。 记住,是以字节为单位。 透传数据格式标准: 字段 字节数 帧类型 1字节 请求ID 4字节 属性数据 N个字节 帧类型: 值(16进制) 说明 0x00 属性上报 0x01 属性设置 0x02 上报数据返回结果 0x03 属性设置设备返回结果 0xff 未知的命令 举例说明 很多人是直接把 10进制 或 16进制 直接转换成 2进制 。 例如 0x020000007b00,转为 2进制 :100000000000000000000000000111101100000000。但是这样是错误的。 以上面 cpu 和 空调温度 举例,要上传属性数据,帧类型为 0x00。 | 属性 | 10进制 | 16进制 | 2进制 | 划一下2进制 | | --------------- | ------ | -------- | ----------- | ----------- | | cpu_temperature | 56 | 38 | 00111000 | 00 11 10 00 | | gree_temperature | 26 | 1a | 00011010 | 00 01 10 10 | 应当这样拆分和设置值: 字节类转 字节数 16进制 2进制 进制表示 无 0x 无 帧类型 1字节 00 00000000 ID 4字节 00 00 00 7b 00000000 00000000 00000000 01111011 cpu_temperature 1 字节 38 00111000 gree_temperature 1 字节 1a 00011010 16进制数据: 0x000000007b381a 2进制数据: 00000000000000000000000000000000011110110011100000011010 将 16进制 或 2进制 的数据存储到 byte[] 变量中,切记要强制转换。 存储时,一个 byte 为一个字节,M个字节,则 byte[M]。 存储: 使用 16进制 存储透传数据,2进制弄不来的。 :joy: :joy: :joy: 有些同学非要用 2进制 存储,反正我是弄不来,用 二进制 数值 存储,这个触发我的知识盲区了。 示例(仅对 AliIoTClientBinary 客户端有效): // 存储透传数据 byte[] b = new byte[7]; b[0] = 0x00; b[1] = 0x00; b[2] = 0x00; b[3] = 0x00; b[4] = 0x7b; b[5] = 0x38; b[6] = 0x1a; // 上传透传数据 client.Thing_Property_UpRaw(b); 如果上报属性,要求 请输入二进制数据Base64编码后的字符串,可以使用 byte[] b = new byte[7]; b[0] = 0x00; b[1] = 0x00; b[2] = 0x00; b[3] = 0x00; b[4] = 0x7b; b[5] = 0x38; b[6] = 0x1a; // client.Thing_Property_UpRaw(b); client.Thing_Property_UpRawToBase64(b); 透传数据的坑很多,这里 CZGL.AliIoTClient 只提供如何处理数据和上传数据,云端的脚本解析请参考 https://help.aliyun.com/document_detail/114621.html?spm=a2c4g.11186623.2.13.209b65b9Q9z0Nx#concept-185365 7)后续说明 其实,每次上传服务器都会作出响应,CZGL.AliIoTClient 默认不接收这些响应信息。 你可以使用 OpenPropertyPostReply() 接收设备属性上传后服务器的响应,应当在连接服务器前使用此方法 使用 Close.PropertyPostReply() 取消接收设备属性上传后服务器的响应。 示例: // 。。。 client.ClosePropertyPostReply(); // 连接服务器 client.ConnectIoT(topics, null, 60); 上传属性数据,可以分开上传,不需要每次都要上传全部的属性。需要更新哪个属性,就上传这个属性。
CZGL.AliIoTClient 将 Topic 分为五种 分别是:普通Topic、属性上报、属性下发、事件上报、服务调用,除了普通 Topic,每种 Topic 都有消息的发送、响应。 普通 Topic ,消息发送或,根据 MQTT 协议,SDK 严格保证消息能够到达另一端。 设备推送属性、事件等数据到服务器,则服务器要响应, 服务器推送(下发)数据到设备,则设备要响应。 当然,这些响应可有可无,无实质的影响。 每种、每个 Topic 都有特定的 MQTT 通讯地址,这些地址已经在 CZGL.AliIoTClient 中自动生成,你仅需填写普通 Topic 的通讯地址即可。 1) 订阅 Topic 订阅 Topic 前,需要在阿里云物联网定义相应的 Topic 以及设置 订阅 权限, 普通 Topic ,使用 string[] 包含列表,然后在调用连接方法时作为参数传入,亦可在连接服务器后,添加需要的订阅。 普通 Topic 可以动态添加,属于 热订阅 。 使用方法: // 设置要订阅的Topic、运行接收内容的Topic string[] topics = new string[] { client.CombineHeadTopic("get"), "/a1xrkGSkb5R/mire/user/get1" }; client.ConnectIoT(topics,null,60); Topic 地址比较长,你可以记录 .../user/ 后的内容,使用 CombineHeadTopic() 可自动生成, client.CombineHeadTopic("get") 2)响应 当你使用属性、事件、服务功能,对服务器进行数据传输时,服务器会做出响应,你可以选择接收响应,也可以不接收响应。 CZGL.AliIoTClient 默认不接收服务器的响应信息。实际上这些响应信息一般只在调测时需要。 响应方法 说明 OpenEventPostReply() 接收上报事件后服务器的响应 OpenPropertyPostReply() 接收设备属性上传后服务器的响应 OpenServicePostRaw() 允许服务器调用服务 新版本已经取消这个设置 OpenPropertyDownPost() 允许服务器下发设置设备属性指令 注意,这些响应,必须在连接客户端前设置,并且在客户端已经连接后,不能再使用以上方法,否则会弹出异常。 已修复,你可以在程序的任意阶段打开接收功能,任意取消和打开。 查看这些功能是否打开: public CZGL.AliIoTClient.OpenTopic getOpenTopic() OpenTopic 具有 8 个属性,用于获取或设置是否打开某种功能的接收。 属性 说明 默认值 CommonTopic 是否接收普通Topic 固定为true,不能更改 PropertyUpRawReplyTopic 设备上传透传属性数据后服务器的响应 false PropertyPostReplyTopic 设备上传Alink json属性数据后服务器的响应 false PropertyDownRaw 服务器下发设置属性的命令,透传 false PropertyDownPost 服务器下发设置属性的命令,Alink json false EventUpRawReply 设备事件上报,接收服务器的响应,透传 false EventPostReply 设备事件上报,接收服务器的响应,Alink json false ServiceDownRaw 服务器调用服务,透传 false ServicePostRaw 服务器调用服务,Alink json false 由于区分 透传 和 Alink json,若是透传,设置上面的 Alink json 项,是无效的,反之亦然。 3)连接服务器后 在连接服务器后,你还可以增加要订阅的普通 Topic: public void TopicAdd(string[] topics, [byte[] QOS = null]) 示例: client.TopicAdd(new string[]{ client.CombineHeadTopic("get") }) 移除已经订阅的 Topic: public void TopicRemove(string[] topics) 在连接服务器后,可以取消接收服务器的响应信息,但是不能再重新接收。 方法 说明 CloseEventPostReply() 不再接收设备上传事件后服务器的响应 ClosePropertyPostReply() 不再接收设备上传属性后服务器的响应 CloseServicePostRaw() 不允许服务器下发调用设备服务的指令已删除此设置 ClosePropertyDownPost() 不允许服务器下发设置设备属性的指令 你可以在客户端连接服务器后,取消接收响应。但是,取消后,不能再订阅! 已修复,你可以在程序任意阶段取消后再打开接收。 4) Topic 说明 获取已经订阅的Topic列表: public string[] GetSubedList { get; } 示例: var topicList = client.GetSubedList; 服务器设置设备属性、调用服务等功能,由于采用 MQTT 通讯,是订阅/推送形式的,所以,无论什么功能的数据传输,本质还是Topic。 因此,那些设备属性上报、设置设备属性等,可以配置 Topic 属性。 CZGL.AliIoTClient 中,客户端可以订阅需要的 Topic,连接服务器后,服务器可以向所有 Topic 发送数据,但只有客户端指定订阅的 Topic,客户端才会接收到推送,不然无论在控制台怎么点击发送,客户端都是不会接收到的。 有个地方需要注意的是,对于普通Topic,是随客户端连接时的配置,决定服务器是否能够推送消息到客户端,动态的。 而属性、事件、服务这些功能,会保存上一次的配置。当你打开 client.OpenPropertyPostReply(); 接收属性上传到服务器的响应后,在程序删除 client.OpenPropertyPostReply(); 再次运行,依然会接收到响应,除非设置 client.ClosePropertyPostReply(); 设备上传数据到服务器,服务器会做出响应,当然,服务器设置设备属性、调用设备服务时,客户端也可以做出响应。将在后面的章节讲述。
1) 客户端连接 CZGL.AliIoTClient 中,有两个连接到阿里云物联网服务器的方法: public CZGL.AliIoTClient.ConnectCode ConnectIoT(string[] SubTopic, [byte[] QOS = null], [ushort keepAlivePeriod = 60]) public System.Threading.Tasks.Task<CZGL.AliIoTClient.ConnectCode> ConnectIoTAsync(string[] topics, [byte[] QOS = null], [ushort keepAlivePeriod = 60]) 参数说明及返回值: 参数名称 类型 说明 SubTopic string[] 要订阅的 Topic 列表,只有先订阅这个 Topic ,才会接收到服务器推送这个 Topic QOS byte[] 每个Topic都配置一个QOS,如果为空,将会为每个Topic设置 QOS=0x00,注意QOS只有0,1,2三种,因此使用byte最合适 keepAlivePeriod ushort 存活监测周期,MQTT 通讯要求每间隔一段时间,客户端及时反馈,以此证明客户端的存活,超过这个周期,服务器会认为客户端已经掉线。 返回值 ConnectCode 是连接返回状态的代码,枚举类型,即使因为密钥错误、网络断开等造成连接失败,不会触发异常,会返回状态码 每个 Topic 都有 一个 QOS 属性,SubTopic 的 长度和 QOS 的长度应当一致,索引位置也要对应。 QOS 的含义: QOS = 0 ,最多一次 QOS = 1,至少一次 QOS = 2,只有一次 ConnectCode: 当客户端尝试与服务器建立连接,可能成功也可能失败,此时会返回具体的连接状态信息,ConnectCode 枚举如下: 枚举名称 枚举值 说明 conn_accepted 0x00 连接成功 conn_refused_prot_vers 0x01 协议版本 conn_refused_ident_rejected 0x02 认证被拒绝 conn_refused_server_unavailable x03 服务器403/404等 conn_refused_username_password 0x04 账号密码错误 conn_refused_not_authorized 0x05 没有授权 unknown_error 0x06 其它未知错误 示例: var code = client.ConnectIoT(topics, null, 60); Console.WriteLine("连接状态:" + code); 2)断开连接 public bool ConnectIoTClose() 断开连接,会彻底释放 AliIoTClientJson 对象,而不仅仅是离线,如需重新连接,请重新 new 一个对象; 示例: client.ConnectIoT(topics,null,60); 3) 查看状态 查看客户端是否与服务器保持连接: public bool isConnected { get; } 示例: Console.WriteLine("是否与服务器连接保持连接:" + client.isConnected);
1) 阿里云物联网 阿里云物联网支持多种通讯协议,CZGL.AliIoTClient 使用了 MQTT 通讯协议,通讯库为 M2MQTT 。 阿里云物联网数据传输有两种数据传输方式,分别是 透传 和 Alink json,两种方式只在属性读/写、事件上报、服务调用这四种 Topic 上有差异, 其它连接通讯、普通 Topic、响应等,无差别。建议使用 Alink json 方式上传下发数据。 传输形式 本质 说明 Alink json json 需按照阿里云物联网文档配置 json 透传 二进制 在使用属性、事件和服务功能时,数据为二进制,有具体的传输位要求 CZGL.AliIoTClient 支持 Alink json 和 透传,SDK 中有两个客户端类, 类 说明 AliIoTClientJson 以Alink json为传输形式的客户端 AliIoTClientBinary: 以二进制为传输形式的客户端 两者很大程度上是一致的,仅在属性事件服务方面的数据传输形式有差异。因此后面主要以 AliIoTClientJson 来说明。 2) 连接到阿里云IOT 2.1) 创建客户端 在创建客户端类时,需要传入 DeviceOptions 对象,需要预先在阿里云物联网控制台,复制设备的密钥等信息,填入到 DeviceOptions 中。 示例: AliIoTClientJson client = new AliIoTClientJson(new DeviceOptions { ProductKey = "a1xrkGSkb5R", DeviceName = "mire", DeviceSecret = "CqGMkOBDiKJfrOWp1evLZC2O6fsMtEXw", RegionId = "cn-shanghai" }); 2.2) 设置要订阅的 Topic 连接客户端前,应当设置需要订阅的普通 Topic 以及配置是否接收属性设置命令、服务调用和响应等 Topic 。 普通topic 设置要订阅的普通 Topic: string[] topics = new string[] { ... , ... , ... }; 要求填写 Topic 完整的长度的 URI ,可到控制台中查看。例如 "/a1xrkGSkb5R/dockertest/user/update/error" 如果你不想这么麻烦,可以使用 string[] topics = new string[] { client.CombineHeadTopic("get") }; 只需输入 Topic 的 /user/ 后面的内容即可,AliIoTClientJson.CombineHeadTopic() 会为你生成完整的 Topic 地址。 例如需要订阅 "/a1xrkGSkb5R/dockertest/user/update/error" string[] topics = new string[] { client.CombineHeadTopic("update/error") }; 除了普通的 Topic 外,还要设备上传各种数据、接收服务器的响应、服务器设置设备属性、服务器调用设备服务等,这些将在后面章节介绍。 3) 设置默认事件 你希望在收到消息时,程序需要做点什么?编写相应的方法,绑定到委托事件中,当条件符合时,这些方法将会被触发。 在本章,使用 CZGL.AliIoTClient 预设置的默认委托方法,后面章节将会详细说明如何自定义方法。 使用默认事件: client.UseDefaultEventHandler(); 4) 客户端连接 已经做好了连接前的配置工作,现在连接到阿里云物联网。 CZGL.AliIoTClient 中,有三个关于连接的方法: 方法 说明 AliIoTClientJson.ConnectIoT(string[], byte[], ushort) 连接到阿里云物联网服务器 AliIoTClientJson.ConnectIoTAsync(string[], byte[], ushort) 使用异步方法连接到阿里云物联网服务器 AliIoTClientJson.ConnectIoTClose() 关闭、释放客户端 5) 示例 在阿里云物联网控制台新建一个产品,再在此产品下新建一个设备,其它功能不需要添加。记录下新建设备的密钥等信息。 在 Visual Studio 中,新建一个 .NET Core 控制台应用,在 Nuget 中找到 CZGL.AliIoTClient 并添加。 控制台代码如下: // 创建客户端 client = new AliIoTClientJson(new DeviceOptions { ProductKey = "a1xrkGSkb5R", DeviceName = "mire", DeviceSecret = "CqGMkOBDiKJfrOWp1evLZC2O6fsMtEXw", RegionId = "cn-shanghai" }); // 设置要订阅的Topic、运行接收内容的Topic string[] topics = new string[] { client.CombineHeadTopic("get") }; // 使用默认事件 client.UseDefaultEventHandler(); // 连接服务器 client.ConnectIoT(topics,null,60); Console.ReadKey(); 打开阿里云物联网控制台,刷新设备列表,即可看到设备在线。 6) 下发数据 一个新建的产品,有几个默认的 Topic ,我们不必作其它修改,就目前来说,可以使用默认的 Topic 做示范。 运行上面已经编写好的控制台程序,不要关闭。 打开阿里云物联网控制台,打开相应的设备,在设备的 Topic列表 里面找到 .../user/get 这个 Topic, 例如: /a1xrkGSkb5R/dockertest/user/get 点击 发布消息 ,然后输入要发送的内容,最后查看控制台是否收到下发的消息。 由于使用了 CZGL.AliIoTClient 中,默认的事件方法,因此除了消息内容,也会输出这条 Topic 消息的一些属性信息。 7) 上传数据 设备的 Topic 列表,有个 .../user/update ,例如 /a1xrkGSkb5R/dockertest/user/update 。这个 Topic 允许客户端上传数据,下面将说明客户端如何上传数据到阿里云物联网服务器。 上传普通 Topic 的方法: I. 上传 byte public int CommonToServer(string topicName, byte[] content) 摘要: 此种方式以 byte[] 形式上传数据,注意 byte[] 的进制 参数:topicName: Topic 的完整名称,可使用 CombineHeadTopic() 方法获取content: 消息内容 返回结果: 消息 ID II. 普通字符串 public int CommonToServer(string topicName, string content) 摘要: 普通方式推送 Topic 到服务器,直接上传字符串 返回结果: 消息 ID III. 其它上传方法 还要其它几个方法,放到一起说明。 public int CommonToServer(string topicName, string content, [System.Text.Encoding encoding = null]) 说明:上传数据到指定 Topic ,指定字符串的编码格式。阿里云物联网默认使用 UTF8。 CZGL.AliIoTClient 也默认使用 UTF8 作为数据的编码,可以自定义上传字符串的编码。 一般不需要改,不然中文字符串会乱码。 public int CommonToServerBase64(string topicName, string content) 说明:传入字符串后,会先进行 Base64 编码,然后再上传。 public int CommonToServerBase64(string topicName, string content, [System.Text.Encoding encoding = null]) 说明:传入字符串后,指定字符串的编码,然后进行 Base64 编码后上传。 8) 创建Topic 你可以在阿里云物联网控制台打开某个产品,在产品里新建一个或多个 Topic ,设定这个 Topic 具有 订阅/发布 权限。 然后修改程序试试是否正常上传、下发数据。
目录 一,安装宝塔面板(V 6.8) 二,使用宝塔安装 Docker,配置阿里云容器服务 三,安装 Rancher (Server) 四,管理 Rancher、添加集群 五,添加 Rancher 应用、服务,与 Nginx 六,ASP.NET Core 应用部署 七,相关文章推荐前言: 本文使用 Centos 7.x 进行操作,Rancher 官方推荐使用 Ubuntu。 Docker 对内核要求 大于 3.10,你可以使用 uname -r 检测系统内容版本。 通过 Rancher,可以很方便地对多个主机进行管理,实现负载均匀、集群、分布式构架、故障转移、状态监控等。 一,安装宝塔面板(V 6.8)宝塔官网提供了详细的安装教程,针对不同系统有不同安装方式,下面只提供部分安装代码。 详细请移步 https://www.bt.cn/bbs/thread-19376-1-1.html 打开服务器 shell 终端 Centos 安装命令: yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.shUbuntu/Deepin 安装命令: wget -O install.sh http://download.bt.cn/install/install-ubuntu_6.0.sh && sudo bash install.sh 等待一段时间后,会出现控制面板地址和账号密码样例如下: Bt-Panel: http://<您的 CVM IP 地址>:8888username: qbqd**password: eeed** 记下你的账号密码 打开面板地址,输入账号密码进行登陆。 注: 阿里云、腾讯云等有安全组限制,可能需要添加 8888 端口,给予访问权限。 二,使用宝塔安装 Docker,配置阿里云容器服务通过宝塔面板的 Docker管理器 ,可以很方便地获取、提交镜像,推送已修改的镜像,对本地的容器进行管理,一切可视化操作,还能单独对容器打开终端,进行命令操控。 1,登陆宝塔面板后,点击左侧导航栏的 “软件管理”,找到 Docker 进行安装。 2,安装完毕后,打开 Docker 管理器(可在 “软件管理” 中找到)。 3,点击镜像管理,添加阿里云镜像账号 填写账号,需要你的 阿里云RAM访问控制(子账号)、镜像仓库、命名空间。 可到以下地址查看或创建,再填写到宝塔面板 命名空间 https://cr.console.aliyun.com/cn-shenzhen/namespaces 镜像仓库 https://cr.console.aliyun.com/cn-shenzhen/repositories 镜像加速器 https://cr.console.aliyun.com/cn-shenzhen/mirrors RAM访问控制 https://ram.console.aliyun.com/overview 5分钟内即可完成。 三,安装 Rancher (Server)Rancher 是一个开源的企业级容器管理平台。通过 Rancher,企业再也不必自己使用一系列的开源软件去从头搭建容器服务平台。 Rancher 提供了在生产环境中使用的管理 Docker 和 Kubernetes 的全栈化容器部署与管理平台。 当然,使用Rancher,带来了管理上的方便,也是会稍微降低服务器的性能的,但是这点影响,可以忽略不计。 1,在 shell 终端 输入命令安装 Rancher sudo docker run -d --restart=always -p 8080:8080 rancher/server 注: Rancher 默认使用了端口 8888 2,访问 Rancher 面板 打开 http://ip:8080,即可访问你的面板,如果无法访问,请查看 下一步。 3,允许端口 由于安全组和防火墙限制,需要在云服务商实例安全组和宝塔面板开放 8080 端口,才能正常访问。建议直接放行 1-9000 范围的端口,后面仍然需要开放几个端口,直接开放一个范围,后面就不必再一个个添加。 4,查看容器、镜像 打开 宝塔面板 的 “软件管理”-Docker管理,可以查看到正在运行的容器和本地镜像,利用宝塔面板很方便地可以对 Docker 进行操控。可以尝试把镜像推送到阿里云仓库。 四,管理 Rancher、添加集群 打开全部应用列表,查看状态,healthcheck 、ipsec (除了 myapp )等是系统服务,如果出现下面的情况(显示红色、出现感叹号),请检查是否开放了 500、4500 端口,然后手动启动运行。 我们安装的 Rancher ,实际上是 Server 端,通过 Rancher,可以完成集群管理、分布式架构。 1,打开了 Rancher,首先要添加管理员账号 点击顶部导航栏的 ADMIN,选择 Access Control ,在出现的多个图标中,选择 LOCAL,然后添加账号密码。 2,试试切换中文 Rancher 底部有语言切换功能,修改为中文,方便后面的讲解。 3,添加主机 要使用 Rancher,至少要一个主机,可以是当前主机的本身。 点击 Rancher 面板顶部导航栏的 ”基础构架“ - ”主机“,然后 添加主机。 这一步,无需修改或进行其他什么配置,页面下方会出现一端脚本代码,把这段代码复制到终端 shell 运行。待 shell运行完毕,点击 Rancher 页面的”关闭“,一定要记得 点击关闭。 4,出现了主机 这时候即可看到以及添加的主机,可以对容器进行监控、修改啦~~~ 这里添加的是 ”Custom“ ,你也可以添加 Azure 等主机。 5,使用仪表盘查看主机状态 在主机列表,点击实例名,即可加入仪表盘。 五,添加 Rancher 应用、服务,与 Nginx这一步主要以 Nginx 为例,通过创建 Nginx 服务器,学会使用 Rancher、相关功能的使用,对其他应用使用类似方式手动部署。 1,放行 4500 端口、500 端口 4500、500 是 Rancher 系统服务需要 2,在 Rancher 中添加应用 点击导航栏的 ”应用“ - ”全部“ ,页面会出现应用列表。 点击 ”添加应用“ ,名称为 myapp 3,在刚刚创建的应用中,添加服务 名称输入 nginx,镜像输入 nginx ,当然你也可以选择 nginx 版本,填写形式跟 Docker 镜像形式一致,两个端口输入 8090 ,其他地方不用填,点击 ”创建“ 即可。这里填写的端口,公开端口即 Docker 向外公开的端口,私有容器端口是容器内其应用的端口。 由于这里不对 nginx 服务器进行更多处理,所以端口可以随便填,只要公开端口没有被占用即可~ 两个端口的关系相当于 docker run -p 8080:8080 ... ... 4,添加负载均匀 在 myapp 应用管理界面,点击 ”添加服务“ -> ”添加负载均匀“ ,端口填写 80/80 ,此端口用于外网访问 Docker 容器内的应用、实现多台主机的负载均匀。通过使用 负载均匀功能 ,负担 nginx 服务的流量。不要设置为 8080 等,不然会出现访问失败。此功能也是把容器应用暴露到外网的功能,通过外网可以直接访问到应用。 一定要选择目标,选择要暴露的应用。 这里选择 80 作为端口,因为开发者使用 nginx 部署网站,网站访问应当使用 80 端口。当然,也可以随意使用其他端口。 5,访问 nginx 打开 http://ip:80 或直接访问 http://ip 即可访问 6,集群负载均匀 通过添加外部服务,再添加负载均匀,重复两个步骤,可以实现集群的负载均匀。这里不再尝试。 在这里注意负载均衡与外部服务的区别, 负载均衡需要配置服务自身的端口,而外部服务需要为其配置,其他链接服务所暴露出的端口,即其他主机暴露出来的公用端口 六,ASP.NET Core 应用部署这里与第五步 的 Nginx 无关,为了便于理解、避免端口占用,我们把创建的 myapp 删除。 笔者作为 .NET 学习者,当然要搞一下ASP.NET Core了~~~。 1,添加新的应用,名为 webapp 2,添加服务 镜像源填写 microsoft/dotnet-samples:aspnetapp 这个镜像为微软官方提供的 ASP.NET Core 默认模板镜像,当然,你也可以把自己的应用打包后推送镜像到仓库,再使用 Rancher 创建容器。 端口为 5000,是因为 ASP.NET Core 默认使用 5000 端口,我们干脆也用这个。没实际意义,不必关注这一点。 3,添加负载均匀 4,访问网站 访问 http://ip:80 或直接访问 你的ip 七,相关文章推荐1,使用 KubernetesKubernetes 是 Google 开源的容器集群管理系统。 其设计目标是在主机集群之间提供一个能够自动化部署、可拓展、应用容器可运营的平台。 Kubernetes 通常结合 docker 容器工具工作,并且整合多个运行着docker容器的主机集群,Kubernetes不仅仅支持 Docker,还支持 Rocket,这是另一种容器技术。 (直接抄来的,哈哈哈哈~~~) 推荐文章 http://blog.51cto.com/13941177/2165668 #文章手把手,非常详细地讲解使用Rancher 安装 K8s,部署集群、配置节点,对主机进行管理、部署Web应用 2,本地 ASP.NET Core 应用容器化Vs2017 以上,已经为 ASP.NET Core 添加 Docker 支持 我们可以更方便地进行开发,实现跨平台、微服务构架等。 笔者 推荐文章 https://www.cnblogs.com/FireworksEasyCool/p/10218384.html 详细讲解了 ASP.NET Core 项目如何添加 Docker 支持,并打包成镜像推送到仓库,在服务器上创建容器并运行应用。 一个逗逗的大学生