Microsoft StreamInsight 构建物联网

最近关于“物联网”(IoT) 的争论有很多,而且理由都很充分。 Ericsson 的 CEO Hans Vestberg 预测到 2020 年将有 500 亿台设备连接到 Web(bit.ly/yciS7r [PDF 下载])。 目前约有 15 亿台 PC 以及不足 10 亿台电话连接到 Web — 500 亿相当于全球每个人约 7 台设备,这可以帮助您直观理解这一数字! 市场研究机构 IDC 则预测到 2015 年将有超过 160 亿台设备连接到 Internet(参见图 1)。 不可否认,也存在一些较保守的预测,但通过每个人提供的数字,我们看到 Internet 的角色正在发生巨大转变 — 从为人们提供信息和娱乐内容到为支持设备的新兴应用程序提供连接服务。

图 1 IDC 预测会出现“嵌入式 Internet”

这些庞大数字看似可信的理由是强大的驱动因素(商机和必然性)正在推动此类解决方案的快速增加。 《经济学家》(economist.com/node/17388368) 最近的一期杂志指出,“…经济大潮的向前滚动不仅仅是为了符合技术公司和雄心勃勃的政治家的利益。 这种发展趋势已获得了动力,因为真的需要此类系统。”图 2 按应用领域显示了此类解决方案的增长态势。 例如,在欧洲强制实施智能能源系统是一个必然结果。 如果我们还不能管理能源消耗,则无法构建所需的能源产生功能。 在商机方面,简单的、无所不在的自动售货机就是一个很好的示例。 在连接该设备之后,可以根据需要而不是某项并非最理想的计划来指派服务人员。 如果本地需求增加或商品接近到期日,甚至还可以动态更改价格。 可以报告停电情况以便督促立即更换易腐商品。 换句话说,连接实现了更高效的全新业务模型。

图 2 按行业划分的应用增长态势

通过引用连接设备的数量来描述此转变肯定很形象,但它也会让人产生一些误解 — 此种转变并不表明几十万传统嵌入式程序员将充分就业。 这些设备仅是将其他设备集成到 Internet 的所有方面(包括分析、云、Web 应用程序、PC 和移动界面等)的复杂解决方案的终结点。

因此,看待此问题的方法是当前构建基于 Web 的应用程序的每个人将需要集成设备并帮助开发新业务和新业务模型。 换句话说,即使您不是嵌入式开发者,也不在构建嵌入式设备的商店中工作,这也是一个值得您评估的非常诱人的商机。 您当前具备的 Microsoft 技能将使您能够在 IoT 方面取得成功。

对设备数据进行分析

“数据已成为新的货币”,Windows Embedded 团队的总经理 Kevin Dallas 在最近的采访中说道 (bit.ly/wb1Td8)。 与当前 Internet 应用程序相比,IoT 涉及信息生成、管理和访问。 让我们比较一下当今典型 Internet 应用与 IoT 应用的数据特征。 您或许和其他几百万人均使用多家金融机构共享的流行机制联机支付帐单。 您每月登录多次,查看一些页面并提交付款信息。 所有这些数据都是使用当您开始与系统互动时所运行的查询从传统数据库中提取的。 您下载的页面量可能很大,但交互非常少,即使它们生成的有价值的信息(付款信息、个人信息更新等)需要长时间保存也是如此。

将此应用与能源管理系统进行对比,该系统中可能有 5000 万座大楼(商业楼和住宅楼)正在提供输入。 输入由内部的多个本地终结点(例如,房子)使用发布到后端的单个聚合视图生成。 该数据包括返回给大楼的成为定价和记帐基础的即时使用信息和强制性控制。 此系统将与价值相对较低的数据进行非常频繁的交互,这些数据在您计算系统的当前状态和该终结点的趋势数据时不一定有意义。 不过,该系统需要能够立即对可能威胁其运行的情况(例如需求剧增引发网格超载和停电)做出响应。 在这种情况下,广播信息可以立即减少能源消耗。 此类系统需要连续分析传入数据并比较发展趋势,以识别指示较高停电风险的模式。

图 3 演示 IoT 应用的典型体系结构。 堆栈底部显示了各种资产或设备,它们根据不同应用领域配备了不同种类的传感器。 这些传感器通常生成应用领域快速处理和分析所需的连续数据源。 根据设备的功能,设备本身或许能够在本地执行一些处理。 这称为本地分析,并且 .NET Micro Framework 之类的工具可帮助您在设备传递数据之前执行该本地处理。 IoT 应用使用 Internet 协议传递设备数据,以便可以对数据进行全局分析。 而全局分析的结果(例如电网的整体运行状况)是管理运营的最终用户或业务决策者关注的内容。 分析还可以驱动根据传入数据中呈现的情况自动采取操作的封闭系统。 如果资产可接收来自全局分析的反馈(例如,影响行为更改或改进操作),则这些方法将非常有用。 需要连续计算推动这些过程的全局分析并尽快提供结果。 另外,分析频繁参考随传感器数据一起提供的时间和时间戳。 因此,仅将此类数据放入数据库中并对其运行定期查询不是适当的方法。 幸运的是,Microsoft StreamInsight 支持不同的方法。

图 3 物联网应用的典型体系结构

Microsoft StreamInsight

Microsoft StreamInsight 旨在对连续到达的数据提供及时反应,而不将数据写入磁盘中以进行分析和查询。 许多 IoT 应用领域需要在从源获取数据后几乎实时地分析传入数据。 考虑我们提到的智能电网应用,它需要对剧增的电力需求快速做出反应以重新平衡电网的运营能力。 许多 IoT 应用具有相同的需求: 需要连续分析的数据处理和引人注目的延迟。 分析必须连续,因为数据源不断地生成新数据。 许多方案需要识别只能通过分析传入数据而呈现的情况并对其快速做出反应,因此它们需要低延迟分析和几乎立即提供的结果。 这些要求使在执行分析之前将数据存储在关系数据库中变得不切实际。

我们将这些应用称为事件驱动应用,而 IoT 正是此类功能发挥作用的一个方案。 StreamInsight 是一个用于构建这些高度可伸缩、低延迟的事件驱动应用的强大平台。 它是自 2008 R2 版本发布以后的 Microsoft SQL Server 的一部分。 在事件驱动处理和基于丰富表达时间的分析方面,StreamInsight 为 SQL Server 提供了补充。 使用 StreamInsight,将以生成数据的速度,而不是处理传统数据库报告的速度提供业务见解。

可供人们立即使用或使应用程序能够自动对事件做出反应的分析结果可帮助企业更及时且更好地了解其相关运营情况,甚至可以自动执行部分运营工作。 它们也可以对传感器或设备数据中出现的重要情况、商机或趋势更快地做出反应。

要编写 StreamInsight 应用程序,开发者可使用 Microsoft .NET Framework、LINQ 和 Microsoft Visual Studio 等熟悉的工具。 图 4 描述了 StreamInsight 应用程序的开发者和运行时体验并介绍了一些关键概念。

图 4 StreamInsight 应用程序开发和运行时

简单的 IoT 应用

让我们更深入地了解可能的 IoT 应用方案;然后我们将构建它。 在我们的端到端示例中,我们将关注一个简单方案,该方案使用运动传感器监视旋转设备,例如涡轮或风车。 这很重要,因为振动过大会导致出现紧急情况,在这种情况下,设备可能出现故障,并且如不立即停止,则会出现严重损坏。 为可靠地检测此情况,每台设备均配备多个跟踪运动的传感器。 单个传感器中的运动激增可能仅指示该传感器的数据读数不可靠,但多个传感器中同时出现异常剧烈的运动则表明出现紧急情况。 例如对于大型涡轮,您可能希望引发警报,甚至自动关闭设备。 除了持续检查此类情况外,我们还希望为操作员提供一个仪表板,它提供了设备状态的近实时视图。

若要构建此方案,我们需要满足以下要求和解决以下技术难题:

  • 设备需要捕获哪些数据?
  • 我们使用哪些传感器来测量数据?
  • 设备如何将其传感器读数传送到 Internet?
  • 我们如何将设备数据收集到一个位置以进行分析?
  • 我们如何可以连续分析传入数据并对紧急情况快速做出反应?
  • 我们如何跨多台设备及时关联传感器读数,以便可以检查全局情况?

让我们看一下满足这些要求并实现端到端方案的方式。

IoT 应用: 实现要点

下面是实现上一节中所述的 IoT 应用的一些关键步骤。 我们将首先讨论设备,再转到输出的可视化,然后转到填充仪表板的跨设备分析。

设备。为构建传感器设备,我们首先从 Netduino Plus 着手,它是运行 .NET Micro Framework、具有 128K SRAM 的受欢迎的小型开发板。 我们添加了名为 WiFly GSX Breakout 的常见爱好者 Wi-Fi 无线电,并在自定义 PCB 板上安装了实际传感器,包括三轴加速计。 我们对设备进行编程,以将传感器读数的每秒更新发送给 Web 服务,该服务充当从所有设备收集数据并进行处理的中心。

我们对 Web 服务使用 RESTful 连接 — 它只是包含逗号分隔名称-值对的 HTTP POST。 当然,您可以从支持 HTTP 的任何种类的设备执行此操作。 我们选择使用 .NET Micro Framework,以便整个应用程序(包括设备、Web 服务、StreamInsight 适配器、Silverlight 仪表板等)全部可以使用单个编程模型 (.NET) 和工具链 (Visual Studio) 进行编写。 很明显,如果您具有 .NET 技能,则无需招聘新员工或将您的 IoT 项目的一部分外包给外部嵌入式商店;您具有完全执行它的技能。 例如,设置加速计时只需几行代码即可访问 AnalogInput 类并调用 Read 方法:

          this.analogInputX = new AnalogInput(pinX);this.analogInputY = new AnalogInput(pinY);this.analogInputZ = new AnalogInput(pinZ);...          rawZ = analogInputZ.Read();rawY = analogInputY.Read();rawX = analogInputX.Read();        

在读取传感器输入并设置 HTTP 消息内容格式后,发送数据所需的一切都包括在图 5 中。

图 5 提交传感器数据

          protected void submitSensorData(string uri, string payload){  // Message format  StringBuilder sb = new StringBuilder(256);  sb.Append(    "POST /Website/Services/DataService.aspx?method=SaveDeviceData HTTP/1.1\n");  sb.Append("User-Agent: NetduinoPlus\n");  sb.Append("Host: 192.168.1.101\n");  sb.Append("Connection: Keep-Alive\n");  sb.Append("Content-Length: ");  sb.Append(payload.Length.ToString());  sb.Append("\n");  sb.Append(payload);  sb.Append("\n");  try  {    HttpResponse response = webServer.SendRequest(uri, 80, request);  }  catch  {    ...          }}        

在服务器端,我们实现方法 SaveDeviceData,设备要将其消息发布给该方法。 我们拆分消息字符串并分析 MAC 地址、时间戳和负载数据,例如来自加速计的运动读数。 我们使用所有这些信息来填充传递给 StreamInsight 以进行后续分析的 DeviceData 对象(请参见图 6)。

图 6 填充 DeviceData 对象

          private int SaveDeviceData(){...          List<string> data = record.Split(',').ToList();  DeviceData deviceData = new DeviceData();  deviceData.MAC = NormalizeMAC(data[0].Trim());  deviceData.DateTime = DateTime.UtcNow;...          deviceData.Motion = Convert.ToDecimal(data[2].Substring(data[2].IndexOf(":") + 1));...          // Communicate each new device data record to StreamInsight             DeviceDataStreaming streaming = (DeviceDataStreaming)    HttpContext.Current.Application[Global.StreamingIdentifier];    streaming.TrackDeviceData(deviceData);...          }        

仪表板。现在我们要构建允许设备操作员查看设备上传感器的当前状态的仪表板。 为便于演示,我们将仅关注一台设备。 图 7 显示了一个此类仪表板的示例。 让我们从左侧开始,查看传感器数据的不同视图。

图 7 用于设备监视的仪表板

移动平均数视图: 左下角的数据网格显示设备的传感器读数,其中包括光线、温度和运动值以及设备 ID 和时间戳。 正如您可以从时间戳中看到的,这些值每秒更新一次。 但仪表板不显示原始传感器值,而是显示 10 秒内传感器数据的移动平均数。 这意味着会使用最近 10 秒内数据的平均数每秒更新一次值。 使用移动平均数是一种常见的简单技术,可防止出现使用低成本传感器时偶尔出现的异常值和不良数据。

趋势线视图: 在右下角,仪表板显示传感器的趋势线。 趋势线视图的走势由左侧数据网格中显示的移动平均数决定。

警报视图: 右上角的视图显示警报的数据网格。 如果检测到临界情况,则会引发显示时间和其他信息(例如严重性和状态)的警报。

分析。现在让我们了解幕后操作并讨论处理传入传感器数据并计算仪表板可视化的结果的分析。 我们使用 StreamInsight 执行分析。 以下类表示设备数据,其中包括 MAC 地址、时间戳和传感器值:

          public class DeviceData{  public string MAC { get; set; }  public DateTime DateTime { get; set; }  public decimal?          Light { get; set; }  public decimal?          Temperature { get; set; }  public decimal?          Motion { get; set; }}        

此类定义单个事件的形状,但我们想要开始讨论许多事件。 为此,我们为 StreamInsight 定义了 Observable 数据源。 这仅是实现 System.IObservable 接口的数据源:

          public class DeviceDataObservable : IObservable<DeviceData>  {    ...          }        

在定义 .NET Framework 序列(例如 Enumerable 或类似的 Observable)后,即可开始编写对这些集合的 StreamInsight 查询。 让我们快速了解一下其中某些关键查询。 第一个查询获取 Observable 作为输入并生成 StreamInsight 点事件流,以使用设备数据中的“DateTime”字段作为 StreamInsight 事件的时间戳。 在下一个 LINQ 语句中,我们获取此流作为输入,并按 MAC 地址对数据进行分组。 对于每个组,我们然后应用窗口大小为 10 秒的跳跃窗口(基于时间的一部分事件),并让窗口每秒重新计算一次。 在每个窗口中,我们计算温度、光线和运动的平均数。 这为我们提供了每秒重新计算一次的每台设备的移动平均数。 图 8 显示了用于返回 StreamInsight 事件流形式的结果的函数实现此过程的代码。

图 8 获取移动平均数

          public static CepStream<AverageSensorValues> GroupedAverages(              Application application,              DeviceDataObservable source)  {    var q1 = from e1 in source.ToPointStream(application,      e => PointEvent.CreateInsert(        new DateTimeOffset(          e.DateTime.ToUniversalTime()),e),      AdvanceTimeSettings.StrictlyIncreasingStartTime,      "Device Data Input Stream")             select e1;    var q2 = from e2 in q1             group e2 by e2.MAC into groups             from w in groups.HoppingWindow(               TimeSpan.FromSeconds(10),               TimeSpan.FromSeconds(1))             select new AverageSensorValues             {               DeviceId = groups.Key,               Timestamp = null,               AvgTemperature = w.Avg(t => t.Temperature),               AvgLight = w.Avg(t => t.Light),               AvgMotion = w.Avg(t => t.Motion)             };    return q2;  }        

这是考虑实现警报查询的最佳位置。 请记住,当有多个运动传感器的读数同时高于运动阈值时,将触发警报。 只需对刚计算的分组平均数使用几个 StreamInsight LINQ 语句便可处理此问题。 通过将警报阈值的更改表示为名为 AlarmThresholdSignal 的事件流,第一个查询 q3 应用了一个极佳的技巧。 此查询将阈值与来自前一个查询的平均数流联接,然后仅筛选高于阈值的事件:

          var q3 = from sensor in GroupedAverages(application, source)         from refdata in AlarmThresholdSignal(application, alarmsthresholds)         where (sensor.AvgMotion !=           null && (double) sensor.AvgMotion > refdata.Threshold)         select new         {           AlarmDevice = sensor.DeviceId,           AlarmInfo = "This is a test alarm for a single device",         };        

下一个查询使用 StreamInsight 快照窗口来识别事件状态更改的时间点。 如果从前一个筛选查询产生了一个新事件,则这是新快照,并且该快照操作生成一个新窗口,其中包含与触发快照窗口的事件一致或重叠的所有事件。 下面的代码对创建快照窗口时高于警报阈值的事件进行计数:

          var alarmcount = from win in q3.SnapshotWindow()                 select new                 {                   AlarmCount = win.Count()                 };        

最后一步检查计数是否显示有多台设备将发出警报指示:

          var filteralarms = from f in alarmcount                   where f.AlarmCount >= 2                   select new AlarmEvent                   {                     AlarmTime = null,                     AlarmInfo = "Now we have an alarm across multiple devices",                     AlarmKind = 0,                     AlarmSeverity = 10,                     AlarmStatus = 1                   };        

现在,我们只需将包含平均传感器值和警报的输出流从 StreamInsight 传送到 UI。

使输出流传送到 UI

使用在服务器端生成结果流的 StreamInsight,我们需要一种方法来将这些流传送给使用者。 使用者可能不在服务器进程中运行,并可能使用轻型 Web 应用程序来可视化结果。 如果您使用 Silverlight,则双工协议很方便,因为它支持从服务器到客户端的连续的基于推送的传送。 HTML5 Web 套接字也是引人注目的替代方法。 无论如何,您都希望轻松地在服务器端添加新分析并能够轻松地将它们与 UI 连接,而无需拆分 UI 和承载 StreamInsight 的进程之间的客户端-服务器接口。 如果 UI 和服务器之间的负载适中,则您可以将服务器端的结果序列化为 XML 并在客户端对其进行反序列化。 这样,您只需关注线路上和您的客户端-服务器接口中的 XML,以及指示要反序列化的类型的附加 Cookie。 下面是几段关键代码。

第一个代码段是 Windows Communication Foundation 协定,它用于传送 XML 序列化字符串形式的事件数据以及指示类型的 GUID:

          [ServiceContract]public interface IDuplexClient{  [OperationContract(IsOneWay = true)]  void Receive(string eventData, Guid guid);}        

现在,我们可以使用数据协定为结果事件结构添加批注以使其可序列化,如图 9 所示。

图 9 为事件结构添加批注

          [DataContract]public class AverageSensorValues : BaseEvent{  [DataMember]  public new static Guid TypeGuid =    Guid.Parse("{F67ECF8B-489F-418F-A01A-43B606C623AC}");  public override Guid GetTypeGuid() { return TypeGuid; }  [DataMember]  public string DeviceId { get; set; }  [DataMember]  public DateTime?          Timestamp { get; set; }  [DataMember]  public decimal?          AvgLight { get; set; }  [DataMember]  public decimal?          AvgTemperature { get; set; }  [DataMember]  public decimal?          AvgMotion { get; set; }}        

现在,我们可以轻松地序列化服务器端的结果事件并将其传送到客户端,如图 10 所示。

图 10 从服务器发送结果事件

          static public void CallClient<T>(T eventData) where T : BaseEvent  {    if (null != client)    {      var xmlSerializer = new XmlSerializer(typeof(T));      var stringBuilder = new StringBuilder();      var stringWriter = new StringWriter(stringBuilder);      xmlSerializer.Serialize(stringWriter, eventData);      client.Receive(stringBuilder.ToString(), eventData.GetTypeGuid());    }  }        

在客户端上,我们反序列化双工服务的回调方法中的事件,然后根据接收到的事件的类型将其分支到不同的方法中,如图 11 所示。

图 11 在客户端上接收和反序列化事件

          void proxy_ReceiveReceived(object sender, ReceiveReceivedEventArgs e){  if (e.Error == null)  {    if (AverageSensorValues.TypeGuid == e.guid)    {      ProcessAverageSensorValues(Deserialize<AverageSensorValues>(e.eventData));    }    else if (AlarmEvent.TypeGuid == e.guid)    {      ProcessAlarms(Deserialize<AlarmEvent>(e.eventData));    }    else    {      ProcessUnknown();    }  }}        

使用这些查询并传送到相应的 Web 应用程序,您现在可以选取几台设备并摇动它们,直到一些设备读数高于警报阈值。 然后,UI 将生成这些红色警报之一,如图 12 所示。

图 12 包含警报的设备仪表板

因为新数据会不断进入几乎实时的仪表板,所以 ObservableCollections 对更新 UI 极其有用。 如果您使数据网格和趋势线基于这些 Observable 集合,则无需担心代码中的更新部分。 这些集合将在后台为您自动执行此操作。

前景

在此实现中,设备与常规 Web 服务通信,该服务可以运行在连接到 Internet 的普通 PC 上。 但云计算是一个吸引人的替代方法;您不一定需要为自己的 Web 服务器拥有硬件并运行软件。 云中的服务可以充当为您的应用程序收集所有设备数据的中心。 这还使您能够在设备数量增加或部署针对设备数据的其他分析时,非常轻松且灵活地扩展您的处理能力。 Microsoft 计划将 StreamInsight 功能作为 Windows Azure 中的一项服务(StreamInsight 项目代码名称“Austin”)提供。 通过提供预定义的通信终结点和协议,Austin 将使您能够轻松地将设备连接到 Microsoft 云中丰富的分析处理功能。 如果您将 IoT 应用程序部署到 Windows Azure 中,则将自动获得灵活扩展和即付即用等云好处,以便管理设备连接和对设备数据执行丰富的分析。

另一个重大转变涉及最近进行的 W3C 标准化工作。 IoT 应用程序的最重要计划是 HTML5 和 Web 套接字。 HTML5 为丰富的 Web 应用程序(例如我们实现的仪表板)提供平台。 而 WebSocket 又简化了浏览器和 Web 服务器之间基于 TCP 的全双工通信,尤其是针对连续处理传感器数据时所要求的结果传送的推送模型。

连接的设备开创了一个令人兴奋的新应用领域,并且 Microsoft 现在提供用于构建这些 IoT 应用程序的工具。 我们在这里介绍了如何在设备级别通过熟悉的接口利用您的 .NET Framework 技能,以及如何通过 Web 服务为 StreamInsight 的强大分析功能提供数据。 立即开始使用连接设备构建您的 IoT 应用程序!

Torsten Grabs 是 Microsoft SQL Server 部门的首席项目经理。 他具有 10 余年 Microsoft SQL Server 产品的使用经验,并获得瑞士苏黎世的瑞士联邦理工学院的计算机科学博士学位。

Colin Miller 在 PC 软件领域工作了 25 年(其中有 15 年效力于 Microsoft),研究方向涉及数据库、桌面发布、消费类产品、Word、Internet Explorer、Passport (LiveID) 及联机服务。 他是 .NET Micro Framework 的产品部经理。

衷心感谢以下技术专家对本文的审阅: Rafael Fernandez MoctezumaLorenzo Tessiore

原文:http://msdn.microsoft.com/zh-cn/magazine/hh852591.aspx

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏c#开发者

框架设计指导方针[翻译]

原文 http://www.codeplex.com/AppArchGuide 本人英语水平较差献丑了 :) 框架设计指导方针 目的 1明白软件架构的概念 ...

37390
来自专栏IT大咖说

做一个不背锅的运维

内容来源:作者:田逸(sery),来自:http://blog.51cto.com/sery/2162642

18140
来自专栏13blog.site

短信发送接口被恶意访问的网络攻击事件(四)完结篇--搭建WAF清理战场

作者:13 GitHub:https://github.com/ZHENFENG13 版权声明:本文为原创文章,未经允许不得转载。 前言 短信发送接口...

41360
来自专栏Java技术栈

阿里巴巴,排行前10的开源项目!

1、FastDFS FastDFS是一个开源的分布式文件系统,她对文件进行管理,功能包括:文件存储、文件同步、文件访问(文件上传、文件下载)等,解决了大容量存储...

48480
来自专栏帘卷西风的专栏

开源CEGUI编辑器之一(MFC重写的LayoutEditor)

转载请注明出处:帘卷西风的专栏(http://blog.csdn.net/ljxfblog)

15720
来自专栏FreeBuf

如何真正成为一个在路上的Linuxer

Linux是工具,更像一个信仰。 写在前面 本文目的不是教你如何成为一个真正的Linuxer,也没有能力教你成为一个真正的linuxer,而是通过笔者的一些想法...

39080
来自专栏北京马哥教育

Nginx与httpd对比

作为一个运维的学习者,对nginx和apache了解的很浅,但是作为以后运维过程中非常重要的两款服务器软件,静态web服务提供者,还是相当有必要深入的了解一下他...

43750
来自专栏Sign

creator创建小游戏子域排行榜

cocos官方有对应的子域接入教程: ? https://github.com/cocos-creator/creator-docs/blob/master/z...

97280
来自专栏程序员宝库

我的爬虫技术经历

1. 前言 爬虫,这个词很多朋友第一次听到,第一感觉应该是各种小虫子,应该不会和某种计算机技术联系在一起。我第一次听到这个词,就是这样一个感觉。但是当这个这个词...

615120
来自专栏工作随笔

改VB.NET“偷懒”技巧

【开篇胡侃】虽然搞软件开发很多年了,但似乎从没有动手写过什么(很丢脸的感觉),因为,我的精力都献给了我的其他爱好,比如健身、美食、旅游等等,反而把自己最该专注的...

330130

扫码关注云+社区

领取腾讯云代金券