среда, 21 декабря 2011 г.

Кроссплатформенный парсинг конца строки


Лирическое вступление. Мое погружение в SNMP.


Компания, в которой я имел счастье работать до начала 2011 года занималась, ни много ни мало, разработкой, изготовлением, внедрением и поддержкой собственного телекоммуникационного оборудования. Занимаясь разработкой программного комплекса призванного мониторить и управлять всем этим хозяйством из тысяч разнотипных устройств вполне логично было столкнуться с использованием протокола SNMP.

Надо сказать, что лично я не считаю протокол SNMP верхом совершенства, хотя определённая красота, в нем, разумеется имеется. Долгое время для мониторинга и управления нашим хозяйством я использовал свою собственную двухуровневую систему протоколов, разрешающей нам все прихоти наших концепций управления и мониторинга, а также физических каналов передачи данных. Однако для поддержания веса продукта требуется поддержка стандартов, поэтому пришлось наконец засучить рукава и вплотную заняться SNMP.

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

У каждого творца есть свой порог детализации используемых им объектов. Все чаще я встречаю разработчиков, которые стараются мыслить категориями наиболее верхних порядков. Меня, наоборот, более привлекают низкоуровневые категории. Отчасти поэтому, я решил подойти к SNMP на максимально низком уровне, т.е. с нуля, без использования какого-либо готового кода. Такой подход не только удовлетворил моё любопытство (за счёт компании, разумеется), но и дал максимальную свободу во внедрении протокола в готовую систему на обоих её концах (клиенте и сервере).

Благо, что сама сущность протокола в системе была выделена на уровне готовой спецификации. Любой новый протокол мог быть вставлен в систему обычной динамической библиотекой, удовлетворяющей сформулированной спецификации. Протокольная сущность должна была не только разрешать проблемы кодирования и декодирования согласно особенностям самого протокола, но и конвертировать логику интерфейса верхних слоев системы в систему запросов конкретного протокола. На момент начала внедрения SNMP система уже работала с двумя разными протоколами (родным K095 и неким msk-json, используемом на одном из типов серверов компании).

Первое с чем я столкнулся, это низкая детализация и популяризация вопроса несмотря на обширное количество Интернет-ресурсов, посвящённых вопросам SNMP. Опираясь на найденные мною материалы крайне тяжело было понять, что же собственно надо написать, чтобы ЭТО заработало.

Прошло около недели изучения разного рода документации, пока, наконец в моей голове появилось понимание таких важных для программирования протокола терминов, как SNMP, MIB, ASN.1, BER в едином контексте поставленной задачи.

Наконец я понял как от терминов верхнего уровня протокола SNMP (запросы get, get-next, set и пр.) перейти к последовательности байт, которые отражают эти запросы в сетевом трафике. С этого момента стало особенно интересно. Началось непосредственное кодирование.

Сначала написал код синтеза запросов и отправил его на готовый сервер. К радости, получил с сервера ответ. Написал парсер ответов. Подошёл вплотную к парсингу MIB-файлов. За два дня удалось выполнить синтаксический разбор MIB-а нашей компании. В общем, получилось некое тестовое приложение, написанное полностью с нуля (относительно категорий протокола SNMP), парсящее MIB-файл параметров устройств компании и выполняющих по нему систему запросов. В общем (творцы меня поймут) радости не было предела.

Кроссплатформенный парсинг конца строки


Теперь, все-таки, пора перейти к главному. Все написанное выше можно рассматривать как затянувшееся вступление, к тому, о чем, собственно, и хотелось рассказать мне в этом посте.

Дело в том, что в процессе парсинга MIB-файла, в очередной раз встала задача обхода символов конца строки конечным автоматом лексического анализатора. Казалось бы тривиальная задача, но, никогда ранее мне не удавалось решение, которое бы я посчитал красивым. На этот раз, кажется, получилось.

Напомню, что конец строки, в разных операционных системах представляется по разному и используется для это манипуляция с кодами 0×0A (line feed) и 0×0D (carret return). Такая ситуация усложняет реализацию счетчика строк при потоковом парсинге файла. Так как речь идет о кроссплатформенной системе, то вопрос особенно актуален. Хотя, даже если речь идёт о какой-то конкретной платформе, не факт, что вы будете иметь дело с файлом подготовленным на такой же платформе, в редакторе, который формирует окончание строки в удобном вам виде.

Детализирую проблему.
1. Мы не знаем, один или два кода используются для разделения строк.
2. Мы не знаем, является ли первым (а может и единственным) кодом, код 0×0A или код 0×0D.

Такая постановка вопроса не позволяет тупо инкрементировать счётчик строк по какому-либо из этих двух кодов. Для тех, кто ещё мучается данной проблемой, возможно, будет интересно посмотреть моё решение.

// m_ucNL - флаг переноса
            // m_iRow - счетчик строк
            // m_iCol - счетчик колонок (позиция символа в строке) 

            // берем очередной символ из потока
            unsigned char ch = pchData[i++];

            if (ch == 0x0a || ch == 0x0d) {
                if (m_ucNL == 0) m_ucNL = ch;
                /* такое разделение условий позволит переключатся только по первому коду!!! */
                if (m_ucNL == ch) {
                    ++m_iRow; // инкрементируем счетчик строк
                }
                m_iCol = 0;
            } else {
                m_ucNL = 0;
                ++m_iCol;
            }

Дополнение от 14 февраля 2012 года

Один из моих знакомых, читателей данного блога, недавно сообщил мне, что нашел ошибку в данном счетчике строк. По его мнению, последовательности типа "\x0A\x0A" (*nix) или "\x0D\x0A\x0D\x0A" (DOS/Windows) будут восприняты таким счетчиком как одна строка.

На случай, если кто-то еще опасается за такую ошибку счетчика, сообщаю, что все нормально. Ошибки нет. Обратите внимание, что флаг m_ucNL приобретает значение только если прежде его значение было равно нулю. Следовательно в последовательности символов разделителей флаг получит значение первого символа. Далее, обратите внимание на то, что инкремент счетчика строк возникает только при условии равенства флага m_ucNL текущему символу разделителя, т.е. счетчик строк будет инкрементироваться только по каждой новой строке.