Июль 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).