вторник, 23 марта 2010 г.

[prog] Забавный баг при реализации TLV unpacker-а

Есть такой удобный формат для организации бинарных данных – TLV (Tag-Length-Value или Type-Length-Value). Его смысл в том, что каждая порция данных предваряется двумя обязательными полями – Tag и Length. Поле Tag содержит целочисленный идентификатор, который указывает, какой смысл несут данные. Поле Length содержит длину данных в байтах. Ну а дальше идут сами данные.

Удобство этого формата в том, что парсер может ничего не знать про содержимое некоторых блоков данных. Парсер считывает Tag и Length. Если значение Tag ему известно, то он обрабатывает данные. Если не известно, то следующие Length байт просто пропускаются. Благодаря этой схеме протоколы на основе TLV очень легко расширяются, сохраняя при этом совместимость с предыдущими версиями. Например, в какой-то PDU (Protocol Data Unit, элемент прикладного протокола, например, сообщение протокола) можно добавить новое поле, и старые клиенты будут его просто игнорировать.

Так же TLV удобен при передаче по потоковым коммуникационным каналам, например, по потоковым TCP/IP сокетам. Ведь в TCP, например, если отправитель отсылает 32K одной операцией write, то получатель может вычитать их не за одно обращение к read, а за два – первый раз, скажем, придет 28K, затем оставшиеся 4K. Вот с TLV-протоколами такая передача данных очень удобна – получатель всегда может определить, получил ли он весь пакет или нужно еще подождать. (С другой стороны, при отправке нужно сначала длину подсчитывать и в начало блока данных записывать Tag+Length, что может негативно сказываться на производительности, но это уже другой вопрос).

TLV-представление имеет очень широкое распространение. Так, ASN.1 BER – это TLV. Да и в Google Protobuf, AFAIK, так же используется принцип TLV.

Ну а теперь к сути повествования. Последние дни делал библиотеку для поддержки простого протокола для взаимодействия с клиентом. Упаковка сообщений выполняется в TLV. При распаковке обнаруживается интересный эффект – упаковывался объект сообщения A, а распаковывается объект сообщения B. При этом ошибок распаковки не возникает – тишь да гладь.

Ларчик, как водится, открывался просто – в парсере для нескольких Tag-ов по ошибке была зарегистрирована одна и та же фабрика (т.е. вместо фабрики объектов A регистрировалась фабрика объектов B). Ну это ладно, до этого я быстро додумался.

Озадачивало меня другое – почему при чтении объектом B полей объекта A не происходит никаких ошибок? Сообщения-то разные, и имеют только одно общее поле. Все остальные поля совершенно разные. Но объект B как-то умудряется их проглатывать, даже не поперхнувшись…

А дело как раз в расширяемости основанных на TLV протоколов :) Объект B, видя во входном потоке чужие поля (поля объекта A) просто игнорировал их, считая, что это новые опциональные поля о которых он не знает и знать не хочет.

Вот так вот – за что боролись, на то и напоролись :)

PS. А unit-тесты рулят!

Отправить комментарий