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.

Но для начала, интерфейс и пара перечислений!

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

Comments: Post a Comment



<< Home

This page is powered by Blogger. Isn't yours?