适用于 C 语言的 Azure IoT 设备 SDK - 有关序列化程序的详细信息Azure IoT device SDK for C – more about serializer

本系列中的第一篇文章介绍了适用于 C 语言的 Azure IoT 设备 SDK 简介。下一篇文章提供了适用于 C 语言的 Azure IoT 设备 SDK - IoTHubClient 的更详细说明。The first article in this series introduced the Introduction to Azure IoT device SDK for C. The next article provided a more detailed description of the Azure IoT device SDK for C -- IoTHubClient. 本文最后的部分提供该 SDK 的剩余组件序列化程序库的更详细说明。This article completes coverage of the SDK by providing a more detailed description of the remaining component: the serializer library.

备注

本文中提到的某些功能(例如云到设备消息传递、设备孪生、设备管理)仅在 IoT 中心的标准层中提供。Some of the features mentioned in this article, like cloud-to-device messaging, device twins, and device management, are only available in the standard tier of IoT hub. 有关基本和标准 IoT 中心层的详细信息,请参阅如何选择合适的 IoT 中心层For more information about the basic and standard IoT Hub tiers, see How to choose the right IoT Hub tier.

该介绍性文章描述了如何使用序列化程序库将事件发送到 IoT 中心和从该中心接收消息。The introductory article described how to use the serializer library to send events to and receive messages from IoT Hub. 在此文章中,通过提供如何使用序列化程序宏语言对数据建模的更完整说明,扩展了这个讨论。In this article, we extend that discussion by providing a more complete explanation of how to model your data with the serializer macro language. 本文还包含更多有关该库如何序列化消息(以及在某些情况下,如何控制序列化行为)的详细信息。The article also includes more detail about how the library serializes messages (and in some cases how you can control the serialization behavior). 此外还会说明某些可以修改的参数,这些参数决定所要创建的模型大小。We'll also describe some parameters you can modify that determine the size of the models you create.

最后,本文将回顾前面文章中讲到的一些主题,例如消息和属性处理。Finally, the article revisits some topics covered in previous articles such as message and property handling. 正如我们了解的那样,这些功能使用序列化程序库的方式与使用 IoTHubClient 库一样。As we'll find out, those features work in the same way using the serializer library as they do with the IoTHubClient library.

本文中所述的所有内容都基于 序列化程序 SDK 示例。Everything described in this article is based on the serializer SDK samples. 如果想要继续,请参阅适用于 C 语言的 Azure IoT 设备 SDK 中包含的 simplesample_amqpsimplesample_http 应用程序。If you want to follow along, see the simplesample_amqp and simplesample_http applications included in the Azure IoT device SDK for C.

可在 GitHub 存储库中找到适用于 C 语言的 Azure IoT 设备 SDK,还可在 C API 参考中查看 API 的详细信息。You can find the Azure IoT device SDK for C GitHub repository and view details of the API in the C API reference.

建模语言The modeling language

本系列中的适用于 C 语言的 Azure IoT 设备 SDK 一文通过 simplesample_amqp 应用程序中提供的示例,介绍了适用于 C 语言的 Azure IoT 设备 SDK 建模语言:The Azure IoT device SDK for C article in this series introduced the Azure IoT device SDK for C modeling language through the example provided in the simplesample_amqp application:

BEGIN_NAMESPACE(WeatherStation);

DECLARE_MODEL(ContosoAnemometer,
WITH_DATA(ascii_char_ptr, DeviceId),
WITH_DATA(double, WindSpeed),
WITH_ACTION(TurnFanOn),
WITH_ACTION(TurnFanOff),
WITH_ACTION(SetAirResistance, int, Position)
);

END_NAMESPACE(WeatherStation);

如你所见,建模语言基于 C 宏。As you can see, the modeling language is based on C macros. 定义请始终以 BEGIN_NAMESPACE 开头,始终以 END_NAMESPACE 结尾。You always begin your definition with BEGIN_NAMESPACE and always end with END_NAMESPACE. 我们通常要为公司的命名空间命名,或者如同本示例一样,为正在处理的项目命名。It's common to name the namespace for your company or, as in this example, the project that you're working on.

在命名空间内部运行的是模型定义。What goes inside the namespace are model definitions. 在本例中,有一个风速计模型。In this case, there is a single model for an anemometer. 同样,可以任意命名此模型,但通常会针对想要与 IoT 中心交换的设备或数据类型来命名此模型。Once again, the model can be named anything, but typically the model is named for the device or type of data you want to exchange with IoT Hub.

模型包含可发送到 IoT 中心的事件(数据)以及可从 IoT 中心接收的消息(操作)的定义。Models contain a definition of the events you can ingress to IoT Hub (the data) as well as the messages you can receive from IoT Hub (the actions). 如同在示例中所见,事件具有类型和名称;操作具有一个名称和可选参数(各有一个类型)。As you can see from the example, events have a type and a name; actions have a name and optional parameters (each with a type).

本示例并未演示 SDK 支持的其他数据类型。What’s not demonstrated in this sample are additional data types that are supported by the SDK. 我们会在稍后讨论。We'll cover that next.

备注

IoT 中心将设备发送到它的数据作为事件,而建模语言将其作为数据(使用 WITH_DATA 进行定义)。IoT Hub refers to the data a device sends to it as events, while the modeling language refers to it as data (defined using WITH_DATA). 同样,IoT 中心将你发送到设备的数据作为消息,而建模语言将其作为操作(使用 WITH_ACTION 进行定义)。Likewise, IoT Hub refers to the data you send to devices as messages, while the modeling language refers to it as actions (defined using WITH_ACTION). 请注意,本文中可能会换用这些术语。Be aware that these terms may be used interchangeably in this article.

支持的数据类型Supported data types

利用 序列化程序 库创建的模型支持以下数据类型:The following data types are supported in models created with the serializer library:

类型Type 说明Description
doubledouble 双精度浮点数double precision floating point number
intint 32 位整数32 bit integer
floatfloat 单精度浮点数single precision floating point number
longlong 长整数long integer
int8_tint8_t 8 位整数8 bit integer
int16_tint16_t 16 位整数16 bit integer
int32_tint32_t 32 位整数32 bit integer
int64_tint64_t 64 位整数64 bit integer
boolbool 布尔值boolean
ascii_char_ptrascii_char_ptr ASCII 字符串ASCII string
EDM_DATE_TIME_OFFSETEDM_DATE_TIME_OFFSET 日期时间偏移date time offset
EDM_GUIDEDM_GUID GUIDGUID
EDM_BINARYEDM_BINARY binarybinary
DECLARE_STRUCTDECLARE_STRUCT 复杂数据类型complex data type

我们从最后一种数据类型开始。Let’s start with the last data type. DECLARE_STRUCT 可用来定义作为其他基元类型的分组的复杂数据类型。The DECLARE_STRUCT allows you to define complex data types, which are groupings of the other primitive types. 这些分组可让我们定义如下所示的模型:These groupings allow us to define a model that looks like this:

DECLARE_STRUCT(TestType,
double, aDouble,
int, aInt,
float, aFloat,
long, aLong,
int8_t, aInt8,
uint8_t, auInt8,
int16_t, aInt16,
int32_t, aInt32,
int64_t, aInt64,
bool, aBool,
ascii_char_ptr, aAsciiCharPtr,
EDM_DATE_TIME_OFFSET, aDateTimeOffset,
EDM_GUID, aGuid,
EDM_BINARY, aBinary
);

DECLARE_MODEL(TestModel,
WITH_DATA(TestType, Test)
);

我们的模型包含 TestType类型的单个数据事件。Our model contains a single data event of type TestType. TestType 是包含多个成员的复杂类型,它们共同演示了序列化程序建模语言支持的基元类型。TestType is a complex type that includes several members, which collectively demonstrate the primitive types supported by the serializer modeling language.

使用类似于这样的模型,我们可以编写代码,以将数据发送到 IoT 中心,如下所示:With a model like this, we can write code to send data to IoT Hub that appears as follows:

TestModel* testModel = CREATE_MODEL_INSTANCE(MyThermostat, TestModel);

testModel->Test.aDouble = 1.1;
testModel->Test.aInt = 2;
testModel->Test.aFloat = 3.0f;
testModel->Test.aLong = 4;
testModel->Test.aInt8 = 5;
testModel->Test.auInt8 = 6;
testModel->Test.aInt16 = 7;
testModel->Test.aInt32 = 8;
testModel->Test.aInt64 = 9;
testModel->Test.aBool = true;
testModel->Test.aAsciiCharPtr = "ascii string 1";

time_t now;
time(&now);
testModel->Test.aDateTimeOffset = GetDateTimeOffset(now);

EDM_GUID guid = { { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F } };
testModel->Test.aGuid = guid;

unsigned char binaryArray[3] = { 0x01, 0x02, 0x03 };
EDM_BINARY binaryData = { sizeof(binaryArray), &binaryArray };
testModel->Test.aBinary = binaryData;

SendAsync(iotHubClientHandle, (const void*)&(testModel->Test));

基本而言,我们要将值赋给 Test 结构的每个成员,然后调用 SendAsync 以将 Test 数据事件发送到云。Basically, we’re assigning a value to every member of the Test structure and then calling SendAsync to send the Test data event to the cloud. SendAsync 是一个帮助器函数,它将单个数据事件发送到 IoT 中心:SendAsync is a helper function that sends a single data event to IoT Hub:

void SendAsync(IOTHUB_CLIENT_LL_HANDLE iotHubClientHandle, const void *dataEvent)
{
    unsigned char* destination;
    size_t destinationSize;
    if (SERIALIZE(&destination, &destinationSize, *(const unsigned char*)dataEvent) ==
    {
        // null terminate the string
        char* destinationAsString = (char*)malloc(destinationSize + 1);
        if (destinationAsString != NULL)
        {
            memcpy(destinationAsString, destination, destinationSize);
            destinationAsString[destinationSize] = '\0';
            IOTHUB_MESSAGE_HANDLE messageHandle = IoTHubMessage_CreateFromString(destinationAsString);
            if (messageHandle != NULL)
            {
                IoTHubClient_SendEventAsync(iotHubClientHandle, messageHandle, sendCallback, (void*)0);

                IoTHubMessage_Destroy(messageHandle);
            }
            free(destinationAsString);
        }
        free(destination);
    }
}

此函数序列化给定的数据事件,并使用 IoTHubClient_SendEventAsync 将其发送到 IoT 中心。This function serializes the given data event and sends it to IoT Hub using IoTHubClient_SendEventAsync. 这是在先前的文章中讨论的相同代码(SendAsync 将逻辑封装到一个方便访问的函数)。This is the same code discussed in previous articles (SendAsync encapsulates the logic into a convenient function).

在以前的代码中使用的另一个帮助器函数是 GetDateTimeOffsetOne other helper function used in the previous code is GetDateTimeOffset. 此函数将给定的时间转换为 EDM_DATE_TIME_OFFSET 类型的值:This function transforms the given time into a value of type EDM_DATE_TIME_OFFSET:

EDM_DATE_TIME_OFFSET GetDateTimeOffset(time_t time)
{
    struct tm newTime;
    gmtime_s(&newTime, &time);
    EDM_DATE_TIME_OFFSET dateTimeOffset;
    dateTimeOffset.dateTime = newTime;
    dateTimeOffset.fractionalSecond = 0;
    dateTimeOffset.hasFractionalSecond = 0;
    dateTimeOffset.hasTimeZone = 0;
    dateTimeOffset.timeZoneHour = 0;
    dateTimeOffset.timeZoneMinute = 0;
    return dateTimeOffset;
}

如果运行此代码,就会将以下消息发送到 IoT 中心:If you run this code, the following message is sent to IoT Hub:

{"aDouble":1.100000000000000, "aInt":2, "aFloat":3.000000, "aLong":4, "aInt8":5, "auInt8":6, "aInt16":7, "aInt32":8, "aInt64":9, "aBool":true, "aAsciiCharPtr":"ascii string 1", "aDateTimeOffset":"2015-09-14T21:18:21Z", "aGuid":"00010203-0405-0607-0809-0A0B0C0D0E0F", "aBinary":"AQID"}

请注意,序列化采用 JSON,这是由序列化程序库生成的格式。Note that the serialization is in JSON, which is the format generated by the serializer library. 另请注意,序列化 JSON 对象的每个成员都与模型中定义的 TestType 成员匹配。Also note that each member of the serialized JSON object matches the members of the TestType that we defined in our model. 值也与代码中使用的值完全匹配。The values also exactly match those used in the code. 但是,请注意,二进制数据是 base64 编码的:“AQID”是 {0x01, 0x02, 0x03} 的 base64 编码。However, note that the binary data is base64-encoded: "AQID" is the base64 encoding of {0x01, 0x02, 0x03}.

此示例演示使用 序列化程序 库的优点 -- 它可让我们将 JSON 发送到云,而不需要在应用程序中显式处理序列化。This example demonstrates the advantage of using the serializer library -- it enables us to send JSON to the cloud, without having to explicitly deal with serialization in our application. 我们只需考虑如何在模型中设置数据事件的值,并调用简单的 API 将这些事件发送到云。All we have to worry about is setting the values of the data events in our model and then calling simple APIs to send those events to the cloud.

有了此信息,我们便可以定义包含受支持数据类型范围的模型,这些数据类型包括复杂类型(甚至可以包含其他复杂类型内的复杂类型)。With this information, we can define models that include the range of supported data types, including complex types (we could even include complex types within other complex types). 不过,上述示例生成的序列化 JSON 突显了一个重点。However, the serialized JSON generated by the example above brings up an important point. 如何 利用 序列化程序 库发送数据完全决定了 JSON 的构成形式。How we send data with the serializer library determines exactly how the JSON is formed. 此特定要点就是接下来要讨论的内容。That particular point is what we'll cover next.

有关序列化的详细信息More about serialization

上一部分重点讲述了 序列化程序 库生成的输出示例。The previous section highlights an example of the output generated by the serializer library. 在本部分,我们将说明该库如何将数据序列化,以及如何使用序列化 API 来控制该行为。In this section, we'll explain how the library serializes data and how you can control that behavior using the serialization APIs.

为了进一步讨论序列化,我们将使用一个基于恒温器的新模型。In order to advance the discussion on serialization, we'll work with a new model based on a thermostat. 首先,让我们针对所要尝试处理的方案提供一些背景信息。First, let's provide some background on the scenario we're trying to address.

我们想要为一个可测量温度和湿度的恒温器建模。We want to model a thermostat that measures temperature and humidity. 每个数据片段以不同的方式发送到 IoT 中心。Each piece of data is going to be sent to IoT Hub differently. 默认情况下,该恒温器每隔 2 分钟引入温度事件一次,每隔 15 分钟引入湿度事件一次。By default, the thermostat ingresses a temperature event once every 2 minutes; a humidity event is ingressed once every 15 minutes. 引入任一事件时,必须包含显示相应温度或湿度测量时间的时间戳。When either event is ingressed, it must include a timestamp that shows the time that the corresponding temperature or humidity was measured.

在此方案中,我们将演示两种不同的数据建模方式,并将说明该建模对序列化输出的影响。Given this scenario, we'll demonstrate two different ways to model the data, and we'll explain the effect that modeling has on the serialized output.

模型 1Model 1

以下是支持前述方案的第一个模型版本:Here's the first version of a model that supports the previous scenario:

BEGIN_NAMESPACE(Contoso);

DECLARE_STRUCT(TemperatureEvent,
int, Temperature,
EDM_DATE_TIME_OFFSET, Time);

DECLARE_STRUCT(HumidityEvent,
int, Humidity,
EDM_DATE_TIME_OFFSET, Time);

DECLARE_MODEL(Thermostat,
WITH_DATA(TemperatureEvent, Temperature),
WITH_DATA(HumidityEvent, Humidity)
);

END_NAMESPACE(Contoso);

请注意,该模型包含两个数据事件:TemperatureHumidityNote that the model includes two data events: Temperature and Humidity. 与上述示例不同,每个事件的类型是使用 DECLARE_STRUCT 定义的结构。Unlike previous examples, the type of each event is a structure defined using DECLARE_STRUCT. TemperatureEvent 包括温度测量和时间戳;HumidityEvent 包含湿度度量和时间戳。TemperatureEvent includes a temperature measurement and a timestamp; HumidityEvent contains a humidity measurement and a timestamp. 此模型可让我们以自然的方式为上述方案的数据建模。This model gives us a natural way to model the data for the scenario described above. 将事件发送到云时,将发送温度/时间戳对或湿度/时间戳对。When we send an event to the cloud, we'll either send a temperature/timestamp or a humidity/timestamp pair.

可以使用类似于下面的代码将温度事件发送到云:We can send a temperature event to the cloud using code such as the following:

time_t now;
time(&now);
thermostat->Temperature.Temperature = 75;
thermostat->Temperature.Time = GetDateTimeOffset(now);

unsigned char* destination;
size_t destinationSize;
if (SERIALIZE(&destination, &destinationSize, thermostat->Temperature) == IOT_AGENT_OK)
{
    sendMessage(iotHubClientHandle, destination, destinationSize);
}

在示例代码中,我们将针对温度和湿度使用硬编码值,但请想象成实际上是从恒温器上相应的传感器采样来检索这些值。We'll use hard-coded values for temperature and humidity in the sample code, but imagine that we’re actually retrieving these values by sampling the corresponding sensors on the thermostat.

上述代码使用前面介绍的帮助器 GetDateTimeOffsetThe code above uses the GetDateTimeOffset helper that was introduced previously. 此代码将序列化与发送事件的任务明确区分,原因在稍后将渐趋明朗。For reasons that will become clear later, this code explicitly separates the task of serializing and sending the event. 前面的代码将温度事件以序列化方式发送到缓冲区。The previous code serializes the temperature event into a buffer. sendMessage 是将事件发送到 IoT 中心的帮助器函数(包括在 simplesample_amqp 中):Then, sendMessage is a helper function (included in simplesample_amqp) that sends the event to IoT Hub:

static void sendMessage(IOTHUB_CLIENT_HANDLE iotHubClientHandle, const unsigned char* buffer, size_t size)
{
    static unsigned int messageTrackingId;
    IOTHUB_MESSAGE_HANDLE messageHandle = IoTHubMessage_CreateFromByteArray(buffer, size);
    if (messageHandle != NULL)
    {
        IoTHubClient_SendEventAsync(iotHubClientHandle, messageHandle, sendCallback, (void*)(uintptr_t)messageTrackingId);

        IoTHubMessage_Destroy(messageHandle);
    }
    free((void*)buffer);
}

此代码是前一部分所述的 SendAsync 帮助器的子集,因此不在此赘述。This code is a subset of the SendAsync helper described in the previous section, so we won’t go over it again here.

运行前面的代码发送温度事件时,事件的序列化形式将发送到 IoT 中心:When we run the previous code to send the Temperature event, this serialized form of the event is sent to IoT Hub:

{"Temperature":75, "Time":"2015-09-17T18:45:56Z"}

我们发送类型为 TemperatureEvent 的温度,该构造包含 TemperatureTime 成员。We're sending a temperature which is of type TemperatureEvent and that struct contains a Temperature and Time member. 这直接反映在序列化数据中。This is directly reflected in the serialized data.

同样,我们可以使用以下代码发送湿度事件:Similarly, we can send a humidity event with this code:

thermostat->Humidity.Humidity = 45;
thermostat->Humidity.Time = GetDateTimeOffset(now);
if (SERIALIZE(&destination, &destinationSize, thermostat->Humidity) == IOT_AGENT_OK)
{
    sendMessage(iotHubClientHandle, destination, destinationSize);
}

发送到 IoT 中心的序列化形式如下所示:The serialized form that’s sent to IoT Hub appears as follows:

{"Humidity":45, "Time":"2015-09-17T18:45:56Z"}

同样,一切都按预期进行。Again, this is as expected.

可以使用此模型来想象如何轻松地添加其他事件。With this model, you can imagine how additional events can easily be added. 可以使用 DECLARE_STRUCT 定义更多结构,使用 WITH_DATA 在模型中包含相应的事件。You define more structures using DECLARE_STRUCT, and include the corresponding event in the model using WITH_DATA.

现在,让我们修改模型,使它包含相同的数据,但具有不同的结构。Now, let’s modify the model so that it includes the same data but with a different structure.

模型 2Model 2

请考虑上述模型的替代模型:Consider this alternative model to the one above:

DECLARE_MODEL(Thermostat,
WITH_DATA(int, Temperature),
WITH_DATA(int, Humidity),
WITH_DATA(EDM_DATE_TIME_OFFSET, Time)
);

在此例中,我们已取消 DECLARE_STRUCT 宏,只需使用建模语言中的简单类型来定义我们的方案中的数据项。In this case we've eliminated the DECLARE_STRUCT macros and are simply defining the data items from our scenario using simple types from the modeling language.

此时,可以忽略 Time 事件。Just for the moment, ignore the Time event. 忽略该事件后,以下是要引入 温度的代码:With that aside, here’s the code to ingress Temperature:

time_t now;
time(&now);
thermostat->Temperature = 75;

unsigned char* destination;
size_t destinationSize;
if (SERIALIZE(&destination, &destinationSize, thermostat->Temperature) == IOT_AGENT_OK)
{
    sendMessage(iotHubClientHandle, destination, destinationSize);
}

此代码将以下序列化事件发送到 IoT 中心:This code sends the following serialized event to IoT Hub:

{"Temperature":75}

发送 Humidity 事件的代码如下所示:And the code for sending the Humidity event appears as follows:

thermostat->Humidity = 45;
if (SERIALIZE(&destination, &destinationSize, thermostat->Humidity) == IOT_AGENT_OK)
{
    sendMessage(iotHubClientHandle, destination, destinationSize);
}

此代码将该事件发送到 IoT 中心:This code sends this to IoT Hub:

{"Humidity":45}

到目前为止,一切都很正常。So far there are still no surprises. 现在,让我们更改 SERIALIZE 宏的用法。Now let's change how we use the SERIALIZE macro.

SERIALIZE 宏可将多个数据事件视为参数。The SERIALIZE macro can take multiple data events as arguments. 这使我们能够将 TemperatureHumidity 事件一起序列化,并在一次调用中将它们发送到 IoT 中心:This enables us to serialize the Temperature and Humidity event together and send them to IoT Hub in one call:

if (SERIALIZE(&destination, &destinationSize, thermostat->Temperature, thermostat->Humidity) == IOT_AGENT_OK)
{
    sendMessage(iotHubClientHandle, destination, destinationSize);
}

或许已猜到,此代码的结果是两个数据事件都发送到 IoT 中心:You might guess that the result of this code is that two data events are sent to IoT Hub:

[[

{"Temperature":75},{"Temperature":75},

{"Humidity":45}{"Humidity":45}

]]

换而言之,你可能预料到此代码与分别发送 TemperatureHumidity 相同,In other words, you might expect that this code is the same as sending Temperature and Humidity separately. 它只是为了便于在同一调用中将两个事件都传递到 SERIALIZEIt’s just a convenience to pass both events to SERIALIZE in the same call. 不过,事实并非如此。However, that’s not the case. 上述代码会将此单个数据事件发送到 IoT 中心:Instead, the code above sends this single data event to IoT Hub:

{"Temperature":75, "Humidity":45}{"Temperature":75, "Humidity":45}

这看起来很奇怪,因为我们的模型将 TemperatureHumidity 定义为两个单独事件:This may seem strange because our model defines Temperature and Humidity as two separate events:

DECLARE_MODEL(Thermostat,
WITH_DATA(int, Temperature),
WITH_DATA(int, Humidity),
WITH_DATA(EDM_DATE_TIME_OFFSET, Time)
);

具体而言,我们没有对这些 TemperatureHumidity 处于相同结构的事件进行建模:More to the point, we didn’t model these events where Temperature and Humidity are in the same structure:

DECLARE_STRUCT(TemperatureAndHumidityEvent,
int, Temperature,
int, Humidity,
);

DECLARE_MODEL(Thermostat,
WITH_DATA(TemperatureAndHumidityEvent, TemperatureAndHumidity),
);

如果我们使用此模型,则可以轻松地了解如何在同一序列化消息中发送 TemperatureHumidityIf we used this model, it would be easier to understand how Temperature and Humidity would be sent in the same serialized message. 不过,如果使用模型 2 将两个数据事件都传递到 SERIALIZE ,可能就无法突显这种工作方式的原因。However it may not be clear why it works that way when you pass both data events to SERIALIZE using model 2.

如果知道 序列化程序 库所做的假设,就更容易了解这种行为。This behavior is easier to understand if you know the assumptions that the serializer library is making. 若要了解这一点,让我们返回到模型:To make sense of this let’s go back to our model:

DECLARE_MODEL(Thermostat,
WITH_DATA(int, Temperature),
WITH_DATA(int, Humidity),
WITH_DATA(EDM_DATE_TIME_OFFSET, Time)
);

请以对象定向的观点思考此模型。Think of this model in object-oriented terms. 在此例中,我们要对物理设备(调温器)进行建模,该设备包括 TemperatureHumidity 之类的属性。In this case we’re modeling a physical device (a thermostat) and that device includes attributes like Temperature and Humidity.

可以利用类似于下面的代码来发送模型的整个状态:We can send the entire state of our model with code such as the following:

if (SERIALIZE(&destination, &destinationSize, thermostat->Temperature, thermostat->Humidity, thermostat->Time) == IOT_AGENT_OK)
{
    sendMessage(iotHubClientHandle, destination, destinationSize);
}

假设已设置温度、湿度和时间值,我们会发现有这样的事件发送到 IoT 中心:Assuming the values of Temperature, Humidity and Time are set, we would see an event like this sent to IoT Hub:

{"Temperature":75, "Humidity":45, "Time":"2015-09-17T18:45:56Z"}

有时,你可能只想将模型的某些属性发送到云(特别是当模型包含大量数据事件时)。Sometimes you may only want to send some properties of the model to the cloud (this is especially true if your model contains a large number of data events). 这时,只发送数据事件的子集相当有用,就像前面的示例一样:It’s useful to send only a subset of data events, such as in our earlier example:

{"Temperature":75, "Time":"2015-09-17T18:45:56Z"}

这将生成完全相同的序列化的事件,就像我们在模型 1 中定义带有 TemperatureTime 成员的 TemperatureEvent 一样。This generates exactly the same serialized event as if we had defined a TemperatureEvent with a Temperature and Time member, just as we did with model 1. 在本例中,我们可以使用不同的模型(模型 2)来生成完全相同的序列化事件,因为我们以不同的方式调用了 SERIALIZEIn this case we were able to generate exactly the same serialized event by using a different model (model 2) because we called SERIALIZE in a different way.

重点是,如果将多个数据事件传递给 SERIALIZE ,则它会假设每个事件都是单个 JSON 对象中的一个属性。The important point is that if you pass multiple data events to SERIALIZE, then it assumes each event is a property in a single JSON object.

最佳方法取决于自己以及对模型的思考方式。The best approach depends on you and how you think about your model. 如果要将“事件”发送到云,且每个事件都包含一组已定义的属性,则第一种方法较为适合。If you’re sending "events" to the cloud and each event contains a defined set of properties, then the first approach makes a lot of sense. 在此情况下,使用 DECLARE_STRUCT 定义每个事件的结构,并使用 WITH_DATA 宏将它们包含在模型中。In that case you would use DECLARE_STRUCT to define the structure of each event and then include them in your model with the WITH_DATA macro. 然后根据上述第一个示例中使用的方法来发送每个事件。Then you send each event as we did in the first example above. 采用此方法时,只会将单个数据事件传递给 SERIALIZERIn this approach you would only pass a single data event to SERIALIZER.

如果以对象定向的方式思考模型,则第二种方法可能比较适合。If you think about your model in an object-oriented fashion, then the second approach may suit you. 在本例中,使用 WITH_DATA 定义的元素是对象的“属性”。In this case, the elements defined using WITH_DATA are the "properties" of your object. 可以根据想要发送到云的“对象”状态详细程度,将事件的任何子集传递给 SERIALIZEYou pass whatever subset of events to SERIALIZE that you like, depending on how much of your "object’s" state you want to send to the cloud.

没有绝对正确或错误的方法。Nether approach is right or wrong. 只需了解 序列化程序 库的工作原理,并挑选最符合需求的建模方法。Just be aware of how the serializer library works, and pick the modeling approach that best fits your needs.

消息处理Message handling

到目前为止,本文只讨论了如何将事件发送到 IoT 中心,而尚未涉及到消息接收。So far this article has only discussed sending events to IoT Hub, and hasn't addressed receiving messages. 这是因为我们需要了解的有关接收消息的内容已在适用于 C 语言的 Azure IoT 设备 SDK 一文中详细介绍。回顾那篇文章,我们知道是通过注册消息回调函数来处理消息的:The reason for this is that what we need to know about receiving messages has largely been covered in the article Azure IoT device SDK for C. Recall from that article that you process messages by registering a message callback function:

IoTHubClient_SetMessageCallback(iotHubClientHandle, IoTHubMessage, myWeather)

然后编写在接收消息时要调用的回调函数:You then write the callback function that’s invoked when a message is received:

static IOTHUBMESSAGE_DISPOSITION_RESULT IoTHubMessage(IOTHUB_MESSAGE_HANDLE message, void* userContextCallback)
{
    IOTHUBMESSAGE_DISPOSITION_RESULT result;
    const unsigned char* buffer;
    size_t size;
    if (IoTHubMessage_GetByteArray(message, &buffer, &size) != IOTHUB_MESSAGE_OK)
    {
        printf("unable to IoTHubMessage_GetByteArray\r\n");
        result = EXECUTE_COMMAND_ERROR;
    }
    else
    {
        /*buffer is not zero terminated*/
        char* temp = malloc(size + 1);
        if (temp == NULL)
        {
            printf("failed to malloc\r\n");
            result = EXECUTE_COMMAND_ERROR;
        }
        else
        {
            memcpy(temp, buffer, size);
            temp[size] = '\0';
            EXECUTE_COMMAND_RESULT executeCommandResult = EXECUTE_COMMAND(userContextCallback, temp);
            result =
                (executeCommandResult == EXECUTE_COMMAND_ERROR) ? IOTHUBMESSAGE_ABANDONED :
                (executeCommandResult == EXECUTE_COMMAND_SUCCESS) ? IOTHUBMESSAGE_ACCEPTED :
                IOTHUBMESSAGE_REJECTED;
            free(temp);
        }
    }
    return result;
}

IoTHubMessage 的这种实现会针对模型中的每个操作调用特定的函数。This implementation of IoTHubMessage calls the specific function for each action in your model. 例如,如果模型定义了此操作:For example, if your model defines this action:

WITH_ACTION(SetAirResistance, int, Position)

必须使用此签名来定义函数:You must define a function with this signature:

EXECUTE_COMMAND_RESULT SetAirResistance(ContosoAnemometer* device, int Position)
{
    (void)device;
    (void)printf("Setting Air Resistance Position to %d.\r\n", Position);
    return EXECUTE_COMMAND_SUCCESS;
}

SetAirResistanceSetAirResistance is then called when that message is sent to your device.

我们还没有说明消息序列化版本的外观。What we haven't explained yet is what the serialized version of message looks like. 换而言之,如果要将 SetAirResistance 消息发送到设备,该消息的外观是怎样的?In other words, if you want to send a SetAirResistance message to your device, what does that look like?

如果要将消息发送给设备,可以通过 Azure IoT 服务 SDK 来完成。If you're sending a message to a device, you would do so through the Azure IoT service SDK. 仍需要知道要发送哪个字符串才能调用特定操作。You still need to know what string to send to invoke a particular action. 用于发送消息的常规格式如下所示:The general format for sending a message appears as follows:

{"Name" : "", "Parameters" : "" }

将使用两个属性来发送序列化 JSON 对象:Name 是操作(消息)的名称,Parameters 包含该操作的参数。You're sending a serialized JSON object with two properties: Name is the name of the action (message) and Parameters contains the parameters of that action.

例如,要调用 SetAirResistance ,可以将以下消息发送给设备:For example, to invoke SetAirResistance you can send this message to a device:

{"Name" : "SetAirResistance", "Parameters" : { "Position" : 5 }}

操作名称必须完全与模型中定义的操作匹配。The action name must exactly match an action defined in your model. 参数名称也必须匹配。The parameter names must match as well. 另请注意大小写。Also note case sensitivity. NameParameters 始终大写。Name and Parameters are always uppercase. 请务必与模型中操作名称和参数的大小写匹配。Make sure to match the case of your action name and parameters in your model. 在本示例中,操作名称是“SetAirResistance”,而不是“setairresistance”。In this example, the action name is "SetAirResistance" and not "setairresistance".

将这些消息发送到设备可以调用其他两个操作 TurnFanOnTurnFanOffThe two other actions TurnFanOn and TurnFanOff can be invoked by sending these messages to a device:

{"Name" : "TurnFanOn", "Parameters" : {}}
{"Name" : "TurnFanOff", "Parameters" : {}}

本部分说明了使用 序列化程序 库发送事件和接收消息时的所有要点。This section described everything you need to know when sending events and receiving messages with the serializer library. 在继续讨论之前,让我们先介绍一些可以配置以控制模型大小的参数。Before moving on, let's cover some parameters you can configure that control how large your model is.

宏配置Macro configuration

如果使用的是 序列化程序 库,那么可在 azure-c-shared-utility 库中找到要注意的 SDK 的重要组成部分。If you’re using the Serializer library an important part of the SDK to be aware of is found in the azure-c-shared-utility library.

如果已从 GitHub 克隆了 Azure-iot-sdk-c 存储库并发出了 git submodule update --init 命令,则将在以下位置找到此共享实用程序库:If you have cloned the Azure-iot-sdk-c repository from GitHub and issued the git submodule update --init command, then you will find this shared utility library here:

.\\c-utility

如果没有克隆此库,则可以在 此处找到它。If you have not cloned the library, you can find it here.

在此共享的实用程序库中,可找到以下文件夹:Within the shared utility library, you will find the following folder:

azure-c-shared-utility\\macro\_utils\_h\_generator.

此文件夹包含名为 macro_utils_h_generator.sln 的 Visual Studio 解决方案:This folder contains a Visual Studio solution called macro_utils_h_generator.sln:

Visual Studio 解决方案 maco_utils_h_generator 的屏幕截图

此解决方案中的程序将生成 macro_utils.h 文件。The program in this solution generates the macro_utils.h file. SDK 附带了一个默认的 macro_utils.h 文件。There’s a default macro_utils.h file included with the SDK. 此解决方案可让用户修改某些参数,并根据这些参数重新创建标头文件。This solution allows you to modify some parameters and then recreate the header file based on these parameters.

要注意两个重要参数:nArithmeticnMacroParameters,这些参数在 macro_utils.tt 的以下两行中定义:The two key parameters to be concerned with are nArithmetic and nMacroParameters which are defined in these two lines found in macro_utils.tt:

<#int nArithmetic=1024;#>
<#int nMacroParameters=124;/*127 parameters in one macro definition in C99 in chapter 5.2.4.1 Translation limits*/#>

这些值是 SDK 随附的默认参数。These values are the default parameters included with the SDK. 每个参数的含义如下:Each parameter has the following meaning:

  • nMacroParameters – 控制可以在一个 DECLARE_MODEL 宏定义中指定的参数数目。nMacroParameters – Controls how many parameters you can have in one DECLARE_MODEL macro definition.
  • nArithmetic – 控制模型中允许的成员总数。nArithmetic – Controls the total number of members allowed in a model.

这些参数之所以重要,是因为它们控制模型的大小。The reason these parameters are important is because they control how large your model can be. 例如,假设有以下模型定义:For example, consider this model definition:

DECLARE_MODEL(MyModel,
WITH_DATA(int, MyData)
);

如前所述,DECLARE_MODEL 只是一个 C 宏。As mentioned previously, DECLARE_MODEL is just a C macro. 模型的名称和 WITH_DATA 语句(也即另一个宏)是 DECLARE_MODEL 的参数。The names of the model and the WITH_DATA statement (yet another macro) are parameters of DECLARE_MODEL. nMacroParameters 定义了 DECLARE_MODEL 中可以包含的参数数目。nMacroParameters defines how many parameters can be included in DECLARE_MODEL. 实际上,这定义了可以指定的数据事件和操作声明数目。Effectively, this defines how many data event and action declarations you can have. 因此,使用默认限制 124 时,可以定义由大约 60 个操作和事件数据组成的模型。As such, with the default limit of 124 this means that you can define a model with a combination of about 60 actions and data events. 如果你试图超过此限制,将收到如下所示的编译器错误:If you try to exceed this limit, you'll receive compiler errors that look similar to this:

宏参数编译器错误的屏幕截图

nArithmetic 参数主要与宏语言的内部工作有关,而与应用程序没有太大的关系。The nArithmetic parameter is more about the internal workings of the macro language than your application. 该参数控制可以在模型中(包括 DECLARE_STRUCT 宏)指定的成员总数。It controls the total number of members you can have in your model, including DECLARE_STRUCT macros. 如果开始看到这样的编译器错误,应该尝试增大 nArithmetic的值:If you start seeing compiler errors such as this, then you should try increasing nArithmetic:

算术编译器错误的屏幕截图

如果想要更改这些参数,请修改 macro_utils.tt 文件中的值,重新编译 macro_utils_h_generator.sln 解决方案并运行已编译的程序。If you want to change these parameters, modify the values in the macro_utils.tt file, recompile the macro_utils_h_generator.sln solution, and run the compiled program. 当你这么做时,将生成一个新的 macro_utils.h 文件,该文件置于 .\common\inc 目录中。When you do so, a new macro_utils.h file is generated and placed in the .\common\inc directory.

为了使用新版本的 macro_utils.h,请从你的解决方案中删除序列化程序 NuGet 包,并在其位置放置序列化程序 Visual Studio 项目。In order to use the new version of macro_utils.h, remove the serializer NuGet package from your solution and in its place include the serializer Visual Studio project. 这样,便可以让代码针对序列化程序库的源代码进行编译。This enables your code to compile against the source code of the serializer library. 这包括更新的 macro_utils.h。This includes the updated macro_utils.h. 如果想要针对 simplesample_amqp 执行此操作,首先从解决方案中删除序列化程序库的 NuGet 包:If you want to do this for simplesample_amqp, start by removing the NuGet package for the serializer library from the solution:

删除序列化程序库的 NuGet 包的屏幕截图

然后将此项目添加到 Visual Studio 解决方案:Then add this project to your Visual Studio solution:

.\c\serializer\build\windows\serializer.vcxproj.\c\serializer\build\windows\serializer.vcxproj

完成后,解决方案应该如下所示:When you're done, your solution should look like this:

simplesample_amqp Visual Studio 解决方案的屏幕截图

现在当你编译解决方案时,二进制文件中包含已更新的 macro_utils.h。Now when you compile your solution, the updated macro_utils.h is included in your binary.

请注意,将这些值增大到足够高的数目可能会超出编译器限制。Note that increasing these values high enough can exceed compiler limits. 对于这一点, nMacroParameters 是要考虑的主要参数。To this point, the nMacroParameters is the main parameter with which to be concerned. C99 规范规定,宏定义中至少允许 127 个参数。The C99 spec specifies that a minimum of 127 parameters are allowed in a macro definition. Microsoft 编译器完全遵循规范(个数限制为 127),因此不能将 nMacroParameters 提高到超出默认值。The Microsoft compiler follows the spec exactly (and has a limit of 127), so you won't be able to increase nMacroParameters beyond the default. 其他编译器可能允许这么做(例如 GNU 编译器支持更高的限制)。Other compilers might allow you to do so (for example, the GNU compiler supports a higher limit).

到目前为止,我们已经介绍了使用序列化程序库编写代码所需了解的所有内容。So far we've covered just about everything you need to know about how to write code with the serializer library. 结束前,来回顾下前面文章中可能感兴趣的一些主题。Before concluding, let's revisit some topics from previous articles that you may be wondering about.

较低级别 APIThe lower-level APIs

这篇文章重点介绍的示例应用程序是 simplesample_amqpThe sample application on which this article focused is simplesample_amqp. 此示例使用较高级别的(非 LL)API 来发送事件和接收消息。This sample uses the higher-level (the non-LL) APIs to send events and receive messages. 如果使用这些 API,将运行后台线程来处理事件发送和消息接收。If you use these APIs, a background thread runs which takes care of both sending events and receiving messages. 不过,可以使用较低级别 (LL) API 以取消此后台线程,并在发送事件或接收来自云的消息时接管显式控制。However, you can use the lower-level (LL) APIs to eliminate this background thread and take explicit control over when you send events or receive messages from the cloud.

前一篇文章中所述,有一组由较高级别 API 构成的函数:As described in a previous article, there is a set of functions that consists of the higher-level APIs:

  • IoTHubClient_CreateFromConnectionStringIoTHubClient_CreateFromConnectionString
  • IoTHubClient_SendEventAsyncIoTHubClient_SendEventAsync
  • IoTHubClient_SetMessageCallbackIoTHubClient_SetMessageCallback
  • IoTHubClient_DestroyIoTHubClient_Destroy

simplesample_amqp 中演示了这些 API。These APIs are demonstrated in simplesample_amqp.

还有一组类似但级别更低的 API。There is also an analogous set of lower-level APIs.

  • IoTHubClient_LL_CreateFromConnectionStringIoTHubClient_LL_CreateFromConnectionString
  • IoTHubClient_LL_SendEventAsyncIoTHubClient_LL_SendEventAsync
  • IoTHubClient_LL_SetMessageCallbackIoTHubClient_LL_SetMessageCallback
  • IoTHubClient_LL_DestroyIoTHubClient_LL_Destroy

请注意,较低级别的 API 的工作原理与前面文章中所述的完全相同。Note that the lower-level APIs work exactly the same way as described in the previous articles. 如果想要使用后台线程来处理事件发送和消息接收,可以使用第一组 API。You can use the first set of APIs if you want a background thread to handle sending events and receiving messages. 如果想要掌握与 IoT 中心之间发送和接收数据时的明确控制权,可以使用第二组 API。You use the second set of APIs if you want explicit control over when you send and receive data from IoT Hub. 上述任何一组 API 都可以很好地配合使用 序列化程序 库。Either set of APIs work equally well with the serializer library.

若要通过示例了解较低级别的 API 如何与序列化程序库配合使用,请参阅 simplesample_http 应用程序。For an example of how the lower-level APIs are used with the serializer library, see the simplesample_http application.

其他主题Additional topics

值得一提的其他几个主题包括属性处理、使用替代设备凭据和配置选项。A few other topics worth mentioning again are property handling, using alternate device credentials, and configuration options. 这些主题均涵盖在 前一篇文章中。These are all topics covered in a previous article. 重点在于,所有这些功能与序列化程序库配合使用的方式与和 IoTHubClient 库配合使用的方式相同。The main point is that all of these features work in the same way with the serializer library as they do with the IoTHubClient library. 例如,如果想要从模型将属性附加到事件,需要以前面所述的相同方式,使用 IoTHubMessage_PropertiesMap_AddorUpdateFor example, if you want to attach properties to an event from your model, you use IoTHubMessage_Properties and Map_AddorUpdate, the same way as described previously:

MAP_HANDLE propMap = IoTHubMessage_Properties(message.messageHandle);
sprintf_s(propText, sizeof(propText), "%d", i);
Map_AddOrUpdate(propMap, "SequenceNumber", propText);

至于事件是从序列化程序库生成,还是使用 IoTHubClient 库手动创建,并不重要。Whether the event was generated from the serializer library or created manually using the IoTHubClient library does not matter.

就替代设备凭据而言,使用 IoTHubClient_LL_Create 分配 IOTHUB_CLIENT_HANDLE 的效果和使用 IoTHubClient_CreateFromConnectionString 一样好。For the alternate device credentials, using IoTHubClient_LL_Create works just as well as IoTHubClient_CreateFromConnectionString for allocating an IOTHUB_CLIENT_HANDLE.

最后,如果使用序列化程序库,则可以使用 IoTHubClient_LL_SetOption 来设置配置选项,就像使用 IoTHubClient 库时一样。Finally, if you're using the serializer library, you can set configuration options with IoTHubClient_LL_SetOption just as you did when using the IoTHubClient library.

序列化程序 库所具有的一个独特功能为初始化 API。A feature that is unique to the serializer library are the initialization APIs. 在开始使用库之前,必须调用 serializer_initBefore you can start working with the library, you must call serializer_init:

serializer_init(NULL);

此操作必须在调用 IoTHubClient_CreateFromConnectionString 之前完成。This is done just before you call IoTHubClient_CreateFromConnectionString.

同样,当使用完该库时,最后调用的对象是 serializer_deinitSimilarly, when you're done working with the library, the last call you’ll make is to serializer_deinit:

serializer_deinit();

除此之外,上面列出的所有其他功能在序列化程序库中的运行方式均与在 IoTHubClient 库中的运行方式相同。Otherwise, all of the other features listed above work the same in the serializer library as they do in the IoTHubClient library. 有关这些主题中任何一个主题的详细信息,请参阅本系列教程中的前一篇文章For more information about any of these topics, see the previous article in this series.

后续步骤Next steps

本文详细介绍了适用于 C 语言的 Azure IoT 设备 SDK 中包含的序列化程序库的独特方面。通过文中提供的信息,你应该能充分了解如何使用模型来发送事件和接收来自 IoT 中心的消息。This article describes in detail the unique aspects of the serializer library contained in the Azure IoT device SDK for C. With the information provided you should have a good understanding of how to use models to send events and receive messages from IoT Hub.

本文也是通过适用于 C 语言的 Azure IoT 设备 SDK 开发应用程序这一系列教程(由三部分组成)的最后一部分。这些信息应该不仅足以让你入门,还能让你彻底了解 API 的工作原理。This also concludes the three-part series on how to develop applications with the Azure IoT device SDK for C. This should be enough information to not only get you started but give you a thorough understanding of how the APIs work. 请了解其他信息,因为还有一些 SDK 中的示例未涵盖在本文中。For additional information, there are a few samples in the SDK not covered here. 除此之外,Azure IoT SDK 文档也是获取其他信息的绝佳资源。Otherwise, the Azure IoT SDK documentation is a good resource for additional information.

若要详细了解如何针对 IoT 中心进行开发,请参阅 Azure IoT SDKTo learn more about developing for IoT Hub, see the Azure IoT SDKs.

若要进一步探索 IoT 中心的功能,请参阅使用 Azure IoT Edge 将 AI 部署到边缘设备To further explore the capabilities of IoT Hub, see Deploying AI to edge devices with Azure IoT Edge.