Выпуск 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
Нас уже 1393. Больше подписчиков — лучше выпуски!

Комментарии к статье