Membangun Hash Service dengan Apache Thrift

Muhammad Arslan 28 Desember 2016

Membangun Hash Service dengan Apache Thrift

Apache Thrift adalah salah satu produk yang dikelola oleh Apache Software Foundation (ASF) yang diperuntukkan memudahkan kita membangun sebuah service berbasis remote procedure call. Aslinya Apache Thrift ini dikembangkan oleh Facebook sampai akhirnya masuk inkubasi dan resmi dikelola oleh ASF. Dengan menggunakan Apache Thrift, kita dapat membuat server untuk menangani service dalam suatu bahasa, dan digunakan oleh berbagai bahasa. Misal bila kita membuat sebuah service perkalian dalam bahasa Python, kita dapat membuat client-nya dalam bahasa Python, Java, PHP, atau Node.js. Sehingga kita dapat memiliki berbagai macam service dalam suatu bahasa pemrograman yang spesifik dengan interoperabilitas tinggi.

Kali ini kita akan mencoba membuat sebuah service yang dapat melakukan hash terhadap suatu string dengan algorimat berupa MD5 atau famili SHA. Kita akan membuat server-nya dalam bahasa pemrograman Python, kemudian kita buat client-nya dalam bahasa Python, Java, dan PHP.

1. Instalasi

Silahkan unduh paket Apache Thrift dari website resminya dan ikuti langkah berikut untuk instalasi:
$ tar -xzvf apache-thrift.tar.gz
$ cd apache-thrift
$ ./configure --without-ruby
$ sudo ./make
$ sudo make install
Selain itu, silahkan unduh library pelengkap untuk tutorial ini pada tautan berikut hello-thrift.zip. Bila Thrift berhasil di-install kita periksa dengan cara berikut:
$ thrift -help
Usage: thrift [options] file
Options:
  -version    Print the compiler version
  -o dir      Set the output directory for gen-* packages
               (default: current directory)
  -out dir    Set the ouput location for generated files.
               (no gen-* folder will be created)
  -I dir      Add a directory to the list of directories
                searched for include directives
  -nowarn     Suppress all compiler warnings (BAD!)
  -strict     Strict compiler warnings on
  -v[erbose]  Verbose mode
  -r[ecurse]  Also generate included files
  -debug      Parse debug trace to stdout
  --allow-neg-keys  Allow negative field keys (Used to preserve protocol
                compatibility with older .thrift files)
  --allow-64bit-consts  Do not print warnings about using 64-bit constants
  --gen STR   Generate code with a dynamically-registered generator.
                STR has the form language[:key1=val1[,key2[,key3=val3]]].
                Keys and values are options passed to the generator.
                Many options will not require values.

Options related to audit operation --audit OldFile Old Thrift file to be audited with 'file' -Iold dir Add a directory to the list of directories searched for include directives for old thrift file -Inew dir Add a directory to the list of directories searched for include directives for new thrift file

Available generators (and options): as3 (AS3): bindable: Add [bindable] metadata to all the struct classes. c_glib (C, using GLib): cocoa (Cocoa): log_unexpected: Log every time an unexpected field ID or type is encountered. validate_required: Throws exception if any required field is not set. async_clients: Generate clients which invoke asynchronously via block syntax. cpp (C++): cob_style: Generate "Continuation OBject"-style classes. no_client_completion: Omit calls to completion__() in CobClient class. no_default_operators: Omits generation of default operators ==, != and < templates: Generate templatized reader/writer methods. pure_enums: Generate pure enums instead of wrapper classes. include_prefix: Use full include paths in generated files. moveable_types: Generate move constructors and assignment operators. csharp (C#): async: Adds Async support using Task.Run. asyncctp: Adds Async CTP support using TaskEx.Run. wcf: Adds bindings for WCF to generated classes. serial: Add serialization support to generated classes. nullable: Use nullable types for properties. hashcode: Generate a hashcode and equals implementation for classes. union: Use new union typing, which includes a static read function for union types. d (D): delphi (delphi): ansistr_binary: Use AnsiString for binary datatype (default is TBytes). register_types: Enable TypeRegistry, allows for creation of struct, union and container instances by interface or TypeInfo() constprefix: Name TConstants classes after IDL to reduce ambiguities events: Enable and use processing events in the generated code. xmldoc: Enable XMLDoc comments for Help Insight etc. erl (Erlang): legacynames: Output files retain naming conventions of Thrift 0.9.1 and earlier. maps: Generate maps instead of dicts. otp16: Generate non-namespaced dict and set instead of dict:dict and sets:set. go (Go): package_prefix= Package prefix for generated files. thrift_import= Override thrift package import path (default:git.apache.org/thrift.git/lib/go/thrift) package= Package name (default: inferred from thrift file name) ignore_initialisms Disable automatic spelling correction of initialisms (e.g. "URL") read_write_private Make read/write methods private, default is public Read/Write gv (Graphviz): exceptions: Whether to draw arrows from functions to exception. haxe (Haxe): callbacks Use onError()/onSuccess() callbacks for service methods (like AS3) rtti Enable @:rtti for generated classes and interfaces buildmacro=my.macros.Class.method(args) Add @:build macro calls to generated classes and interfaces hs (Haskell): html (HTML): standalone: Self-contained mode, includes all CSS in the HTML files. Generates no style.css file, but HTML files will be larger. noescape: Do not escape html in doc text. java (Java): beans: Members will be private, and setter methods will return void. private-members: Members will be private, but setter methods will return 'this' like usual. nocamel: Do not use CamelCase field accessors with beans. fullcamel: Convert underscored_accessor_or_service_names to camelCase. android: Generated structures are Parcelable. android_legacy: Do not use java.io.IOException(throwable) (available for Android 2.3 and above). option_type: Wrap optional fields in an Option type. java5: Generate Java 1.5 compliant code (includes android_legacy flag). reuse-objects: Data objects will not be allocated, but existing instances will be used (read and write). sorted_containers: Use TreeSet/TreeMap instead of HashSet/HashMap as a implementation of set/map. generated_annotations=[undated|suppress]: undated: suppress the date at @Generated annotations suppress: suppress @Generated annotations entirely javame (Java ME): js (Javascript): jquery: Generate jQuery compatible code. node: Generate node.js compatible code. ts: Generate TypeScript definition files. json (JSON): merge: Generate output with included files merged lua (Lua): ocaml (OCaml): perl (Perl): php (PHP): inlined: Generate PHP inlined files server: Generate PHP server stubs oop: Generate PHP with object oriented subclasses rest: Generate PHP REST processors nsglobal=NAME: Set global namespace validate: Generate PHP validator methods json: Generate JsonSerializable classes (requires PHP >= 5.4) py (Python): new_style: Generate new-style classes. twisted: Generate Twisted-friendly RPC services. tornado: Generate code for use with Tornado. utf8strings: Encode/decode strings using utf8 in the generated code. coding=CODING: Add file encoding declare in generated file. slots: Generate code using slots for instance members. dynamic: Generate dynamic code, less code generated but slower. dynbase=CLS Derive generated classes from class CLS instead of TBase. dynexc=CLS Derive generated exceptions from CLS instead of TExceptionBase. dynimport='from foo.bar import CLS' Add an import line to generated code to find the dynbase class. rb (Ruby): rubygems: Add a "require 'rubygems'" line to the top of each generated file. namespaced: Generate files in idiomatic namespaced directories. st (Smalltalk): xsd (XSD):

Sekarang mari kita coba buat sebuah folder dengan nama hello-thrift. Kemudian di dalamnya silahkan buat sebuah nama file dengan nama hashservice.thrift. Kemudian kita akan membuat sebuah kode konfigurasi thrift di dalamnya, seperti berikut:

namespace php tutorial
namespace java tutorial

service HashService {
  string ping(),
  string hash(1:string msg, 2:string hasher)
}

Pada konfigurasi diatas, nantinya wrapper HashService untuk PHP akan diberi namespace awalan tutorial, sedangkan untuk Java akan diberi package tutorial juga. Disini kita akan mencoba membuat sebuah class HashService yang memiliki dua metod yaitu ping() dan hash, dimana keduanya melemparkan suatu nilai berupa string.

2. Membangun server untuk service

Selanjutnya kita akan membuat sebuah service untuk hashing pada string yang diberikan oleh client. Algorimat yang akan digunakan adalah MD5 dan varian SHA. Untuk memulainya, kita harus meng-generate class Thrift yang mengacu kepada hashservice.thrift. Dengan mengacu kepada struktur tersebut, kita akan dibuatkan beberapa class yang mengakses library Thrift secara langsung.

Pertama kita lakukan setup berikut:

$ pip install thrift
$ thrift --gen py hashservice.thrift
$ mkdir python
$ mv gen-py python
$ touch client.py
$ touch server.py

Pada beberapa perintah diatas, kita dibuatkan class yang terkait dengan Thrift dan disimpan di dalam folder gen-py. Kemudian kita buat sebuah folder Python untuk menempatkan kode server dan client. Kita pindahkan juga kode hasil generate Thrift ke dalam folder Python. Dan kita buat sebuah file dengan nama client.py dan server.py.

Sedangkan berikut adalah kode server yang ditulis ke dalam file python/server.py:

import sys
sys.path.append('gen-py')

from hashservice import HashService

from thrift.transport import TSocket
from thrift.transport import TTransport
from thrift.protocol import TBinaryProtocol
from thrift.server import TServer

import socket
import logging
import hashlib

class HashServiceHandler:

    def __init__(self):
        self.log = {}

    def ping(self):
        print "ping di dalam server!"

        return "PING!"

    def hash(self, msg, hasher):
        print "Pesan: %s" % (msg)
        print "Hasher: %s" % (hasher)

        encoded = "0"

        if hasher == 'md5':
            h = hashlib.md5()
            h.update(msg)
            encoded = h.hexdigest()

        elif hasher == 'sha1':  
            h = hashlib.sha1()
            h.update(msg)
            encoded = h.hexdigest()

        elif hasher == 'sha224':
            h = hashlib.sha224()
            h.update(msg)
            encoded = h.hexdigest()

        elif hasher == 'sha256':    
            h = hashlib.sha256()
            h.update(msg)
            encoded = h.hexdigest()

        elif hasher == 'sha384':    
            h = hashlib.sha384()
            h.update(msg)
            encoded = h.hexdigest()

        elif hasher == 'sha512':    
            h = hashlib.sha512()
            h.update(msg)
            encoded = h.hexdigest()

        print "Encoded: %s" % encoded

        return encoded

handler = HashServiceHandler()
processor = HashService.Processor(handler)
transport = TSocket.TServerSocket(port=2990)
tfactory = TTransport.TBufferedTransportFactory()
pfactory = TBinaryProtocol.TBinaryProtocolFactory()

server = TServer.TSimpleServer(processor, transport, tfactory, pfactory)
logging.basicConfig(level=logging.DEBUG)

print "Starting thrift server :D"
server.serve()
print "shutting down thrift server :("

Pada kode diatas, kita buat sebuah class handler yang akan menangani dua method yaitu ping() dan hash(). Kemudian kita atur port yang digunakan di 2990 dan kita gunakan TSimpleServer untuk menjalankan server tersebut. Berikut bila kita jalankan melalui konsol:

$ python server.py
Starting thrift server :D

Kode diatas sudah dapat dijalankan, namun kita harus membuat client-nya terlebih dahulu untuk mengakses kedua method tersebut.

3. Membangun client dengan Python

Di dalam folder python kita tempatkan file client.py bersama dengan file server.py dan folder gen-py. Kita gunakan juga class HashService kemudian kita atur port tujuan yaitu 2990 dan kita buat sebuah client dengan menggunakan protokol yang telah diatur. Kita juga gunakan dua method yaitu ping() dan berbagai parameter yang berbeda terhadap method hash():
import sys
sys.path.append('gen-py')

from hashservice import HashService

from thrift import Thrift from thrift.transport import TSocket from thrift.transport import TTransport from thrift.protocol import TBinaryProtocol

try:

transport = TSocket.TSocket("localhost", 2990)
transport = TTransport.TBufferedTransport(transport)
protocol = TBinaryProtocol.TBinaryProtocol(transport)
client = HashService.Client(protocol)

transport.open()

msg = client.ping()
print "Dari server: %s" % (msg)

msg = client.hash('codepolitan12345', 'md5')
print "Dari server: %s" % (msg)

msg = client.hash('codepolitan12345', 'sha1')
print "Dari server: %s" % (msg)

msg = client.hash('codepolitan12345', 'sha224')
print "Dari server: %s" % (msg)

msg = client.hash('codepolitan12345', 'sha256')
print "Dari server: %s" % (msg)

msg = client.hash('codepolitan12345', 'sha384')
print "Dari server: %s" % (msg)

msg = client.hash('codepolitan12345', 'sha512')
print "Dari server: %s" % (msg)

transport.close()

except Thrift.TException, tx: print "%s" % (tx.message)

Bila kita jalankan kode diatas, maka akan tampil output seperti berikut:

$ python client.py 
Dari server: PING!
Dari server: 6f58615495efbd3573642cc658f9e9a6
Dari server: 0d0340c88325322ff06570ebae32388eeeab45ba
Dari server: 617dcfb27ac2269a626d34b08de6a77bed036b4c4699630dae216b52
Dari server: 03de07d331e32ff8697e0f41ef15d1adcfbde607b867577a3cdefd5c8bd17f32
Dari server: 37bc05661b92fcc911e6a8c55ecd4a7008a7c82826993f17f2eb36e56d5079a7429bc858a683a3c204c5f67042d8cf47
Dari server: 1d05e0e1feb72435b332a34220e6fc9851c1dc0f676e2fc3bfa22c1e9c6c581d9f839f3d25371235a7a97131f99830b2da52ee5b7c26db874af4420fa5ee63cd

4. Membangun client dengan PHP

Sekarang mari kita buat client untuk versi PHP. Pertama kita akan menggunakan library Thrift yang disediakan oleh Apache Thrift dan menggunakan kode HashService yang di-generate oleh Thrift. Silahkan ikuti langkah ini terlebih dahulu di dalam folder hello-thrift:
$ thrift --gen php hashservice.thrift
$ mkdir php
$ cp gen-php php
$ cd php
$ mkdir vendor
$ touch client.php
Sekarang silahkan copy folder Thrift ke dalam folder php/vendor. Yang nantinya akan digunakan oleh client.php. Sekarang silahkan buat kode berikut di dalam client.php:
<?php

namespace tutorial\apps;

error_reporting(E_ALL);

$GEN_DIR = realpath(dirname(FILE)).'/gen-php';

require_once DIR.'/vendor/Thrift/ClassLoader/ThriftClassLoader.php';

use Thrift\ClassLoader\ThriftClassLoader;

$loader = new ThriftClassLoader(); $loader->registerNamespace('Thrift', DIR . '/vendor'); $loader->registerDefinition('shared', $GEN_DIR); $loader->registerDefinition('tutorial', $GEN_DIR); $loader->register();

use Thrift\Protocol\TBinaryProtocol; use Thrift\Transport\TSocket; use Thrift\Transport\THttpClient; use Thrift\Transport\TBufferedTransport; use Thrift\Exception\TException;;

try { $socket = new TSocket('localhost', 2990); $transport = new TBufferedTransport($socket, 1024, 1024); $protocol = new TBinaryProtocol($transport); $client = new \tutorial\HashServiceClient($protocol);

$transport-&gt;open();

$msg = $client-&gt;ping();
print "dari server: ".$msg."\n";

$msg = $client-&gt;hash('codepolitan12345', 'md5');
print "dari server: ".$msg."\n";

$msg = $client-&gt;hash('codepolitan12345', 'sha1');
print "dari server: ".$msg."\n";

$msg = $client-&gt;hash('codepolitan12345', 'sha224');
print "dari server: ".$msg."\n";

$msg = $client-&gt;hash('codepolitan12345', 'sha256');
print "dari server: ".$msg."\n";

$msg = $client-&gt;hash('codepolitan12345', 'sha384');
print "dari server: ".$msg."\n";

$msg = $client-&gt;hash('codepolitan12345', 'sha512');
print "dari server: ".$msg."\n";

$transport-&gt;close();

} catch (TException $tx) { print 'TException: '.$tx->getMessage()."\n"; }

Hampir sama dengan kode client yang Python, kita impor terlebih dahulu class Thrift untuk PHP kemudian mengatur namespace untuk folder gen-php kemudian kita buat kode client yang dibungkus oleh try..catch. Bila kita jalankan maka akan muncul output seperti berikut:

$ php client.php
Dari server: PING!
Dari server: 6f58615495efbd3573642cc658f9e9a6
Dari server: 0d0340c88325322ff06570ebae32388eeeab45ba
Dari server: 617dcfb27ac2269a626d34b08de6a77bed036b4c4699630dae216b52
Dari server: 03de07d331e32ff8697e0f41ef15d1adcfbde607b867577a3cdefd5c8bd17f32
Dari server: 37bc05661b92fcc911e6a8c55ecd4a7008a7c82826993f17f2eb36e56d5079a7429bc858a683a3c204c5f67042d8cf47
Dari server: 1d05e0e1feb72435b332a34220e6fc9851c1dc0f676e2fc3bfa22c1e9c6c581d9f839f3d25371235a7a97131f99830b2da52ee5b7c26db874af4420fa5ee63cd

5. Membangun client dengan Java

Terakhir, kita akan membuat client dalam kode Java. Dimana ini adalah salah satu bagian yang paling tricky. Pertama silahkan ikuti command berikut di dalam folder hello-thrift:
$ thrift --gen java hashservice.thrift
$ mkdir java
$ cp gen-java java
$ cd java
$ cd gen-java
$ touch Client.java
Kemudian copy libthrift.jar dan slf4j.jar yang ada di dalam kode pelengkap ke dalam folder java/gen-java bersama dengan file Client.java. Sekarang mari kita buat kodenya terlebih dahulu sebelum di-compile:
import tutorial.*;

import org.apache.thrift.TException; import org.apache.thrift.transport.TSSLTransportFactory; import org.apache.thrift.transport.TTransport; import org.apache.thrift.transport.TSocket; import org.apache.thrift.transport.TSSLTransportFactory.TSSLTransportParameters; import org.apache.thrift.protocol.TBinaryProtocol; import org.apache.thrift.protocol.TProtocol;

public class Client {

public static void main(String [] args) { try { TTransport transport; String msg = "";

    transport = new TSocket("localhost", 2990);
    transport.open();

    TProtocol protocol = new  TBinaryProtocol(transport);
    HashService.Client client = new HashService.Client(protocol);

    msg = client.ping();
    System.out.println("dari server: " + msg);

    msg = client.hash("codepolitan12345", "md5");
    System.out.println("dari server: " + msg);

    msg = client.hash("codepolitan12345", "sha1");
    System.out.println("dari server: " + msg);

    msg = client.hash("codepolitan12345", "sha224");
    System.out.println("dari server: " + msg);

    msg = client.hash("codepolitan12345", "sha256");
    System.out.println("dari server: " + msg);

    msg = client.hash("codepolitan12345", "sha384");
    System.out.println("dari server: " + msg);

    msg = client.hash("codepolitan12345", "sha512");
    System.out.println("dari server: " + msg);

    transport.close();

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

} }

Sekarang mari kita jalankan kode diatas dengan meng-compile-nya di dalam folder java/gen-java:

$ javac -cp 'libthrift.jar:slf4j.jar' tutorial/*.java Client.java
$ java Client
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
dari server: PING!
dari server: 6f58615495efbd3573642cc658f9e9a6
dari server: 0d0340c88325322ff06570ebae32388eeeab45ba
dari server: 617dcfb27ac2269a626d34b08de6a77bed036b4c4699630dae216b52
dari server: 03de07d331e32ff8697e0f41ef15d1adcfbde607b867577a3cdefd5c8bd17f32
dari server: 37bc05661b92fcc911e6a8c55ecd4a7008a7c82826993f17f2eb36e56d5079a7429bc858a683a3c204c5f67042d8cf47
dari server: 1d05e0e1feb72435b332a34220e6fc9851c1dc0f676e2fc3bfa22c1e9c6c581d9f839f3d25371235a7a97131f99830b2da52ee5b7c26db874af4420fa5ee63cd

6. Penutup

Menarik bukan? Hanya dengan membuat konfigurasi melalui Thrift, kamu dapat membangun sebuah service dengan bahasa pemrograman yang kamu suka dan dengan menggunakan struktur dan protokol yang sama, dapat juga membuat client untuk bahasa pemrograman lain. Selain menggunakan teknik diatas, kamu juga dapat mengirimkan request berupa JSON ataupun file binary. Kamu dapat mengeksplorasi lebih lanjut melalui dokumentasi Apache Thrift.

7. Referensi

  • Apache Thrift Official Documentation
  • Apache Thrift Official Example
(arslan/thrift)