- Поддержка большого числа протоколов и типов серверов, среди них http, jabber, SOAP, LDAP, MySQL, Postgres, NFS и др.
- Очень гибкая настройка сценариев тестирования, основанная на XML
- Высокая скорость работы и возможность создания ботнетов из клиентов
Но что делать, если мы хотим протестировать наш собственный кастомный протокол, используя все вкусности, который предоставляет 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/1, response/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'). -record( p_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_md5hex, attributes=Attrs}, Config=#config{curid = Id, session_tab = Tab, sessions = [CurS | _], dynvar=DynVar, subst = SubstFlag, match=MatchRegExp}) -> Req = case ts_config:getAttr(string,Attrs, datasize) of [] -> Value = ts_config:getAttr(string, Attrs, value), #p_md5hex{value=Value} end, ts_config:mark_prev_req(Id-1, Tab, CurS), Msg=#ts_request{ack = parse, subst = SubstFlag, match = MatchRegExp, param = Req}, ets:insert(Tab,{{CurS#session.id, Id},Msg#ts_request{endpage=true, dynvar_specs=DynVar}}), lists:foldl( fun(A,B)->ts_config:parse(A,B) end, 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() -> {ok, true}. %%---------------------------------------------------------------------- %% 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(closed, State) -> {State#state_rcv{ack_done = true}, [], true}; parse(Data, State) -> {State#state_rcv{datasize = size(Data)}, [], false}. parse_bidi(Data, State) -> 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), {Packet, S}. %%---------------------------------------------------------------------- %% Function: parse_config/2 %% Purpose: parse tags in the XML config file related to the protocol %% Returns: List %%---------------------------------------------------------------------- parse_config(Element, Conf) -> ts_config_p_md5hex:parse_config(Element, Conf). %%---------------------------------------------------------------------- %% Function: add_dynparams/4 %% Purpose: add dynamic parameters to build the message %%---------------------------------------------------------------------- add_dynparams(_, [], Param, _Host) -> Param; add_dynparams(true, DynData, OldReq, _Host) -> subst(OldReq, DynData#dyndata.dynvars); add_dynparams(_Subst, _DynData, Param, _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(Value, DynData)}. init_dynparams() -> #dyndata{}. %%---------------------------------------------------------------------- %% Function: md5hex/1 %% Purpose: Retrieve md5hex message from the request. %%---------------------------------------------------------------------- md5hex(Data) -> element(2, ts_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 хэшем.
Исходники здесь.