Июль 2015
ТОМ 30 ВЫПУСК 7
Windows Azure изнутри - Event Hubs для аналитики и визуализации. Часть 3
Продукты и технологии:
Microsoft Azure, ASP.NET Web API, Node.js, Raspberry Pi, Интернет вещей (IoT)
В статье рассматриваются:
• завершение проекта Интернета вещей (Internet of Things, IoT);
• создание приложения Node.js для отображения веб-данных;
• визуализация данных на мобильном клиенте.
Если вы следили за этой серией статей, то сейчас наконец увидите реализацию главной цели всей предыдущей работы: визуализацию данных, поступающих от устройств Raspberry Pi. Это заключительная статья цикла по сценарию использования Интернета вещей (Internet of Things, IoT). Предыдущие две статьи см. в номерах за апрель (msdn.microsoft.com/magazine/dn948106) и июнь (msdn.microsoft.com/magazine/mt147243).
Высокоуровневые цели на этот раз весьма просты. Я создам приложение Node.js, действующее как веб-сайт и предоставляющее данные мобильному устройству, которое затем будет отображать столбчатую диаграмму с данными по атмосферным осадкам. Мне нужен веб-сервер на основе Node.js, поскольку, в целом, не рекомендуется прямой доступ мобильного устройства к хранилищам данных.
Эту функцию могли бы выполнять другие технологии, например ASP.NET Web API или Microsoft Azure API Apps. Большинство архитекторов посоветовало бы вам фильтровать, сортировать и разбирать данные в промежуточном уровне на сервере, а не на самом мобильном устройстве. Дело в том, что мобильные устройства обладают меньшей вычислительной мощью и вы вряд ли захотите посылать по сети большие объемы информации на устройство. Короче говоря, позвольте серверу выполнять тяжелую работу и оставьте мобильному устройству лишь визуализацию результатов.
Приступаем
Чтобы выполнять примеры в этой статье, вам потребуется закончить предыдущие два из первых двух статей за апрель и июнь. В ином случае вы можете вывернуться, вручную создав собственное хранилище данных DocumentDB.
Предполагая, что вы ранее создали хранилище данных DocumentDB, отправимся на Azure Portal и просмотрим базу данных DocumentDB. На этом портале имеется отличный инструментарий, который позволяет просматривать данные и выполнять различные запросы. Как только вы проверите, что данные на месте, и поймете их базовую структуру, вы можете приступать к созданию веб-сервера Node.js.
Разработчики обычно предпочитают проверять свои программы по мере того, как они их создают. Многие, например, начинают с модульного тестирования с помощью таких инструментов, как Mocha или Jasmine. Так, прежде чем создавать мобильный клиент, имеет смысл убедиться в том, что веб-сервер Node.js функционирует, как ожидалось. Один из подходов — использовать веб-прокси вроде Fiddler. Это упрощает выдачу веб-запросов (например, HTTP GET) и просмотр ответа в родном формате JSON. Это может быть полезно, поскольку, если при создании мобильного клиента возникнут какие-либо проблемы, вы сможете быть уверенным, что они относятся именно к мобильному клиенту, а не к веб-сервису.
Чтобы не усложнять картину, используйте в качестве клиента устройство с Windows Phone, пусть даже устройства с iOS и Android доминируют на рынке. Возможно, лучшим подходом будет одна из веб-технологий. Это позволит вам использовать или визуализировать данные с любого устройства, не ограничивая себя лишь мобильными устройствами. Другой подход — использовать кросс-платформенные продукты, такие как Xamarin.
При создании мобильного веб-клиента можно использовать несколько источников информации, среди которых не последнюю роль играет комитет по стандартам W3C. О нем можно узнать больше в документах стандартов по ссылке bit.ly/1PQ6uOt. Apache Software Foundation также проделал немалую работу в этой области. Более подробно об этой организации см. на cordova.apache.org. Я сосредоточился здесь на Windows Phone потому, что соответствующий код прямолинеен, прост в отладке и восприятии.
DocumentDB на портале
Я буду опираться на предыдущую работу, в ходе которой мной было создано хранилище данных DocumentDB для информации по городам и температурам. Чтобы найти свою базу данных DocumentDB (TemperatureDB), просто щелкните элемент меню Browse и выберите DocumentDB Accounts. Вы также могли бы закрепить свою базу данных как плитку на начальной странице, щелкнув правой кнопкой мыши и сделав начальную страницу Azure Portal информационной панелью (dashboard). Прекрасное сводное описание инструментария портала для взаимодействия с базой данных DocumentDB см. по ссылке bit.ly/112L4X1.
Одно из наиболее полезных средств на новом портале — функциональность запроса и просмотра данных в DocumentDB, позволяющая видеть данные по городам и температурам в их родном формате JSON. Это значительно упрощает изменение веб-сервера Node.js и формирование любых необходимых запросов.
Создание веб-сервера Node.js
Одна из первых задач при создании своего веб-сервера на основе Node.js — подключение к хранилищу данных DocumentDB. Нужные строки подключения вы найдете на Azure Portal. Чтобы получить строки подключения с портала, выберите хранилище TemperatureDB, затем щелкните All Settings и Keys.
Оттуда вам понадобятся две части информации. Первая — это URI для хранилища данных DocumentDB, а вторая — ключ защиты информации (называемый Primary Key на портале), который дает возможность вводить в действие защищенный доступ от веб-сервера Node.js. Вы увидите информацию о подключении и базе данных в следующем коде:
{
"HOST" : "https://temperaturedb.documents.azure.com:443/",
"AUTH_KEY" : "secret key from the portal",
"DATABASE" : "TemperatureDB",
"COLLECTION" : "CityTempCollection"
}
Веб-сервер Node.js будет считывать эту конфигурационную информацию. Поле host в вашем конфигурационном файле будет другим, так как все URI глобально уникальны. Ключ авторизации также уникален.
В предыдущих статьях база данных по температурам была названа TemperatureDB. В каждой базе данных есть несколько наборов, но в данном случае присутствует только один набор — CityTemperature. Набор — не что иное, как список документов. В этой модели данных один документ представляет город с данными по температурам за 12 месяцев.
Когда вы углубитесь в детали кода для веб-сервера Node.js, вы сможете задействовать преимущества обширной экосистемы вспомогательных библиотек для Node.js. Кроме того, в этом проекте вы будете использовать две библиотеки (называемые Node Packages). Первый пакет предназначен для функциональности DocumentDB. Команда установки пакета DocumentDB выглядит так: npm install documentdb. Второй пакет предназначен для чтения конфигурационного файла; установите его командой npm install nconf. Эти пакеты предоставляют дополнительные возможности, отсутствующие в изначальной Node.js. Подробное руководство по созданию приложения Node.js для DocumentDB см. в документации Azure по ссылке bit.ly/1E7j5Wg.
В веб-сервере Node.js семь разделов (рис. 1). В разделе 1 описываются подключения к некоторым установленным пакетам, чтобы вы имели к ним доступ далее в коде. В этом же разделе определяется порт по умолчанию, к которому будет подключаться мобильный клиент. При развертывании в Azure номер порта управляется самой Azure, чем и объясняется наличие конструкции process.env.port.
Рис. 1. Веб-сервер на основе Node.js
// +-----------------------------+
// | Раздел 1 |
// +-----------------------------+
var http = require('http');
var port = process.env.port || 1337;
var DocumentDBClient = require('documentdb').DocumentClient;
var nconf = require('nconf');
// +-----------------------------+
// | Раздел 2 |
// +-----------------------------+
// Сообщаем nconf, какой конфигурационный файл
// следует использовать
nconf.env();
nconf.file({ file: 'config.json' });
// Считываем конфигурационные данные
var host = nconf.get("HOST");
var authKey = nconf.get("AUTH_KEY");
var databaseId = nconf.get("DATABASE");
var collectionId = nconf.get("COLLECTION");
// +-----------------------------+
// | Раздел 3 |
// +-----------------------------+
var client = new DocumentDBClient(host, {masterKey: authKey});
// +-----------------------------+
// | Раздел 4 |
// +-----------------------------+
http.createServer(function (req, res) {
// Прежде чем запрашивать Items в хранилище документов,
// нужно убедиться, что у вас есть база данных
// с каким-либо набором, а затем использовать
// набор для чтения документов
readOrCreateDatabase(function (database) {
readOrCreateCollection(database, function (collection) {
// Выполняем запрос для получения и отображения данных
listItems(collection, function (items) {
var userString = JSON.stringify(items);
var headers = {
'Content-Type': 'application/json',
'Content-Length': userString.length
};
res.write(userString);
res.end();
});
});
});
}).listen(8124,'localhost'); // 8124 – номер порта, который
// сработал на моем компьютере,
// используемом для разработок
// +-----------------------------+
// | Раздел 5 |
// +-----------------------------+
// Если базы данных нет, создаем ее или возвращаем объект
// базы данных. Используйте queryDatabases, чтобы проверить,
// существует ли база данных с этим именем. Если вам не удалось
// ее найти, создайте новую базу данных через createDatabase
// с переданным идентификатором (из конфигурационного файла)
// в заданной конечной точке (из того же файла).
var readOrCreateDatabase = function (callback) {
client.queryDatabases('SELECT * FROM root r WHERE r.id="' +
databaseId + '"').toArray(function (err, results) {
console.log('readOrCreateDatabase');
if (err) {
// Произошла какая-то ошибка, сгенерировать ее заново
// для передачи далее
throw (err);
}
if (!err && results.length === 0) {
// Ошибки нет, но никаких результатов не получено,
// что указывает на отсутствие базы данных,
// соответствующей запросу
client.createDatabase({ id: databaseId }, function (
err, createdDatabase) {
console.log('client.createDatabase');
callback(createdDatabase);
});
} else {
// База данных найдена
console.log('found a database');
callback(results[0]);
}
});
};
// +-----------------------------+
// | Раздел 6 |
// +-----------------------------+
// Если в указанной базе данных нет набора, создаем его
// или возвращаем объект набора. Как и в случае
// readOrCreateDatabase, этот метод сначала пытается найти
// набор с переданным идентификатором. Если таковой есть,
// он возвращается, иначе он создается за вас.
var readOrCreateCollection = function (database, callback) {
client.queryCollections(database._self, 'SELECT *
FROM root r WHERE r.id="' + collectionId + '"').toArray(
function (err, results) {
console.log('readOrCreateCollection');
if (err) {
// Произошла какая-то ошибка, сгенерировать ее заново
// для передачи далее
throw (err);
}
if (!err && results.length === 0) {
// Ошибки нет, но никаких результатов не получено,
// что указывает на отсутствие набора в указанной
// базе данных, соответствующей запросу
client.createCollection(database._self,
{ id: collectionId },
function (err, createdCollection) {
console.log('client.createCollection');
callback(createdCollection);
});
} else {
// Набор найден
console.log('found a collection');
callback(results[0]);
}
});
};
// +-----------------------------+
// | Раздел 7 |
// +-----------------------------+
// Запрашиваем в предоставленном наборе все невыполненные
// элементы. Используйте queryDocuments для поиска всех
// документов в наборе, которые пока не завершены или где
// completed = false. При этом используется грамматика запросов
// DocumentDB, которая основана на ANSI-SQL. Это сделано
// специально, чтобы продемонстрировать это привычное,
// но мощное средство запросов.
var listItems = function (collection, callback) {
client.queryDocuments(collection._self, 'SELECT c.City,
c.Temperatures FROM c where c.id="WACO- TX"').toArray(
function (err, docs) {
console.log('called listItems');
if (err) {
throw (err);
}
callback(docs);
});
}
В разделе 2 код считывает файл config.json, который содержит информацию о подключении, включая базу данных и набор документов. Всегда имеет смысл брать строковые литералы, относящиеся к информации о подключении и помещать их раздельно в конфигурационный файл.
Раздел 3 — это клиентский объект соединения, используемый вами для взаимодействия с DocumentDB. Соединение передается конструктору DocumentDBClient.
Раздел 4 представляет код, выполняемый, когда мобильный клиент подключается к веб-серверу Node.js. Конструкция createServer является базовым примитивом для приложений Node.js, которая поддерживает ряд концепций, относящихся к циклу обработки сообщений и к обработке HTTP-запросов. Подробнее об этой конструкции см. по ссылке bit.ly/1FcNq1E.
Она представляет высокоуровневую точку входа для клиентов, соединяющихся с веб-сервером Node.js. Она также отвечает за вызов других частей кода на основе Node.js, который считывает JSON-данные из DocumentDB. Полученные данные запаковываются как полезные JSON-данные и доставляются на мобильный клиент. Он использует объекты запроса и ответа, которые являются параметрами функции createServer: (http.createServer(function (req, res)...).
В разделе 5 начинается обработка запросов к DocumentDB. Ваше хранилище данных DocumentDB может содержать несколько баз данных. Цель раздела 5 — сузить данные в DocumentDB URI и указать на конкретную базу данных. В нашем случае это TemperatureDB. Вы заметите некоторый дополнительный код, не используемый напрямую, но присутствующий исключительно в целях обучения. Вы также увидите код для создания базы данных, если таковой еще нет. Большая часть логики в разделе 5 и далее основана на npm-пакете DocumentDB, установленном ранее.
Раздел 6 представляет следующий этап процесса получения данных. Этот код автоматически вызывается в результате выполнения кода из раздела 5. Раздел 6 еще больше сужает данные — до набора документов, используя базу данных (TemperatureDB), указанную в разделе 5. Вы заметите выражение Select, которое включает блок where для набора CityTemperature. Здесь же находится некоторый код, который создает набор, если такового еще нет.
Раздел 7 представляет конечный запрос, выполняемый до возврата данных мобильному клиенту. Чтобы не усложнять общую картину, в запрос «зашит» возврат температурных данных по городу Уэйко (Waco), штат Техас. В реалистичном сценарии мобильный клиент передавал бы название конкретного города (на основе пользовательского ввода или местонахождения устройства). Затем веб-сервер Node.js принимал бы переданное название города и дописывал бы его в блок where в разделе 7.
Веб-сервер Node.js теперь полностью закончен и готов к работе. После запуска он будет бесконечно ожидать клиентские запросы от мобильного устройства. Веб-сервер Node.js вы будете выполнять локально, на компьютере для разработок. К этому моменту имеет смысл использовать Fiddler, чтобы начать тестирование веб-сервера Node.js. Fiddler позволяет выдавать HTTP-запросы (в нашем случае — GET) веб-сервису и просматривать ответ. Проверка поведения в Fiddler может помочь в устранении любых проблем перед созданием мобильного клиента.
Теперь вы готовы создать мобильный клиент, который включает два базовых архитектурных компонента: XAML UI и CS Code-Behind (где находится программная логика). Код, показанный на рис. 2, представляет разметку визуального интерфейса на мобильном клиенте.
Рис. 2. XAML-разметка для столбчатой диаграммы на основном экране мобильного клиента
<Page
x:Class="CityTempApp.MainPage"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:CityTempApp"
xmlns:d="https://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
xmlns:charting="using:WinRTXamlToolkit.Controls.DataVisualization.Charting"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid>
<!-- WinRTXamlToolkit chart control -->
<charting:Chart
x:Name="BarChart"
Title=""
Margin="5,0">
<charting:BarSeries
Title="Rain (in)"
IndependentValueBinding="{Binding Name}"
DependentValueBinding="{Binding Value}"
IsSelectionEnabled="True"/>
</charting:Chart>
</Grid>
</Page>
Заметьте, что код включает WinRTXamlToolkit, который можно найти на CodePlex по ссылке bit.ly/1PQdXwO. Этот инструментальный набор содержит ряд интересных элементов управления. Здесь используется один из них: элемент управления Chart. Для вывода данных на диаграмму, просто создайте набор «имя-значение» и подключите его к этому элементу управления. В нашем случае нужно создать набор «имя-значение» с данными по атмосферным осадкам за каждый месяц в данном городе.
Прежде чем представлять конечное решение, должен предупредить вас о некоторых подвохах. Можно было бы оспорить использование «родного» приложения Windows Phone в пользу более веб-ориентированного подхода.
Мобильный клиент, созданный здесь, предельно сокращен, чтобы предоставить минимально необходимую функциональность. Например, столбчатая диаграмма появится сразу после запуска клиента Windows Phone. Дело в том, что из обработчика события OnNavigatedTo выдается веб-запрос веб-серверу Node.js. Он автоматически выполняется один раз при запуске клиента Windows Phone. Вы можете увидеть это в разделе 1 кода мобильного клиента, показанного на рис. 3.
Рис. 3. Код для мобильного клиента
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
this.NavigationCacheMode = NavigationCacheMode.Required;
}
// +-----------------------------+
// | Раздел 1 |
// +-----------------------------+
protected override void OnNavigatedTo(NavigationEventArgs e)
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(
"http://localhost:8124");
request.BeginGetResponse(MyCallBack, request);
}
// +-----------------------------+
// | Раздел 2 |
// +-----------------------------+
async void MyCallBack(IAsyncResult result)
{
HttpWebRequest request =
result.AsyncState as HttpWebRequest;
if (request != null)
{
try
{
WebResponse response = request.EndGetResponse(result);
Stream stream = response.GetResponseStream();
StreamReader reader = new StreamReader(stream);
JsonSerializer serializer = new JsonSerializer();
// +-----------------------------+
// | Раздел 3 |
// +-----------------------------+
// Структуры данных, возвращаемые из Node
List<CityTemp> cityTemp = (List<CityTemp>)serializer.
Deserialize(reader, typeof(List<CityTemp>));
// Структура данных, подходящая для элемента
// управления Chart в Phone
List<NameValueItem> items = new List<NameValueItem>();
string[] months = { "Jan", "Feb", "Mar", "Apr", "May",
"Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
for (int i = 11; i >= 0; i-- )
{
items.Add(new NameValueItem { Name = months[i],
Value = cityTemp[0].Temperatures[i] });
}
// +-----------------------------+
// | Раздел 4 |
// +-----------------------------+
// Предоставляем данные для столбчатой диаграммы
await this.Dispatcher.RunAsync(
CoreDispatcherPriority.Normal, () =>
{
this.BarChart.Title = cityTemp[0].City + ", 2014";
((BarSeries)this.BarChart.Series[0]).
ItemsSource = items;
});
}
catch (WebException e)
{
return;
}
}
}
}
// +-----------------------------+
// | Раздел 5 |
// +-----------------------------+
// Структуры данных, возвращаемые из Node
public class CityTemp
{
public string City { get; set; }
public List<double> Temperatures { get; set; }
}
// Структура данных, подходящая для элемента
// управления Chart в Phone
public class NameValueItem
{
public string Name { get; set; }
public double Value { get; set; }
}
Кроме того, в разделе 1 вы заметите, что происходит подключение к веб-серверу Node.js, выполняемому на локальном хосте (localhost). Очевидно, вы указали бы другую конечную точку, если бы разместили свой веб-сервер Node.js в общедоступном облаке, например в Azure Web Sites. После выдачи веб-запроса вы подготавливаете обратный вызов, используя BeginGetResponse. Веб-запросы асинхронны, поэтому в коде подготавливается обратный вызов (MyCallBack). Это приводит нас к разделу 2, где данные принимаются, обрабатываются и загружаются в элемент управления Chart.
Обратный вызов в разделе 2 для асинхронного веб-запроса, упомянутый в разделе 1, обрабатывает полезные данные, возвращаемые веб-сервисом. Код обрабатывает веб-ответ и сериализует данные, которые находятся в формате JSON. В разделе 3 происходит преобразование этих JSON-данных в две структуры данных, определенные в разделе 5. Цель — создать список массивов или списков «имя-значение». Класс NameValueItem является структурой, необходимой элементу управления Chart.
В разделе 4 используется GUI-поток, чтобы присвоить список «имя-значение» элементу управления Chart. Вы можете увидеть присваивание элемента в наборе ItemsSource. Синтаксис await this.Dispatcher.RunAsync использует GUI-поток для обновления визуальных элементов управления. Код не будет правильно работать, если вы попытаетесь обновить визуальный интерфейс, используя тот же поток, где обрабатываются данные и выдается веб-запрос.
Теперь вы можете запустить мобильный клиент. Однако вы можете потерять некоторые температурные данные, поэтому появятся не все элементы управления Bar.
Заключение
На этом серия из трех статей завершается. В них я продемонстрировал полный IoT-сценарий — от процесса получения данных до их сохранения и визуализации. Я начинал эту серию с получения данных программой на C, выполняемой в Ubuntu; она симулировала код, который должен выполняться на устройстве Raspberry Pi. Вы можете вставлять данные, захваченные датчиком температуры, в Azure Event Hubs. Однако хранящиеся здесь данные эфемерны, и вам нужно перемещать их в более постоянное хранилище.
Во второй статье фоновый процесс принимал данные от Event Hubs и перемещал их как в Azure SQL Database, так и в DocumentDB. И в заключительной статье эти сохраненные данные предоставлялись мобильным устройствам, используя промежуточный уровень, выполняющий Node.js.
Здесь возможна масса потенциальных расширений и усовершенствований. Например, одна из областей, которые вы могли бы исследовать, — понятие машинного обучения и аналитики. Визуализация в виде столбчатой диаграммы отвечает на базовый вопрос: «Что произошло?». Более интересным мог бы быть вопрос: «Что произойдет?». Иначе говоря, тогда вы смогли бы прогнозировать будущие атмосферные осадки. И кульминацией мог бы стать ответ на вопрос: «Что я должен сделать сегодня, опираясь на прогнозы будущего?».
Бруно Теркали (Bruno Terkaly) — ведущий инженер программного обеспечения в Microsoft. Его основная цель — обеспечить разработку лидирующих в отрасли приложений и сервисов, способных работать на любых устройствах. Отвечает за развитие главных возможностей облачных и мобильных технологий на территории США и за ее пределами. Помогает партнерам в выводе их приложений на рынок, обеспечивая руководство при проектировании архитектур и глубокие технические знания на этапах оценки, разработки и развертывания приложений, создаваемых независимыми поставщиками ПО (ISV). Кроме того, тесно взаимодействует с группами облачных и мобильных технологий, организуя обратную связь и влияя на их дорожные карты.
Выражаю благодарность за рецензирование статьи экспертам Microsoft Лиленду Холмквисту (Leland Holmquest), Дэвиду Магнотти (David Magnotti) и Нику Трафу (Nick Trogh).