从 0 开始搭建 IoT 平台

课程背景

物联网应用开发,并不像 Web 开发那样有固定的模式和框架可以学习,开发者往往还是需要从协议这一层慢慢往上搭积木,学习曲线比较陡。本课程结合物联网应用开发常用的设计模式以及作者多年的开发经验,带你从 0 开始搭建一个物联网平台,希望本课程所体现的架构和思路能够帮助你少走弯路、少踩坑。

课程亮点

从 0 开始搭建专属物联网平台

  • 大量实战代码,手把手逐行讲解
  • 使用开源组件,掌握大厂设计思路和实现逻辑

“踩坑”经验沉淀,教你快速「搭积木」

  • 优化物联网应用开发学习曲线
  • 覆盖物联网开发中 80% 的场景以及可能遇到的设计和架构问题

你能收获什么?

  • 学习物联网应用的常用架构和设计模式
  • 学会独立设计和开发可以用于支撑多个物联网应用的物联网平台
  • 学会使用和编写 EMQ X 的插件
  • 获得一套完整的、可运行的物联网平台代码,包括服务端和设备端 SDK

前置知识

学习和熟悉 MQTT 协议的基本概念和特性,对阅读本课程有非常大的帮助。

作者介绍

付强,某物联网 Startup 联合创始人兼 CTO,之前就职于趋势科技,诺基亚中国,在物联网领域从业多年,曾经在德国、硅谷的 Startups 工作过。

适宜人群

  • 物联网应用开发者
  • 物联网架构师
  • 物联网平台开发者

课程目录

从 0 开始搭建 IoT 平台

课程内容

开篇词:开发物联网应用,光会 MQTT 还不够

导读

大家好,我是付强,我现在是一家物联网 Startup 的联合创始人兼 CTO,在自己创业之前,我曾经在趋势科技和诺基亚工作过。从 2011 年我在硅谷参与的第一个物联网项目开始算起,到 2015 年开始在物联网方向创业并运营到现在,从 0 到 1,1 到盈利,我在物联网这个行业已经摸爬滚打快 8 年了。

我的上一门课程《MQTT 协议快速入门》详细讲解了 MQTT 协议及其各种特性。在课程的交流群中,读者们也提了很多问题,除了关于 MQTT 协议本身的内容以及特性相关的问题之外,还有很多问题是关于物联网软件设计和架构方面的,比如:

  • 我应该如何管理我的设备和设备的状态?
  • 业务服务端应该怎么接收、处理和存储来自设备的数据?
  • 我的设备数量很多, Broker 端应该怎么架设来确保性能和扩展性?
  • 我的设备处理能力有限,除了使用 MQTT 协议以外,还有没有其他的选择?
  • ……

这让我意识到,单单学会和熟悉 MQTT 协议,离开发和架构一个成熟的物联网产品还是有一段不小的距离,其实仔细想想,这也没有什么不对的:拿 Web 开发做一个类比,我们只学习 HTTP 协议,就能够开发一个成熟的网站或者基于 Web 的服务吗? 答案也是否定的。

回想一下我们是怎么学习 Web 开发的。首先我们会了解一下 HTTP 协议,然后选择一个框架,比如 Java 的Spring Boot、 Python 的 Django、Ruby 的 Rails 等。这些框架提供固定的模式,对软件进行了高度的抽象和分层,比如集成了一些 Web 开发的 Good Practice; 你会知道在 Model 层处理业务的逻辑,用 ORM 来进行数据库操作,在 Controller 层处理输入输出和跳转,在 View 层渲染 HTML 页面,这样一个网站和 Web 服务就能够很快被开发出来,除了性能优化的时候,你几乎不用去想 HTTP 协议的细节。

回到物联网开发,抛开设备端的异构性,单说服务端的架构这块,并不像 Web 开发这块有一个 Well Known 的模式、架构或者开发框架。开发者往往还是需要从协议这一层慢慢往上搭积木,学习曲线还是比较陡的。

我的经历

2015 年中的时候我开始在物联网方向创业,我的第一个决定就是先实现一个供业务系统和设备使用的物联网平台。当时阿里云的 IoT 平台已经上线,由于功能性和定制性方面暂时满足不了我们的需求,最后还是决定自行开发。

我们自行开发的物联网平台实现了设备的管理和接入,设备数据的存储和处理,并抽象和封装了基于 MQTT 协议的数据传输,比如设备的数据上报和服务端的指令下发等,提供了业务服务端使用的服务端 API,和设备端使用的设备端 SDK, 业务服务器和设备不再需要处理数据传输和接入等方面的细节,它们甚至不知道数据是通过 MQTT 协议传输的,这一切对业务服务器和设备都是透明的。

这个平台很好地支持了业务服务端和设备端的快速迭代,也支撑着业务从 0 到 1,从 1 再到盈利。同时,我也在密切地关注着各大云服务商(比如阿里云、AWS 等)提供的 IoT 平台,在一些功能上,我们的设计思路和实现逻辑是非常相似的,同时我也会把在云 IoT 平台上的新功能或者更好的实现集成到自研的物联网平台上。

从 0 开始搭建 IoT 平台

在这个过程中,我踩了很多坑,同时也积累了一些物联网平台在架构和设计模式等方面的经验。在这门课程中,我想把这些物联网平台架构以及设计方面的知识和经验分享给大家,这应该可以覆盖大家在物联网开发中 80% 的场景和可能遇到的设计和架构问题

推荐阅读 ?《从 0 开始搭建 IoT 平台》

如何学习

2019 年的阿里云 IoT 平台功能已经非常强大了,在本门课程中,我们将使用开源的组件,从第一行代码开始,一步步地实现一个具有阿里云 IoT 平台大部分的功能的”物联网平台”,在这个过程中穿插讲解在物联网平台开发中可以用到的模式和架构的选择, Pros and Cons,以及一些 Best Practice 等。与《MQTT 协议快速入门》侧重协议内容和理论不同,本门课程包含大量的实战代码,毕竟代码是程序员之间交流的最好语言。

这里给这个”物联网平台”起了一个 Codename: Maque IotHub,简称 IotHub。

本课程属于实战性课程,所以不会再详细地讲解 MQTT 协议的概念和特性了,预先学习和熟悉 MQTT 协议的基本概念和特性,对阅读本课程有非常大的帮助。你可以访问 mqtt.org 查阅协议的文档,也可以通过阅读我的《MQTT 协议快速入门》来进行快速学习。

课程特色

目前物联网开发的的实战课程,特别是成体系的实战课程是很少的,本课程涵盖了从物联网平台到设备端开发的大量场景和设计模式,并不像其他教程那样只是罗列知识点。本课程各节内容之间就像搭积木一样关联性很强,从第一行代码开始搭建一个物联网平台,轻理论而重实战,专注于你无法在互联网上找到参考的实战内容,干货十足。

  • 大量实战代码,从第一行开始手把手讲解
  • 来自实际运营的物联网项目的开发/架构经验
  • 覆盖 80% 物联网开发场景
  • 在互联网其他地方找不到的原创课程

课程介绍

开篇词:开发物联网应用,光会 MQTT 还不够

附录:如何运行 Maque IotHub

第一部分(第 1-1 ~ 1-9 课):设备生命周期管理

作为课程的第一部分,我们会设计和开发 IotHub 的最基础功能,对设备的创建、接入、 上线/下线、 禁用/恢复和删除的整个生命周期进行管理,引入设备的发布订阅权限管理。同时也会把服务端代码和设备端代码的框架搭建起来,便于后续的迭代开发。这部分最后也会讲解如何横向和纵向地扩展 EMQ X

第二部分(第 2-1 ~ 2-6 课):上行数据处理

在课程的第二部分,我们会设计和实现 IotHub 的上行数据处理功能,在这部分我们会学习 EMQ X 的 Hook 机制,以及它如何为 IotHub 的开发带来便利性,同时也会第一次对 IotHub 中的 MQTT 主题名进行规划,把 MQTT 的 Broker-Client 模式转换为 Server-Client 模式。

第三部分(第 3-1 ~ 3-5 课):下行数据处理

在课程的第三部分,我们会设计和实现 IotHub 的指令下发功能,在这部分我们会学习使用 EMQ X 的监控管理 API,并对指令下发的主题进行规划,实现可靠的指令下发流程。

第四部分(第 4-1 ~ 4-15 课):进一步抽象

在第四部分的课程中,我们会利用第二部分和第三部分实现的上行和下行通道,来实现物联网应用中一些常见的功能,比如 OTA 升级、设备分组等。我们会学习如果将这些功能抽象出来放入 IotHub 中,使多个物联网应用可以通过 IotHub 复用这些功能。最后我们还会学习和实现在各大云服务商提供的 IoT 平台中非常常见的设备影子功能。

第五部分(第 5-1 ~ 5-6 课):扩展 EMQ X

在前面的课程中,我们使用大量的 EMQ X 自带插件功能来完善 IotHub, 在这部分课程中,我们将学习如何编写一个 EMQ X 插件。通过这部分的学习,读者将会掌握根据自己的业务逻辑来扩展 EMQ X 的能力。

第六部分(第 6-1 ~ 6-2 课):CoAP

在课程的最后一部分,我们会将目光暂时从 MQTT 上移开,讨论 MQTT 在某些场景下的缺陷,并学习另外一种物联网协议 CoAP。最后我们会将 CoAP 无缝地接入现有 IotHub 的设备体系。

适合阅读的人群

本课程适合以下人群阅读:

  • 物联网应用开发者
  • 物联网架构师
  • 物联网平台开发者

推荐阅读 ?《从 0 开始搭建 IoT 平台》

课程寄语

在我的另一篇达人课《MQTT 协议快速入门》,我尝试着用一门课来解答大家在物联网应用设计和开发上遇到的问题,在交流中发现,大家的问题不只局限在协议上。这是也促成我编写这篇课程的原因之一。

物联网应用开发不像 Web 开发那样有固定的模式和框架可以学习,本课程实现阿里云 Iothub 的部分功能,并结合我在设计和实现一个已上线运行多年的物联网平台的经验,来阐述在物联网应用中常用的设计模式和思路,目的也是希望大家在学习本课程之后,可以少走点弯路、少踩点坑。如果你不需要实现一个物联网平台(比如直接使用云服务商提供的物联网平台)也没关系,这门课所体现的架构和思路也能帮助你更好地设计物联网应用

我希望读者在学习完本课的内容后,能够将学到的设计思路和模式运用到实际工作中去,希望本课程能够解答你在物联网应用开发中能遇到大部分设计和实现的问题。毕竟 5G 时代已经来临,物联网的发展势头会越来越快。

学完后的收获

  • 熟悉物联网应用的常用架构和设计模式
  • 学会独立设计和开发可以用于支撑多个物联网应用的物联网平台
  • 学会使用和编写 EMQ X 的插件
  • 获得一套完整的、可运行的物联网平台代码,包括服务端和设备端 SDK
第 1-1 课:准备工作台

在本课中,我们将安装开发物联网平台时使用到的组件,并把物联网平台的开发环境搭建起来。

安装组件

首先在开发机上面安装开发需要的组件。

MongoDB

MongDB 是一个基于分布式文件存储的数据库,我们会把 MongoDB 作为物联网平台主要的数据存储工具。

可以在这里找到 MongoDB 的安装文档,根据文档在对应的系统上安装和运行 MongoDB。

Redis

Redis 是一个高效的内存数据库,物联网平台会使用 Redis 来实现缓存和简单的队列功能。

请根据这里的文档,在对应的系统上安装和运行 Redis。

Node.js

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境,我们会使用 Node.js 来开发物联网平台的主要功能。

请根据这里的文档在对应的系统上面安装 Node.js。

RabbitMQ

RabbitMQ 是使用 Erlang 编写的 AMQP Broker,物联网平台使用 RabbitMQ 作为队列系统实现物联网平台内部以及物联网平台到业务系统的异步通信。

请根据这里在对应的系统上按照和运行 RabbitMQ。

EMQ X

EMQ X 是一个使用 Erlang 编写的 MQTT Broker,物联网平台使用 EMQ X 来实现 MQTT/CoAP 协议接入,并使用 EMQ X 的一些高级功能来简化和加速开发。

可以在这里找到 EMQ X 的安装文档,安装完毕之后,在控制台运行:

/bin/emqx start

如果命令行输出为 emqx 3.2.0 is started successfully! 那说明 EMQ X 已经成功安装并运行了。

本课程在编写时,使用的版本为EMQ X Broker V 3.2.0。 EMQ X Enterprise为 EMQ X 的付费版本,注意不要安装错了。

那么开发物联网平台需要的组件就安装完了,接下来我们简单介绍一下物联网平台的各个组成部分。

首先给这个物联网平台取一个代号,就叫它 Maque IotHub 吧,寓意麻雀虽小,五脏俱全。 接下来我们把后续课程中会出现的实体都定义一下:

  • Maque IotHub:我们将要开发的物联网平台,简称 IotHub。
  • Maque IotHub Server API:Maque IotHub 的服务端 API,以 Restful API 的形式将功能提供给外部业务系统调用,简称 Server API。
  • Maque IotHub Server:Maque IotHub 的服务端,包含了 Server API 和主要的 IotHub 服务端功能代码,简称为 IotHub Server。
  • 业务系统:指实现特定物联网应用服务端的业务逻辑系统,它通过调用 Maque IotHub Server API 的方式来控制设备/使用设备上报的数据,Maque IotHub 为它屏蔽了和设备交互的细节。
  • Maque IotHub DeviceSDK: Maque IotHub 提供的设备端 SDK,设备通过调用 SDK 提供的 API 接入 Maque IotHub,并和业务系统进行交互,简称 DeviceSDK。
  • 设备应用代码:实现设备具体功能的代码,比如打开灯、在屏幕上显示温度等,它调用 Maque IotHub DeviceSDK 来使用 Maque IotHub 提供的功能。

在后续的课程中就用 IotHub 来指代我们开发的这个物联网平台,用 DeviceSDK 来指代设备端的 SDK 代码,用 IotHub Server 来指代物联网平台的服务端代码。

项目结构

本课程会使用两个 Node.js 的项目来进行开发,分别是物联网平台的服务端代码 IotHub Server 和设备端代码 DeviceSDK。

Maque IotHub Server

服务端代码以一个 Express 项目作为开始, Express 是一个 Node.js 轻量级的 Web 开发框架,非常适合开发 Restful API,项目的结构如下图所示:

从 0 开始搭建 IoT 平台

这个项目包含了 Maque IotHub Server API 以及 Maque IotHub 服务端的一些其他功能。

Maque IotHub DeviceSDK

测试 MQTT 连接

首先我们来验证一下 EMQ X Broker 是否已经配置正确,并可以接受 MQTT 连接了。

同样地,这里使用一个 Node.js 项目开发 Maque IotHub DeviceSDK 代码。

package.json 中添加 MQTT 的 Node.js 库:

"dependencies": {    "mqtt": "^2.18.8"  }

然后运行 npm install

我们可以写一小段代码来测试一下 MQTT 连接:

//test_mqtt.jsvar mqtt = require('mqtt')var client = mqtt.connect('mqtt://127.0.0.1:1883')client.on('connect', function (connack) {    console.log(`return code: ${connack.returnCode}`)    client.end()})

如果不出意外的话,控制台会输出:return code: 0

重新组织代码

为了方便设备应用代码的开发, DeviceSDK 会把和 MQTT 相关代码,以及同Maque IotHub Server交互的相关的代码进行封装,这里我们实现一个类 IotDevice作为 DeviceSDK 的入口:

//iot_device.js"use strict";var mqtt = require('mqtt')const EventEmitter = require('events');class IotDevice extends EventEmitter {    constructor(serverAddress = "127.0.0.1:8883") {        super();        this.serverAddress = `mqtts://${serverAddress}`    }    connect() {        this.client = mqtt.connect(this.serverAddress, {            rejectUnauthorized: false        })        var self = this        this.client.on("connect", function () {            self.emit("online")        })        this.client.on("offline", function () {            self.emit("offline")        })        this.client.on("error", function (err) {            self.emit("error", err)        })    }    disconnect() {        if (this.client != null) {            this.client.end()        }    }}module.exports = IotDevice;

这段代码做了这几件事。

  • 封装了 MQTT Client 的 connect 和 disconnect 。
  • 通过设定 MQTT 连接地址为 mqtts://127.0.0.1:8883 的方式,在传输层使用 SSL。 EMQ X 默认使用一个自签署的的证书,所以我们需要设定 rejectUnauthorized: false
  • 通过 Events 和设备应用代码进行交互。

那么使用 IotDevice 类进行连接设备应用代码如下:

var device = new IotDevice()device.on("online", function () {    console.log("device is online")    device.disconnect()})device.on("offline", function () {    console.log("device is offline")})device.connect()

Maque IotHub DeviceSDK 项目结构如下图所示:

从 0 开始搭建 IoT 平台

  • 在 sdk 目录中是 DeviceSDK 的代码。
  • 在 samples 目录中是调用 DeviceSDK 的示例代码。

工作台准备完毕! 准备好开发环境以后,下面就开始实现第一个功能:设备注册

推荐阅读 ?《从 0 开始搭建 IoT 平台》

注意!!! 为了方便学习和技术交流,特意创建了读者群,入群方式放在 第 1-5 课 文末,欢迎已购本课程的同学入群交流。

第 1-2 课:设备注册(一)

在本节中,我们将设计 IotHub 的设备认证机制。

设备三元组

EMQ X 在默认的情况是允许匿名连接的,所以在上一节课程中,IotDevice类在连接 MQTT Broker 的时候没有指定 username 和 password 也能成功。

当然,我们肯定不希望任意一个设备都能连接到 Maque IotHub。首先我们需要到 Maque IotHub 注册一个设备,然后设备再通过由 Maque IotHub 下发的 username/password 连接到 Maque IotHub,以实现一机一密。

阿里云 IoT 平台用一个三元组(ProductKey, DeviceName, Secret)来标识一个逻辑上的设备,ProductKey 是指设备所属的产品,DeviceName 用来标识这个设备的唯一名称,Secret 是指这个设备连接物联网平台使用的密码。我认为这是一个很好的设计,因为即使在同一家公司内部,往往也会有多个服务不同业务的物联网产品需要接入,所以多一个 ProductKey 对后续的主题名、数据存储和分发等进行一个区分是很有必有的。

Maque IotHub 将使用类似的三元组(ProductName, DeviceName, Secret)来标识逻辑上的一台设备。ProductName由业务系统提供,可以是一个有意义的 ASCII 字符串,DeviceName 由 IotHub 自动生成,(ProductName, DeviceName)应该是全局唯一的。

这里我们约定,对一个设备(ProductName1, DeviceName1, Secret1),它接入 Maque IotHub 的 username 为 ProductName1/DeviceName1,password 为 Secret1

为什么说三元组标识的是逻辑上的一台设备而不是物理上的一台设备? 比如说:移动应用接入 Maque IotHub 来订阅某个主题,假如有一个用户在多个移动设备上用同一个账号登录,他使用的应该是同一个三元组,因为他订阅的消息在每个设备上都应该能收到,那么在这种情况下一个三元组实际上是对应多个物理设备。后面我们再来讲怎么来区分物理设备。

为什么要用”/”做分隔符,这里先不作说明,在讲到下行数据处理的部分再来解释。

EMQ X 认证方式

EMQ X 通过插件提供了多种灵活的认证方式,你可以在这里找到 EMQ X 自带的插件列表,Maque IotHub 使用 MongoDB 作为数据存储,所以这里我们选择 MongoDB 认证插件。除了使用 MongoDB 认证以外,我们还会使用 JWT 的认证方式来提供一种临时性的接入认证。

在启用认证插件之前,我们需要关闭 EMQ X 的默认匿名认证。

编辑 /etc/emqx.conf,修改以下配置项:

allow_anonymous = false

然后重新启动 EMQ X /bin/emqx restart

MongoDB 认证

MongoDB 的认证插件功能逻辑很简单:将设备的 username、password 存储在 MongoDB 的某个 Collection 中,当设备发起 Connect 的时候,Broker 再查找这个 Collection,如果 username/password 能匹配得上,则允许连接,否则拒绝连接。

/etc/plugins/emqx_auth_mongo.conf 可以对 MongoDB 认证进行配置,配置项很多,在这里我们看几个关键的配置项。

  • MongoDB 地址: auth.mongo.server = 127.0.0.1:27017
  • 用于认证的数据库: auth.mongo.database = mqtt 存储设备 username 和 password 的数据库,这里暂时用默认值。
  • 用于认证的 Collection: auth.mongo.auth_query.collection = mqtt_user 存储设备 username 和password 的 Collection, 这里暂时使用默认值。
  • password 字段名: auth.mongo.auth_query.password_field = password
  • password 加密方式: auth.mongo.auth_query.password_hash = plain, password 字段的加密方式,这里选择不加密。
  • 是否打开超级用户查询: auth.mongo.super_query = off,设置为关闭。
  • 是否打开权限查询: auth.mongo.acl_query = off,这里我们暂时不打开 Publish 和 Subscribe 的权限控制。

然后我们在 MongoDB 插入一条记录,在 MongoDB Shell 中运行:

use mqttdb.createCollection("mqtt_user")db.mqtt_user.insert({username: "test", password: "123456"})

然后加载 MongoDB 认证插件:

/bin/emqx_ctl plugins load emqx_auth_mongo

不出意外的话控制台会输出:

Start apps: [emqx_auth_mongo]Plugin emqx_auth_mongo loaded successfully.

这个时候如果我们运行 test_mqtt.js,会得到以下输出:Error: Connection refused: Bad username or password

接下来,我们在连接的时候指定刚才存储在 MongoDB 的 username/password: test/123456

...var client = mqtt.connect('mqtt://127.0.0.1:1883', {    username: "test",    password: "123456"})...

重新运行 test_mqtt.js,如果

return code: 0

说明基于 MongoDB 的认证方式已经生效了。

如果返回的是Error: Connection refused: Bad username or password,你需要检查:

  • 插件的配置文件是否按照课程中的方式进行配置;
  • MongoDB 插件是否成功加载,可通过运行 /bin/emqx_ctl plugins load emqx_auth_mongo 查看;
  • 对应的设备数据是否添加到 MongoDB 对应的 collection 中。

可以通过运行/bin/emqx_ctl plugins list方式查看插件列表,已加载的插件会显示 active=true。

JWT (JSON Web Token) 认证

使用 MongoDB 认证插件已经能够满足我们对设备注册的需求,但是我在这里还想再引入一种新的认证方式:JWT 认证。为什么呢?考虑以下两个场景:

  • 在浏览器中,使用 WebSocket 方式进行接入时,你需要将接入 Maque IotHub 的 username 和 password 传给前端的 js 的代码,那么在浏览器的 Console 里就可以看见 username 和 password,这非常不安全。如果使用 JWT 认证方式,你只需要将一段有效期很短的 JWT 传给前端的 js 代码,即使泄露了,可以操作的时间窗口也很短。
  • 有时候你需要绕过注册设备这个流程来连接到 Maque IotHub,EMQ X 会在一些内部的系统主题上发布与 Broker 相关的状态信息,比如连接数、消息数等。如果你需要用一个 Client 连接到 Maque IotHub 并订阅这些主题的话,先创建一个 Device 并不是很好的选择,这种情况下,用 JWT 作为一次性的密码为这些系统内部的接入做认证就会非常好。

JSON Web Token(JWT,读作 [/dʒɒt/]),是一种基于 JSON 的、用于在网络上声明某种主张的令牌(Token),更详细的介绍可以参考 jwt.io

EMQ X 提供了 JWT 认证插件来提供 JWT 方式的认证,在/etc/plugins/emqx_auth_mongo.conf 可以对 JWT 认证插件进行配置:

  • JWT Secret: auth.jwt.secret = emqxsecret, 这里我们使用默认值,当然在实际生产中你需要使用一个长且复杂的字符串。
  • 是否开启 Claim 验证: auth.jwt.verify_claims = on 打开 Claim 验证。
  • Claim 验证字段: auth.jwt.verify_claims.username = %u 需要验证 Claim 中的 username 字段。

下面我们来看下如何使用 JWT 来接入 Maque IotHub:

...var jwt = require('jsonwebtoken')var password = jwt.sign({    username: "jwt_user",    exp: Math.floor(Date.now() / 1000) + 10}, "emqxsecret")var client = mqtt.connect('mqtt://127.0.0.1:1883', {    username: "jwt_user",    password: password})...

在这里我们使用 EMQ X 预设的 JWT Secret 签发了一个有效期为 10 秒的 JWT token 进行连接,重新运行 test_mqtt.js,如果输出为:

return code: 0

说明基于 JWT 的认证方式已经生效了。

如果返回的是Error: Connection refused: Bad username or password,你需要检查:

  • 插件的配置文件是否按照课程中的方式进行配置;
  • JWT插件是否成功加载,可通过运行 /bin/emqx_ctl plugins list 查看;
  • 是否是使用课程中指定的 payload 来生成 JWT 的。

认证链

我们加载了 MongoDB 和 JWT 两个认证插件,EMQ X 就可以用这两个插件组成的认证链来对接入的 Client 进行认证。简单来说,设备既可以使用存储在 MongoDB 里的 username 和 password,也可以使用 JWT 来接入 EMQ X Broker。

EMQ X 在加载一个插件后,会把这个插件的名字写入 /data/loaded_plugins, EMQ X 在每次启动时都会自动加载这个文件里面包含的插件,所以我们只需要手动加载一次这两个插件就可以了。


在这一节我们讨论了 Maque IotHub 中设备的认证方式,以及 EMQ X 是如何支持这些认证方式的。下一节我们会写一些代码,把这些功能集成到 Maque IotHub 的设备注册流程中去。

推荐阅读 ?《从 0 开始搭建 IoT 平台》

注意!!! 为了方便学习和技术交流,特意创建了读者群,入群方式放在 第 1-5 课 文末,欢迎已购本课程的同学入群交流。

第 1-3 课:设备注册(二)

在本节课中,我们将设计和实现从设备注册到接入 IotHub 的主要流程

首先我们定义一下设备注册到接入 IotHub 的流程。

注册流程

  1. 业务系统调用 Maque IotHub Server API 的设备注册 API,提供要注册设备的 ProductName。
  2. Maque IotHub Server 根据业务系统提供的参数生成一个三元组(ProductName, DeviceName, Secret),然后将该三元组存储到 MongoDB,同时存储到 MongoDB 的还有该设备接入 EMQ X 的用户名: ProductName/DeviceName。
  3. Maque IotHub Server API 将生成的三元组返回给业务系统,业务系统应该保存这个三元组,以后调用 Maque IotHub Server API 时需要使用。
  4. 业务系统通过某种方式,例如烧写 Flash,将这个三元组”写”到物联网设备上。
  5. 设备应用代码调用 Maque IotHub DeviceSDK,传入三元组。
  6. Maque IotHub DeviceSDK 使用 username: ProductName/DeviceName, password: Secret连接到 EMQ X Broker。
  7. EMQ X Broker 到 MongoDB 里面查询 ProductName/DeviceName 和 Secret,如果匹配,则允许连接。

注册流程如下图所示。

从 0 开始搭建 IoT 平台

设备注册 API

接下在 IotHub_Server 项目里实现 Maque IotHub Server API 的设备注册 API:

我们在 MongoDB 里创建一个名为 IotHub 的数据来存储设备信息。

定义设备模型

这里,我们使用 mongoose 来做 MongoDB 相关的操作,首先定义 Device 模型:

// IotHub_Server/models/device.jsconst deviceSchema = new Schema({    //ProductName    product_name: {        type: String,        required: true    },    //DeviceName    device_name: {        type: String,        required: true,    },    //接入 EMQ X 时使用的 username    broker_username: {        type: String,        required: true    },    //secret    secret: {        type: String,        required: true,    }})

Restful API 实现

每次在生成新设备的时候,由系统自动生成 DeviceName 和 Secret,DeviceName 和 Secret 应该是随机且唯一的字符串,例如 UUID,这里,我们用 shortid 来生成稍短一点的随机唯一字符:

// routes/devices.js...router.post("/", function (req, res) {    var productName = req.body.product_name    var deviceName = shortid.generate();    var secret = shortid.generate();    var brokerUsername = `${productName}/${deviceName}`    var device = new Device({        product_name: productName,        device_name: deviceName,        secret: secret,        broker_username: brokerUsername    })    device.save(function (err) {        if(err){            res.status(500).send(err)        }else{            res.json({product_name: productName, device_name: deviceName, secret: secret})        }    })})...

接着我们将这个 router 挂载到 /devices 下面,并连接到 MongoDB:

//app.js...mongoose.connect('mongodb://iot:iot@localhost:27017/iothub', { useNewUrlParser: true })var deviceRouter = require('./routes/devices');app.use('/devices', deviceRouter);...

运行 bin/www 启动 Web 服务器,然后在命令行用 curl 调用这个接口:

curl -d "product_name=IotApp" -X POST http://localhost:3000/devices

输出为:{"product_name":"IotApp","device_name":"V5MyuncRK","secret":"GNxU20VYTZ"}

ProductName 包含的字符是有限制的,不能包含# / +以及 IotHub 预留的一些字符,为了演示,这里跳过了输入参数的校验,但是在实际项目中,是需要加上的。

到这里,设备注册就成功了,我们需要记录下这个三元组。

修改 emqx_auth_mongo.conf

接下来需要按照我们定义的数据库结构来修改 EMQ X MongoDB 认证插件的配置,下面是需要在上一节内容上修改的项:

# 存储用户名和密码的 databaseauth.mongo.database = iothub# 存储用户名和密码的 collectionauth.mongo.auth_query.collection = devices# 密码字段auth.mongo.auth_query.password_field = secret# 查询记录时的 selectorauth.mongo.auth_query.selector = broker_username=%u

编辑完成以后重载下 MongDB 认证插件: /bin/emqx_ctl plugins reload emqx_auth_mongo

修改 DeviceSDK

接下在 IoTHub_Device 项目里对 DeviceSDK 进行修改,接受三元组作为初始化参数:

// sdk/iot_device.js...class IotDevice extends EventEmitter {    constructor({serverAddress = "127.0.0.1:8883", productName, deviceName, secret} = {}) {        super();        this.serverAddress = `mqtts://${serverAddress}`        this.productName = productName        this.deviceName = deviceName        this.secret = secret        this.username = `${this.productName}/${this.deviceName}`    }    connect() {        this.client = mqtt.connect(this.serverAddress, {            rejectUnauthorized: false            username: this.username,            password: this.secret        })        ...    }    ...}   ...

然后我们用刚才记录下的三元组作为参数调用 DeviceSDK 接入 Maque IotHub:

// samples/connect_to_server.js...var device = new IotDevice({productName: "IotApp", deviceName: "V5MyuncRK", secret: "GNxU20VYTZ"})...

然后再运行samples/connect_to_server.js,会得到以下输出:

device is online

这说明设备已经完成注册并成功接入 IotHub 了。


这一节我们完成了设备注册到接入的主要流程,下一节,我们将继续完善细节。

推荐阅读 ?《从 0 开始搭建 IoT 平台》

注意!!! 为了方便学习和技术交流,特意创建了读者群,入群方式放在 第 1-5 课 文末,欢迎已购本课程的同学入群交流。

第 1-4 课:设备注册(三)

这一节,我们将对设备注册接入的细节进行完善。

添加数据库索引

我们需要对 Devices 的 product_name 和 device_name 做一个索引,因为在后面会经常通过这两个字段对 devices 进行查询,在 MongoDB shell 里面输入:

use iothubdb.devices.createIndex({    "production_name" : 1,    "device_name" : 1}, { unique: true })

MongoDB 插件在每次设备接入的时候都会使用 brokername 来查询 Devices Collectiion, 所以我们也需要在 brokername 上加一个索引:

use iothubdb.devices.createIndex({    "broker_username" : 1})

使用持久化连接

细心的读者可能已经发现了, DeviceSDK 在连接到 Broker 的时候并没有指定 Client Identifier。没错,到目前为止,我们使用的都是在连接时自动分配的 Client Identifer, 没有办法很好地使用 QoS1 和 QoS2 的消息。

Client Identifier 是用来唯一标识 MQTT Client 的,由于我们之前的设计保证了(ProductName, DeviceName)是全局唯一的,所以一般来说用这个二元组作为 Client Identifier 就足够了。 但是,之前我也提到过,在某些场景下,可能会出现多个设备使用同样的设备三元组接入 Maque IotHub,综合这些情况,我们这样来设计 Maque IotHub 里的 Client Identifier。

设备提供一个可选的 ClientID 来标识自己,可以是硬件编号、AndroidID 等,如果设备提供 ClientID,那么使用 ProductName/DeviceName/ClientID 作为连接 Broker 的Client Identifier,否则使用ProductName/DeviceName。 根据这个规则对 DeviceSDK 进行修改。

// IotHub_Device/sdk/iot_devices.js...class IotDevice extends EventEmitter {    constructor({serverAddress = "127.0.0.1:8883", productName, deviceName, secret, clientID} = {}) {        super();        this.serverAddress = `mqtts://${serverAddress}`        this.productName = productName        this.deviceName = deviceName        this.secret = secret        this.username = `${this.productName}/${this.deviceName}`        //根据 ClientID 设置        if(clientID != null){            this.clientIdentifier = `${this.username}/${clientID}`        }else{            this.clientIdentifier = this.username        }    }    connect() {        this.client = mqtt.connect(this.serverAddress, {            rejectUnauthorized: false,            username: this.username,            password: this.secret,            //设置 ClientID 和 clean session            clientId: this.clientIdentifier,            clean: false        })        ...   } ... 

之后你可以再运行一次samples/connect_to_server.js看下效果。

Node.js 的 MQTT 库自带了断线重连功能,所以这里就不用我们来实现了。

更多的 Server API

我们还需要几个接口来完善注册流程.

获取某个设备的信息

当业务系统查询设备信息的时候,我们并不是把 Device 的所有字段都返回。首先定义下返回内容:

// IotHub_Server/models/device.js//定义 device.toJSONObjectdeviceSchema.methods.toJSONObject = function () {    return {        product_name: this.product_name,        device_name: this.device_name,        secret: this.secret    }}

然后进行接口实现:

// IotHub_Server/routes/devices.jsrouter.get("/:productName/:deviceName", function (req, res) {    var productName = req.params.productName    var deviceName = req.params.deviceName    Device.findOne({"product_name": productName, "device_name": deviceName}, function (err, device) {        if (err) {            res.send(err)        } else {            if (device != null) {                res.json(device.toJSONObject())            } else {                res.status(404).json({error: "Not Found"})            }        }    })})
curl http://localhost:3000/devices/IotApp/V5MyuncRK{"product_name":"IotApp","device_name":"V5MyuncRK","secret":"GNxU20VYTZ"}

列出某个产品下的所有设备

// IotHub_Server/routes/devices.jsrouter.get("/:productName", function (req, res) {    var productName = req.params.productName    Device.find({"product_name": productName}, function (err, devices) {        if (err) {            res.send(err)        } else {            res.json(devices.map(function (device) {                return device.toJSONObject()            }))        }    })})
curl http://localhost:3000/devices/IotApp[{"product_name":"IotApp","device_name":"V5MyuncRK","secret":"GNxU20VYTZ"}]

获取接入 Broker 的一次性密码(JWT)

// IotHub_Server/routes/tokens.jsvar express = require('express');var router = express.Router();var shortid = require("shortid")var jwt = require('jsonwebtoken')//这个值应该和 EMQ X etc/plugins/emqx_auth_jwt.conf 中的保存一致const jwtSecret = "emqxsecret"router.post("/", function (_, res) {    var username = shortid.generate()    var password = jwt.sign({        username: username,        exp: Math.floor(Date.now() / 1000) + 10 * 60    }, jwtSecret)    res.json({username: username, password: password})})module.exports = router
// IotHub_Server/app.jsvar tokensRouter = require('./routes/tokens')app.use('/tokens', tokensRouter)

通过这个接口,可以签发一个有效期为 1 分钟的 username/password:

curl http://localhost:3000/tokens -X POST{"username":"apmE_JPll","password":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFwbUVfSlBsbCIsImV4cCI6MTU2ODMxNjk2MSwiaWF0IjoxNTU4MzE2OTYxfQ.-SnqvBGdO3wjSu7IHR91Bo58gb-VLFuQ28BeN6hlTLk"}

大家可能还发现了,在 ServerAPI 里面没有对调用者的身份进行认证和权限控制,也没有对输入参数进行校验,输出列表时也没有进行分页等的处理,当然在实际的项目中,这些都是有必要的。 但是这些属于 Web 编程的范畴,我想大家应该都非常熟悉了,所以在本课程中就跳过了,让课程的内容紧贴主题。

从环境变量中读取配置

根据 The Twelve-Factor App 的理念,从环境变量中读取配置项是一个非常好的 Practice,在我们的项目中有两个地方要用到配置:

  • ServerAPI,比如 mongoDB 的地址;
  • DeviceSDK 端的 samples 里的代码会经常使用到预先注册的三元组(ProductName, DeviceName, Secret)。

这里我们使用 dotenv 来管理环境变量,它可以从一个 .env 文件中读取并设置环境变量。

// IotHub_Server/app.js require('dotenv').config()mongoose.connect(process.env.MONGODB_URL, { useNewUrlParser: true })
// IotHub_Server/routes/tokens.jsconst jwtSecret = process.env.JWT_SECRET
# IotHub_Server/.envMONGODB_URL=mongodb://iot:iot@localhost:27017/iothubJWT_SECRET=emqxsecret
// IotHub_Device/samples/connect_to_server.jsrequire('dotenv').config()var device = new IotDevice({    productName: process.env.PRODUCT_NAME,    deviceName: process.env.DEVICE_NAME,    secret: process.env.SECRET})
# otHub_Device/samples/.envPRODUCT_NAME=注册接口获取的 ProductNameDEVICE_NAME=注册接口获取的 DeviceNameSECRET=注册接口获取的 Secret

在这一节里,我们补全了设备注册流程的所有功能,完善了细节,接下来我们看如何实现监控设备的在线状态。

推荐阅读 ?《从 0 开始搭建 IoT 平台》

注意!!! 为了方便学习和技术交流,特意创建了读者群,入群方式放在 第 1-5 课 文末,欢迎已购本课程的同学入群交流。

附录: 如何运行 Maque IotHub
第 1-5 课:设备在线状态管理(一)
第 1-6 课:设备在线状态管理(二)
第 1-7 课:设备禁用与删除
第 1-8 课:设备权限管理
第 1-9 课:加一点扩展性
第 2-1 课:选择一个可扩展的上行数据处理方案
第 2-2 课:功能设计
第 2-3 课:实现(一)
第 2-4 课:实现(二)
第 2-5 课:设备状态上报
第 2-6 课:时序数据库
第 3-1 课:选择下行数据处理方案
第 3-2 课:功能设计
第 3-3 课:设备端实现
第 3-4 课:服务端实现(一)
第 3-5 课:服务端实现(二)
第 4-1 课:RPC 式调用(一)
第 4-2 课:RPC 式调用(二)
第 4-3 课:设备数据请求
第 4-4 课:NTP 服务
第 4-5 课:设备分组——功能设计
第 4-6 课:设备分组——服务端实现
第 4-7 课:设备分组——设备端实现
第 4-8 课:设备间通信
第 4-9 课:OTA 升级——功能设计
第 4-10 课:OTA 升级——服务端实现
第 4-11 课:OTA 升级——设备端实现
第 4-12 课:设备影子概览
第 4-13 课:设备影子——服务端实现
第 4-14 课:设备影子——设备端实现
第 4-15 课:IotHub 状态监控
第 5-1 课:EMQ X 的插件系统
第 5-2 课:我们会用到的 Erlang 特性
第 5-3 课:搭建开发和编译环境
第 5-4 课:编写 emqx-rabbitmq-hook(一)
第 5-5 课:编写 emqx-rabbitmq-hook(二)
第 5-6 课:使用 emqx-rabbitmq-hook
第 6-1 课:CoAP 简介
第 6-2 课:IotHub 接入 CoAP

阅读全文: http://gitbook.cn/gitchat/column/5d3a7c335cb084142168b3fc

本文章来源于互联网,如有侵权,请联系删除!原文地址:从 0 开始搭建 IoT 平台