Иногда мне приходится писать раного рода парсеры для двоичный форматов. Почти всегда это работа по сети с разными устройствами по разным протоколам. Для примера возьмем netSDR — протокол работы с цифровыми приемниками. https://www.moetronix.com/files/NetSdrInterfaceSpec105.pdf
Парсеры должны быть довольно быстрыми и просто написанными (для того чтобы ошибки были маловероятны, легко находимы и устранимы).
Но я не могу выработать для себя идельный набор best practices.
Мой последний список такой
Максимально использовать терминологию протокола, хорошо помогает начать использовать библиотеку тем пользователям которые все-таки читают документацию)
Использовать System.IO.Pipelines для управления буферами памяти
Использовать события C# для генерации
Использовать Span<T> и структуры чтобы не выделять память в куче
Пока не ясные моменты:
Кажется уместным по соображениям удобстра комбинирования событий использовать IObservable<T>, но метод OnNext(T) не допускает передачи параметров по ссылке (ref или in), что меня расстраивает)
Для некоторых сообщений общий формат такой
8 bit Length lsb , 3 bit type 5 bit Length msb, потом N байт тела сообщения.
Приходится делать событие OnDataMessageReceived(TypeAsEnum messageType, int len, Span<byte> dataBytes), а потом реинтерпритировать dataBytes в зависимости от типа. Хочется иметь событие OnDataMessageReceived(in
ТутКакойТоТипСПолями1Тип2Длина3Данные data). Но структуру неизвестного во время компиляции размера не объявишь, не особо страшно но хочется идеала)
Какие правилами, практиками вы пользуетесь?
Какие практики и правила кажутся вам уместными?
Здравствуйте, Flem1234, Вы писали:
F>Но я не могу выработать для себя идельный набор best practices. F>Мой последний список такой F>Использовать события C# для генерации
В смысле `event`!?
F>Пока не ясные моменты: F>Кажется уместным по соображениям удобстра комбинирования событий использовать IObservable<T>, но метод OnNext(T) не допускает передачи параметров по ссылке (ref или in), что меня расстраивает)
Можно использовать массивы (например) из пула — копировать в них разобранные спаны, отдавать на обработку, потом возвращать в пул.
F>Для некоторых сообщений общий формат такой F>8 bit Length lsb , 3 bit type 5 bit Length msb, потом N байт тела сообщения. F>Приходится делать событие OnDataMessageReceived(TypeAsEnum messageType, int len, Span<byte> dataBytes), а потом реинтерпритировать dataBytes в зависимости от типа. Хочется иметь событие OnDataMessageReceived(in F>ТутКакойТоТипСПолями1Тип2Длина3Данные data). Но структуру неизвестного во время компиляции размера не объявишь, не особо страшно но хочется идеала)
А как вы собираетесь написать код для обработки "структуры неизвестного во время компиляции размера"?
Мне кажется удобным по типу сообщения создать структуру, в которую передаётся спан и другое необходимое и внутри неё уже этот спан парсить, оттадавая вовне интерпретацию байтов/чаров в зависимости от типа.
Help will always be given at Hogwarts to those who ask for it.
Здравствуйте, _FRED_, Вы писали:
F>>Использовать события C# для генерации
_FR>В смысле `event`!?
Да, а что?
F>>Пока не ясные моменты: F>>Кажется уместным по соображениям удобстра комбинирования событий использовать IObservable<T>, но метод OnNext(T) не допускает передачи параметров по ссылке (ref или in), что меня расстраивает)
_FR>Можно использовать массивы (например) из пула — копировать в них разобранные спаны, отдавать на обработку, потом возвращать в пул.
Да, так и есть — пулы везде.
_FR>А как вы собираетесь написать код для обработки "структуры неизвестного во время компиляции размера"?
Так далеко моя мысль еще не заходила) Самый распространенный случай — это сообщение в заголовке содержит длину массива, а в теле сам массив. Но длина может быть разная для разных устройств и т.п.
_FR>Мне кажется удобным по типу сообщения создать структуру, в которую передаётся спан и другое необходимое и внутри неё уже этот спан парсить, оттадавая вовне интерпретацию байтов/чаров в зависимости от типа.
Так и делаю.
Основная проблема при этом то, что протоколы, в общем-то похожи друг на друга, форматы сообщений тоже. Но нет совершенства, все думаю как бы сделать лучше)
Здравствуйте, Flem1234, Вы писали:
F>Какие правилами, практиками вы пользуетесь? F>Какие практики и правила кажутся вам уместными?
Для эффективного чтения устройств можно посмотреть что-то полезное тут.
Не понимаю, почему разобранное сообщение не должно попадать в кучу. В крайнем случае, можно сделать большой буфер повторно используемых объектов сообщений, чтобы количество новых сообщений в нём было всегда меньше количества читаемых. Пример такого подхода.
Парсер, чтобы быть эффективным, должен заранее уметь разбирать всех форматы входящих сообщений. Его удобно разделить на сканер и построитель. Задача сканера — скопировать тело сообщения в свой буфер (тут пригодится ArrayPool<T>, пример использования или что-то вроде этого) и выбрать правильный построитель сообщения. Задача построителя — собственно разобрать тело сообщения (буфер сканера) и сформировать объект сообщения с конкретным типом (либо создавать их, либо повторно использовать объекты из пула). Сканер и построитель имеют только синхронный код и работают с ReadonlySequence/Memory/Span.
Для чтения сообщений можно использовать новый API Channels.
Здравствуйте, Flem1234, Вы писали:
F>Иногда мне приходится писать раного рода парсеры для двоичный форматов. Почти всегда это работа по сети с разными устройствами по разным протоколам. Для примера возьмем netSDR — протокол работы с цифровыми приемниками. https://www.moetronix.com/files/NetSdrInterfaceSpec105.pdf F>Парсеры должны быть довольно быстрыми и просто написанными (для того чтобы ошибки были маловероятны, легко находимы и устранимы).
Чтобы ошибки были маловероятными будет уместно задействовать кодогенерацию и декларативное описание как сделано в Kaitai.
У библиотеки, к сожалению, есть описанные вами недостатки, т.к. разрабатывалась она до появления Span-ов.
Но это легко решаемо через правки библиотеки или через пост обработку кода.
Здравствуйте, _NN_, Вы писали:
_NN>Чтобы ошибки были маловероятными будет уместно задействовать кодогенерацию и декларативное описание как сделано в Kaitai.
Да, я смотрел в сторону Kaitai. Если есть опыт, поделитесь, пожалуйста. Интересует универсальность — можно ли описать большую часть бинарных форматов и что делать если не получается выразить что-то декларативно. Например, в некоторых протоколах встречались выражения по типу — длина тела пакета считается по формуле: длина заголовка (два варианта в зависимости от версии протокола) плюс длина подзаговока, плюс количество элементов в массиве значений умноженных на 4 (размер элемента).
Идея Kaitai мне нравится)
Здравствуйте, Flem1234, Вы писали:
F>Здравствуйте, _NN_, Вы писали:
_NN>>Чтобы ошибки были маловероятными будет уместно задействовать кодогенерацию и декларативное описание как сделано в Kaitai.
F>Да, я смотрел в сторону Kaitai. Если есть опыт, поделитесь, пожалуйста. Интересует универсальность — можно ли описать большую часть бинарных форматов и что делать если не получается выразить что-то декларативно. Например, в некоторых протоколах встречались выражения по типу — длина тела пакета считается по формуле: длина заголовка (два варианта в зависимости от версии протокола) плюс длина подзаговока, плюс количество элементов в массиве значений умноженных на 4 (размер элемента). F>Идея Kaitai мне нравится)
Формат описания данных очень крутой.
Можно описать практически всё.
Из недостатков это нет поддержки нулевых ссылок и повсеместное использование массивов вместо Span.
Мы решили это пост обработкой генерируемого кода.
По хорошему конечно лучше решить это правкой библиотеки