0.3.0 • Published 3 years ago

message-reader v0.3.0

Weekly downloads
12
License
MIT
Repository
github
Last release
3 years ago

Message reader

Message reader позволяет выполнять чтение и разбор бинарных сообщений, передаваемых посредством протокола TCP. В Message reader используется предположение, что синтаксис сообщения можно описать подходящими регулярными выражениями и грамматикой LALR(1) (или LR(1)). Такой подход с одной стороны, в общем, конечно ограничивает спектр сообщений для которых данный функционал может быть использован, но с другой стороны позволяет, особенно если для сообщения уже определена стандартизированная грамматика, разработать достаточно быстро и достаточно компактный по размеру модуль для разбора сообщений конкретного типа.

Функционал Message reader опирается на материалы, описанные в Aho, Lam, Sethi, Ullman, Compilers: Principles, Techniques, and Tools. Исходный код, в основном, написан на Rust, который затем транслируется в код WebAssembly при помощи wasm-bindgen. WebAssembly позволяет увеличить производительность (по сравнению с javascript) и в тоже время оставаться коду кроссплатформенным. Message reader также поддерживает вставки кода на языке javascript, с помощью которых возможно управление процессом разбора сообщения.

Установка

Для Message reader требуетcя node v9.4.0 или выше.

npm install message-reader

Примеры использования

Calc

Http request parser

Описание

Чтобы Message reader смог обработать сообщение определенного типа для него необходимо задать набор регулярных выражений и грамматику, которые будут распознавать это сообщение. Регулярные выражения используются для автоматического построения лексического анализатора, распознавающего последовательности байтов в сообщении в виде токенов, а грамматика для автоматического построения парсера, распознающего синтаксическую структуру сообщения. Процесс чтения и распознавания сообщения кратко выглядит следующим образом. Вначале поток байтов сообщения подается на вход лексического анализатора. Далее токены, распознанные лексическим анализатором, подаются на вход парсера, где они преобразуется в терминальные символы с такими же именами, как и у токенов. Парсер, используя набор терминальных символов и правила грамматики, производит свертки продукций до стартовой продукции, выполняя таким образом распознавание сообщения.

Синтаксис регулярных выражений

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

Формат строки для распознавания токена

name reg_exp DEF

разделителями являются один или несколько пробельных символов

  • name - имя токена, состоящее из букв латинского алфавита, цифр или знака подчеркивания и начинающееся с буквы или знака подчеркивания. Также определен альтернативный способ задания имени токена, заключенного в одинарных кавычках;
  • reg_exp - регулярное выражение, распознающее токен. Язык для определения регулярных выражений:

    • Специальные символы: + - * | ? , ( ) [ ] { }
    • Визуальные символы, представляющие сами себя, (за исключением спец. символов) преобразуются в байтовое представление символа юникода в формате UTF-8
    • Экранирование символа, например спец. символов, при помощи двойного обратного слеша \\, например:

      \\+
    • Числовое значение байта, записанное двумя шестнадцатеричными числами, предваряемое \\x, например:

      \\xF5
      \\x2b
    • Представление символа юникода в виде последовательности четырех шестнадцатеричных цифр, предваряемое \\u, например символ A (латинского алфавита) можно представить следующим образом:

      \\u0041
    • Квантификаторы:

      • ? - ноль или одно повторение
      • * - ноль или более повторений
      • + - одно или более повторение
      • {n} - ровно n повторений
      • {m,n} - от m до n повторений включительно
      • {m,} - не менее m повторений
      • {,n} - не более n повторений
    • Альтернативы задаются при помощи спец. символа |, например:

      A|B
    • Наборы и диапазоны задаются при помощи квадратных скобок, например:

      [ABC]
      [A-Fa-f0-9]
    • Группа задается при помощи круглых скобок, например:

      (AB)|(CD)
  • DEF (опционально) - строка символов DEF служит для указания что имя будет являться определением. Определения не распознаются лексическим анализатором как самостоятельные токены. Определения, заключенное в фигурные скобки, можно использовать в регулярных выражениях других токенов (с целью избежания дублирования в описании регулярного выражения);

  • {action_code} (опционально) - заключенный в фигурные скобки фрагмент javascript кода, который будет выполнен непосредственно после распознавания токена с именем name. Фрагмент кода должен располагаться только в одной строке (которая описывает токен) и не должен содержать внутри дополнительных фигурных скобок, однако может содержать вызовы javascript функций (см. примеры). Внутри фрагмента кода контекстом является объект распознаваемого сообщения. Внутри фрагмента кода доступны функции get, set, set_name, pass:
    • get(): Uint8Array - возвращает значение токена в виде типизированного массива Uint8Array;
    • set(value: Array | Uint8Array | Buffer) - изменяет значение токена (устанавливает равным значению массива value);
    • set_name(name: String) - изменяет имя распознанного токена (устанавливает равным значению аргумента name);
    • pass() - заставляет лексический анализатор проигнорировать распознанный токен, без передачи его парсеру, и сразу же приступить к распознаванию следующего токена из входного потока (сообщения);

Пример определения токенов с помощью регулярных выражений

digit   [0-9]       DEF
letter  _|[A-Za-z]  DEF
id      {letter}({letter}|{digit})*
number  {digit}({digit}|{digit})*
'+'     \\+
'*'     \\*
'('     \\(
')'     \\)

Синтаксис грамматики

Грамматика также задается в виде текста, каждая строка которого представляет собой шаблон для описания продукции грамматики. В тексте допускаются пустые строки. Самая первая продукция является стартовой (к которой будут сворачиваться все продукции сообщения), порядок расположения остальных продукций в тексте значения не имеет.

Формат продукции грамматики

prod_name : symbol_name_1 ... symbol_name_n_1 [rust_action_code] ;

разделителями являются один или несколько пробельных символов

  • prod_name - имя продукции грамматики. Имя продукции состоит из букв латинского алфавита, цифр или знака подчерчивания, первой должна стоять буква или знак подчерчивания;
  • symbol_name_1 (,... ,symbol_name_n_1, symbol_name_n) (опционально) - имена символов грамматики. Терминальные символы грамматики указываются в одинарных кавычках, нетерминальные без них (это соглашение, позволяющее более наглядно увидеть в тексте грамматики терминальные и нетерминальные символы, парсер их не различает), правила их именования такие же как и для имени продукции;
  • rust_action_code - заключенный в квадратные скобки псевдокод, который будет выполнен при свертке продукции prod_name. Синтаксис:
    • set(index, index2 ,index4) - устанавливает значение символа prod_name равным значению символа по индексу index. Если дополнительно установлены index2 и т.д., то значение prod_name будет установлено равным конкатенации значений соответствующих символов. Данный псевдокод аналогичен выполнению функции javascript set (см. ниже), но его выполнение происходит в среде WebAssembly, без переключения в javascript, что позволяет быстрее выполнить операцию. Индекс символа определяется следующим образом. Самый правый символ в правой части продукции (symbol_name_n) имеет индекс - 0, символ стоящий рядом с ним левее (symbol_name_n_1) на единицу больше - 1 и так далее;
  • {action_code} (опционально) - заключенный в фигурные скобки фрагмент javascript кода, который будет выполнен при свертке продукции prod_name. Фрагмент кода должен располагаться только в одной строке (которая описывает продукцию) и не должен содержать внутри дополнительных фигурных скобок, однако может содержать вызовы javascript функций (см. примеры). Внутри фрагмента кода контекстом является объект распознаваемого сообщения. Внутри фрагмента кода доступны функции bind, id, get, lookup, set, set_val, set_name, set_name_from_hash, push_after:
    • bind(id: Number) - связывает целочисленное число id с создаваемым нетерминальным символом prod_name, которое может служить идентификатором некоторой сущности. Т.о. позволяет связать сущность с символом грамматики;
    • id(index: Number): Number - возвращает идентификатор, ранее связанный с символом грамматики по индексу index функцией bind;
    • get(index: Number): Uint8Array - возвращает значение символа грамматики по индексу index в виде объекта Uint8Array;
    • lookup(): Uint8Array - возвращает значение предпросмотренного (lookahead) символа грамматики;
    • set(index: Number) - устанавливает значение символа prod_name равным значению символа по индексу index;
    • set_val(value: Array | Uint8Array | Buffer) - устанавливает значение символа prod_name равным value;
    • set_name(name: String) - изменяет имя создаваемого символа грамматики (устанавливает равным значению аргумента name);
    • set_name_from_hash(hash_name: Number) - выполняет тоже что и функция set_name, за исключением того, что агрумент hash_name является числовым идентификатором (хешем) имени создаваемого символа грамматики, выполняется несколько быстрее set_name (хеш для имени можно получить используя функцию hash(name: String) : Number);
    • push_after(name: String , insert_name: String , size: Number) - вставляет во входной поток терминальный символ после ближайшего символа с именем name, insert_name - имя вставляемого символа, если не указано, то вставляется символ - признак окончания сообщения, insert_value - значение вставляемого символа, если не указано, то будет установлено null, size - задание параметра size переводит лексический анализатор из режима распознавания токенов в виде, заданном регулярными выражениями, в режим простого чтения последовательности байтов из входного потока. Прочитанные байты доступны через функцию onTknData, задаваемую в прототипе для контекста сообщения (см. ниже Сборка сервера для чтения сообщений). После прочтения числа байт длиной size лексический анализатор автоматически переводится обратно в режим распознавания токенов рягулярными выражениями.

Для сокращения записи можно объединять две продукции с одинаковым именем в одну строку, используя символ | в качестве разделителя. Так, например, следующая запись

E: E '+' T | E '-' T {console.log('js_action')};

эквивалентна следующей

E: E '+' T {console.log('js_action')};
E: E '-' T {console.log('js_action')};

Пример грамматики

start: E;
E: E '+' T | T;
T: T '*' F | F;
F: '(' E ')' | 'id' | 'number';

Сборка сервера (клиента) для чтения сообщений

Сборка производится функцией build(options: Object | Array<Object>, type: String): Server, которая возвращает экземпляр сервера или клиента:

  • options.regexp: String - строка, содержащая регулярные выражения
  • options.grammar: String - строка, содержащая грамматику
  • options.parserType: ParserType (опционально) - тип парсера (ParserType.LALR1 - может быть использован для разбора грамматики LALR(1), ParserType.LR1 - более мощный парсер, может быть использован для разбора грамматики LR(1). По умолчанию используется ParserType.LALR1)
  • options.proto: Object (опционально) - прототип для контекста сообщения (через прототип можно определять дополнительные методы для обработки сообщения). Специальные методы:
    • onBeforeParse() - если определен, вызывается перед стартом распознавания каждого сообщения из входного потока;
    • onAfterParse() - если определен, вызывается после каждого успешно распознанного сообщения;
    • onTknData(tknName: Number, tknData: Uint8Array, end: Boolean) - если определен, вызывается при чтении лексическим анализатором потока байтов из входного потока в режиме последовательного чтения байтов. tknName - имя токена, соответсвующего массиву байтов, tknData - массив прочитанных байтов, end - true, если поток байтов, соответсвующих токену tknName закончился, false в противном случае;
  • type: String - тип создаваемого экземпляра: сервер - server или клиент - client. По умолчанию значение - server;

    Допускается возможность сборки экземпляра распознающего несколько наборов регулярных выражений и грамматик. В этом случае они указываются в параметре options в виде массива, при этом автоматически будут активированы регулярные выражения и грамматика из первого элемента массива (с индексом 0). Динамически активировать другие регулярные выражения и грамматику можно при помощи функции setOptions (см. Постобработка распознанных сообщений и вспомогательные обработчики событий);

Пример:

const {build} = require('message-reader');
const regexp = `
  digit   [0-9]       DEF
  letter  _|[A-Za-z]  DEF
  id      {letter}({letter}|{digit})*
  number  {digit}({digit}|{digit})*
  '+'     \\+
  '*'     \\*
  '('     \\(
  ')'     \\)
`;
const grammar = `
  start: E;
  E: E '+' T | T;
  T: T '*' F | F;
  F: '(' E ')' | 'id' | 'number';
`;
const server = build({regexp, grammar});
const client = build({regexp, grammar}, 'client');

Запуск сервера на прослушивание входящий соединений

Запуск на прослушивание производится методом сервера listen(options: Object): Server. Внутри сервера для транспорта данных используются объекты стандартных классов node: net.Server или tls.Server. Опции:

  • options.tls: Boolean (опционально) - если true, то для транспорта данных используется tls.Server, в противном случае net.Server
  • options.port
  • options.host (опционально)
  • options.key (опционально)
  • options.cert (опционально)
  • options.ca (опционально)
  • options.requestCert (опционально)

Постобработка распознанных сообщений и вспомогательные обработчики событий

После успешного распознавания сообщения возможно провести с ним дополнительные действия. Для этого необходимо определить обработчик или обработчики используя метод сервера use(middleware: Function): Server в стиле мидлвар Koa. В функцию middleware передаются два параметра: ctx - контекст, представляет собой объект распознанного сообщения и next - ссылка на следующий обработчик, если он определен. В качестве обработчиков можно использовать асинхронные функции. Первый обработчик, определенный с помощью use будет вызван автоматически, последующие необходимо вызывать вручную, обращаясь к next.

Дополнительно можно определить вспомогательные обработчики событий при помощи метода handler:

  • handler('connection', (socket) => {const connection = {}; return connection;}): Server - обработчик нового клиентского соединения. socket - net.Socket - сокет для обмена данными с клиентом. Обработчик может вернуть произвольный объект, который может быть использован для хранения каких-либо дополнительных настроек данного соединения. Если обработчик ничего не вернул, объект connection будет создан автоматически. У объекта connection* автоматически устанавливаеются свойства:
    • connection.socket: net.Socket - сокет для обмена данными с клиентом;
    • connection.setOptions(index: Number) - функция, позволяющая сменить текущие регулярные выражения и грамматику;
  • handler('errorConnection', (conn, err) => {}): Server - обработчик ошибки клиентского соединения. conn - объект дополнительных настроек соединения connection, err - объект ошибки;
  • handler('closeConnection', (conn, hadError) => {}): Server - обработчик закрытия клиентского соединения. conn - объект connection, hadError: Boolean - true, если сокет был закрыт из-за ошибки передачи;
  • handler('listening', () => {}): Server - обработчик готовности сервера к приему входящих соединений, вызывается после запуска метода listen;
  • handler('close', () => {}): Server - обработчик закрытия сервера, вызывается после запуска метода close;

    Пример:

    server
    .use(async (ctx, next) => {
      //Код обработчика 1 сообщения
      await next(); //Вызов обработчика 2
    })
    .use(async (ctx, next) => {
      //Код обработчика 2 сообщения
    })
    .handler('errorConnection', (conn, err) => {
      //Код обработчика ошибки
    });

    У объекта ctx автоматически устанавливается свойство connection: Object, хранящее информацию о клиентском подлючении (см. выше).

Подключение клиента к серверу

Подключение клиента производится методом connect(options: Object): Client. Опции:

Обработка распознанных сообщений и вспомогательные обработчики событий для клиента

Для клиента используются такие же методы обработки, что и для сервера, за исключением вспомогательных обработчиков событий listening и close , которые не используются в случае клиента.

Пример:

client
.use(async (ctx, next) => {
  //Код обработчика сообщения
})
.handler('errorConnection', (conn, err) => {
  //Код обработчика ошибки
})
.handler('closeConnection', (conn, hadError) => {
  //Код обработчика закрытия соединения с сервером
});

Закрытие

Закрытие сервера или клиента производится методом close. Сервер окончательно будет закрыт после закрытия всех его клиентских соединений.

Пересборка модуля WebAssembly

Модуль WebAssembly, входящий в Message reader при необходимости можно перекомпилировать. Для этого потребуется установить Rust и wasm-pack. Пересборка осуществляется следующей командой:

wasm-pack build --target nodejs

Трассировка сообщений парсера

Для отладки работы грамматики может оказаться полезным режим трассировки сообщений парсера. В данном режиме на экране последовательно отображаются все переносы (Shift) и свертки (Reduce) символов грамматики, осуществляемые парсером. Для включения режима трассировки необходимо перекомпилировать модуль WebAssembly с флагом отладки:

wasm-pack build --debug --target nodejs
0.3.0

3 years ago

0.2.0

3 years ago

0.1.2

4 years ago

0.1.1

4 years ago

0.1.0

4 years ago