среда, 28 августа 2013 г.

Конфигурация nginx: вложенные if

Как известно, вложенные if в конфигурации nginx запрещены, поскольку это вовсе не языковая конструкция nginx, а обычная директива из модуля rewrite, использование которой в ее же собственном контексте if не предусмотрено - так уж она сделана. Использование списка условий, разделенных логическими операторами и и или тоже не предусмотрено. Обычно для эмуляции вложенных условий с использованием директивы if предлагают использовать регулярные выражения. Например if в
        location /test_if.html {
            set $cond $host::$arg_a;
            if ($cond ~* '^localhost::.+') {
                echo "Matched: host = '$host', a = '$arg_a'";
                break;
            }
            echo "Not matched: host = '$host', a = '$arg_a'";
        }
проверяет, что имя, указанное в HTTP хедере Host, соответствует значению localhost и в URI присутствует аргумент a (в этом случае переменная $arg_a будет не пустой).

К сожалению, условие в директиве if имеет ограничения. По крайней мере, это не выражение, и, как я уже сказал, оно не может быть составлено в виде цепочки выражений, разделенных логическими операторами. Кроме того, в случае сравнения чего-то с регулярным выражением, это что-то может быть только именем переменной, именно поэтому нам пришлось ввести новую переменную $cond, а не записать это просто как
            if ($host::$arg_a ~* '^localhost::.+') {
В данном случае nginx просто не запустится, заявив unknown "host::$arg_a" variable. Выражение для регулярного выражения в условии if также имеет ограничение: в нем нельзя использовать переменные - они просто не будут расширяться. Но что тогда делать, если мы хотим сравнить $host на соответствие не константному выражению localhost, а значению, определенному в какой-нибудь переменной, например $goodhost? Ответ - использовать другую переменную $cond и другое регулярное выражение.
        location /test_if.html {
            set $goodhost 'localhost';
            set $cond $host::$goodhost::$arg_a;
            if ($cond ~* '^(.*)::\1::.+') {
                echo "Matched: host = '$host', a = '$arg_a'";
                break;
            }
            echo "Not matched: host = '$host', a = '$arg_a'";
        }
В данном примере мы соединили через произвольно выбранный нами разделитель :: три переменных $host, $goodhost и $arg_a и присвоили это значение переменной $cond. А регулярное выражение, с которым мы сопоставляем это значение, проверяет, что его первая часть (до разделителя ::) и вторая часть (до второго разделителя ::) равны, а последняя часть (после второго разделителя) не пуста.

Идея понятна, последний пример - трансляция псевдокода
            if ($host == $goodhost && not_empty($arg_a)) {
                if ($arg_a == 'ok') {
                    echo "Matched: host = '$host', a is Ok";
                } else {
                    echo "Matched: host = '$host', a is '$arg_a'";
                }
            } else {
                echo "Not matched: host = '$host', a = '$arg_a'";
            }
Результат трансляции:
        location /test_if.html {
            set $goodhost 'localhost';
            set $cond $host::$goodhost::$arg_a;
            if ($cond ~* '^(.*)::\1::ok$') {
                echo "Matched: host = '$host', a is Ok";
                break;
            }
            if ($cond ~* '^(.*)::\1::.+$') {
                echo "Matched: host = '$host', a = '$arg_a'";
                break;
            }
            echo "Not matched: host = '$host', a = '$arg_a'";
        }

вторник, 20 августа 2013 г.

vim: редактирование файлов от имени суперпользователя, добавление новых сегментов Powerline

Настроить редактирование системных файлов от имени суперпользователя на домашнем компьютере несложно. Заводим файл в директории /etc/sudoers.d/ с произвольным именем, например users. Помещаем в него строки
Host_Alias    LOCAL = desktop
Cmnd_Alias    VIM = /usr/bin/vim, /usr/bin/vimdiff
Defaults!VIM  env_keep += "HOME"
username      LOCAL = NOPASSWD: VIM
Здесь имя desktop соответствует имени локального хоста. Если вы специально не изменяли это имя в вашей системе, то вместо desktop напишите localhost. Алиас хоста LOCAL используется в последней строке и разрешает редактирование файлов от имени суперпользователя только с локального компьютера. Имя username соответствует имени учетной записи пользователя, которому будет позволено редактировать файлы с помощью sudo. В данном примере мы разрешили пользователю username запускать программы vim и vimdiff от имени суперпользователя без ввода пароля. Кроме того, в третьей строке мы попросили sudo использовать оригинальное значение переменной окружения $HOME: это нужно для того, чтобы vim загружал скрипт .vimrc и все дополнительные плагины из домашней директории пользователя username, а не из директории суперпользователя.

Что здесь нехорошо? Собственно то, что мы дали пользователю username возможность выполнить любую системную команду с привилегией суперпользователя! Не забываем, что из vim можно выполнить системную команду с помощью :!. Именно поэтому в первом предложении я написал на домашнем компьютере: не стоит давать такие привилегии обычному пользователю username на промышленном сервере.

Последнее украшательство - добавление алиасов в файл .bashrc:
alias svim='sudo vim'
alias svimdiff='sudo vimdiff'
Теперь vim с правам суперпользователя можно вызывать как svim, а vimdiff - как svimdiff.

Всё это прекрасно, но хотелось бы, чтобы vim каким-нибудь образом отмечал то, что мы редактируем файл от имени суперпользователя. Что ж, это достаточно легко сдедать с помощью плагина Powerline. Лично я по-прежнему использую старую версию плагина, которую можно загрузить отсюда, поэтому следующий рецепт относится именно к ней.

Итак, наша задача - добавить новый сегмент, на котором, в случае, если пользователь редактирует файл от имени суперпользователя, во всех режимах vim будет красоваться жирная, хорошо заметная надпись SUDO на красном фоне. Поскольку это сообщение достаточно важное, пусть оно будет находиться в начале статусной строки. Для этого в файле .vim/autoload/Powerline/Segments.vim перед строкой
    \ Pl#Segment#Create('paste_indicator' , '%{&paste ? "PASTE" : ""}', Pl#Segment#Modes('!N')),
добавим похожую строку
    \ Pl#Segment#Create('sudo_indicator'  , '%{empty($SUDO_USER) ? "" : "SUDO"}', Pl#Segment#Modes('!N')),
Это и есть новый сегмент, который мы назвали sudo_indicator (первый аргумент функции Pl#Segment#Create()). Второй аргумент Pl#Segment#Create() соответствует коду, который будет выполнен для определения текста сегмента. В нашем случае мы опираемся на тот простой факт, что команда sudo определяет переменную окружения $SUDO_USER, в которой записано имя пользователя, получившего привилегии суперпользователя, соответственно, если эта переменная определена (то есть vim был запущен из sudo), то второй аргумент будет равен SUDO, иначе - пустой строке. Третий параметр: Pl#Segment#Modes('!N') - говорит о том, что если текущее окно не в фокусе, то данный сегмент не будет отображаться - это нам вполне подходит.

Далее помещаем новый сегмент на первую позицию списка всех сегментов. Для этого в файле .vim/autoload/Powerline/Themes/default.vim добавляем строку
        \ , 'sudo_indicator'
перед строкой
        \ , 'paste_indicator'
а в файле .vim/autoload/Powerline/Colorschemes/default.vim описываем цветовые характеристики сегмента:
    \
    \ Pl#Hi#Segments(['sudo_indicator'], {
        \ 'n': ['brightgreen''brightestred', ['bold']],
        \ }),
(это можно поместить, например, за определением
    \ Pl#Hi#Segments(['SPLIT'], {
        \ 'n': ['white''gray2'],
        \ 'N': ['white''gray0'],
        \ 'i': ['white''darkestblue'],
        \ }),
). Если вы используете другую тему или цветовую схему Powerline (то есть не default), то данные определения нужно поместить в соответствующие файлы. Итак, почти все готово, обновляем кэш Powerline:
:PowerlineClearCache
и открываем какой-нибудь файл с помощью svim. Вот какую статусную строку я увидел после этих изменений:


Хорошо, но не очень. Видите темно-красные артефакты вокруг стрелки с SUDO? Исследование показало, что они связаны с сегментом paste_indicator, когда его текст соответствует пустой строке. Вы можете убедиться в этом выполнив
:set paste
и
:set nopaste
несколько раз. К сожалению, как я понял, данная версия Powerline не способна удовлетворительно решить проблему с сегментом, который может содержать пустое значение. Если такой сегмент находится не в самом начале списка сегментов, то мы будем время от времени получать подобные стрелочные артефакты. В нашем случае возможны четыре решения данной проблемы:
  1. удалить сегмент paste_indicator (плохо);
  2. сделать так, чтобы надпись в paste_indicator присутствовала всегда (т.е. PASTE или NOPASTE - тоже не очень хорошо - зачем нам избыточная информация);
  3. перенести индикатор SUDO в комбинированный сегмент fileinfo (см. .vim/autoload/Powerline/Segments.vim), тоже не очень хорошо - придется использовать серый фон;
  4. сделать так, чтобы фон индикатора paste_indicator всегда соответствовал фону следующего постоянного сегмента mode_indicator. В этом случае цвет стрелочных артефактов будет совпадать с цветом фона следующего сегмента и, соответственно, они должны бесследно исчезнуть;
Лично я воспользовался последним вариантом и переопределил paste_indicator в .vim/autoload/Powerline/Colorschemes/default.vim как
    \
    \ Pl#Hi#Segments(['paste_indicator'], {
        \ 'n': ['brightestred''brightgreen', ['bold']],
        \ 'i': ['brightestred''white', ['bold']],
        \ 'v': ['brightestred''brightorange', ['bold']],
        \ 'r': ['brightgreen''brightred', ['bold']],
        \ 's': ['brightestred''gray5', ['bold']],
        \ }),
После перзагрузки кэша Powerline и запуска svim я получил следующие картинки: 


В нормальном режиме при установленном paste это выглядит так:

 

четверг, 1 августа 2013 г.

Пара приемов визуального анализа исполнения программы

Из тех, с которыми мне доводилось работать самому. Вообще-то лучший визуальный анализ - это просмотр исходного кода. Однако, он дает только качественное представление об исследуемых параметрах программы, да и не изобрели еще автоматического транслятора человеческих мыслей в персистентные формы. Итак, рассмотрим два, на мой взгляд, потрясающих инструмента для тестирования работы программ и визуализации результатов тестирования. Это gprof2dot и massif-visualizer. По правде говоря, обе программы - всего лишь оболочки над, соответственно, еще более потрясающим инструментом valgrind. Все что они делают - это читают результаты прогона тестируемой программы из-под valgrind и генерируют отчеты в визуально привлекательной форме, то есть в виде изображений.

Итак, напишем простейшую программу, которую мы будем тестировать, и поместим ее в файл test.cc.
class  A
{
    public:
        A() : a_( 0 ) { a(); }

        ~A() { delete a_; }

        void a( void ) { a_ = new int( 0 ); }

        void  realloc( void ) { delete a_; a_ = new int( 0 ); }

    private:
        int *  a_;
};

int  main( void )
{
    A  a[ 10 ];

    for ( int  i( 0 ); i < 10; ++)
    {
        a[ i ].realloc();
    }
}
В функции main() создаем массив a из десяти объектов типа A. В конструкторе класса A с помощью функции-члена a() выделяется динамическая память под объект типа int, а в деструкторе она освобождается. Кроме того, в классе A имеется функция-член realloc(), которая освобождает выделенную память и снова выделяет ее. Функция realloc() вызывается для всех элементов массива a внутри функции main(). Две функции внутри класса A нам будут нужны для демонстрации работы gprof2dot, а манипуляции с динамической памятью - для massif-visualizer.

Откомпилируем нашу программу
g++ -g -o test test.cc
и приступим к разбору утилит.

gprof2dot. Эта утилита позволяет выводить красивые диаграммы вызова функций на основании результатов профилирования тестируемой программы. Самый стандартный профилировщик для gcc - это gprof, отсюда и название gprof2dot. Однако мы не будем использовать gprof, так как для его поддержки пришлось бы компилировать программу со специальными флагами. Вместо этого будем использовать инструмент callgrind, входящий в состав valgrind, а gprof2dot сообщим, что мы использовали callgrind с помощью опции --format=callgrind. Кроме того, для получения результирующего изображения нам понадобится пакет graphviz, который предоставляет утилиту dot (вторая часть в названии gprof2dot), которая, в свою очередь, умеет генерировать из промежуточного dot-файла изображение заданного формата.

Запускаем тестируемую программу из-под valgrind
valgrind --tool=callgrind --callgrind-out-file=callgrind.out ./test
и строим изображение
gprof2dot --format=callgrind --output=out.dot -s callgrind.out
dot -Tpng out.dot -o out.png
Опция -s команды gprof2dot позволяет записывать имена функций более компактно: это весьма полезно, когда полученное изображение оказывается очень большим (а это всегда так).

Вот так выглядит полученная картинка out.png (кликните по изображению для его увеличения):


Изображение состоит из набора элементов двух типов: прямоугольников - функций и стрелок - вызовов функций. Каждый элемент сопровождается профильной информацией. Для функций - это библиотека или исполняемая программа, которой принадлежит функция, имя функции, время ее выполнения в процентах от общего времени исполнения программы и количество ее вызовов. Стрелки сопровождаются информацией о времени выполнения всей иерархии функций, вызовы которых они порождают и количестве вызовов дочерней функции из родительской, соединенных этой стрелкой. Качественная информация о времени выполнения отдельных функций передается цветом прямоугольника: чем теплее цвет - тем больше времени заняло выполнение данной функции. Так, корневая функция, вместо имени которой указан адрес (верхний прямоугольник), окрашена красным цветом и время ее выполнения соответствует 100%, так как вызовы всех остальных функций были порождены ею, значение в скобках - 0.00% - это время выполнения ее тела. Функции, которые выполнялись за меньшее время окрашены в оранжевый, желтый и зеленый цвета, самые быстрые - в синий.

Соответственно, на нашем изображениии видно, что львиную долю времени заняли вызовы системных функций линковщика, в частности _dl_relocate_object() была вызвана 7 раз и заняла 88.92% всего времени. Наша главная функция main() потратила всего лишь 2.19% от общего времени. И это не удивительно в случае такой простой программы, как наша. Конструктор A::A() вызывался 10 раз из функции main() и потратил 0.95% общего времени, функция-член A::realloc() вызывалась тоже 10 раз и потратила 1.04% общего времени. Все время обоих функций было затрачено на вызов оператора new() и, затем, функции malloc().

Что если нас не интересуют системные вызовы? Утилита gprof2dot позволяет строить вызовы функций из определенного корня с помощью опции -z. Например, чтобы построить вызовы всех фунций, начиная с нашей функции main(), нужно выполнить
gprof2dot --format=callgrind --output=out_main.dot -s -z main callgrind.out
dot -Tpng out_main.dot -o out_main.png
Полученное изображение out_main.png представляет собой небольшой участок общей картинки:


Есть еще одна полезная опция -l - это когда нужно изобразить все, что вызывает указанную функцию.

massif-visualizer. Эта утилита полезна, когда нужно визуализировать выделение и освобождение динамической памяти в процессе прогона тестируемой программы. Она тоже является простой оболочкой над инструментом massif из состава valgrind. При сборке программы вручную важно учесть, что она зависит от библиотек разработки KDE.

Запустим программу test под massif
valgrind --tool=massif --massif-out-file=massif.out --time-unit=B --detailed-freq=1 ./test
и откроем сгенерированный файл massif.out с помощью massif-visualizer
massif-visualizer massif.out
Откроется вот такое окно:


В центральной области расположен график, изображающий этапы выделения и освобождения динамической памяти. Горизонтальная ось графика - время работы программы, выраженное в байтах, это соответствует опции massif --time-unit=B. Время, выраженное в байтах, лучше всего подходит для небольших тестовых программ, каковой и является наша программа test. Другие возможные единицы измерения времени в massif - количество инструкций (по умолчанию) и миллисекунды. Опция --detailed-freq=1 в нашем случае также имеет важное значение: она задает самое детализированное накопление снапшотов, то есть столбцов графика и элементов из списка справа от него. В случае продолжительно работающих программ ее значение по умолчанию 10 является более предпочтительным, чем 1.

По вертикальной оси отложено количество выделенной динамической памяти. Очевидно, пиковое значение 40 байт соответствует десятикратному значению размера типа int (4 байта * 10 элементов массива a). Память, выделенная функцией A::a(), обозначена желтым цветом. Она постепенно снижалась за счет вызова функции A::realloc() в цикле for внутри main(). Вызовам A::realloc() соответствуют синие столбцы на графике. В итоге, в конце программы вся память была освобождена, поскольку на правом крае графика столбцы отсутствуют.

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

среда, 24 июля 2013 г.

C++: члены класса в тени формальных параметров конструктора

Возьмем простую программу
class  A
{
    public:
        explicit A( int  a ) : a( a )
        {}

    private:
        int  a;
};

int  main( void )
{
    A  a( 0 );

    return 0;
}
Что здесь плохо? Видите ли вы потенциальную опасность? Попробуем скомпилировать с включенными предупреждениями:
g++ -Wall -o test test.cc
Всё хорошо. Запуск программы на выполнение также не приводит к каким-либо проблемам (и это неудивительно). А теперь попробуем так:
g++ -Wall -Wshadow -o test test.cc
test.cc: In constructor «A::A(int)»:
test.cc:23:30: предупреждение: декларация «a» перекрывает элемент класса, на который указывает 'this' [-Wshadow]
         explicit A( int  a ) : a( a )
                              ^
Итак, с помощью опции компилятора -Wshadow нам удалось получить интересное предупреждение, которое намекает на то, что если мы будем использовать имя a внутри тела конструктора, то это никоим образом не будет член класса A, но одноименный формальный параметр, переданный в конструктор. Вот так то! Всё бы ничего, мы ведь сами не дураки и знаем, что если нам понадобится член класса, то мы сможем обратиться к нему через указатель this.

Казалось бы, в чем проблема, просто не использовать -Wshadow и дело с концом. А теперь представьте, что ваш код, написанный в таком стиле, когда имя формального параметра совпадает с именем члена класса, инициализируемого этим параметром, является частью большого проекта, при сборке которого было решено добавить -Wshadow, и вы не можете убрать эту опцию при сборке вашей части (такое вполне возможно, если система построения проекта хорошо автоматизирована и вы можете изменять на своей стороне лишь часть деклараций)! Компиляция вашего участка превратится в настоящий кошмар с выводом на экран огромного количества предупреждений, подобных приведенному выше (особенно если вы предпочитаете помещать реализации конструкторов внутри заголовочных файлов).

Как же с этим бороться? Самый очевидный способ - рефакторинг. Имена формальных параметров конструкторов и членов класса должны отличаться. Например, именем формального параметра в приведенном примере остается a, а имя члена класса переименовываем в a_. Лично мне такой стиль совершенно не импонирует. В самом деле, инициализацию членов класса через формальные параметры конструктора хотелось бы рассматривать как чисто языковой элемент, не вдаваясь в подробности реализации, такие как копирование одного объекта в другой. Иначе страдает абстракция и такой, казалось бы, простой шаблон, как инициализация членов класса формальными параметрами конструктора, начинает обрастать ненужными деталями.

Второй способ - обрамление объявлений конструкторов прагмами компилятора, отключающими диагностические сообщения. Например, в случае gcc конструктор A из приведенного выше примера может быть записан так:
#pragma GCC diagnostic ignored "-Wshadow"
        explicit A( int  a ) : a( a )
        {}
#pragma GCC diagnostic pop
Минусы здесь очевидны. Во-первых, мы так и не добились желаемого уровня абстракции шаблона инициализации членов класса, засорив его еще менее понятными объявлениями. Во-вторых, прагмы, как известно, не переносимы, и для разных компиляторов они будут отличаться. В-третьих, если проект достаточно большой, то вставка большого количества прагм замусорит код.

В общем, как правильно решить такую проблему мне пока не ясно. Очень хочется сохранить простой шаблон инициализации членов класса формальными параметрами конструктора, в котором оба языковых элемента (то есть члены класса и формальные параметры конструктора) рассматриваются абстрактно и имеют одно имя. Однако C++, к сожалению, не является настолько выразительным, чтобы это можно было сделать безопасно, и включение опции -Wshadow напоминает нам об этом. Увы, по всей видимости, использование разных имен для членов класса и формальных параметров конструктора - это единственный полностью безопасный подход.

вторник, 16 апреля 2013 г.

Собственный плагин tsung

Давно хотел написать про tsung - замечательный инструмент для нагрузочного тестирования сервера. Замечательных сторон у tsung много, я назову три основные:
  1. Поддержка большого числа протоколов и типов серверов, среди них http, jabber, SOAP, LDAP, MySQL, Postgres, NFS и др.
  2. Очень гибкая настройка сценариев тестирования, основанная на XML
  3. Высокая скорость работы и возможность создания ботнетов из клиентов
Последнее качество обеспечивается тем, что tsung написан на Erlang.

Но что делать, если мы хотим протестировать наш собственный кастомный протокол, используя все вкусности, который предоставляет tsung? Ответ: написать плагин для tsung. Судя по всему раньше в сети была некая инструкция или статья о том, как это сделать, но сейчас она недоступна. Все, что я нашел, это небольшая статья с общими тезисами, расположенная здесь. Однако, ее оказалось достаточно для начала работы.

Итак, приступим к созданию тестового плагина tsung. Прежде всего определимся с протоколом. Пусть клиент посылает произвольную строку, а сервер вычисляет ее md5 хэш и возвращает исходную строку и хэш. Детали таковы - клиент и сервер сериализуют посылаемые данные во внутренние структуры с помощью boost::serialization. Для указания размера сериализованного архива перед его посылкой и тот и другой отправляют сначала 4 байта со значением размера. После ответа клиенту сервер тут же закрывает соединение.

Данные для сериализации поместим в файл data.h:
#ifndef DATA_H
#define DATA_H

#include <string>
#include <boost/serialization/string.hpp>

struct  Request
{
    std::string  value;

    template < typename  Archive >
    void  serialize( Archive &  ar, unsigned int  /* version */ )
    {
        ar & value;
    }
};

struct  Reply
{
    std::string  value;

    std::string  md5hex;

    template < typename  Archive >
    void  serialize( Archive &  ar, unsigned int  /* version */ )
    {
        ar & value;
        ar & md5hex;
    }
};

#endif
Тут все просто. Сервер построим на основе асинхронной модели boost::asio. Я опять-таки не буду вдаваться в подробности, поскольку это один из примеров, которые можно найти здесь, адаптированный под наш протокол. Для вычисления md5 хэша использована библиотека OpenSSL.
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <boost/bind.hpp>
#include <boost/asio.hpp>
#include <boost/cstdint.hpp>
#include <boost/archive/binary_iarchive.hpp>
#include <boost/archive/binary_oarchive.hpp>
#include <openssl/md5.h>
#include <arpa/inet.h>

#include "data.h"

using namespace  boost::asio;
using            boost::asio::ip::tcp;

namespace
{
    void  calc_md5hex( unsigned char  src[ MD5_DIGEST_LENGTH ],
                       char  dst[ MD5_DIGEST_LENGTH * 2 ] )
    {
        std::sprintf( dst, "%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x"
                           "%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x",
                      src[ 0 ],  src[ 1 ],  src[ 2 ],  src[ 3 ],
                      src[ 4 ],  src[ 5 ],  src[ 6 ],  src[ 7 ],
                      src[ 8 ],  src[ 9 ],  src[ 10 ], src[ 11 ],
                      src[ 12 ], src[ 13 ], src[ 14 ], src[ 15 ] );
    }
}

class  Session
{
    public:
        explicit Session( io_service &  io ) : socket_( io ), size_( 0 )
        {
        }

        tcp::socket &  socket( void )
        {
            return socket_;
        }

        void  start( void )
        {
            async_read( socket_, buffer( data_, sizeof( boost::uint32_t ) ),
                        boost::bind( &Session::handle_read_size, this,
                                     placeholders::error,
                                     placeholders::bytes_transferred ) );
        }

    private:
        void handle_read_size( const boost::system::error_code &  error,
                               size_t  bytes_transferred )
        {
            if ( ! error )
            {
                size_ =  *( ( boost::uint32_t * )data_ );

                async_read( socket_, buffer( data_, ntohl( size_ ) ),
                            boost::bind( &Session::handle_read, this,
                                         placeholders::error,
                                         placeholders::bytes_transferred ) );
            }
            else
            {
                delete this;
            }
        }

        void handle_read( const boost::system::error_code &  error,
                          size_t  bytes_transferred )
        {
            if ( ! error )
            {
                std::istringstream  message_str;
                Request             request;

                message_str.str( std::string( data_, bytes_transferred ) );

                {
                    boost::archive::binary_iarchive  archive( message_str );
                    archive >> request;
                }

                std::ostringstream  reply_str;
                unsigned char       md5sum[ MD5_DIGEST_LENGTH ];
                char                md5hex[ MD5_DIGEST_LENGTH * 2 ];

                MD5( ( unsigned char * )request.value.c_str(),
                     request.value.size(), md5sum );

                calc_md5hex( md5sum, md5hex );

                Reply  reply = { request.value, md5hex };

                {
                    boost::archive::binary_oarchive  archive( reply_str );
                    archive << reply;
                }

                std::string  data( reply_str.str() );

                if ( data.size() > max_length )
                    throw std::runtime_error( "Too big message" );

                std::memcpy( data_, data.c_str(), data.size() );

                size_ = htonl( data.size() );

                async_write( socket_,
                             buffer( &size_, sizeof( boost::uint32_t ) ),
                             boost::bind( &Session::handle_write_size, this,
                                          boost::asio::placeholders::error,
                                          bytes_transferred ) );
            }
            else
            {
                delete this;
            }
        }

        void handle_write_size( const boost::system::error_code &  error,
                                size_t  size )
        {
            if ( ! error )
            {
                async_write( socket_, buffer( data_, ntohl( size_ ) ),
                             boost::bind( &Session::handle_write, this,
                                          boost::asio::placeholders::error ) );
            }
            else
            {
                delete this;
            }
        }

        void handle_write( const boost::system::error_code &  error )
        {
            if ( ! error )
            {
                socket_.shutdown( tcp::socket::shutdown_both );
            }
            else
            {
                delete this;
            }
        }
    private:
        enum { max_length = 1024 };

    private:
        tcp::socket      socket_;

        char             data_[ max_length ];

        boost::uint32_t  size_;
};

class  Server
{
    public:
        Server( io_service &  io, short  port ) : io_( io ),
            acceptor_( io_, tcp::endpoint( tcp::v4(), port ) )
        {
            start_accept();
        }

    private:
        void  start_accept( void )
        {
            Session *  new_session = new Session( io_ );

            acceptor_.async_accept( new_session->socket(),
                                    boost::bind( &Server::handle_accept, this,
                                        new_session, placeholders::error ) );
        }

        void  handle_accept( Session *  new_session,
                                    const boost::system::error_code &  error )
        {
            if ( ! error )
            {
                new_session->start();
            }
            else
            {
                delete new_session;
            }

            start_accept();
        }

    private:
        io_service &   io_;

        tcp::acceptor  acceptor_;
};

int  main( int  argc, char *  argv[] )
{
    try
    {
        if ( argc != 2 )
        {
            std::cerr << "Usage: server <port>\n";

            return 1;
        }

        io_service  io_;

        Server  serv( io_, std::atoi( argv[ 1 ] ) );

        io_.run();
    }
    catch ( const std::exception &  e )
    {
        std::cerr << "Exception: " << e.what() << "\n";
    }

    return 0;
}
Теперь напишем элементарный тестовый синхронный клиент (файл client.cc).
#include <cstring>
#include <sstream>
#include <iostream>
#include <boost/asio.hpp>
#include <boost/archive/binary_iarchive.hpp>
#include <boost/archive/binary_oarchive.hpp>
#include <boost/cstdint.hpp>
#include <arpa/inet.h>

#include "data.h"

using namespace  boost::asio;
using            boost::asio::ip::tcp;

namespace
{
    enum { max_length = 1024 };
}

int  main( int  argc, char *  argv[] )
{
    try
    {
        if ( argc != 3 )
        {
            std::cerr << "Usage: client <host> <port>\n";

            return 1;
        }

        io_service  io_;

        tcp::resolver            resolver( io_ );
        tcp::resolver::query     query( tcp::v4(), argv[ 1 ], argv[ 2 ] );
        tcp::resolver::iterator  iterator( resolver.resolve( query ) );
        tcp::socket              sock( io_ );

        connect( sock, iterator );

        std::cout << "Enter message: ";

        char  message[ max_length ];

        std::cin.getline( message, max_length );

        std::ostringstream  message_str;
        Request             request = { message };

        {
            boost::archive::binary_oarchive  archive( message_str );
            archive << request;
        }

        std::string  data( message_str.str() );

        if ( data.size() > max_length )
            throw std::runtime_error( "Too big message" );

        boost::uint32_t  size( htonl( data.size() ) );

        write( sock, buffer( &size, sizeof( boost::uint32_t ) ) );
        write( sock, buffer( data.c_str(), data.size() ) );

        size_t    reply_length( read( sock, buffer( message,
                                                sizeof( boost::uint32_t ) ) ) );

        if ( reply_length != sizeof( boost::uint32_t ) )
            throw std::runtime_error( "Error while reading message size" );

        size = ntohl( *( ( boost::uint32_t * )message ) );

        reply_length = read( sock, buffer( message, size ) );

        if ( reply_length != size )
            throw std::runtime_error( "Error while reading message" );

        std::istringstream  reply_str;
        Reply               reply;

        reply_str.str( std::string( message, size ) );

        {
            boost::archive::binary_iarchive  archive( reply_str );
            archive >> reply;
        }

        std::cout << "Reply is: {'" << reply.value << "', '" << reply.md5hex <<
                "'}" << std::endl;
    }
    catch ( const std::exception &  e )
    {
        std::cerr << "Exception: " << e.what() << "\n";
    }

    return 0;
}
Скомпилируем сервер и клиент и запустим их на выполнение.
g++ -Wall -g -o server server.cc -lboost_system-mt -lboost_serialization-mt \
        -lcrypto -lpthread
g++ -Wall -g -o client client.cc -lboost_system-mt -lboost_serialization-mt \
         -lpthread
./server 5555
(я разбил команды g++ на две строки, чтобы они уместились по ширине в колонку блога), в другом терминале запустим клиент:
./client localhost 5555
Enter message: Hello world!
Reply is: {'Hello world!', '86fb269d190d2c85f6e0468ceca42a20'}
Отлично, работает. Однако, для тестирования с помощью tsung тестовый клиент нам не нужен, поскольку tsung должен сам уметь отправлять клиентские запросы на сервер. Поскольку tsung написан на Erlang, а привязку к boost::serialization вряд ли получится просто реализовать на Erlang, то нам понадобится интерфейс erl_nif (NIF расшифровывается как Native Implemented Functions). Хороший пример его использования можно найти здесь. Поместим наш C++ интерфейс в файл erl_nif.cc:
#include <erl_nif.h>

#include <cstring>
#include <sstream>
#include <boost/archive/binary_iarchive.hpp>
#include <boost/archive/binary_oarchive.hpp>
#include <boost/cstdint.hpp>
#include <arpa/inet.h>

#include "data.h"

namespace
{
    enum { max_length = 1024 };
}

extern "C"
{
    static ERL_NIF_TERM  request( ErlNifEnv *  env, int  argc,
                                  const ERL_NIF_TERM  argv[] )
    {
        if ( argc < 1 )
            return enif_make_badarg( env );

        char    message[ max_length + sizeof( boost::uint32_t ) ];
        size_t  len( 0 );

        if ( ( len = enif_get_string( env, argv[ 0 ],
                                      message + sizeof( boost::uint32_t ),
                                      max_length, ERL_NIF_LATIN1 ) ) <= 0 )
            return enif_make_badarg( env );

        std::ostringstream  message_str;
        Request             request = { message + sizeof( boost::uint32_t ) };

        {
            boost::archive::binary_oarchive  archive( message_str );
            archive << request;
        }

        std::string        data( message_str.str() );
        boost::uint32_t *  size( ( boost::uint32_t * )&message[ 0 ] );

        *size = htonl( data.size() );

        ErlNifBinary       result;

        enif_alloc_binary( sizeof( boost::uint32_t ) + ntohl( *size ), &result );

        std::memcpy( message + sizeof( boost::uint32_t ), data.c_str(),
                     data.size() );
        std::memcpy( result.data, message,
                     sizeof( boost::uint32_t ) + ntohl( *size ) );

        result.size = sizeof( boost::uint32_t ) + ntohl( *size );

        return enif_make_binary( env, &result );
    }

    static ERL_NIF_TERM  response( ErlNifEnv *  env, int  argc,
                                   const ERL_NIF_TERM  argv[] )
    {
        if ( argc < 1 )
            return enif_make_badarg( env );

        ERL_NIF_TERM  message( argv[ 0 ] );

        if ( ! enif_is_binary( env, message ) )
            return enif_make_badarg( env );

        ErlNifBinary    bin;

        enif_inspect_binary( env, message, &bin );

        if ( bin.size < sizeof( boost::uint32_t ) )
            return enif_make_badarg( env );

        //boost::uint32_t *  size( ( boost::uint32_t * )&bin.data );

        //if ( bin.size != sizeof( boost::uint32_t ) + ntohl( *size ) )
            //return enif_make_badarg( env );

        std::istringstream  reply_str;
        Reply               reply;

        reply_str.str( std::string(
                                ( char * )bin.data + sizeof( boost::uint32_t ),
                                bin.size - sizeof( boost::uint32_t ) ) );

        {
            boost::archive::binary_iarchive  archive( reply_str );
            archive >> reply;
        }

        ErlNifBinary  value;
        ErlNifBinary  md5hex;

        enif_alloc_binary( reply.value.size(), &value );
        std::memcpy( value.data, reply.value.c_str(), reply.value.size() );
        value.size = reply.value.size();

        enif_alloc_binary( reply.md5hex.size(), &md5hex );
        std::memcpy( md5hex.data, reply.md5hex.c_str(), reply.md5hex.size() );
        md5hex.size = reply.md5hex.size();

        return enif_make_tuple2( env, enif_make_binary( env, &value ),
                                 enif_make_binary( env, &md5hex ) );
    }

    static ErlNifFunc  ts_p_md5hex_funcs[] =
    {
        { "request", 1, request },
        { "response", 1, response }
    };
}

ERL_NIF_INIT( ts_p_md5hex_nif, ts_p_md5hex_funcs, NULL, NULL, NULL, NULL )
Здесь нужны небольшие пояснения, хотя сам код достаточно прозрачен. Во-первых, здесь мы определили две эрланговские функции request() и response() и задали имя будущего эрланговского модуля ts_p_md5hex_nif (в данном случае я использую p_, подразумевая слово plugin, хотя это увеличит в дальнейшем размеры имен в коде на Erlang). Эти определения реализуются в самой последней строке приведенного кода. Запрос в функции request() отличается от аналога из client.cc тем, что размер и сериализованный архив посылаются в одном запросе, но это не должно быть существенным различием. Функция request() возвращает буфер типа ErlNifBinary, готовый к отправке на сервер. Функция response() наоборот, парсит ответ сервера и возвращает кортеж (tuple), содержащий в себе исходную строку и вычисленный хэш. Вычисление размера архива (первые 4 байта в ответе сервера) в функции response() закомментировано, так как этот размер нам не нужен - сервер сам разрывает соединение после посылки ответа, и tsung это легко обнаружит самостоятельно.

Компилируем erl_nif.cc в разделяемую библиотеку ts_p_md5hex_nif.so (не забудьте предварительно установить Erlang, причем не старше версии R14B, поскольку erl_nif в более старых версиях не поддерживается).
g++ -Wall -fPIC -shared -o ts_p_md5hex_nif.so -I/usr/lib64/erlang/usr/include \
 erl_nif.cc -lboost_system-mt -lboost_serialization-mt -lpthread
Теперь напишем эрланговский интерфейс к erl_nif.cc, назовем его ts_p_md5hex_nif.erl.
-module(ts_p_md5hex_nif).
-author('garuda @ blogspot.com').

-export([request/1response/1]).

-on_load(init/0).

init() ->
    ok = erlang:load_nif("./ts_p_md5hex_nif"0).

request(_Value->
    exit(nif_library_not_loaded).

response(_Content->
    exit(nif_library_not_loaded).
Все, интерфейс erl_nif готов. Осталось написать собственно модуль для tsung. И здесь мы будем следовать шагам, упомянутым в статье, ссылку на которую я привел в самом начале. Прежде всего нужно скачать исходники tsung. Далее переходим в директорию с исходниками и добавляем определения для нашего модуля, который мы назовем p_md5hex в файл tsung-1.0.dtd. Я не хочу приводить здесь файл целиком, а diff получается очень широким, поэтому скажу лишь, что в список type из ATTLIST session и список new_type из ATTLIST change_type нужно добавить слово ts_p_md5hex, а в список ELEMENT request - слово p_md5hex. Кроме того нужно добавить описание элемента p_md5hex:
<!ELEMENT p_md5hex EMPTY >
<!ATTLIST p_md5hex
    value  CDATA   #REQUIRED
>
Этот элемент будет использоваться в XML сценариях в определении тэгов request, значение value - это строка, которую мы будем передавать в запросе. Элементы ts_p_md5hex будут использоваться в тэгах session и, если понадобится, change_type для определения протокола сессии.

Теперь нужно создать эрланговский хедер-файл ts_p_md5hex.hrl, в котором будет описан тип данных p_md5hex с единственным полем value. Файл должен находится в директории include/ относительно корня исходников tsung.
-vc('$Id$ ').
-author('garuda @ blogspot.com').

-recordp_md5hex, {
          value,
          bug %% see comment after member 'bug' in ts_raw.hrl
} ).
Как видим, элемент value оказался не единственным, второй элемент bug нужен из-за какого-то бага внутри tsung -  комментарий рядом с ним предлагает посмотреть описание бага в другом файле. Без этого поля наш плагин действительно не заработает.

Теперь создадим исходник для поддержки парсинга конфигурации ts_config_p_md5hex.erl в директории src/tsung_controller/.
-module(ts_config_p_md5hex).
-vc('$Id$ ').
-author('garuda @ blogspot.com').

-export([parse_config/2]).

-include("ts_profile.hrl").
-include("ts_config.hrl").
-include("ts_p_md5hex.hrl").

-include("xmerl.hrl").

%%----------------------------------------------------------------------
%% Function: parse_config/2
%% Purpose:  parse a request defined in the XML config file
%% Args:     Element, Config
%% Returns:  List
%%----------------------------------------------------------------------
%% Parsing other elements
parse_config(Element = #xmlElement{name=dyn_variable}, Conf = #config{}) ->
    ts_config:parse(Element,Conf);

parse_config(Element = #xmlElement{name=p_md5hexattributes=Attrs},
             Config=#config{curid = Idsession_tab = Tab,
                            sessions = [CurS | _], dynvar=DynVar,
                            subst    = SubstFlagmatch=MatchRegExp}) ->
    Req = case ts_config:getAttr(string,Attrsdatasizeof
               [] ->
                   Value = ts_config:getAttr(stringAttrsvalue),
                   #p_md5hex{value=Value}
          end,
    ts_config:mark_prev_req(Id-1TabCurS),
    Msg=#ts_request{ack     = parse,
                    subst   = SubstFlag,
                    match   = MatchRegExp,
                    param   = Req},
    ets:insert(Tab,{{CurS#session.idId},Msg#ts_request{endpage=true,
                                                         dynvar_specs=DynVar}}),
    lists:foldlfun(A,B)->ts_config:parse(A,Bend,
                 Config#config{dynvar=[]},
                 Element#xmlElement.content);

%% Parsing other elements
parse_config(Element = #xmlElement{}, Conf = #config{}) ->
    ts_config:parse(Element,Conf);

%% Parsing non #xmlElement elements
parse_config(_Conf = #config{}) ->
    Conf.
Тут мне трудно что-либо прокомментировать за исключением того, что файл создан как комбинация исходников ts_config_raw.erl (как не очень большого по размеру) и ts_config_http.erl (как наиболее подходящего для нашего протокола). Единственное, что важно - элемент ack в конструкторе #ts_request должен быть равен parse.

Теперь собственно наш модуль ts_p_md5hex.erl, который следует разместить в директории src/tsung/.
-module(ts_p_md5hex).
-author('garuda @ blogspot.com').

-behavior(ts_plugin).

-include("ts_profile.hrl").
-include("ts_p_md5hex.hrl").

-export([init_dynparams/0,
         add_dynparams/4,
         get_message/2,
         session_defaults/0,
         dump/2,
         parse/2,
         parse_bidi/2,
         parse_config/2,
         decode_buffer/2,
         new_session/0,
         md5hex/1]).

%%----------------------------------------------------------------------
%% Function: session_defaults/0
%% Purpose:  default parameters for session (ack_type and persistent)
%% Returns:  {ok, true|false}
%%----------------------------------------------------------------------
session_defaults() ->
    {oktrue}.

%%----------------------------------------------------------------------
%% Function: decode_buffer/0
%% Purpose:  decode buffer for matching or dyn_variables
%% Returns:  decoded buffer
%%----------------------------------------------------------------------
decode_buffer(Buffer, #p_md5hex{}) ->
    Buffer.

%%----------------------------------------------------------------------
%% Function: new_session/0
%% Purpose:  initialize session information
%% Returns:  record or []
%%----------------------------------------------------------------------
new_session() ->
    #p_md5hex{}.

%%----------------------------------------------------------------------
%% Function: parse/2
%% Purpose:  Parse the given data and return a new state
%% Args:     Data (binary), State (record)
%% Returns:  NewState (record)
%%----------------------------------------------------------------------
parse(closedState->
    {State#state_rcv{ack_done = true}, [], true};

parse(DataState->
    {State#state_rcv{datasize = size(Data)}, [], false}.

parse_bidi(DataState->
    ts_plugin:parse_bidi(Data,State).

dump(A,B->
    ts_plugin:dump(A,B).

%%----------------------------------------------------------------------
%% Function: get_message/1
%% Purpose:  Build a message/request
%% Args:     #p_md5hex
%% Returns:  binary
%%----------------------------------------------------------------------
get_message(#p_md5hex{value=Value}, #state_rcv{session=S}) ->
    Packet=ts_p_md5hex_nif:request(Value),
    {PacketS}.

%%----------------------------------------------------------------------
%% Function: parse_config/2
%% Purpose:  parse tags in the XML config file related to the protocol
%% Returns:  List
%%----------------------------------------------------------------------
parse_config(ElementConf->
    ts_config_p_md5hex:parse_config(ElementConf).

%%----------------------------------------------------------------------
%% Function: add_dynparams/4
%% Purpose:  add dynamic parameters to build the message
%%----------------------------------------------------------------------
add_dynparams(_, [], Param_Host->
    Param;

add_dynparams(trueDynDataOldReq_Host->
    subst(OldReqDynData#dyndata.dynvars);

add_dynparams(_Subst_DynDataParam_Host->
    Param.

%%----------------------------------------------------------------------
%% Function: subst/2
%% Purpose:  Replace on the fly dynamic element of the request.
%%----------------------------------------------------------------------
subst(Req=#p_md5hex{value=Value}, DynData->
    Req#p_md5hex{value=ts_search:subst(ValueDynData)}.

init_dynparams() ->  #dyndata{}.

%%----------------------------------------------------------------------
%% Function: md5hex/1
%% Purpose:  Retrieve md5hex message from the request.
%%----------------------------------------------------------------------
md5hex(Data->
    element(2ts_p_md5hex_nif:response(Data)).
Здесь больше комментариев, чем кода. В модуле tsung должны быть определены все функции, перечисленные в секции export, за исключением последней md5hex(), которую мы добавили сами и планируем использовать для проверки правильности ответа сервера в XML сценарии. Реализация почти всех этих функций соответствует минимальному стандарту, который можно увидеть в файле ts_raw.erl в этой же директории. Исключение составляют две важные функции: get_message() и parse(). Функция get_message() создает запрос к серверу с помощью функции request() из нашего пакета ts_p_md5hex_nif, а функция parse() парсит ответ сервера. В нашем случае parse() просто добавляет размер полученных данных в поле datasize записи state_rcv (без этого размер принятых данных будет неверно рассчитан как нулевой), а при закрытии соединения сервером правильно завершает обработку  принятых данных на стороне tsung. Функция md5hex() парсит ответ сервера с помощью ts_p_md5hex_nif:response() и возвращает второй элемент кортежа (т.е. md5 хэш).

Перед сборкой tsung надо перенести файл ts_p_md5hex_nif.erl в директорию src/tsung/. После этого собираем и устанавливаем tsung стандартной цепочкой ./configure; make; make install.

После установки tsung настала пора протестировать наш плагин. Для этого создаем какую-нибудь директорию, например ~/tsung-runtime, копируем в нее библиотеку ts_p_md5hex_nif.so (в модуле ts_p_md5hex_nif.erl мы определили, что будем искать библиотеку в текущей рабочей директории) и переходим в нее. Создаем простой тестовый сценарий в файле scenario.xml.
<?xml version="1.0"?>
<!DOCTYPE tsung SYSTEM "/usr/local/share/tsung/tsung-1.0.dtd" [] >
<tsung loglevel="info">
  <clients>
    <client host="localhost" use_controller_vm="true"/>
  </clients>

  <servers>
    <server host="192.168.0.2" port="5555" type="tcp"></server>
  </servers>

  <load>
    <arrivalphase phase="1" duration="60" unit="second">
      <users arrivalrate="10" unit="second"></users>
    </arrivalphase>
    <arrivalphase phase="2" duration="60" unit="second">
      <users arrivalrate="20" unit="second"></users>
    </arrivalphase>
  </load>

  <sessions>
    <session name="md5hex_1" probability="50" type="ts_p_md5hex">
      <request>
        <match do='continue' when='match' apply_to_content='ts_p_md5hex:md5hex'>
          827ccb0eea8a706c4c34a16891f84e7b
        </match>
        <p_md5hex value="12345"/>
      </request>
    </session>
    <session name="md5hex_2" probability="50" type="ts_p_md5hex">
      <request>
        <match do='continue' when='match' apply_to_content='ts_p_md5hex:md5hex'>
          1e01ba3e07ac48cbdab2d3284d1dd0fa
        </match>
        <p_md5hex value="67890"/>
      </request>
    </session>
  </sessions>
</tsung>
На домашней странице tsung имеется прекрасный раздел с документацией, в котором можно изучить все тонкости написания сценариев. В данном сценарии используется единственный клиент, запускаемый на локальном хосте. Предполагается, что сервер запущен на хосте с адресом 192.168.0.2 (просто localhost здесь не сработает) и прослушивает порт 5555. Определены две стадии тестирования длительностью по 60 секунд, в первой стадии каждую секунду посылаются 10 новых запросов на сервер, во второй - 20. Запросы имеют тип ts_p_md5hex и равновероятно отправляют сериализованные значения 12345 или 67890. Хэш в ответе проверяется с помощью вызова функции ts_p_md5hex:md5hex() внутри тэгов match путем сопоставления с заранее посчитанным значением.

Запускаем tsung:
tsung -f scenario.xml -l ~/tmp/log/ start
и переходим в директорию с результатами (tsung выводит ее на экран). Внутри этой директории запускаем скрипт /usr/lib/tsung/bin/tsung_stats.pl, который генерирует статистический отчет, содержащий помимо сухих цифр некоторое количество замечательных картинок. Этот скрипт можно запускать неоднократно во время работы сценария или после того, как тестирование завершилось. Вот несколько картинок с результатами теста:

Длительность запроса и установки соединения:

Скорость генерации запросов:

Сетевой трафик:
Запросы, соответствующие условиям тэга match:

На втором, третьем и четвертом рисунках видно наличие двух фаз теста по 60 секунд каждая. На первом рисунке видно, что один запрос в среднем занимал чуть более одной миллисекунды, а скорость установки соединения равнялась половине миллисекунды, при этом увеличение нагрузки во второй фазе не отразилось на этих цифрах. На втором рисунке видно, что в первой фазе в среднем генерировалось 10 запросов в секунду, а во второй - 20, что соответствует сценарию теста. На третьем рисунке показан объем исходящего и входящего трафиков, соотношения кривых соответствуют нашему протоколу взаимодействия (ответ сервера примерно в два раза больше клиентского запроса). Четвертый рисунок показывает, что все ответы сервера были с правильно рассчитанным md5 хэшем.

Исходники здесь.