Программирование в ACE: обмен данными
В ПРОШЛОМ НОМЕРЕ МЫ
ПОЗНАКОМИЛИСЬ С МОЩНОЙ БИБЛИОТЕКОЙ ПРОМЕЖУТОЧНОГО УРОВНЯ ACE. МЫ ИЗУЧИЛИ,
КАК С ЕЕ ПОМОЩЬЮ МОЖНО СОЗДАВАТЬ ПЕРЕНОСИМЫЕ МНОГОПОТОЧНЫЕ И МНОГОЗАДАЧНЫЕ
ПРИЛОЖЕНИЯ И КАК СИНХРОНИЗИРОВАТЬ В НИХ ДАННЫЕ.
Теперь настал черед разобраться с коммуникационными возможностями,
предоставляемыми ACE. А именно - с классами представления данных, фасадами
сокетов и утилитой протоколирования работы.
Представление данных
Кроме функциональности, для обеспечения переноса приложений с одной платформы
на другую ACE имеет солидный набор классов для представления данных. Во-первых,
это классы-аналоги контейнеров STL, таких как map, list, vector и прочих.
Естественно, библиотека реализует соответствующие аллокаторы и итераторы для
этих классов. Кратко перечислим основные классы ACE, предоставляющие
функциональность часто используемых контейнеров STL: ACE_Map,
ACE_Hash_Map_Manager_Ex, ACE_Intrusive_List, ACE_Array, ACE_Vector, ACE_CString
и так далее. В книгах и статьях об ACE нередко пишут, что следует использовать
именно контейнеры данных ACE, поскольку они более стабильны и надежны, чем
контейнеры STL. Мое мнение по этому поводу не столь категорично. Да,
использовать контейнеры ACE стоит, если на целевой платформе стоит древний и
ненадежный STL вроде MSVC 6.0. Если же есть возможность работать, например, с
STLPort, то лучше использовать наработанный STL-код.
Возможно самым неоднозначным классом из всех классов ACE является
ACE_Message_Block. Еще бы, первый взгляд на него вызывает отвращение: какие-то
торчащие наружу указатели, смещения в буфере памяти, - зачем это все нужно, если
есть std::vector? Однако когда речь заходит о создании сетевых приложений,
которые взаимодействуют с помощью нескольких протоколов, лежащих друг поверх
друга, этот класс оказывается на редкость эффективным. А в анализаторах сетевого
трафика или любых последовательно структурированных данных, которые требуется
анализировать, этот класс просто незаменим. Рассмотрим простейший пример.
Допустим, мы перехватываем на сетевом интерфейсе запрос из браузера клиента к
HTTP-серверу. Это блок данных размером в 500 байт. Он состоит из следующих
сегментов: данные Ethernet, IPv4, TCP и HTTP. Это четыре различных протокола,
которые выполняют разные задачи. Если бы мы хранили данные в массиве памяти или
векторе STL, то нам пришлось бы на каждый уровень анализатора протоколов
передавать весь буфер данных и для каждого из этих протоколов указывать
смещение, либо просто вырезать после анализа все ненужные данные из нижнего
уровня. Класс ACE_Message_Block позволяет нам хранить весь буфер данных и иметь
два оперативных указателя на чтение текущих данных и на запись порции новой
информации. Каждый раз, когда мы обрабатываем текущий протокол своим уровнем
анализатора, мы изменяем указатель на чтение данных, устанавливая смещение на
новый протокол. Далее мы передаем Message_Block верхнему уровню анализатора,
который уже «знает», по какому смещению лежат его данные. Более того, на каждом
уровне анализатора мы можем запомнить указатель на свой блок данных, и если нам,
скажем, на уровне HTTP понадобится информация об IPv4, то мы всегда сможем
вернуться вниз по стеку протоколов. Следующий пример демонстрирует эту работу с
различными протоколами.
#include <ace/Message_Block.h>
#include <ace/OS.h>
const size_t MAX_PACKET_SIZE = 1024;
int main(int argc, char* argv[])
{
//Создаем пустой блок
ACE_Message_Block * dataMb;
dataMb = new ACE_Message_Block( MAX_PACKET_SIZE );
//Копируем в него данные
dataMb->copy( argv[0], ACE_OS::strlen(argv[0]) );
//Создаем блок, ссылающийся на существующие данные
ACE_Message_Block * refMb;
refMb = new ACE_Message_Block(argv[1],
ACE_OS::strlen(argv[1]) );
//Добавим этот блок как продолжение первого блока
dataMb->cont( refMb );
//Анализируем Ethernet-сегмент
const EthernetPacket * pEthernetPacket =
(EthernetPacket *)dataMb->rd_ptr();
//Получаем нужные заголовки из Ethernet-пакета
//...
//Устанавливаем смещение на IPv4-сегмент
dataMb->rd_ptr( sizeof(EthernetPacket) );
//Анализируем IPv4-сегмент
const IPv4Packet * pIPv4Packet =
(IPv4Packet *)dataMb->rd_ptr();
//Получаем нужные заголовки из IPv4-пакета
//...
//Устанавливаем смещение на TCP-сегмент
dataMb->rd_ptr( sizeof(IPv4Packet) );
//Анализируем TCP-сегмент
const TCPPacket * pTCPPacket =
(TCPPacket *)dataMb->rd_ptr();
//Получаем нужные заголовки из TCP-пакета
//...
//Устанавливаем смещение на HTTP-сегмент
dataMb->rd_ptr( sizeof(TCPPacket) );
//Анализируем HTTP-сегмент
const HTTPPacket * pHTTPPacket =
parseHTTP( dataMb->rd_ptr() );
//Получаем нужные заголовки из HTTP-пакета
//...
//Освобождаем ненужные блоки сообщений
dataMb->release();
refMb->release();
return 0;
}
Возможность гибко управлять чтением и записью в Message_Block - не
единственная отличительная особенность этого класса. Конструкторы и методы
класса позволяют нам создавать блоки данных, которые как владеют данными, так и
просто ссылаются на них, что очень удобно для оптимизации скорости работы
программы (минимизация копирования данных). Главное очень осторожно обращаться с
указателями и не «терять» их. Кроме того, класс ACE_Message_Block позволяет
задавать тип и приоритет сообщения, что дает возможность более гибко управлять
набором или очередью сообщений в стеке сообщений. Если мы работаем с
сегментированным протоколом, таким как, например, TCP, то полезной может
оказаться возможность хранить составные сообщения, доступ к которым возможен
через метод cont(), возвращающий указатель на следующий блок сообщения. При этом
применяется паттерн Composite. Объекты ACE_Message_Block могут быть использованы
в связке с другими классами ACE, например, с очередью сообщений
ACE_Message_Queue.
Сокеты
Для реализации сетевого взаимодействия в большинстве случаев используется
программный интерфейс Socket API. Это достаточно старый интерфейс, разработанный
в BSD Unix. На сегодняшний день он является стандартом de facto для реализации
межпроцессного взаимодействия. Одной из самых распространенных ошибок при
разработке кросс-платформенного кода является использование дескриптора сокета
как объекта ввода/вывода. Для некоторых платформ (например, Windows) такая
взаимозаменяемость недопустима, и это ведет к непредсказуемому поведению
программы. Если рассмотреть реализацию сокетов в Windows (так называемые
Win-сокеты) более внимательно, то можно заметить следующие факты. Во-первых,
Windows определяет некоторые константы и макросы, которые отсутствуют в
*nix-реализациях сокетов. Во-вторых, Windows иначе использует некоторые
аргументы сокетных функций. В-третьих, в данной реализации сокетов присутствуют
два API, первый из которых является аналогом BSD-сокетов, за исключением двух
вышеперечисленных фактов. Второй является неким переосмыслением BSD-сокетов,
связанным с тем, что API Windows не позволяет реализовать асинхронные
(неблокируемые) сокеты так же, как это сделано в любом *nix. Эта вторая
реализация сокетов непереносима в принципе, поэтому о переносе кода
неблокируемых сокетов можно забыть. Кроме объективных проблем, связанных с
переносимостью кода, существуют и субъективные проблемы, связанные с
типо-безопасностью. Целочисленный дескриптор сокета можно перепутать с другим
дескриптором: неправильно использовать, преобразовать в другой тип и так далее.
Интерфейсные фасады сокетов ACE позволяют избежать множества этих проблем и
обеспечивают следующие преимущества: повышенная типо-безопасность,
кросс-платформенность кода и уменьшение количества кода, за счет реализации его
в ACE.
Сетевая адресация
Как известно, при работе с сокетами сетевые адреса задаются с помощью
структуры sockaddr. Конечный код получается громоздким, неудобочитаемым и, кроме
того, небезопасным с точки зрения приведения типов. В ACE эта проблема решается
использованием базового (обобщенного) для любой адресации класса ACE_Addr. В
зависимости от того, какое именно взаимодействие реализуется (с помощью сокетов,
пайпов и так далее), используются различные производные от ACE_Addr классы.
Например, ACE_INET_Addr, ACE_SPIPE_Addr, ACE_DEV_Addr и другие. Фактически,
класс ACE_Addr реализует контейнер, инкапсулирующий тип адреса и буфер памяти,
который хранит этот адрес. Этот класс позволяет сравнивать адреса, устанавливать
и получать тип адреса и буфер, хранящий адрес. Для конкретного применения
адресации в смысле сетевых адресов применяется производный от ACE_Addr класс
ACE_INET_Addr, позволяющий управлять именами узлов, IP-адресами и номерами
портов. Следующий пример демонстрирует работу с этим классом.
#include <ace/INET_Addr.h>
#include <ace/OS.h>
int main(int argc, char* argv[])
{
//Устанавливаем адрес
ACE_INET_Addr addr;
if( addr.set( 80, "localhost" )==-1 )
return -1;
//Выводим его в консоль
ACE_OS::printf( "Host: %s\n",
addr.get_host_name() );
ACE_OS::printf( "Port: %u\n",
addr.get_port_number() );
ACE_OS::printf( "IP: %s\n",
addr.get_host_addr() );
return 0;
}
Дескриптор сокета
Аналогично фасаду сетевых адресов, в ACE существует фасад и для дескриптора
сокета. Этим классом является ACE_IPC_SAP, инкапсулирующий само понятие
дескриптора. По сути это просто С++ обертка вокруг обобщенного дескриптора. В
зависимости от конкретной задачи разработчиками используются более детальные
классы ACE_SOCK (для сокетов), ACE_SPIPE (для пайпов) и так далее. Класс
ACE_SOCK предоставляет следующие возможности: создание и удаление сокетов,
установка и получение опций сокета и получение сетевых адресов. Класс ACE_SOCK
определен как абстрактный класс и доступен для использования только производным
классам, которые непосредственно предоставляют пользователю возможности по
установлению соединений и передачи данных.
Два типа сокетов
При работе с протоколом TCP, подразумевающим установление соединения,
наиболее часто встречающейся ошибкой программистов является замена сокета, по
которому устанавливается соединение, на сокет, по которому должен идти обмен
данными, или наоборот. Например, программист может попытаться отправить данные
до соединения с сервером или использовать для ответа подсоединившемуся к серверу
клиенту не тот сокет, который вернул accept(), а тот, на котором прослушиваются
подключения клиентов. Чтобы избежать этой ситуации, ACE выделяет два типа
сокетов: сокеты для передачи данных (классы ACE_SOCK_IO и ACE_SOCK_Stream) и
сокеты, управляющие установлением соединения (классы ACE_SOCK_Connector и
ACE_SOCK_Acceptor). Класс ACE_SOCK_IO определяет базовый интерфейс для всех
классов межпроцессного взаимодействия. ACE_SOCK_Stream расширяет его интерфейс
для конкретного применения в смысле сокетов. Оба эти класса предоставляют
семейство методов send() и recv(), варьируя их аргументы и количество
передаваемых буферов. Класс ACE_SOCK_Stream поддерживает как блокируемый, так и
неблокируемый ввод/вывод, причем по умолчанию используется блокируемый. Никакой
другой функциональности, кроме передачи данных, эти классы не содержат.
Клиент
Вся функциональность, связанная с установлением соединения со стороны клиента
и завершением этого соединения, реализуется в двух методах класса
ACE_SOCK_Connector. Метод connect() позволяет клиенту установить соединение с
сервером, а метод complete() завершает это соединение. Одним из параметров этих
методов является временной интервал, в течение которого ACE будет ожидать
установления/разрыва соединения. Реализуем клиент, который отсылает на сервер
строку, принимает ее и выводит в консоль.
#include <ace/INET_Addr.h>
#include <ace/SOCK_Connector.h>
#include <ace/SOCK_Stream.h>
#include <ace/OS.h>
const size_t MAX_RECV_BUFFER_SIZE = 1024;
int main(int argc, char* argv[])
{
if( argc<2 )
{
ACE_OS::printf( "USAGE: client inputstring" );
return -1;
}
//Устанавливаем адрес
ACE_INET_Addr addr;
if( addr.set( 80, "locahost" )==-1 )
return -1;
//Соединяемся с сервером
ACE_SOCK_Connector connector;
ACE_SOCK_Stream client;
if( connector.connect( client, addr )==-1 )
return -1;
//Отправляем строку, указанную в качестве аргумента программе
ssize_t transfered;
transfered = client.send_n( argv[1],
ACE_OS::strlen(argv[1]), 0 );
if( transfered==-1 )
{
connector.complete( client );
return -1;
}
//Получаем строку-ответ от сервера
char buffer[ MAX_RECV_BUFFER_SIZE ];
transfered = client.recv_n( buffer,
MAX_RECV_BUFFER_SIZE );
if( transfered==-1 )
{
connector.complete( client );
return -1;
}
//Выводим строку в консоль
buffer[transfered] = 0;
ACE_OS::printf( "%s\n", buffer );
//Завершаем соединение
connector.complete( client );
return 0;
}
Сервер
Серверную сторону соединения реализует класс ACE_SOCK_Acceptor. Метод open()
этого класса аналогичен вызову функции listen(), а метод accept() аналогичен
такому же вызову Socket API. Метод accept() также принимает временной интервал,
с помощью которого можно указать время ожидания соединения. Теперь реализуем
сервер, который принимает от клиента строку и отправляет ее обратно.
#include <ace/INET_Addr.h>
#include <ace/SOCK_Acceptor.h>
#include <ace/SOCK_Stream.h>
#include <ace/OS.h>
const size_t MAX_RECV_BUFFER_SIZE = 1024;
int main(int argc, char* argv[])
{
//Устанавливаем адрес
ACE_INET_Addr addr;
if( addr.set( 80 )==-1 )
return -1;
//Переходим в состояние прослушивания
ACE_SOCK_Acceptor acceptor;
ACE_SOCK_Stream server;
if( acceptor.open( addr )==-1 )
return -1;
while(1)
{
//Ожидаем подключения клиентов
if( acceptor.accept( server )==-1 )
return -1;
//Получаем строку-запрос от клиента
ssize_t bytes;
char buffer[ MAX_RECV_BUFFER_SIZE ];
bytes = server.recv_n( buffer,
MAX_RECV_BUFFER_SIZE );
if( bytes==-1 )
{
server.close();
continue;
}
//Отправляем строку клиенту
bytes = server.send_n( buffer, bytes, 0 );
if( bytes==-1 )
{
server.close();
continue;
}
server.close();
}
return 0;
}
Протоколирование работы
В ходе работы приложения очень часто требуется вести протокол работы – лог.
Возникает вопрос, а что делать, если требуется возможность динамически выбирать
приемник записей – консоль, файл на диске или, например, демон протоколирования?
А что, если требуются гибкие политики управления приоритетами (масками) записей,
дампы данных и так далее. Библиотека ACE предлагает класс ACE_Log_Msg для
решения всех этих проблем без изобретения очередного велосипеда. Это
класс-синглтон, поэтому клиенту не нужно создавать экземпляр этого класса и
следить за его жизненным циклом – ACE удаляет его автоматически при завершении
работы. Данный пример демонстрирует работу с классом ACE_Log_Msg.
#include <ace/Log_Msg.h>
#include <fstream>
const size_t MAX_BUFFER_SIZE = 1024;
int main(int argc, char* argv[])
{
//Очищаем предыдущие флаги логгера
ACE_LOG_MSG->clr_flags(ACE_LOG_MSG->flags());
//Сконфигурируем нужные маски
ACE_LOG_MSG->set_flags(LM_DEBUG);
ACE_LOG_MSG->log_priority_enabled(LM_DEBUG);
//Установим режим вывода логов в stderr
ACE_LOG_MSG->set_flags(ACE_Log_Msg::STDERR);
//Выведем запись протокола
ACE_LOG_MSG->log( LM_DEBUG, "Debug information\n" );
char buffer[MAX_BUFFER_SIZE];
for( int i=0; i<MAX_BUFFER_SIZE; ++i )
buffer[i] = i;
ACE_LOG_MSG->log_hexdump( LM_DEBUG,
buffer, MAX_BUFFER_SIZE );
//Установим режим вывода логов в файл
ACE_LOG_MSG->clr_flags(ACE_Log_Msg::STDERR);
std::ofstream *ofs = new std::ofstream("test.log");
ACE_LOG_MSG->msg_ostream (ofs, 1);
ACE_LOG_MSG->set_flags(ACE_Log_Msg::OSTREAM);
//Выведем запись протокола
ACE_LOG_MSG->log( LM_DEBUG, "Debug information\n" );
ACE_LOG_MSG->log_hexdump( LM_DEBUG,
buffer, MAX_BUFFER_SIZE );
return 0;
}
Время
Многие методы классов ACE позволяют указать в качестве параметра временной
интервал. Тип этого временного интервала определен как класс ACE_Time_Value.
Интерфейс этого класса позволяет создавать объект из структуры timeval, пары
значений sec-usec, типа timespec_t и временной отметки, взятой из Win32 FILETIME.
Интерфейс класса позволяет преобразовывать время из одного типа в другой,
добавлять, вычитать, умножать на другое время, а также сравнивать между собой
два времени.
Тokenizer
В библиотеке Boost есть мощный контейнер tokenizer, который позволяет разбить
строку на токены, используя набор заданных разделителей. Аналогичный класс есть
и в ACE – ACE_Tokenizer.
ACE_Utils
Кроме пространства имен ACE_OS, в библиотеке определено пространство имен
ACE_Utils, содержащее в себе вспомогательные классы для генерации уникальных
идентификаторов (UUID), функторов для операций присваивания и копирующих
конструкторов (Auto_Functor, Auto_Functor_Ref), а также класс для приведения
целочисленных типов к типу int (Truncate).
Аргументы программы
Для распознавания аргументов, заданных в командной строке, программисты часто
пишут специальные парсеры этих аргументов. Существует великое множество
различных вариаций ключей и их значений. ACE предусматривает специальный класс
ACE_Get_Opt, который позволяет задать маску нужных ключей и их значений и
позволяет проитерироваться по всем указанным пользователем ключам. Следующий код
демонстрирует, как можно раз и навсегда решить проблему парсера аргументов
командной строки.
#include <ace/Get_Opt.h>
#include <ace/SString.h>
#include <ace/OS.h>
int main (int argc, char* argv[])
{
int option;
static char options [] = ":f:p:x";
ACE_Get_Opt cmd_opts(argc, argv, options);
ACE_CString filename;
ACE_CString property;
int trace_mask = 0;
while ((option = cmd_opts()) != EOF)
{
switch(option)
{
case 'f':
filename = cmd_opts.opt_arg();
break;
case 'p':
property = cmd_opts.opt_arg();
break;
case 'x' :
trace_mask |= LM_DEBUG;
break;
default :
usage();
return 0;
}
}
return 0;
}
Динамическая загрузка библиотек
Динамическая загрузка библиотек является еще одной проблемной стороной при
разработке кросс-платформенных приложений. Разработчики ОС считают своим долгом
каждый раз изобрести велосипед помудреней, чтобы пользователи ломали головы над
тем, как создать переносимый код, хотя функциональные возможности API OC в
контексте загрузки динамических библиотек похожи как две капли воды. ACE
предоставляет класс ACE_DLL_Manager, при помощи которого можно загрузить
разделяемую библиотеку в runtime–методы open_dll() и close_dll(). Метод open_dll()
возвращает объект класса ACE_DLL_Handler, для которого можно вызвать метод close(),
а также метод symbol(), возвращающий указатель на нужный символ библиотеки. Для
реализации каркаса библиотеки, функции которого вызываются при загрузке и
закрытии библиотеки, существует абстрактный интерфейс ACE_DLL. Пользователь
должен реализовать этот интерфейс, и при загрузке и закрытии библиотеки будут
вызваться методы open() и close() соответственно.
В следующий раз
В этой статье мы изучили коммуникационные возможности, предоставляемые
библиотекой ACE, а также классы представлений данных и вспомогательные утилиты,
которые могут заметно облегчить жизнь разработчикам кросс-платформенного кода. В
следующий раз мы поднимемся вверх по иерархии классов ACE и займемся каркасами
приложений, которые на порядок увеличивают повторное использование кода и
предоставляют готовые решения для реализации любых событийно управляемых
моделей.
Ссылки
http://www.cs.wustl.edu/~schmidt/ACE.html - официальный сайт библиотеки ACE
http://www.dre.vanderbilt.edu/Doxygen - онлайн документация по технологиям
DOC
http://www.riverace.com –
сайт компании Стивена Хьюстона, предоставляющей услуги по поддержке и освоению
ACE
http://www.rsdn.ru/Forum/Info.aspx?name=FAQ.network.socket.winlin – описание
некоторых граблей, на которые можно наступить при кросс-платформенной разработке
|
Обсуждение статьи
|
|
|
|
RE: Программирование в ACE: обмен данными Best wishes to you! nice site! replica watches are well sell these years. replica omega All popular Replica Tag Heuer, Breitling. |
|
RE: Программирование в ACE: обмен данными We professional wholesale new era cap,the absolute reputation guarantee and the absolute quality assurance. Various of style. Here, I believe you can find yourself like , you can choose and buy online, we will prompt delivery once you pay. |
|
RE: Программирование в ACE: обмен данными Что-то ваши примеры плохо работают :( |
|
|
Keywords: zPOSTz zCODEz z10038z
Для Авторов: edit delete
Автор: Антон Палагин Дата: 16.12.2008 11:52:10©
|