Недавно мы ознакомили вас с идеей подключить IoT-устройство (Internet of Things) — в нашем случае это Raspberry Pi — к облаку (Microsoft Azure), чтобы доставлять извещающие (всплывающие) уведомления на мобильное устройство всякий раз, когда кто-то нажимает дверной звонок на пороге вашего дома. Это позволяет вам увидеть, кто стоит у вашей двери, из любой точки мира, используя мобильное устройство.
В прошлом месяце в статье «Soup to Nuts: From Raw Hardware to Cloud-Enabled Device» (msdn.microsoft.com/magazine/dn781356) мы пошагово прошли весь процесс интеграции устройства с хранилищем Blobs, используя Azure Mobile Services для получения Shared Access Signature (SAS). Это дает возможность IoT-устройству напрямую загружать файл в контейнер хранилища безо всякого уровня сервисов. Теперь мы можем загрузить снимок в облако, но некоторые вещи, необходимые для отправки уведомления на мобильное устройство, остались недоделанными.
До настоящего момента в этом проекте использовались Azure Mobile Services и Azure Storage. Теперь нам понадобятся очереди шины сервисов (service bus) и сервисы планируемых задач (scheduled tasks services) для отправки сообщений от устройства Raspberry Pi на ваше мобильное устройство. Эти сервисы будут интегрированы с Azure Mobile Services, которая будет принимать сообщения из очереди шины сервисов и записывать их в базу данных. В духе решений новой волны с открытым исходным кодом мы будем применять базу данных MongoDB, размещаемую и поддерживаемую MongoLab; ее можно добавить бесплатно как надстройку Azure (Azure Add-on).
Шина сервисов для распределенного обмена сообщениями
В контексте IoT-вычислений очереди шины сервисов предоставляют мощные абстракции. Они поддерживают асинхронный обмен сообщениями между конечными точками и могут масштабироваться для поддержки практически любой рабочей нагрузки. Кроме того, они позволяют взаимодействовать приложениям в облаке и на предприятии, не требуя создания виртуальных сетей, которые могут оказаться кошмаром с точки зрения логистики и безопасности.
Часть вопросов мы рассмотрели в статье «The Windows Azure Service Bus and the Internet of Things» в номере за февраль 2014 г. (msdn.microsoft.com/magazine/dn574801). Azure предлагает другой сервис очередей — очереди хранилища (storage queues), которые имеют схожие возможности, но отличаются несколькими ключевыми особенностями. Мы предпочли очереди шины сервисов из-за поддержки шаблона «публикатор-подписчик», большей масштабируемости в обработке сообщений и возможности простого преобразования под другие сервисы этой шины. Подробнее об очередях хранилища см. по ссылке bit.ly/XSJB43.
Очереди шины сервисов предоставляют обычный RESTful API, который поддерживает возможность приема сообщений с помощью так называемых длинных опросов (long polling). Это способ открытия HTTP-соединения на определенный период. Длинные опросы — отличный прием в сценариях IoT-вычислений, так как он обеспечивает поддержку интервалов ожидания. Это позволяет устройствам закрывать соединения до следующего длинного опроса, что уменьшает энергопотребление устройствами и нагрузку на сетевые ресурсы.
Мы располагаем устройствами SmartDoor, связанными с одним мобильным сервисом по принципу «многие к одному». Точнее, эта форма публикации-подписки будет включать множество публикаторов сообщений и единственный подписчик на эти сообщения. Подписчиком в данном случае является мобильный сервис, извлекающий сообщения из очереди. Одно или более устройств SmartDoor будут публикаторами. В случае, если вам нужно посылать сообщение обратно на устройство, можно сделать все наоборот.
Существует четыре типа коммуникационных шаблонов, обычно применяемых в сценариях с IoT. Как видно на рис. 1, одно или более устройств Raspberry Pi публикуют сообщения в очереди шины сервисов. Azure Mobile Services — единственный подписчик на очереди шины сервисов.
Увеличить
Рис. 1. Архитектурная схема Azure Mobile Services, считывающей информацию из Service Bus
Service Bus | Шина сервисов |
Queues | Очереди |
Azure Mobile Services | Azure Mobile Services |
60 Seconds Scheduled Tas | Задача, запланированная на каждые 60 секунд |
В Azure Mobile Services интегрирован планировщик задач, с помощью которого можно запускать задачи через фиксированные интервалы. Мы читаем из очереди шины сервисов через каждые 60 секунд. Подробную информацию о подключении мобильных сервисов к очереди шины сервисов см. в нашем блоге bit.ly/1o1nUrY. С помощью нашей запланированной задачи, doorBellListener (рис. 2), мы читаем из очереди, используя API шины сервисов в Node.js Azure SDK и собственную строку подключения.
Рис. 2. Запланированная задача doorbellListener в Azure Mobile Services
// Получаем ссылку на модуль azure
var azure = require('azure');
// Получаем строку подключения к шине сервисов
var connectionString = process.env.ServiceBusConnectionString;
// Эта задача будет выполняться через каждые 60 секунд,
// поэтому начальный период ожидания должен быть равен 60
var c_Timeout = 60;
function doorbellListener() {
// Получаем текущее время unix в секундах
var date = new Date();
var time = date.getTime();
var startSeconds = time / 1000;
var sb = azure.createServiceBusService(connectionString);
listenForMessages(c_Timeout);
// Определяем функцию, которая будет прослушивать сообщения
// в очереди в течение интервала, заданного значением seconds
function listenForMessages(seconds) {
console.log('Doorbell Listener Started for timeout: ' + seconds);
// Длинный опрос шины сервисов в течение seconds
sb.receiveQueueMessage("smartdoor", { timeoutIntervalInS: seconds },
function(err, data) {
if(err){
// Сюда попадаем, если не получили никаких сообщений
console.log(err);
}
else{
// Сообщение от устройства получено
var ringNotification = JSON.parse(data.body);
console.log('recieved message from doorbell ' +
ringNotification.doorbellId + '
with image link ' + ringNotification.imagePointer)
function continueListeningForMessages(){
// Возвращаемся и ждем следующее сообщение
// в течение работы этой задачи
var currentDate = new Date();
var currentSeconds = currentDate.getTime() / 1000;
console.log('currentSeconds ' + currentSeconds);
console.log('startSeconds ' + startSeconds);
// Вычисляем количество секунд между запуском
// этой задачи и текущим временем. Это время,
// в течение которого мы будем выполнять
// длинный опрос шины сервисов.
var newTimeout = Math.round((c_Timeout - (currentSeconds - startSeconds)));
if(newTimeout > 0){
// Примечание: функция receiveQueueMessage принимает
// значения типа int – никаких десятичных дробей!
listenForMessages(newTimeout);
}
}
Из-за асинхронной природы Node.js и отправки сообщения шине сервисов с применением длинного опроса вы должны вычислять значение периода ожидания, которое передается в receiveQueueMessage; при этом следует учитывать, сколько времени выполняется данный экземпляр запланированной задачи. Это предотвратит одновременный запуск нескольких экземпляров этой задачи.
Удобно, что шина сервисов предоставляет RESTful API. Какое бы устройство вы ни использовали, вы можете отправлять ей сообщения. В Azure есть различные SDK для основных языков, таких как Python, Ruby, Node.js и C#. Поскольку нам нужно, чтобы код транслировался под любую платформу, мы задействуем RESTful API напрямую.
Чтобы отправить сообщение, нужно создать политику для очереди шины сервисов. Эта политика — ключ, который разрешает определенные операции в шине сервисов. Для каждой очереди предусматривается некое количество политик. Учитывайте это в своей предметной области.
Мы сгенерируем политику DevicePolicy, которая будет разрешать пользователям лишь отправлять сообщения в очередь шины сервисов (рис. 3).
Рис. 3. DevicePolicy разрешит отправлять сообщения в очередь шины сервисов
Это гарантирует, что, если ключ когда-либо попадет в чужие руки, никто посторонний не сможет слушать сообщения в шине сервисов. Машинный код для Raspberry Pi, позволяющий отправить сообщение в очередь шины сервисов, показан на рис. 4.
Рис. 4. Отправка сообщения, указывающего на успешную загрузку снимка
Console.WriteLine("Sending notification to service bus queue");
WebRequest sbRequest = WebRequest.Create(
"https://smartdoordemo.servicebus.Windows.net/smartdoorqueue/messages");
var headers = sbRequest.Headers;
sbRequest.Method = "POST";
using (var sbMessageStream = sbRequest.GetRequestStream())
{
string body = JsonConvert.SerializeObject(new DoorBellNotification()
{
doorBellID = deviceID,
imageUrl = photoResp.photoId
});
var bytes = Encoding.UTF8.GetBytes(body);
sbMessageStream.Write(bytes, 0, bytes.Length);
headers.Add("Authorization", createToken(
"https://smartdoordemo.servicebus.Windows.net/smartdoorqueue/
messages", "DevicePolicy",
ConfigurationManager.AppSettings["ServiceBusSharedAccessKey"]));
}
try
{
Console.WriteLine("Sending door bell notification for " + deviceID);
using (var response = sbRequest.GetResponse())
{
Console.WriteLine("Sucessfully Sent");
}
}
catch (Exception e)
{
Console.WriteLine("Couldn't post to service bus -" + e);
}
Полный код program.cs см. по ссылке bit.ly/1qFYAF2. В этом коде вы выдаем запрос POST к RESTful API очереди шины сервисов и предоставляем SAS, аналогичный сигнатуре, которую мы получали от мобильных сервисов для хранилища Blob. Этот SAS состоит из шифровального ключа, использующего алгоритм SHA-256. Он определяет ресурс, к которому происходит доступ, а также срок истечения действия SAS. Метод createToken конструирует SAS на основе Shared Access Key из политики DevicePolicy.
Сконструировав SAS, мы помещаем его в HTTP-заголовок, а сериализованное сообщение (в формате JSON) — в тело запроса. После выдачи запроса POST сообщение отправляется в очередь шины сервисов, чтобы указать на успешную загрузку снимка. Сообщение содержит ссылку на загруженный двоичный объект (blob) и уникальный идентификатор данного устройства «дверной звонок», который берется из параметров приложения. Этот идентификатор можно найти в XML-файле app.config — он указывается следующим за исполняемым файлом слушателя устройства:
<appSettings>
<add key="DoorBellID" value="123456"/>
...
</appSettings>
Теперь устройство отсылает сообщения шине сервисов, когда оно работает. Перейдя на вкладку Log на нашей странице Mobile Services, мы можем обращаться к выводу в журнале сервиса. При выполнении кода на C# в Visual Studio можно увидеть, что мобильный сервис получил содержимое сообщения через вывод журнала на портале.
Хотя Azure Mobile Services предоставляет прекрасные таблицы (поддерживаемые SQL Server) для хранения, мы намерены пойти несколько иным путем. Мы задействуем MongoDB в качестве решения для базы данных. MongoDB отлично работает с Node.js, потому что это ориентированная на документы база данных; она напоминает набор JSON-объектов в хранилище. Подробнее о проекте MongoDB с открытым исходным кодом см. на сайте mongodb.org. Мы будем использовать MongoLab, провайдер Database as a Service (DaaS), для хостинга своей базы данных.
Нам нужно, чтобы база данных отслеживала две вещи:
- устройства DoorBell — индивидуальные устройства Raspberry Pi;
- снимки — индивидуальные снимки, сделанные устройством DoorBell.
Подготовив базу данных MongoDB, можно использовать Mongoose как драйвер MongoDB для нашего Node.js-кода в Azure Mobile Services путем его установки в наш репозитарий Git мобильных сервисов. Этот процесс идентичен тому, который мы проходили при установке модуля qs node:
Передача локального репозитария заставит мобильный сервис установить Mongoose в свой репозитарий. Именно так мы получаем любой пакет Node.js в Azure Mobile Services. Как и в случае любого другого модуля Node.js, мы можем ссылаться на него, используя RequireJs, и инициализировать Mongoose нашей строкой подключения к MongoDB в запланированной задаче:
var mongoose = require('mongoose');
Mongoose будет отвечать за создание структурной схемы в базе документов (рис. 5). Мы используем две сущности: doorbell и photo. Каждый объект doorbell в нашей базе данных представляет одно устройство Raspberry Pi. Он будет содержать уникальный идентификатор, doorBellID, и массив объектов photo. Каждый объект photo содержит ссылку на снимок в хранилище Blob, а также метку времени, генерируемую сервисом, когда он получает сообщение от шины сервисов.
Рис. 5. Схемы, определенные в Mongoose
var photoSchema = mongoose.Schema({
url : String,
timestamp: String
});
var doorbellSchema = mongoose.Schema({
doorBellID: String,
photos: [photoSchema]
});
var Photo = mongoose.model('Photo', photoSchema)
var DoorBell = mongoose.model('DoorBell', doorbellSchema);
// Предоставляем доступ к этим схемам
exports.DoorBell = DoorBell;
exports.Photo = Photo;
Мы предоставляем схемы DoorBell и Photo как общедоступные через ключевое слово exports. Заметьте, что photoSchema располагается между уровнями doorbellSchema. Именно так будут отражены данные при их сохранении в базе данных.
Мы поместим этот код в папку shared нашего репозитария мобильных сервисов. Это позволяет нам использовать схемы в нашем сервисе где угодно. Чтобы ссылаться на схемы в запланированной задаче, нужно просто импортировать их выражением require:
var schemas = require('../shared/schemas.js');
Теперь мы можем использовать эти схемы в запланированной задаче, чтобы определить новые сущности в базе данных. Мы задействуем специализированную функцию, чтобы гарантировать подключение к MongoDB, и выполним обратный вызов. При получении сообщения от шины сервисов мобильный сервис должен:
- проверить, существует ли doorbell с указанным в сообщении doorBellID в базе данных;
- если его нет, создаем новую сущность doorbell с photo;
- если существует, просто добавляем новый photo в существующий массив photo объекта doorbell.
Соответствующая логика в коде показана на рис. 6.
Рис. 6. Логика проверки doorbell и управление photo
// Создаем запись в базе данных для этого изображения
dbConnectAndExecute(function(err){
if(err){
console.log('could not record image to database -' + err);
return;
}
DoorBell.findOne({ doorBellID: ringNotification.doorBellID},
function(err, doorbell){
if(err){
return console.error(err);
}
if(doorbell === null){
console.log('Doorbell not found in DB, creating a new one');
// Принимаем json всего тела, предполагая,
// что оно соответствует этой схеме
var entityObject = {
doorBellID : ringNotification.doorBellID,
photos : []
};
entityObject.photos.push({
url : ringNotification.imageUrl,
// Ставим метку времени согласно
// текущему времени на сервере
timestamp : ((new Date()).getTime()).toString()
})
var doorbell = new DoorBell(entityObject);
}
else{
// Это устройство уже есть в базе данных —
// добавляем снимок
doorbell.photos.push({
url : ringNotification.imageUrl,
// Ставим метку времени согласно
// текущему времени на сервере
timestamp : ((new Date()).getTime()).toString()
});
}
// Передаем изменения в db
doorbell.save(function (err, entity) {
if(err){
return console.error(err);
}
console.log('sucessfully created new entity for: ' + entity);
return;
});
});
});
Полную демонстрацию doorbelllistener см. по ссылке bit.ly/VKrlYU.
Мы вызываем функцию dbConnectAndExecute, чтобы обеспечить подключение к базе данных MongoDB в MongoLabs. Затем мы запрашиваем от базы данных doorbell с идентификатором, указанным в сообщении. Если результат пуст, мы создаем новую сущность doorbell. В ином случае мы добавляем photo в извлеченную сущность и передаем изменения базе данных.
Теперь посмотрите на рис. 7, что происходит, когда мы отправляем снимок и сообщение с нашего устройства Raspberry Pi.
Рис. 7. Результат отправки снимка и сообщения
Анализ журнала мобильных сервисов на портале после того, как Raspberry Pi отправил сообщение, показывает, что снимок был обработан и добавлен в базу данных.
На всякий случай можно проверить базу данных MongoDB, чтобы убедиться в том, что мы действительно отслеживаем снимки. MongoLab предоставляет богатый интерфейс для анализа базы данных MongoDB на своем веб-портале. Рис. 8 демонстрирует, как может выглядеть сущность doorbell после загрузки пары снимков.
Рис. 8. MongoDB регистрирует записи
Теперь Raspberry Pi полностью интегрирован в наш облачный сервис. Устройство может загружать файл напрямую в облачное хранилище, уведомлять облачный сервис через шину сервисов и сохранять информацию в базе данных.
Следующий этап в этой серии будет заключаться в интеграции мобильного приложения с серверной частью в облаке для получения уведомлений об изображениях. Мы задействуем собственные API для доступа приложения к объектам из базы данных и интегрируем сторонний API для распознавания лиц на снимках и концентратор уведомлений (notification hub) для поддержки передачи уведомлений на клиент. Вы можете изучить репозитарии кода и самостоятельно скомпилировать и настроить решение. Серверная часть Azure Mobile Services находится по ссылке bit.ly/1mHCl0q, а код для Raspberry Pi — по ссылке bit.ly/1l8aOtF.
Интеграция устройств Raspberry PI с облаком крайне важна. Использование технологий с открытым исходным кодом, таких как Node.js, имеет смысл во многих сценариях, где вы применяете облегченную версию серверной части для масштабирования под большое количество устройств.
Шина сервисов Azure предоставляет безопасный и удобный способ для периодически подключаемых устройств, которые используют инфраструктуру обмена сообщениями, способную к масштабированию. Наконец, применение хранилищ NoSQL — популярный способ сохранения данных и использования привычного синтаксиса JavaScript как на уровне данных, так и на уровне сервиса Node.js в серверной части.