Выпуск 24. Февраль 2015
← Тестирование черного ящика | Содержание | Fuzzing-тестирование perl-интерпретатора с помощью afl →Реализация удаленного вызова процедур (RPC) в Perl с помощью Thrift
Рассмотрены основы работы со Thrift в Perl
Thrift — разработка Facebook, которая была открыта и передана организации Apache. Особенность данной реализации RPC — в бинарном протоколе и автоматической генерации кода для обработки сообщений для разных языков программирования. В качестве конфигурации RPC служит файл спецификации, где на C++-подобном синтаксисе описываются типы данных и сервисы с методами.
Где это может пригодиться? Thrift может быть полезен в проектах, где применяются разные языки программирования, когда необходимо наладить обмен данными и удаленно вызывать процедуры в отдельно запущенных сервисах. RPC также подойдет тогда, когда написание XS-кода усложняется, например, наличием сложной логики инициализации или использованием тредов.
Установка Thrift
На момент написания статьи Thrift присутствовал в Debian-дистрибутиве только в виде компилятора, без заголовков и библиотек для разработки. Поэтому собираем из исходников. Установка в систему не требуется, достаточно при генерации кода правильно указать пути к файлам.
Скачивание исходников
Исходники находятся на сайте Apache http://thrift.apache.org/download.
Компилирование
Вначале устанавливаем необходимые библиотеки для компилирования:
sudo apt-get install libboost-dev libboost-test-dev \
libboost-program-options-dev libboost-system-dev \
libboost-filesystem-dev libevent-dev automake libtool \
flex bison pkg-config g++ libssl-dev
Для поддержки Perl необходимо установить модули Bit::Vector
и Class::Accessor
и при конфигурации указать опцию --with-perl
.
./configure --with-perl
Чтобы собралась библиотека для C++, необходимо зайти в lib/cpp
и запустить make
:
cd lib/cpp
make
Если при компилировании возникает подобная ошибка:
lib/cpp/.libs/libthrift.so: undefined reference to `TLSv1_method'
lib/cpp/.libs/libthrift.so: undefined reference to `SSL_connect'
lib/cpp/.libs/libthrift.so: undefined reference to `sk_pop_free'
lib/cpp/.libs/libthrift.so: undefined reference to `BIO_ctrl'
...
Заходим в lib/cpp/test
и вручную добавляем -lssl
:
/bin/bash ../../../libtool --tag=CXX --mode=link g++ -Wall -Wextra -pedantic -g \
-O2 -std=c++11 -L/lib64 -o Benchmark Benchmark.o libtestgencpp.la -lrt \
-lpthread -lssl
Теперь в lib/cpp/.libs
лежат библиотеки libthrift.a
и libthrift.so
.
Установка Perl-модулей:
Заходим в директорию lib/perl
и устанавливаем модуль как обычно через cpanm
:
cpanm .
На данный момент есть скомпилированный Thrift и установлены необходимые Perl-модули. Рассмотрим несколько примеров.
Примеры использования
Простейший ping-pong
Вначале для ознакомления с принципами написания Thrift-обвязок реализуем два Perl-сервиса (клиент и сервер), при котором клиент будет отправлять на сервер метод ping
и ожидать ответа pong
.
Thrift-спецификация может выглядеть следующим образом (файл example1.thrift
):
namespace perl Example1
service Service {
string ping()
}
namespace
необходим для того, чтобы генерировать Perl-пакеты с префиксом Example1
. Далее определяется сервис с методом ping()
, возвращающим строку.
Теперь генерируем Perl-код с помощью Thrift-компилятора:
mkdir -p example1/lib
thrift-0.9.2/compiler/cpp/thrift --gen perl -out example1/lib example1.thrift
Теперь в директории example1/lib
будет примерно следующая структура:
example1/
lib/
Example1/
Constants.pm
Service.pm
Types.pm
Сгенерированный код нас не сильно интересует, в нем находится обработка Thrift-сообщений: упаковка, отправка, прием и т.п. Этот код не меняется и не требует доработки. Нам же необходимо реализовать клиент и сервер.
Пример реализации клиента:
#!/usr/bin/env perl
use strict;
use warnings;
use Thrift;
use Thrift::BinaryProtocol;
use Thrift::Socket;
use Thrift::BufferedTransport;
use Example1::Service;
my $socket = Thrift::Socket->new('localhost', 9090);
my $transport = Thrift::BufferedTransport->new($socket, 1024, 1024);
my $protocol = Thrift::BinaryProtocol->new($transport);
my $client = Example1::ServiceClient->new($protocol);
$transport->open();
eval { print $client->ping(), "\n"; } or do {
my $e = $@;
print "Error: $e->{message}\n";
};
$transport->close();
Модули Thrift::
это внутренние модули Thrift, а Example1::
это модули, которые были сгенерированны ранее. В данном примере для транспорта используется сокет. Thrift поддерживает и другие транспорты, выбор которых зависит от задачи.
В данном коде вызываем метод ping
и печатаем ответ от сервер. В случае ошибки печатаем и ее.
Пример реализации сервера:
#!/usr/bin/perl
use strict;
use warnings;
use Thrift::Socket;
use Thrift::Server;
use Example1::Service;
package ServiceHandler;
use base 'Example1::ServiceIf';
sub new {
my $class = shift;
my $self = {};
bless $self, $class;
return $self;
}
sub ping { 'pong' }
package main;
my $handler = ServiceHandler->new;
my $processor = Example1::ServiceProcessor->new($handler);
my $serversocket = Thrift::ServerSocket->new(9090);
my $forkingserver = Thrift::ForkingServer->new($processor, $serversocket);
print "Starting the server...\n";
$forkingserver->serve();
Код сервера заключен в модуле ServiceHandler
, который наследует сгенерированный интерфейс Example::ServiceIf
. Этот интерфейс необходим для проверки соответствия реализации Thrift-спецификации. Если в ServiceHandler
не будет реализован метод ping
, то при вызове получим подобную ошибку:
implement interface at lib/Example1/Service.pm line 133.
Далее запускаем ./server.pl
и ./client.pl
. Клиент отправит ping
, напечатает ответ и завершится:
pong
Обмен сложными структурами
Thrift поддерживает обмен и сложными структурами. Например, пусть клиент передает хеш, сервер его модифицирует и возвращает. Thrift-файл будет выглядить следующим образом:
namespace perl Example2
struct Hash {
1: string foo = "bar"
}
service Service {
Hash ping(1:Hash arg)
}
В реализации метода ping
в качестве первого аргумента будет объект класса Example2::Hash
. Меняем значение поля foo
и возвращаем клиенту:
#!/usr/bin/perl
use strict;
use warnings;
use Thrift::Socket;
use Thrift::Server;
use Example2::Service;
package ServiceHandler;
use base 'Example2::ServiceIf';
sub new {
my $class = shift;
my $self = {};
bless $self, $class;
return $self;
}
sub ping {
my $self = shift;
my ($arg) = @_;
$arg->foo('baz');
return $arg;
}
package main;
my $handler = ServiceHandler->new;
my $processor = Example2::ServiceProcessor->new($handler);
my $serversocket = Thrift::ServerSocket->new(9090);
my $forkingserver = Thrift::ForkingServer->new($processor, $serversocket);
print "Starting the server...\n";
$forkingserver->serve();
На стороне клиента, во-первых, подключаем модуль Example2::Types
, где определен тип Example2::Hash
, создаем экземпляр структуры и отправляем серверу:
#!/usr/bin/env perl
use strict;
use warnings;
use Thrift;
use Thrift::BinaryProtocol;
use Thrift::Socket;
use Thrift::BufferedTransport;
use Example2::Service;
use Example2::Types;
my $socket = Thrift::Socket->new('localhost', 9090);
my $transport = Thrift::BufferedTransport->new($socket, 1024, 1024);
my $protocol = Thrift::BinaryProtocol->new($transport);
my $client = Example2::ServiceClient->new($protocol);
$transport->open();
eval {
my $in = Example2::Hash->new;
my $out = $client->ping($in);
print $out->foo, "\n";
} or do {
my $e = $@;
print "Error: $e->{message}\n";
};
$transport->close();
Таких аргументов и типов может быть сколь угодно. Подробнее о типах можно почитать в исчерпывающей документации Thrift.
Исключения
Часто сервисы могут бросать исключения при ошибках в методах. Thrift поддерживает обмен исключениями, а также позволяет указывать, какой метод какие исключения может бросить.
Например, в следующем примере метод ping
может бросить системное исключение:
namespace perl Example3
exception SystemError {
1: i32 code,
2: string message
}
service Service {
string ping() throws (1:SystemError err)
}
а реализация метода ping
на серверной стороне может выглядеть следующим образом:
sub ping {
die Example3::SystemError->new(
{
code => 500,
message => 'System error'
}
);
}
А на стороне клиента:
eval { print $client->ping(), "\n"; } or do {
my $e = $@;
print "Error: ", $e->message, "\n";
};
Void и неблокирующие вызовы
Если метод ничего не возвращает, достаточно указать тип возвращаемого значения void
:
service Service {
void restart()
}
Однако, клиент будет ожидать завершения операции restart
. Чтобы вызов был неблокирующим, т.е. сразу же получить ответ и отключиться, указывается ключевое слово oneway
:
service Service {
oneway void restart()
}
Дуплексный RPC
Недостатком клиент-серверной архитектуры является невозможность сервера связаться с клиентом по своей инициативе. Этот недостаток мешает реализации, например, асинхронных уведомлений от сервера. Одним из решений данной проблемы является реализация второго реверсного RPC-канала на стороне сервера. Сервер таким образом сможет отправлять клиенту уведомления в виде неблокирующих сообщений.
В качестве примера рассмотрим простейшую подписку клиентом на уведомления, а сервер при запросе подписки запускает клиента и отправляет периодические уведомления. Здесь мы не будет углубляться в реализацию с помощью forkов, главное показать принцип.
Спецификация:
namespace perl Example4
service Service {
oneway void subscribe()
}
Спецификация сервера уведомлений:
namespace perl Example4Publisher
service Service {
oneway void notify(1:string notification)
}
Реализация клиента:
#!/usr/bin/env perl
use strict;
use warnings;
use Thrift;
use Thrift::BinaryProtocol;
use Thrift::Socket;
use Thrift::BufferedTransport;
use Thrift::Server;
use Example4::Service;
use Example4Publisher::Service;
package ServiceHandler;
use base 'Example4Publisher::ServiceIf';
sub new {
my $class = shift;
my $self = {};
bless $self, $class;
return $self;
}
sub notify {
my $self = shift;
my ($notification) = @_;
print $notification, "\n";
}
package main;
my $socket = Thrift::Socket->new('localhost', 9090);
my $transport = Thrift::BufferedTransport->new($socket, 1024, 1024);
my $protocol = Thrift::BinaryProtocol->new($transport);
my $client = Example4::ServiceClient->new($protocol);
$transport->open();
$client->subscribe();
$transport->close();
my $handler = ServiceHandler->new;
my $processor = Example4Publisher::ServiceProcessor->new($handler);
my $serversocket = Thrift::ServerSocket->new(9091);
my $forkingserver = Thrift::ForkingServer->new($processor, $serversocket);
print "Starting the notification server...\n";
$forkingserver->serve();
Реализация сервера:
#!/usr/bin/perl
use strict;
use warnings;
use Thrift::Socket;
use Thrift::Server;
use Example4::Service;
use Example4Publisher::Service;
package ServiceHandler;
use base 'Example4::ServiceIf';
sub new {
my $class = shift;
my $self = {};
bless $self, $class;
return $self;
}
sub subscribe {
my $socket = Thrift::Socket->new('localhost', 9091);
my $transport = Thrift::BufferedTransport->new($socket, 1024, 1024);
my $protocol = Thrift::BinaryProtocol->new($transport);
my $client = Example4Publisher::ServiceClient->new($protocol);
$transport->open();
while (1) {
$client->notify(time);
sleep 1;
}
$transport->close();
}
package main;
my $handler = ServiceHandler->new;
my $processor = Example4::ServiceProcessor->new($handler);
my $serversocket = Thrift::ServerSocket->new(9090);
my $forkingserver = Thrift::ForkingServer->new($processor, $serversocket);
print "Starting the server...\n";
$forkingserver->serve();
В данном примере используются два порта: 9090 и 9091. По первому осуществляется подписка на уведомления, а по второму собственно они и рассылаются. Для более полной реализации необходимы простейшие менеджер процессов и обзервер, что остается в качестве упражнения читателю :)
← Тестирование черного ящика | Содержание | Fuzzing-тестирование perl-интерпретатора с помощью afl →