July 27, 2005
tlv (tag length value)
Этот свой рассказ хочу посвятить такой вещи как Tlv или попросту Tag Length Value.
Уже не раз я встречал необходимость реализации подобной структуры при разборе телекомуникационных протоколов. Обычно в таких структурах хранятся параметры сообщений. Возьмём, к примеру, radius или SMPP. В памяти всплывает ещё ISUP, но это не обязательно так =)
Что такое tlv
Tlv -- это, как следует из названия, структура, которая содержит в себе всего 3 поля: tag(название), length(длина), value(значение). В перечисленных выше протоколах используется для представления параметров протоколов. Название и длина имеют фиксированный размер, в то время как длина поля "значение" зависит непосредственно от значения =)
Основная сложность с tlv -- в разных протоколах её поля могут иметь разную длину, порядок и сериализоваться по-разному. Например в radius, название и длина -- размером в 1 байт, а в SMPP -- 2 байта. В radius длина содержит длину всего атрибута, включая 2 байта, отведённые на название и длину, а в SMPP длина -- это размер только значащего поля.
Обобщив эти различия, и придумав ещё то, что многобайтовые значения могут сериализоваться как в BigEndian (network byte order) так и в LittleEndian (host byte order) я решил взяться за реализацию обобщенного класса, предоставляющего функциональность Tlv.
Но для начала, интерфейс и пара перечислений!
Собственно, определяем порядок полей в структуре.
Порядок следования байт при сериализации.
Всё просто. Можно написать несколько классов, которые реализуют этот интерфейс для каждого конкертного протокола, они будут работать быстро и эффективно, но это будет много классов. Своей основной целью я вижу универсальность. Если меня не устроит производительность универсального метода -- я всегда могу написать менее универсальный, но реализующий тот же интерфейс, уже написанный код мне в любом случае менять не придётся.
Реализация
Глупо, наверное, выкладывать весь код, но я сделаю это, благо его не так и много.
Далее пойдёт описание полей. Абсолютно тривиальное, но необходимое. Единственное, на что хочу обратить внимание -- поле длины. _length хранит полную длину всей структуры в байтах. А вот уже свойство Length возвращает ту длину, которая соответствует требованиям протокола (включая или нет длину заголовка).
Теперь то, ради чего это всё задумывалось. Код, который сериализует и десериализует tlv выбранного формата в массив байт.
Ничего сложного. Всё кристально ясно. Просто в зависимости от порядка полей мы их записываем в поток, который создан на основе массива байт, длина которого как раз соответствует длине структуры.
А теперь восстановим состояние из массива байт. Последовательность действий обратна сериализации =) Надо только немного исхитриться, чтобы определить длину всей структуры и прочитать ровно столько байт, сколько надо.
Внимательный читатель заметил, что сериализация полей _length и _tag доверяется методу _int2Bytes. Смысл его в том, чтобы превратить число типа int в массив байт нужной длины и нужной последовательности (big или little endian).
Обратное преобразование так же имеет место при десериализации.
Ну и так.. фишечка напоследок. Полезна для создания новых структур с такими же параметрами как и у данной.
Вот и всё. Хотя нет.
Вот например, как можно создать на основе этого класса реализовать RadiusTlv.
Должно работать =)
Проверил. Работает.
ЗЫ: По мере написания сего опуса пришла мне в голову идея сравнить быстродействие общей и частной реализаций Tlv. Ждите вестей =)
(с)2005, dW
Уже не раз я встречал необходимость реализации подобной структуры при разборе телекомуникационных протоколов. Обычно в таких структурах хранятся параметры сообщений. Возьмём, к примеру, radius или SMPP. В памяти всплывает ещё ISUP, но это не обязательно так =)
Что такое tlv
Tlv -- это, как следует из названия, структура, которая содержит в себе всего 3 поля: tag(название), length(длина), value(значение). В перечисленных выше протоколах используется для представления параметров протоколов. Название и длина имеют фиксированный размер, в то время как длина поля "значение" зависит непосредственно от значения =)
Основная сложность с tlv -- в разных протоколах её поля могут иметь разную длину, порядок и сериализоваться по-разному. Например в radius, название и длина -- размером в 1 байт, а в SMPP -- 2 байта. В radius длина содержит длину всего атрибута, включая 2 байта, отведённые на название и длину, а в SMPP длина -- это размер только значащего поля.
Обобщив эти различия, и придумав ещё то, что многобайтовые значения могут сериализоваться как в BigEndian (network byte order) так и в LittleEndian (host byte order) я решил взяться за реализацию обобщенного класса, предоставляющего функциональность Tlv.
Но для начала, интерфейс и пара перечислений!
public enum FieldOrder {
TagLengthValue = 0,
LengthTagValue = 1
}
Собственно, определяем порядок полей в структуре.
public enum ByteOrder {
LittleEndian = 0,
BigEndian = 1
}
Порядок следования байт при сериализации.
public interface ITlv{
// Tlv parameters
FieldOrder FieldOrder{ get; }
ByteOrder ByteOrder{ get; }
bool IncludeHeaderLength{ get; }
int TagSize{ get; }
int LengthSize{ get; }
int HeaderSize{ get; }
// main fields
int Tag{ get; set; }
int Length{ get; }
byte[] Value{ get; set; }
// operations
byte[] Serialize();
void Deserialize( byte[] bytes );
}
Всё просто. Можно написать несколько классов, которые реализуют этот интерфейс для каждого конкертного протокола, они будут работать быстро и эффективно, но это будет много классов. Своей основной целью я вижу универсальность. Если меня не устроит производительность универсального метода -- я всегда могу написать менее универсальный, но реализующий тот же интерфейс, уже написанный код мне в любом случае менять не придётся.
Реализация
Глупо, наверное, выкладывать весь код, но я сделаю это, благо его не так и много.
public class Tlv: ITlv, ICloneable{
// параметры, которые задаются при создании
// характеризуют саму структуру, а не её содержание
private readonly int _tagSize;
private readonly int _lengthSize;
private readonly ByteOrder _byteOrder;
private readonly FieldOrder _fieldOrder;
private readonly bool _includeHeader;
// а вот это уже содержание структуры
private int _tag;
// total length of the whole Tlv including Length field
private int _length;
private byte[] _value;
public Tlv( int tagSize, int lengthSize, ByteOrder byteOrder, FieldOrder fieldOrder )
:this( tagSize, lengthSize, byteOrder, fieldOrder, true ){}
public Tlv( int tagSize, int lengthSize,
ByteOrder byteOrder, FieldOrder fieldOrder, bool includeHeader ){
_tagSize = tagSize;
_lengthSize = lengthSize;
_byteOrder = byteOrder;
_fieldOrder = fieldOrder;
_includeHeader = includeHeader;
_length = _tagSize + _lengthSize;
_value = new byte[ 0 ];
}
Далее пойдёт описание полей. Абсолютно тривиальное, но необходимое. Единственное, на что хочу обратить внимание -- поле длины. _length хранит полную длину всей структуры в байтах. А вот уже свойство Length возвращает ту длину, которая соответствует требованиям протокола (включая или нет длину заголовка).
public FieldOrder FieldOrder {
get {
return _fieldOrder;
}
}
public ByteOrder ByteOrder {
get {
return _byteOrder;
}
}
public bool IncludeHeaderLength {
get {
return _includeHeader;
}
}
public int TagSize {
get {
return _tagSize;
}
}
public int LengthSize {
get {
return _lengthSize;
}
}
public int HeaderSize {
get {
return _tagSize + _lengthSize;
}
}
public int Tag {
get {
return _tag;
}
set {
_tag = value;
}
}
public int Length {
get {
return ( _includeHeader ) ? _length : _length - _lengthSize - _tagSize;
}
}
public byte[] Value {
get {
return _value;
}
set {
_value = value;
_length = _value.Length + _lengthSize + _tagSize;
}
}
Теперь то, ради чего это всё задумывалось. Код, который сериализует и десериализует tlv выбранного формата в массив байт.
public byte[] Serialize() {
byte[] res = new byte[ _length ];
using( MemoryStream stream = new MemoryStream( res ) ){
BinaryWriter writer = new BinaryWriter( stream );
byte[] bTag = _int2Bytes( _tag, _tagSize, _byteOrder );
byte[] bLength = _int2Bytes( this.Length, _lengthSize, _byteOrder );
switch( _fieldOrder ){
case FieldOrder.TagLengthValue:
writer.Write( bTag );
writer.Write( bLength );
break;
case FieldOrder.LengthTagValue:
writer.Write( bLength );
writer.Write( bTag );
break;
default:
throw new NotSupportedException( "Unknown filed order." );
}
writer.Write( _value );
}
return res;
}
Ничего сложного. Всё кристально ясно. Просто в зависимости от порядка полей мы их записываем в поток, который создан на основе массива байт, длина которого как раз соответствует длине структуры.
А теперь восстановим состояние из массива байт. Последовательность действий обратна сериализации =) Надо только немного исхитриться, чтобы определить длину всей структуры и прочитать ровно столько байт, сколько надо.
public void Deserialize( byte[] bytes ) {
using( MemoryStream stream = new MemoryStream( bytes ) ){
BinaryReader reader = new BinaryReader( stream );
byte[] bTag;
byte[] bLength;
switch( _fieldOrder ){
case FieldOrder.TagLengthValue:
bTag = reader.ReadBytes( _tagSize );
bLength = reader.ReadBytes( _lengthSize );
break;
case FieldOrder.LengthTagValue:
bLength = reader.ReadBytes( _lengthSize );
bTag = reader.ReadBytes( _tagSize );
break;
default:
throw new NotSupportedException( "Unknown field order." );
}
_tag = _bytes2Int( bTag, _byteOrder );
_length = _bytes2Int( bLength, _byteOrder );
if( !_includeHeader ){
_length += _tagSize + _lengthSize;
}
_value = reader.ReadBytes( _length - _tagSize - _lengthSize );
}
}
Внимательный читатель заметил, что сериализация полей _length и _tag доверяется методу _int2Bytes. Смысл его в том, чтобы превратить число типа int в массив байт нужной длины и нужной последовательности (big или little endian).
private static byte[] _int2Bytes( int x, int length, ByteOrder byteOrder ) {
byte[] bytes = BitConverter.GetBytes( x );
byte[] res = new byte[ length ];
for( int i = length; i > 0; --i ){
switch( byteOrder ){
case ByteOrder.BigEndian:
res[ i-1 ] = bytes[ length-i ];
break;
case ByteOrder.LittleEndian:
res[ i-1 ] = bytes[ i-1 ];
break;
default:
throw new NotSupportedException( "Unknown byte order. Imposible!" );
}
}
return res;
}
Обратное преобразование так же имеет место при десериализации.
private static int _bytes2Int( byte[] bytes, ByteOrder byteOrder ) {
byte[] res = BitConverter.GetBytes( 0 );
int length = bytes.Length;
for( int i = length; i > 0; --i ){
switch( byteOrder ){
case ByteOrder.BigEndian:
res[ length-i ] = bytes[ i-1 ];
break;
case ByteOrder.LittleEndian:
res[ i-1 ] = bytes[ i-1 ];
break;
default:
throw new NotSupportedException( "Unknown byte order. Imposible!" );
}
}
return BitConverter.ToInt32( res, 0 );
}
Ну и так.. фишечка напоследок. Полезна для создания новых структур с такими же параметрами как и у данной.
public object Clone() {
return new Tlv( _tagSize, _lengthSize, _byteOrder, _fieldOrder, _includeHeader );
}
}
Вот и всё. Хотя нет.
Вот например, как можно создать на основе этого класса реализовать RadiusTlv.
public class RadiusTlv: Tlv{
public RadiusTlv()
:base( 1, 1, // по одному байту на название и длину
ByteOrder.BigEndian, // с одним байтом неважно =)
FieldOrder.TagLengthValue, // согласно rfc2865
true ){}
}
Должно работать =)
Проверил. Работает.
ЗЫ: По мере написания сего опуса пришла мне в голову идея сравнить быстродействие общей и частной реализаций Tlv. Ждите вестей =)
(с)2005, dW