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

счётчики производительности

Сегодня хочу расскахать о своём опыте работы с системными счётчиками производительности или, как их называют в оригинале, Performance
counters.

Счётчики эти существуют, насколько мне известно, во всех NT Виндовсах. (Ну, по крайней мере, в 2000, ХР и 2003 точно есть.) Вещь исключительно удобная! "А чем же она хороша?" - спросите вы. А вот чем. Это простой и удобный механизм, который позволяет исследовать временные и количественные характеристики своего приложения. Кроме того, в ОС уже поддерживается большое количество системных счётчиков, по состоянию которых можно судить о производительности самой системы. Более того, так как они являются частью операционной системы, то им есть смысл доверять =) Если злой дядя спросит вас о быстродействии вашего приложения можно показать ему на показания счётчиков (и даже на график, который строится на основе отсчётов) и с полным правом утверждать, что цифры эти не придуманы, а посчитаны механизмом, которому можно доверять.

Рассказ мой будет основываться на использованиия счётчиков производительности в среде Microsoft .Net. Дотнет выбран по нескольким причиинам: во-первых, мне понадобились счётчики именно в приложении .net, а кроме того, я не смог найти в MSDN рассказа об API счётчиков вне .net Framework (хотя, и не особо-то искал).

Рассказывать обо всех наворотах работы со счётчиками я не стану, это сделали уже без меня. Почитать можно вот тут. Ссылка для тех, кому не лень по-англицки. В действительности, там много воды и по сути можно читать только код. А ещё там есть ссылка на MSDN, который все мы любим за оооооочень много инфы.

Основные понятия

Перечислю ключевые слова: категория, счётчик, инстанс.
Категория -- логическая группа счётчиков, которая имеет имя. Счётчик не может существовать вне группы. Каждая группа содержит один и более счётчиков, хотя, наверное, может содержать и 0 счётчиков, хотя кому она такая нужна.
Счётчик -- собственной персоной! Бывают простые, просто меряют количество чего-то, например, каких-то событий в системе или приложении. Бывают сложные, могут мерять среднее значение величины или скорость событий в единицу времени. Единицей времени выступает секунда.
Инстанс -- экземпляр, наверное так можно перевести. Я с ними особо не заморачивался, но идея в том, что по одному имени счётчика можно получать два разных его экземпляра. Отличаться они будут по имени инстанса и значения будут иметь каждый своё. Да, ксати, забыл сказать, но это уже и так ясно, счётчик является системным объектом, и существует в системе в единственном экземпляре! Отсюда вывод: если
одновременно два приложения считают количество обрабатываемых записей, например, то вполне резонно, что значение счётчика будет вдвое больше!

Как использовать счётчики
Начнём с того, что получим уже сущестующий счётчик. Кому как, а мне всегда было интересно узнать uptime моей системы. Я даже для этого специальную утилитку недавно скачал. А теперь вот вам простой код, который говорит аптайм системы в секундах.

using System;
using System.Diagnostics;
using System.Threading;

namespace dwns{
public class UptimeApp{
public static void Main( string[] args ){
PerformanceCounter counter = new PerformanceCounter(
"System", // имя категории
"System Up Time", // имя самого счётчика
true ); // счётчик только для чтения
// интересно, а можно ли открыть системный счётчик и
// поменять в нём значение?! надо будет попробовать

// повторим для верности 10 раз
for( int i = 0; i < 10; ++i ){
// получаем значение счётчика
Console.WriteLine( counter.NextValue() );
Thread.Sleep( 1000 ); // поспим секунду
}
}
}
}

Собственно, всё ясно... Одно только не совсем понятно... Почему первое значение равно 0. Курим MSDN... Ясно! Если значение зависит от двух отсчётов то первый вызов NextValue возвратит 0. Значит всё в порядке. Играемся дальше.

Как создавать свои счётчики
Хватит пользоваться дарами природы. Теперь хотим мерять параметры своего приложения. Например, среднее время обработки чего-нибудь.
Если бы счётчик уже был в системе, то задача сводилась бы к первой и особого интереса не представляла. Предположим, что в системе наш счётчик не зарегистрирован. Надо создавать.

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

if( !PerformanceCounterCategory.Exists( "My Category" ) ){
CounterCreationDataCollection counters =
new CounterCreationDataCollection();

CounterCreationData avgTime = new CounterCreationData();
avgTime.CounterType = PerformanceCounterType.AverageTimer32;
avgTime.CounterName = "Average Time Of Process";
avgTime.CounterHelp = "Average Time Of Process Help String";
counters.Add( avgTime );

CounterCreationData avgTimeBase = new CounterCreationData();
avgTimeBase.CounterType = PerformanceCounterType.AverageBase;
avgTimeBase.CounterName = "Average Time Of Process Base";
counters.Add( avgTimeBase );

PerformanceCounterCategory.Create( "My Category",
"My Description", counters );
}


Что следует отметить в этом коде?
При создании категории обязательно надо проверять существует ли она уже на данной машине. Каждый счётчик описывается при помощи CounterCreationData, где указывается его тип, имя и, возможно, описание (Есть ещё ряд необязательных полей).

После заполнения коллекции CounterCreationDataCollection её можно передать в метод Create класса PerformanceCounterCategory, и требуемая категрия будет создана.

Зачем создаётся два счётчика? Вот вопрос! Дело в том, что счётчики типа Average не могут существовать отдельно. Им необходима некая база, которой является счётчик типа AverageBase. Это вполне логично, грубо говоря, для того чтобы посчитать среднее значение результата эксперимента необходимо знать не только результат каждого эксперимента, но и то, сколько раз он проводился. (Кстати, после каждого счётчика типа Average должен обязательно следовать счётчик типа AverageBase, иначе категория не создастся.)

Испробуем счётчик в действии. Для этого пишем вот что:

// сначала получим собственно счётчики
PerformanceCounter avgTime =
new PerformanceCounter(
"My Category", // имя категории
"Average Time Of Process", // именно! имя счётчика
false ); // зачем нам Readonly... будем туда писать!
PerformanceCounter avgTimeBase =
new PerformanceCounter(
"My Category",
"Average Time Of Process Base",
false );

Random rnd = new Random( 100 );

for( int i = 0; i < 100: ++i ){
// запоминаем время начала обработки
long ticks = DateTime.Now.Ticks;
// имитируем обработку со случаным временем
System.Threading.Thread.Sleep( rnd.Next( 100 ) );
// увеличиваем значение счётчика на количество тиков, прошедшее с
// момента начала обработки
avgTime.IncrementBy( DateTime.Now.Ticks - ticks );
// увеличим базу
avgTimeBase.Increment();
}

Фух... Вроде справились. Работает. Ну или должно =) Правильно работать будет только первый раз. Потом надо будет обнулять значения счётчиков. Делается это при помощи свойства RawValue.

Наверное, здесь мне стоит закончить своё повествование и отправить читателя в собственный путь. Но, позволю себе ещё кусочек кода, который лично мне кажется полезным для работы с Average счётчиками.

// полезность для работы с Average счётчиками
// создаётся из двух счётчиков, среднего и базового
public class AverageCounterWrapper{
private readonly PerformanceCounter _counter;
private readonly PerformanceCounter _counterBase;
private long _ticks = 0;

public AverageCounterWrapper( PerformanceCounter counter,
PerformanceCounter counterBase ){
_counter = counter;
_counterBase = counterBase;
}

// сброс значений счётчиков
public void Reset() {
this.Reset( 0, 0 );
}

public void Reset( long raw, long rawBase ) {
_counter.RawValue = raw;
_counterBase.RawValue = rawBase;
}

// вызвать в начале процесса
public long Mark(){
_ticks = DateTime.Now.Ticks;
return _ticks;
}

// вызвать по окончании процесса
public long Check(){
long ticks = DateTime.Now.Ticks;
this.Increment( ticks - _ticks );
return ticks;
}

public void Increment( long raw ) {
_counter.IncrementBy( raw );
_counterBase.Increment();
}

}



// полезность для работы с несколькими счётчиками одновременно
public class SimultaneousCounters{

private ArrayList _counters = new ArrayList();

public SimultaneousCounters(){
}

// добавить счётчик
public void Add( PerformanceCounter counter ) {
_counters.Add( counter );
}

// удалить счётчик
public void Delete( PerformanceCounter counter ) {
_counters.Remove( counter );
}

// сброс всех счётчиков
public void ResetAll() {
this.ResetAll( 0 );
}

public void ResetAll( long raw ) {
foreach( PerformanceCounter c in _counters ){
c.RawValue = raw;
}
}

// увеличить значение всех счётчиков
public void IncrementAll() {
foreach( PerformanceCounter c in _counters ){
c.Increment();
}
}

public void IncrementAll( long raw ) {
foreach( PerformanceCounter c in _counters ){
c.IncrementBy( raw );
}
}
}


Классы простые, но очень полезные и удобные.

(c)2005, dW

July 19, 2005

И снова здрасьте

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

Почему я поменял название?! Да просто так... Решил, что это более соответствует тому, что я решил тут писать.

Раньше я писал всякую лабуду. Лабуда останется, куда же без неё.

И всё-таки, я решил, чтобы контент был всегда, надо писать о том, что интересует в данный момент. Собственно, поэтому я и вернулся сюда.

Сейчас заинтересовался языками стилей. И решил, что лучшего места для экспериментов, чем собственный блог не найти ;-)

Буду думать как сделать свой блог приятнее на вид, ну и, конечно, снабдить его нормальным содержанием.

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