One of the important aspects of building micro-services towards implementing Service Oriented Architecture (SOA) is to design how these services communicate. HTTP is a common choice, you can just create RESTful endpoints for your service with JSON as the serialization. The advantage of this is that most of are already familiar to most developers so they’re very trivial to implement.

However there are times when HTTP just isn’t enough, when you don’t need the extra overhead of HTTP or a more compact serialization than JSON. And that’s where Thrift comes in, its basically an RPC server with built-in binary serialization. So your services communicates via RPC with the messages in binary form which has the advantage of faster and smaller payload. And not only that, your services can be written in any language supported by Thrift and still communicate between each other.

Pheuw! now that’s out of the way, lets look at how to use it. First we need to create a thrift file which defines our services, types, etc. We’re going to create a sample phonebook service so lets create the following file:

// contact.thrift

struct Contact {
    1: required string id,
    2: required string name,
    3: required string phone,
    4: optional string email,
    5: required string created  // RFC3339
}

service ContactSvc {
    Contact create(1:Contact contact),
    Contact read(1:string contactId),
    Contact update(1:Contact contact),
    void destroy(1:string contactId),
    list fetch(),
    void reset()
}

This is Thrift’s Interface Definition Language (IDL), we’ve defined a struct named Contact with 5 fields in it. Those numbers before each fields are called identifiers, they must be present and unique for each field. Followed by the field’s requiredness, a field can either be required, default, optional or undefined (no requiredness, just blank). Its rules is based on the following table:

Field Write behavior Read behavior
required always written must read or error
default always written read if present
optional written if set read if present
undefined undefined ignored

Next we have the type, Thrift supports the following common types like bool, byte, double, string and integer but also special types like binary. It also supports structs and containers like (list, set, map) that correspond to commonly available interfaces in most programming languages. Then finally, the field name.

Then defined ContactSvc service, this service is what the RPC server will actually serve. And as you can it has a bunch of methods we can call against it. The definition is pretty straight forward and that number before each param is again an identifier just like on the struct. Based on this thrift file we can now generate stub code on ang launage that thrift supports, we can then use this generated stub code in our own application to serve the service and as a client that talks to that service.

Lets generate for two languages:

// Go: for the server
thrift -gen go:thrift_import=git-wip-us.apache.org/repos/asf/thrift.git/lib/go/thrift contact.thrift

// Python: for the client
thrift -gen py contact.thrift

Notice that the one for Go is a bit lengthy since that go lib import path changed and the one being used on the generated stub code is outdated so we need to do this so it generated with the updated one. By now you should have the two genetated folders gen-go and gen-py, move the folder gen-go/contact to a new folder called services/go and the same for gen-py/contact to services/py this way we don’t overwrite then when we generate again plus I like this new structure.

Then we need to define our handler which is where the client request to the server will be proxied to.

// handler.go

package phonebook

import (
    "fmt"
    "sync"

    "github.com/marconi/phonebook/services/go/contact"
)

type ContactHandler struct {
    contacts map[string]*contact.Contact
    sync.RWMutex
}

func NewContactHandler() *ContactHandler {
    return &ContactHandler{
        contacts: make(map[string]*contact.Contact),
    }
}

func (ch *ContactHandler) Create(contact *contact.Contact) (*contact.Contact, error) {
    ch.Lock()
    defer ch.Unlock()
    ch.contacts[contact.Id] = contact
    return contact, nil
}

func (ch *ContactHandler) Read(contactId string) (*contact.Contact, error) {
    contact, ok := ch.contacts[contactId]
    if !ok {
        return nil, fmt.Errorf("Contact with ID '%s' does not exist", contactId)
    }
    return contact, nil
}

func (ch *ContactHandler) Update(contact *contact.Contact) (*contact.Contact, error) {
    ch.Lock()
    defer ch.Unlock()
    ch.contacts[contact.Id] = contact
    return contact, nil
}

func (ch *ContactHandler) Destroy(contactId string) error {
    if _, ok := ch.contacts[contactId]; ok {
        ch.Lock()
        defer ch.Unlock()
        delete(ch.contacts, contactId)
    }
    return nil
}

func (ch *ContactHandler) Fetch() ([]*contact.Contact, error) {
    var contacts []*contact.Contact
    for _, contact := range ch.contacts {
        contacts = append(contacts, contact)
    }
    return contacts, nil
}

func (ch *ContactHandler) Reset() error {
    ch.Lock()
    defer ch.Unlock()
    ch.contacts = make(map[string]*contact.Contact)
    return nil
}

That’s a bit lengthy but those pretty basic Go stuff, we have a struct called ContactHandler which contains a mapping from contact id to a Contact instance. Also has a mutex so we can lock the map whenever one of its method changes it. One thing to notice is that we’re using the Contact struct generated by thrift, this is very important so that our handler conforms to the service we defined earlier.

Then we need a server that will serve this handler:

// server.go

package phonebook

import (
    "fmt"

    "git-wip-us.apache.org/repos/asf/thrift.git/lib/go/thrift"
    "github.com/marconi/phonebook/services/go/contact"
)

type PhonebookServer struct {
    host             string
    handler          *ContactHandler
    processor        *contact.ContactSvcProcessor
    transport        *thrift.TServerSocket
    transportFactory thrift.TTransportFactory
    protocolFactory  *thrift.TBinaryProtocolFactory
    server           *thrift.TSimpleServer
}

func NewPhonebookServer(host string) *PhonebookServer {
    handler := NewContactHandler()
    processor := contact.NewContactSvcProcessor(handler)
    transport, err := thrift.NewTServerSocket(host)
    if err != nil {
        panic(err)
    }

    transportFactory := thrift.NewTFramedTransportFactory(thrift.NewTTransportFactory())
    protocolFactory := thrift.NewTBinaryProtocolFactoryDefault()
    server := thrift.NewTSimpleServer4(processor, transport, transportFactory, protocolFactory)
    return &PhonebookServer{
        host:             host,
        handler:          handler,
        processor:        processor,
        transport:        transport,
        transportFactory: transportFactory,
        protocolFactory:  protocolFactory,
        server:           server,
    }
}

func (ps *PhonebookServer) Run() {
    fmt.Printf("server listening on %s\n", ps.host)
    ps.server.Serve()
}

func (ps *PhonebookServer) Stop() {
    fmt.Println("stopping server...")
    ps.server.Stop()
}

The interesting bits is how Thrift’s RPC server is built, first we started off by creating a processor which just wraps our handler then a raw socket. Next a framed transport factory, Thrift supports different types of transport factory and this one in particular is framed. If you’ve ever done message framing using TCP before this works exactly like that. Then followed by a binary protocol factory, Thirft supports another binary one called compact, its basically just binary with compression so you can trade a bit of cpu for network latency. Finally, all those 4 builds up the RPC server.

Lets create a simple program that will run this server:

// main.go

package main

import "github.com/marconi/phonebook"

func main() {
    host := "localhost:9090"
    server := phonebook.NewPhonebookServer(host)
    server.Run()
}

Now what remains is a client that will call our service:

# client.py

import sys
import uuid
from datetime import datetime
sys.path.append('services/py')

from thrift.transport import TSocket, TTransport
from thrift.protocol import TBinaryProtocol
from contact import ContactSvc, ttypes

socket = TSocket.TSocket('localhost', 9090)
transport = TTransport.TFramedTransport(socket)
protocol = TBinaryProtocol.TBinaryProtocol(transport)
client = ContactSvc.Client(protocol)

transport.open()

c1 = ttypes.Contact(uuid.uuid4().hex, 'Bob', '111-1111', 'bob@wonderland.com', datetime.now().isoformat())
c1 = client.create(c1)

contacts = client.fetch()
print contacts

First we make sure we can import the generated python stub, then we build a client by importing the same components that builds up our server. Its imperative that they match otherwise you’ll run into problems when the server can understand the request and the client can’t with the response. Then we open the transport, and call the client’s create method which is the same create method we defined on our handler. Our c1 contact will then be serialized by thrift via the generated stub code and forwarded to the server on that binary protocol. The server will then decode it back thanks to the matching protocol of the server and client, then proxies the request to the processor and down to the handler. The response goes through the same path and when it reaches the client its as if we just called a local function.

Lets go ahead and run both the server and client:

$ go run main.go
server listening on localhost:9090

then the client:

$ python client.py
[Contact(phone='111-1111', email='bob@wonderland.com', created='2014-11-25T08:04:25.128271', id='a765369730c447d79cfb16ecf23206e2', name='Bob')]

So even though our server was in Go and the client was in Python, they were still able to communicate like as if the client was just calling a local method. Note that even though the data goes through a lot of layers, these all happens very fast so you get optimum performance that you simply can’t with HTTP + JSON. In case you want to play with it, you can download all the code here.