在 Windows 上浏览 Azure IoT Edge 体系结构

本文详细演练 Hello World 示例代码,演示 Azure IoT Edge 体系结构的基本组件。 该示例使用 Azure IoT Edge 生成一个简单的网关,每隔 5 秒将“hello world”消息记录到文件中。

本文介绍的内容包括:

  • Hello World 示例体系结构:说明如何将 Azure IoT Edge 体系结构概念应用到 Hello World 示例,以及如何将这些组件组合到一起。
  • 如何生成示例:生成示例所需的步骤。
  • 如何运行示例:运行示例所需的步骤。
  • 典型输出:运行示例时预期获得的输出的示例。
  • 代码片段:代码片段的集合,显示 Hello World 示例如何实现重要的 IoT Edge 网关组件。

Hello World 示例体系结构

Hello World 示例体现了上一部分所述概念。 Hello World 示例所实现的 IoT Edge 网关具有一个管道,该管道包含两个 IoT Edge 模块:

  • hello world 模块每 5 秒创建一条消息,并将该消息传递给 logger 模块。
  • Logger 模块将接收的消息写入文件。

使用 Azure IoT Edge 构建的 Hello World 示例的体系结构

如前一部分所述,Hello World 模块并不是每隔 5 秒就直接将消息传递给记录器模块, 而是每隔 5 秒将消息发布到中转站。

Logger 模块从中转站接收消息并对消息执行操作,将消息内容写入文件。

Logger 模块只使用来自中转站的消息,而不会将新消息发布到中转站。

中转站如何在 Azure IoT Edge 中的模块之间路由消息

上图显示了 Hello World 示例的体系结构,同时显示了在 存储库中对示例不同部分进行实现的源文件的相对路径。 请自行浏览代码,或使用下面的代码段作为指导。

安装必备组件

  1. 安装 Visual Studio 2015 或 2017。 如果满足授权要求,可以使用免费的社区版。 请务必包含 Visual C++ 和 NuGet 包管理器。

  2. 安装 git 并确保可从命令行运行 git.exe。

  3. 安装 CMake 并确保可从命令行运行 cmake.exe。 建议使用 CMake 版本 3.7.2 或更高版本。 .msi 安装程序是 Windows 上最简单的选项。 安装程序提示时,至少为当前用户将 CMake 添加到 PATH。

  4. 安装 Python 2.7。 确保通过“控制面板”->“系统”->“高级系统设置”->“环境变量”将 Python 添加到 PATH 环境变量。

  5. 在命令提示符中,运行以下命令,将 Azure IoT Edge GitHub 存储库克隆到本地计算机上:

    git clone https://github.com/Azure/iot-edge.git
    

如何生成示例

现在可在本地计算机上生成 IoT Edge 运行时和示例:

  1. 打开“VS 2015 开发人员命令提示”或“VS 2017 开发人员命令提示”命令提示符。

  2. 浏览到 iot-edge 存储库本地副本中的根文件夹。

  3. 如下所示运行生成脚本:

    tools\build.cmd --disable-native-remote-modules
    

此脚本创建 Visual Studio 解决方案文件并生成解决方案。 可以在 iot-edge 存储库本地副本的 build 文件夹中找到 Visual Studio 解决方案。 如果想要生成并运行单元测试,请添加 --run-unittests 参数。 如果想要生成并运行端到端测试,请添加 --run-e2e-tests

Note

每次运行 build.cmd 脚本时,都会删除 iot-edge 存储库本地副本的根文件夹中的 build 文件夹并重新生成。

如何运行示例

build.cmd 脚本在 iot-edge 存储库本地副本的 build 文件夹中生成输出。 此输出包括此示例中使用的两个 IoT Edge 模块。

生成脚本将 logger.dll 放在 build\modules\logger\Debug 文件夹中,将 hello_world.dll 放在 build\modules\hello_world\Debug 文件夹中。 如以下 JSON 设置文件中所示,将这些路径用于 module path 值。

hello_world_sample 过程使用 JSON 配置文件的路径作为命令行参数。 以下示例 JSON 文件在 SDK 存储库的以下路径中提供:samples\hello_world\src\hello_world_win.json。 除非修改了生成脚本,将 IoT Edge 模块或示例可执行文件放置在非默认位置,否则,此配置文件可按原样工作。

Note

模块路径相对于 hello_world_sample.exe 所在的目录。 示例 JSON 配置文件默认为在当前工作目录中写入“log.txt”。

{
  "modules": [
    {
      "name": "logger",
      "loader": {
        "name": "native",
        "entrypoint": {
          "module.path": "..\\..\\..\\modules\\logger\\Debug\\logger.dll"
        }
      },
      "args": { "filename": "log.txt" }
    },
    {
      "name": "hello_world",
      "loader": {
        "name": "native",
        "entrypoint": {
          "module.path": "..\\..\\..\\modules\\hello_world\\Debug\\hello_world.dll"
        }
      },
      "args": null
      }
  ],
  "links": [
    {
      "source": "hello_world",
      "sink": "logger"
    }
  ]
}
  1. 导航到 iot-edge 存储库本地副本根目录中的 build 文件夹。

  2. 运行以下命令:

    samples\hello_world\Debug\hello_world_sample.exe ..\samples\hello_world\src\hello_world_win.json
    

典型输出

下面的示例演示由 Hello World 示例写入日志文件的输出。 为方便阅读,输出已设置格式:

[{
    "time": "Mon Apr 11 13:48:07 2016",
    "content": "Log started"
}, {
    "time": "Mon Apr 11 13:48:48 2016",
    "properties": {
        "helloWorld": "from Azure IoT Gateway SDK simple sample!"
    },
    "content": "aGVsbG8gd29ybGQ="
}, {
    "time": "Mon Apr 11 13:48:55 2016",
    "properties": {
        "helloWorld": "from Azure IoT Gateway SDK simple sample!"
    },
    "content": "aGVsbG8gd29ybGQ="
}, {
    "time": "Mon Apr 11 13:49:01 2016",
    "properties": {
        "helloWorld": "from Azure IoT Gateway SDK simple sample!"
    },
    "content": "aGVsbG8gd29ybGQ="
}, {
    "time": "Mon Apr 11 13:49:04 2016",
    "content": "Log stopped"
}]

代码片段

本部分讨论 hello_world 示例中代码的重要部分。

创建 IoT Edge 网关

必须实现一个网关进程。 此程序创建内部基础结构(中转站)、加载 IoT Edge 模块,以及配置网关进程。 IoT Edge 提供 Gateway_Create_From_JSON 函数,用于从 JSON 文件启动网关。 若要使用 Gateway_Create_From_JSON 函数,请将 JSON 文件的路径传递给它,以便指定要加载的 IoT Edge 模块。

可以在 Hello World 示例的 main.c 文件中找到网关进程的代码。 为了增强可读性,以下代码片段显示的是简化版网关进程代码。 此示例程序创建一个网关,在解除该网关之前,会等待用户按 ENTER 键。

int main(int argc, char** argv)
{
    GATEWAY_HANDLE gateway;
    if ((gateway = Gateway_Create_From_JSON(argv[1])) == NULL)
    {
        printf("failed to create the gateway from JSON\n");
    }
    else
    {
        printf("gateway successfully created from JSON\n");
        printf("gateway shall run until ENTER is pressed\n");
        (void)getchar();
        Gateway_LL_Destroy(gateway);
    }
    return 0;
}

JSON 设置文件包含要加载的 IoT Edge 模块的列表以及模块之间的链接。 每个 IoT Edge 模块必须指定以下项:

  • name:模块的唯一名称。
  • loader:一个知道如何加载所需模块的加载程序。 加载程序是一个扩展点,用于加载不同类型的模块。 IoT Edge 提供了用于以原生 C、Node.js、Java 和 .NET 编写的模块的加载程序。 Hello World 示例仅使用了本机 C 加载程序,因为此示例中的所有模块都是以 C 编写的动态库。有关如何使用以不同语言编写的 IoT Edge 模块的详细信息,请参阅 Node.jsJava.NET 示例。

    • name:用来加载模块的加载程序的名称。
    • entrypoint:包含模块的库的路径。 在 Linux 上,此库是一个 .so 文件;在 Windows 上,此库是一个 .dll 文件。 该入口点特定于所使用的加载程序的类型。 Node.js 加载程序入口点是一个 .js 文件。 Java 加载程序入口点是类路径加类名。 .NET 加载程序入口点是程序集名加类名。
  • args:模块所需的任何配置信息。

以下代码显示了 Linux 上用来声明 Hello World 示例的所有 IoT Edge 模块的 JSON。 模块是否需要参数取决于模块的设计。 在此示例中,logger 模块使用的参数是输出文件的路径,而 hello_world 模块不使用任何参数。

"modules" :
[
    {
        "name" : "logger",
        "loader": {
          "name": "native",
          "entrypoint": {
            "module.path": "./modules/logger/liblogger.so"
        }
        },
        "args" : {"filename":"log.txt"}
    },
    {
        "name" : "hello_world",
        "loader": {
          "name": "native",
          "entrypoint": {
            "module.path": "./modules/hello_world/libhello_world.so"
        }
        },
        "args" : null
    }
]

JSON 文件还包含要传递到中转站的模块之间的链接。 链接具有两个属性:

  • source:来自 modules 部分的模块名称,或 \*
  • 接收器:来自 modules 部分的模块名称。

每个链接都会定义消息路由和方向。 来自 source 模块的消息将传递到 sink 模块。 可将 source 模块设置为 \*,用于指示 sink 模块接收来自任何模块的消息。

以下代码显示了 Linux 上用来配置 hello_world 示例中所用模块之间的链接的 JSON。 模块 hello_world 生成的每条消息将由模块 logger 使用。

"links":
[
    {
        "source": "hello_world",
        "sink": "logger"
    }
]

Hello_world 模块消息发布

可在“hello_world.c”文件中找到 hello_world 模块发布消息时使用的代码。 以下代码片段显示修改的代码版本,其中添加了注释,并删除了部分处理错误的代码以提高可读性:

int helloWorldThread(void *param)
{
    // create data structures used in function.
    HELLOWORLD_HANDLE_DATA* handleData = param;
    MESSAGE_CONFIG msgConfig;
    MAP_HANDLE propertiesMap = Map_Create(NULL);

    // add a property named "helloWorld" with a value of "from Azure IoT
    // Gateway SDK simple sample!" to a set of message properties that
    // will be appended to the message before publishing it. 
    Map_AddOrUpdate(propertiesMap, "helloWorld", "from Azure IoT Gateway SDK simple sample!")

    // set the content for the message
    msgConfig.size = strlen(HELLOWORLD_MESSAGE);
    msgConfig.source = HELLOWORLD_MESSAGE;

    // set the properties for the message
    msgConfig.sourceProperties = propertiesMap;

    // create a message based on the msgConfig structure
    MESSAGE_HANDLE helloWorldMessage = Message_Create(&msgConfig);

    while (1)
    {
        if (handleData->stopThread)
        {
            (void)Unlock(handleData->lockHandle);
            break; /*gets out of the thread*/
        }
        else
        {
            // publish the message to the broker
            (void)Broker_Publish(handleData->brokerHandle, helloWorldMessage);
            (void)Unlock(handleData->lockHandle);
        }

        (void)ThreadAPI_Sleep(5000); /*every 5 seconds*/
    }

    Message_Destroy(helloWorldMessage);

    return 0;
}

Hello_world 模块消息处理

hello_world 模块永远不会处理其他 IoT Edge 模块发布到中转站的消息。 因此,hello_world 模块中的消息回调的实现是一个 no-op 函数。

static void HelloWorld_Receive(MODULE_HANDLE moduleHandle, MESSAGE_HANDLE messageHandle)
{
    /* No action, HelloWorld is not interested in any messages. */
}

记录器模块消息发布和处理

logger 模块接收来自中转站的消息,并将其写入文件中。 它不发布任何消息。 因此,Logger 模块的代码不会调用 Broker_Publish 函数。

logger.c 文件中的 Logger_Receive 函数是中转站发起的回叫,用于将消息传递给 Logger 模块。 以下代码片段显示修改的版本,其中添加了注释,并删除了部分处理错误的代码以提高可读性:

static void Logger_Receive(MODULE_HANDLE moduleHandle, MESSAGE_HANDLE messageHandle)
{

    time_t temp = time(NULL);
    struct tm* t = localtime(&temp);
    char timetemp[80] = { 0 };

    // Get the message properties from the message
    CONSTMAP_HANDLE originalProperties = Message_GetProperties(messageHandle); 
    MAP_HANDLE propertiesAsMap = ConstMap_CloneWriteable(originalProperties);

    // Convert the collection of properties into a JSON string
    STRING_HANDLE jsonProperties = Map_ToJSON(propertiesAsMap);

    //  base64 encode the message content
    const CONSTBUFFER * content = Message_GetContent(messageHandle);
    STRING_HANDLE contentAsJSON = Base64_Encode_Bytes(content->buffer, content->size);

    // Start the construction of the final string to be logged by adding
    // the timestamp
    STRING_HANDLE jsonToBeAppended = STRING_construct(",{\"time\":\"");
    STRING_concat(jsonToBeAppended, timetemp);

    // Add the message properties
    STRING_concat(jsonToBeAppended, "\",\"properties\":"); 
    STRING_concat_with_STRING(jsonToBeAppended, jsonProperties);

    // Add the content
    STRING_concat(jsonToBeAppended, ",\"content\":\"");
    STRING_concat_with_STRING(jsonToBeAppended, contentAsJSON);
    STRING_concat(jsonToBeAppended, "\"}]");

    // Write the formatted string
    LOGGER_HANDLE_DATA *handleData = (LOGGER_HANDLE_DATA *)moduleHandle;
    addJSONString(handleData->fout, STRING_c_str(jsonToBeAppended);
}

后续步骤

本文中运行了将消息写入日志文件的简单 IoT Edge 网关。 若要运行将消息发送到 IoT 中心 的示例,请参阅 IoT Edge - 通过模拟设备使用 Linux 发送设备到云的消息IoT Edge - 通过模拟设备使用 Windows 发送设备到云的消息