Pull to refresh

Выход из зоны комфорта: с nodejs на dlang

Reading time5 min
Views4.6K

В 2017м году я начал писать проект на nodejs — реализацию протокола ObjectServer от Weinzierl для доступа к значениям KNX. В процессе написания было изучено: работа с бинарными протоколами, представление данных, работа с сокетами(unix sockets в частности), работа с redis базой данных и pub/sub каналами.


Проект достиг стабильной версии. В это время я потихоньку ковыряю другие языки, в частности Dart и Flutter как его приложение. На полке пылится без действия купленный во времена студенчества справочник Г.Шилдта.


Настойчивая мысль переписать проект на C поселилась в голове. Рассматриваю варианты Go, Rust, отталкивающие иными синтаксическими конструкциями. Начать никак не получается, идея откладывается на время.


В мае этого года решил посмотреть язык D, почему-то уверенный в том, что буква D означает dynamic. Долго гадал откуда и почему эта мысль была в голове, так ответа не нашел. НО это уже не важно, поскольку переписыванием увлекся я на все лето.


Суть проекта


Модули KNX BAOS 830/832/838 подключены через UART к компьютеру, протокол ObjectServer обернут в FT1.2. Приложение устанавливает соединение с /dev/ttyXXX, обрабатывает входящие данные, отправляет туда же конвертированные из JSON сообщения байты пользовательского запроса, приходящего на PUB/SUB канал, либо в очередь заданий на базе списков Redis-а (для nodejs очереди реализованы пакетом bee-queue).


queue.on("job", data => {
  // предполагая валидное задание:
  // конвертировать данные, отправить в серийный порт
  // возвратить промис, который разрешится при входящем ответе
}); 

baos.on("data", data => {
  // понять, что это: индикация или ответ
  // если ответ, то разрешить промис из очереди
  // если индикация - обработать и отправить в pub/sub
});

Динамичность


JSON в js — вещь нативная, как обработка происходит в статически типизированных языках я представления не имел. Как оказалось, разницы немного. Для примера взять метод get value. В качестве аргументов он принимает либо число — номер датапоинта, либо массив номеров.


В js выполняются проверки:


if (Array.isArray(payload)) {
  // получить значения для массива

  return values;
}
if (typeof id === "number") {
  // получить значения одного объекта

  return value;
}

throw new Error("Неправильный id");

По сути то же самое на D:


if (payload.type() == JSONType.integer) {
  // вернуть одно значение
} else if (payload.type() === JSONType.array) {
  // вернуть массив значений
} else {
  throw Errors.wrong_payload_type;
}

Почему-то на момент рассмотрения Rust-a меня затормозило именно отсутствие представления о работе с JSON. Другой момент, связанный с динамичностью: массивы. В js привыкаешь к тому, что достаточно вызвать метод push для добавления элемента. На C динамичность реализуется ручным выделением памяти, а лезть туда не очень то и хотелось. Dlang, как оказалось, поддерживает динамические массивы.


ubyte[] res;

// хорошая практика - сначала сделать массив больше
res.length = 1000;

// а после заполнения изменить длину на нужную
res.length = count;

// чем менять каждый шаг длину массива на 1

Входящие UART данные в js конвертировались в Object. Для этих целей в D отлично подходят структуры, перечисления со значениями и объединения.


enum OS_Services {
  unknown,
  GetServerItemReq = 0x01,
  GetServerItemRes = 0x81,
  SetServerItemReq = 0x02,
  SetServerItemRes = 0x82,
  // ...
}

// ...
struct OS_Message {
  OS_Services service;
  OS_MessageDirection direction;
  bool success;
  union {
    // union of possible service returned structs
    // DatapointDescriptions/DatapointValues/ServerItems/ParameterBytes
    OS_DatapointDescription[] datapoint_descriptions;
    OS_DatapointValue[] datapoint_values;
    OS_ServerItem[] server_items;
    Exception error;
  };
}

При входящем сообщении:


ubyte mainService = data.read!ubyte();
ubyte subService = data.read!ubyte();
try {
  if (mainService == OS_MainService) {
    switch(subService) {
      case OS_Services.GetServerItemRes:
        result.direction = OS_MessageDirection.response;
        result.service= OS_Services.GetServerItemRes;
        result.success = true;
        result.server_items = _processServerItemRes(data);
        break;
      case OS_Services.SetServerItemRes:
        result.direction = OS_MessageDirection.response;
        // ...

В js я байтовые значения хранил в массиве, при входящих данных делал поиск и возвращал строку с именем сервиса. Структуры, перечисления и объединения выглядят строже.


Работа с массивами байтовых данных


Node.js мне нравится абстракция Buffer. Для примера: преобразования двух байтов в беззнаковое целое удобно выполнять методом readUInt16BE(offset), для записи — writeUInt16BE(value, offset), буфферы активно использовал для работы с бинарным протоколом. Для dlang я изначально начал шерстить репозиторий пакетов на что-либо похожее. Ответ нашелся в стандартной библиотеке std.bitmanip. Для беззнаковых целых длиной 2 байта: ushort start = data.read!ushort(), для записи: result.write!ushort(start, 2);, где 2й аргумент — смещение.


EE, promises, async/await.


Самым тяжелым представлялось программирование без EventEmitter. В node.js просто регистрируются функции слушатели, и при событии они вызываются. Таким образом, сильно думать не надо. В dlang пакетах tinyredis и serialport (зависимости моего приложения) есть неблокирующие методы для обработки сообщений. Решение простое: пока истина получать по очереди сообщения серийного порта и pub/sub канала. В случае входящего пользовательского запроса на pub/sub канал программа должна отправить сообщение в серийный порт, получить результат и отправить пользователю обратно в pub/sub. Методы для серийных запросов решено было сделать блокирующими.


while(!(_responseReceived || _resetInd || _interrupted)) {
  try {
    processIncomingData();
    processIncomingInterrupts();
    if (_resetInd || _interrupted) {
      _response.success = false;
      _response.service = OS_Services.unknown;
      _response.error = Errors.interrupted;

      _responseReceived = true;
      _ackReceived = true;
    }
// ...
// ...

return _response;

В цикле while данные опрашиваются неблокирующим методом processIncomingData(). Так же предусмотрена вероятность того, что KNX модуль может быть перезагружен (отключен и подключен заново к шине KNX или программно). Также обработчик processIncomingInterrupts() проверяет сервисный pub/sub канал на запрос reset. Никаких промисов и асинхронных функций, в отличие от предыдущих реализаций на js. Пришлось подумать над структурой программы (а именно последовательности вызова функций), но, засчет отсутствия лишних абстракций, программировать стало проще. По сути, когда в js коде вызывается await someAsyncMethod — асинхронная функция вызывается как блокирующая, проходя при этом через event loop. Сама возможность языка — это хорошо, но ведь можно обойтись и без нее.


Отличия


Очередь заданий. В node.js реализации для этой цели используется пакет bee-queue. В реализации на D запросы отправляются только через pub/sub.
В остальном все практически идентично.


Оперативной памяти компилируемая версия потребляет в 10 раз меньше, что может быть важно для одноплатных компьютеров.


Компиляция


Компиляция проводилась при помощи ldc на платформе aarch64.


Для установки ldc:


curl -fsS https://dlang.org/install.sh | bash -s ldc

Была собрана плата, состоящая из трех основных компонентов: NanoPi Neo Core2 качестве компьютера, KNX BAOS module 830 для связи с шиной KNX и Silvertel Ag9205 для PoE питания, на которой и осуществлялось программирование.


Внешний вид платы


Заключение


Не буду судить, какой язык лучше или хуже. Каждому свое: js отлично подходит для изучения, уровень абстракций(промисы, эмиттеры) позволяют достаточно легко и быстро строить структуру приложения. К реализации на dlang я подошел уже с ясным, заученным за полтора года, планом что делать. Когда знаешь какие данные необходимо обрабатывать и каким образом, статическая типизация не страшна. Неблокирующие методы позволяют организовать рабочий цикл. Это была первая моя работа на D, работа увлекательная и познавательная.


Насчет выхода из зоны комфорта (как указано в названии): в моем случае — у страха были глаза велики, что долго мешало мне попробовать что-то, помимо nodejs.


Исходные коды открыты и могут быть найдены на github.com/dobaos/dobaos

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+9
Comments3

Articles