在 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 秒创建一条消息,并将该消息传递给记录器模块。
  • 记录器 模块将接收的消息写入文件。

使用 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 环境变量。 转到“控制面板” > “系统和安全” > “系统” > “高级系统设置” > “环境变量”。 将 C:\Python27 添加到路径。

  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 模块:build\modules\logger\Debug\logger.dllbuild\modules\hello_world\Debug\hello_world.dll
  • 可执行文件:build\samples\hello_world\Debug\hello_world_sample.exe。 此过程使用 JSON 配置文件作为命令行参数。

在此示例中使用的第四个文件不在生成文件夹中,但克隆它时 iot-edge 存储库中包括:

  • JSON 配置文件:samples\hello_world\src\hello_world_win.json。 此文件包含两个模块的路径。 它还声明 logger.dll 将其输出写入到的位置。 默认值是当前工作目录中的 log.txt

    Note

    如果移动示例模块,或者为测试添加自己的模块,请更新配置文件中的 module.path 值以进行匹配。 模块路径相对于 hello_world_sample.exe 所在的目录。

若要运行该示例,请遵循以下步骤:

  1. 导航到 iot-edge 存储库本地副本根目录中的 build 文件夹。

  2. 运行以下命令:

    samples\hello_world\Debug\hello_world_sample.exe ..\samples\hello_world\src\hello_world_win.json
    
  3. 下面的输出表示示例成功运行:

    gateway successfully created from JSON
    gateway shall run until ENTER is pressed
    
  4. Enter 键停止该进程。

典型输出

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

[{
    "time": "Mon Apr 11 13:42:50 2016",
    "content": "Log started"
}, {
    "time": "Mon Apr 11 13:42:50 2016",
    "properties": {
        "helloWorld": "from Azure IoT Gateway SDK simple sample!"
    },
    "content": "aGVsbG8gd29ybGQ="
}, {
    "time": "Mon Apr 11 13:42:55 2016",
    "properties": {
        "helloWorld": "from Azure IoT Gateway SDK simple sample!"
    },
    "content": "aGVsbG8gd29ybGQ="
}, {
    "time": "Mon Apr 11 13:43:00 2016",
    "properties": {
        "helloWorld": "from Azure IoT Gateway SDK simple sample!"
    },
    "content": "aGVsbG8gd29ybGQ="
}, {
    "time": "Mon Apr 11 13:45:00 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 模块永远不会处理其他 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 模块接收来自中转站的消息,并将其写入文件中。 它不发布任何消息。 因此,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 中心的示例,请参阅: