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

от автора

В 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


ссылка на оригинал статьи https://habr.com/ru/post/459014/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *