Apache ThriftでRPCを試す

Apache ThriftでRPCを試したメモです。

Apache ThriftでRPCを試してみました。

Apache Thrift

Apache ThriftはRPC(リモートプロシージャコール)のフレームワークです。
facebookが開発し、その後Apache Projectに移されたようです。
Apache Thrift - Home

ThriftではIDL(インタフェース記述言語)でインターフェースを定義して、
各言語ごとにソースコードを生成して実装していきます。

サポートされている言語は現時点で25以上あります。
Apache Thrift - Language and Feature Matrix

MacでThrift 0.10.0で試してみます。
言語はjavaperlで試してみます。

Install

下記にMacでのインストール手順が載っています。
Apache Thrift - Index of install/
Apache Thrift - OS X Install

事前にboostとlibeventをインストールした後に、各言語(今回はjavaperl)で必要なライブラリもインストールします。
そしてThriftをインストールします。

boost

boostをインストールします。
Boost Downloads

$ curl -L -O https://dl.bintray.com/boostorg/release/1.65.1/source/boost_1_65_1.tar.gz
$ tar xvzf boost_1_65_1.tar.gz
$ cd boost_1_65_1
$ ./bootstrap.sh
$ sudo ./b2 threading=multi address-model=64 variant=release stage install
libevent

libeventをインストールします。
libevent

$ curl -L -O https://github.com/libevent/libevent/releases/download/release-2.1.8-stable/libevent-2.1.8-stable.tar.gz
$ tar xvzf libevent-2.1.8-stable.tar.gz
$ cd libevent-2.1.8-stable
$ ./configure --prefix=/usr/local 
$ make
  GEN      test/rpcgen-attempted
  GEN      include/event2/event-config.h
/Applications/Xcode.app/Contents/Developer/usr/bin/make  all-am
  CC       buffer.lo
  CC       bufferevent.lo
  CC       bufferevent_filter.lo
  CC       bufferevent_pair.lo
  CC       bufferevent_ratelim.lo
  CC       bufferevent_sock.lo
  CC       event.lo
  CC       evmap.lo
  CC       evthread.lo
  CC       evutil.lo
  CC       evutil_rand.lo
  CC       evutil_time.lo
  CC       listener.lo
  CC       log.lo
  CC       select.lo
  CC       poll.lo
  CC       kqueue.lo
  CC       signal.lo
  CC       evdns.lo
  CC       event_tagging.lo
  CC       evrpc.lo
  CC       http.lo
  CCLD     libevent.la
  CCLD     libevent_core.la
  CCLD     libevent_extra.la
  CC       evthread_pthread.lo
  CCLD     libevent_pthreads.la
  CC       libevent_openssl_la-bufferevent_openssl.lo
bufferevent_openssl.c:66:10: fatal error: 'openssl/bio.h' file not found
#include <openssl/bio.h>
         ^~~~~~~~~~~~~~~

1 error generated.
make[1]: *** [libevent_openssl_la-bufferevent_openssl.lo] Error 1
make: *** [all] Error 2

makeでエラーが出てしまいました。
どうやらopensslのバージョンが古いらしい。
opensslをアップデート。

$ openssl version
OpenSSL 0.9.8zh 14 Jan 2016
$ brew install openssl
$ brew link openssl --force
Warning: Refusing to link: openssl
Linking keg-only openssl means you may end up linking against the insecure,
deprecated system OpenSSL while using the headers from Homebrew's openssl.
Instead, pass the full include/library paths to your compiler e.g.:
  -I/usr/local/opt/openssl/include -L/usr/local/opt/openssl/lib

またエラーが出た。
opensslはlink --force出来なくなったようです。

ということでpathに設定。

$ vi .zshrc
# 下記を追記
export export PATH=/usr/local/Cellar/openssl/1.0.2l/bin:$PATH
$ source .zshrc
$ openssl version
OpenSSL 1.0.2l  25 May 2017

無事opensslをバージョンアップできた。

configureにLDFLAGSとCPPFLAGSを指定して再度ビルド。
https://stackoverflow.com/questions/33165174/fatal-error-openssl-bio-h-file-not-found

$ ./configure --prefix=/usr/local LDFLAGS='-L/usr/local/Cellar/openssl/1.0.2l/lib' CPPFLAGS='-I/usr/local/Cellar/openssl/1.0.2l/include'
$ make
$ sudo make install

インストール出来ました。

java requirements

https://thrift.apache.org/docs/install/#language-requirements

Language requirementsを確認すると、
javaで必要なのはjava1.7以上とApache antです。
javaは1.8が入っているので、antだけインストールします。

$ curl -L -O http://ftp.yz.yamagata-u.ac.jp/pub/network/apache//ant/binaries/apache-ant-1.10.1-bin.tar.gz\n
$ tar xvzf apache-ant-1.10.1-bin.tar.gz
$ sudo mv apache-ant-1.10.1 /usr/local
$ cd
$ vi .zshrc
# 下記を追記
export ANT_HOME=/usr/local/apache-ant-1.10.1
export PATH=$PATH:$ANT_HOME/bin
$ sourcr .zshrc
$ which ant
/usr/local/apache-ant-1.10.1/bin/ant
perl requirements

https://thrift.apache.org/docs/install/#language-requirements

Language requirementsを確認すると、
perlで必要なのはBit::VectorとClass::Accessorです。

ライブラリが存在するか確認。
(何も表示されなければ入っている)

$ perl -MClass::Accessor -e ''

Can't locate Class/Accessor.pm in @INC (you may need to install the Class::Accessor module) (@INC contains: /usr/local/Cellar/perl/5.26.0/lib/perl5/site_perl/5.26.0/darwin-thread-multi-2level /usr/local/Cellar/perl/5.26.0/lib/perl5/site_perl/5.26.0 /usr/local/Cellar/perl/5.26.0/lib/perl5/5.26.0/darwin-thread-multi-2level /usr/local/Cellar/perl/5.26.0/lib/perl5/5.26.0 /usr/local/lib/perl5/site_perl/5.26.0).
BEGIN failed--compilation aborted.

$ perl -MBit::Vector -e ''
Can't locate Bit/Vector.pm in @INC (you may need to install the Bit::Vector module) (@INC contains: /usr/local/Cellar/perl/5.26.0/lib/perl5/site_perl/5.26.0/darwin-thread-multi-2level /usr/local/Cellar/perl/5.26.0/lib/perl5/site_perl/5.26.0 /usr/local/Cellar/perl/5.26.0/lib/perl5/5.26.0/darwin-thread-multi-2level /usr/local/Cellar/perl/5.26.0/lib/perl5/5.26.0 /usr/local/lib/perl5/site_perl/5.26.0).
BEGIN failed--compilation aborted.

どちらも入っていないので、cpanでインストール。

$ sudo perl -MCPAN -e shell
cpan[1]> install Class::Accessor
cpan[2]> install Bit::Vector

$ perl -MClass::Accessor -e ''
$ perl -MBit::Vector -e ''
Thrift

事前準備が終わったので、やっとThriftをインストールします。

$ curl -L -O http://ftp.riken.jp/net/apache/thrift/0.10.0/thrift-0.10.0.tar.gz
$ tar -xvzf thrift-0.10.0.tar.gz
$ cd thrift-0.10.0/
$ ./configure --prefix=/usr/local/ --with-boost=/usr/local --with-libevent=/usr/local

するとbisonのversionのチェックでエラーになりました。

checking whether make sets $(MAKE)... (cached) yes
checking for bison... yes
checking for bison version >= 2.5... no
configure: error: Bison version 2.5 or higher must be installed on the system!

bisonをアップデートします。

$ bison -V
bison (GNU Bison) 2.3
$ brew install bison
$ brew link bison --force
$ bison -V
bison (GNU Bison) 3.0.4

再度ビルドします。

$ ./configure --prefix=/usr/local/ --with-boost=/usr/local --with-libevent=/usr/local
$ make
$ sudo make install
$ thrift -version
Thrift version 0.10.0

インストール出来ました。

※補足
makeで下記の様なエラーが出る場合は、またopensslの問題なので、LDFLAGSとCPPFLAGSを指定するとうまくいくと思います。

libtool: compile:  g++ -DHAVE_CONFIG_H -I. -I../.. -I../../lib/cpp/src/thrift -I../../lib/c_glib/src/thrift -I/usr/local/include -I/usr/local/include -I./src -D__STDC_LIMIT_MACROS -Wall -Wextra -pedantic -g -O2 -std=c++11 -MT src/thrift/transport/TSSLSocket.lo -MD -MP -MF src/thrift/transport/.deps/TSSLSocket.Tpo -c src/thrift/transport/TSSLSocket.cpp  -fno-common -DPIC -o src/thrift/transport/.libs/TSSLSocket.o
src/thrift/transport/TSSLSocket.cpp:41:10: fatal error: 'openssl/err.h' file not found
#include <openssl/err.h>
         ^~~~~~~~~~~~~~~
$ make LDFLAGS='-L/usr/local/Cellar/openssl/1.0.2l/lib' CPPFLAGS='-I/usr/local/Cellar/openssl/1.0.2l/include'

Thrift

IDL(interface description language)

ThriftのIDLファイルを作成してインターフェースを定義する必要があります。
下記に仕様が載っています。

Apache Thrift - Interface Description Language (IDL)
Apache Thrift - Thrift Type system

MyService.thriftというファイルを作成して定義してみます。

MyService.thrift

// (1)
namespace java com.example.thrift
namespace perl exampleThrift

// (3)
enum Country {
    AMERICA=0,
    JAPAN=1,
    CANADA=2
}

// (2)
struct Person {
    1: required string name;
    2: required i32 age;
    3: required Country country;
    4: string hobby;
}

// (4)
service PeopleService {
    list<Person> searchByName(1:string query)
}

(1)
namespaceを定義します。これは言語ごとに設定する必要があります。
それぞれjavaperlのパッケージ名になります。

(2)
structでPersonの構造体を定義します。
name, age, country, hobbyのそれぞれのフィールドを型を指定して定義。

(3)
enumで列挙型を定義します。
Countryを定義し、Personのcountryフィールドで指定しています。

(4)
serviceでインターフェースを定義します。
関数名、引数、戻り値を指定します。
string queryを引数に取り、Personのlistを返すsearchByNameを定義します。

Thriftコンパイル

Thriftコンパイルを使用して作成したIDLファイルから各言語のソースを生成します。
今回はjavaperlで生成します。

生成方法はthriftコマンドの後に--genオプションで言語を指定し、thriftファイルを指定します。

thrift --gen <language> <Thrift filename>

javaのソースを生成。
gen-javaというディレクトリが作成され、ソースコードが生成されています。

$ thrift --gen java MyService.thrift
$ ll gen-java/com/example/thrift/
total 136
drwxr-xr-x  5 xxxxxx  staff   170B 12 11 20:42 .
drwxr-xr-x  3 xxxxxx  staff   102B 12 11 20:42 ..
-rw-r--r--  1 xxxxxx  staff   938B 12 11 20:42 Country.java
-rw-r--r--  1 xxxxxx  staff    37K 12 11 20:42 PeopleService.java
-rw-r--r--  1 xxxxxx  staff    21K 12 11 20:42 Person.java

perlのソースを生成。
gen-perlというディレクトリが作成され、ソースコードが生成されています。

$ thrift --gen perl MyService.thrift
$ ll gen-perl/exampleThrift
total 32
drwxr-xr-x  5 xxxxxx  staff   170B 12 12 19:45 .
drwxr-xr-x  3 xxxxxx  staff   102B 12 12 19:45 ..
-rw-r--r--  1 xxxxxx  staff   209B 12 12 19:45 Constants.pm
-rw-r--r--  1 xxxxxx  staff   7.1K 12 12 19:45 PeopleService.pm
-rw-r--r--  1 xxxxxx  staff   3.1K 12 12 19:45 Types.pm

下記のようにいっぺんに生成も出来ます。

$ thrift --gen java --gen perl MyService.thrift

生成されたコードから各言語でサーバとクライアントを実装していきます。
各言語でのサンプルは下記に載っています。
https://thrift.apache.org/tutorial/#examples-clients-and-servers

Thrift Server(Java)

javaでサーバを作成してみます。

新規のMavenプロジェクトを作成し、pom.xmlにlibthriftを追加します。

pom.xml

<dependency>
    <groupId>org.apache.thrift</groupId>
    <artifactId>libthrift</artifactId>
    <version>0.10.0</version>
</dependency>

Thriftコンパイルで生成したコードを配置します。
生成されたPeopleService.javaの中に、下記のようにIfaceインターフェースが定義されているので、実装します。

PeopleService.java

public class PeopleService {

  public interface Iface {

    public java.util.List<Person> searchByName(java.lang.String query) throws org.apache.thrift.TException;

  }
      :
      :
}

Server.javaを作成し、実装してみます。

Server.java

    public static void main(String[] args) {
        try {
            Processor<PeopleService.Iface> processor = new Processor<>(new PeopleService.Iface() {
                @Override
                public List<Person> searchByName(String query) {

                    System.out.println("query: " + query);

                    // dummy response.(emulate search by query from DB)
                    Person alice = new Person("Alice Wall", 33, Country.findByValue(1), "trick");
                    Person bobby = new Person("Bobby Wall", 29, Country.findByValue(0), "yo-yo");
                    List<Person> people = Arrays.asList(alice, bobby);

                    people.forEach(System.out::println);

                    return people;
                }
            }); // (1)

            TServerTransport serverTransport = new TServerSocket(8080); // (2)

            TServer server = new TSimpleServer(new Args(serverTransport).processor(processor)); // (3)

            System.out.println("Starting the server... ");

            new Thread(server::serve).start(); // (4)

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

(1)
PeopleService.Processorインスタンスを生成します。

searchByNameで本来は引数のクエリを元にDBから検索することを想定しています。
ダミーのデータを返すようにします。

lambdaで書けばもっと短く書けます。

    Processor<PeopleService.Iface> processor = new Processor<>(query -> {

        Person alice = new Person("Alice", 33, Country.findByValue(1), "trick");
        Person bobby = new Person("Bobby", 29, Country.findByValue(0), "yo-yo");

        return Arrays.asList(alice, bobby);
    });

(2)
サーバポート指定

(3)
TSimpleServerはシングルスレッドのサーバなので、
マルチスレッドにする場合は下記のようにTThreadPoolServerを使用します。

TServer server = new TThreadPoolServer(new TThreadPoolServer.Args(serverTransport).processor(processor));

(4)
TServer.serve()でサーバ起動します。

実行してサーバ起動。

Starting the server... 
Thrift Client(Perl)

perlでクライアントを作成してみます。

まず、thriftのtar.gzのインストールパッケージの中にperl用のThriftライブラリが入っているので、
適当な場所にコピーします。

$ mkdir perl_client
$ cd perl_client
$ cp -pR ~/xxx/thrift-0.10.0/lib/perl/lib .

クライアントはPerlClient.plというファイルで作成してみます。

PerlClient.pl

#!/usr/local/bin/perl

use strict;
use warnings;

#(1)
use lib './lib';
use lib '../gen-perl';

use Thrift;
use Thrift::BinaryProtocol;
use Thrift::Socket;
use Thrift::BufferedTransport;

use exampleThrift::Types;
use exampleThrift::PeopleService;

use Data::Dumper;

my $socket    = new Thrift::Socket('localhost',8080); #(2)
my $transport = new Thrift::BufferedTransport($socket,1024,1024);
my $protocol  = new Thrift::BinaryProtocol($transport);
my $client    = new exampleThrift::PeopleServiceClient($protocol); #(3)

eval{
    $transport->open();
    
    my $response = $client->searchByName("Wall"); #(4)
    print "response: " . Dumper($response);

    $transport->close();

}; if($@){
    warn(Dumper($@));
}

(1)
コピーしたThriftのlibと、生成したgen-perlへライブラリのパスを通します。

(2)(3)
サーバのポートを指定し、PeopleServiceClientを作成。

(4)
searchByName()を実行。
ローカルのメソッドのようにsearchByName()を呼べます。
結果を標準出力にダンプしてみます。

実行してみます。

クライアント側の標準出力

$ perl PerlClient.pl
response: $VAR1 = [
          bless( {
                   'name' => 'Alice Wall',
                   'age' => 33,
                   'hobby' => 'trick',
                   'country' => 1
                 }, 'exampleThrift::Person' ),
          bless( {
                   'country' => 0,
                   'hobby' => 'yo-yo',
                   'name' => 'Bobby Wall',
                   'age' => 29
                 }, 'exampleThrift::Person' )
        ];

サーバ側の標準出力

query: Wall
Person(name:Alice Wall, age:33, country:JAPAN, hobby:trick)
Person(name:Bobby Wall, age:29, country:AMERICA, hobby:yo-yo)

うまくいきました。

Thrift Server(Perl)

今度は逆でServer(Perl) と Client(Java)で試してみます。

Perlでサーバを実装してみます。

PerlServer.plという名前で作成します。

PerlServer.pl

#!/usr/local/bin/perl

use strict;
use warnings;

# (1)
use lib '../gen-perl';
use lib './lib';

use Thrift::Socket;
use Thrift::Server;
use Thrift::ServerSocket;

use exampleThrift::Types;
use exampleThrift::PeopleService;

package PeopleServiceHandler;
use base qw(exampleThrift::PeopleServiceIf); # (2)

use Data::Dumper;

sub new {
    my $classname = shift;
    my $self      = {};

    return bless($self,$classname);
}

sub searchByName{
  my $self = shift;
  my $query = shift;

  print "query: $query \n";

  # (3)
  # dummy response.(emulate search by query from DB)
  my $alice = new exampleThrift::Person({name => 'Alice Wall', age => 33, country => exampleThrift::Country::JAPAN, hobby => 'trick'});
  my $bobby = new exampleThrift::Person({name => 'Bobby Wall', age => 29, country => exampleThrift::Country::AMERICA, hobby => 'yo-yo'});

  my @people;
  push @people, $alice;
  push @people, $bobby;
  print Dumper(\@people);

  return \@people;
}

eval {
  my $handler       = new PeopleServiceHandler;
  my $processor     = new exampleThrift::PeopleServiceProcessor($handler);
  my $serversocket  = new Thrift::ServerSocket(8080);
  my $forkingserver = new Thrift::SimpleServer($processor, $serversocket); # (4)

  print "Starting the server... \n";
  $forkingserver->serve(); # (5)
  print "done.\n";

}; if ($@) {
  if ($@ =~ m/TException/ and exists $@->{message}) {
    my $message = $@->{message};
    my $code    = $@->{code};
    my $out     = $code . ':' . $message;
    die $out;
  } else {
    die $@;
  }
}

(1)
Clientの時と同じようにlibを設定します。

(2)
Thriftコンパイルで生成されたPeopleService.pmに定義してあるexampleThrift::PeopleServiceIfを実装します。

(3)
ここも本来DBから検索するのを想定してますが、ダミーでレスポンスを返してます。

(4)
Thrift::SimpleServerはシンブルプロセスのサーバです。
マルチプロセスにするには下記の様にThrift::ForkingServerを使用します。

  my $forkingserver = new Thrift::ForkingServer($processor, $serversocket);

(5)
Thrift::SimpleServer->serve()でサーバ起動。

実行して起動してみます。

$ perl PerlServer.pl
Starting the server...
Thrift Client(Java)

Javaでクライアントを実装してみます。

public class Client {
    public static void main(String [] args) {

        try {
            TTransport transport = new TSocket("localhost", 8080); // (1)
            transport.open();

            TProtocol protocol = new TBinaryProtocol(transport);
            PeopleService.Client client = new PeopleService.Client(protocol); // (2)

            perform(client);

            transport.close();
        } catch (TException x) {
            x.printStackTrace();
        }
    }

    private static void perform(PeopleService.Client client) throws TException
    {
        List<Person> people = client.searchByName("Wall"); // (3)
        people.forEach(System.out::println);
    }
}

(1)
サーバ側のポートを指定

(2)
Thriftコンパイルで生成されたPeopleService.Clientを生成。

(3)
searchByNameを実行し、結果を標準出力にダンプ。
ローカルのメソッドのようにsearchByName()を呼べます。

クライアント側の標準出力

Person(name:Alice Wall, age:33, country:JAPAN, hobby:trick)
Person(name:Bobby Wall, age:29, country:AMERICA, hobby:yo-yo)

サーバ側の標準出力

query: Wall
$VAR1 = [
          bless( {
                   'hobby' => 'trick',
                   'country' => 1,
                   'name' => 'Alice Wall',
                   'age' => 33
                 }, 'exampleThrift::Person' ),
          bless( {
                   'hobby' => 'yo-yo',
                   'country' => 0,
                   'name' => 'Bobby Wall',
                   'age' => 29
                 }, 'exampleThrift::Person' )
        ];

うまくいきました。

終わり。


ソースは下記にあげました。

java
github.com

perl
github.com