Apache thrift is an open source, cross-language serialization and remote procedure call RPC framework. I tried to learn it from its official website, but it is pretty hard to follow for new learners. Then I found this book Programmer’s Guide to Apache Thrift.
In this article, I will try to share what I learned from this book with two hello world examples:
- RPC with TCP in Node.js
- RPC with HTTP in Node.js server and JavaScript in html
Install Thrift
The first step is to install thrift compiler so we can use it to compile IDLs. There are multiple ways to do it.
First we can build it from source. The steps are as follows:
- prepare environment: thrift.apache.org/docs/install/debian.html
- download source: thrift.apache.org/download
- build from source: thrift.apache.org/docs/BuildingFromSource
- test: github.com/apache/thrift
The second way is to install directly from prebuilt binary. So for ubuntu, just use below command:
apt-get update
apt-cache search thrift
apt-get install -y thrift-compiler
thrift --version
The third way is to use prepared docker images. Just search from docket hub, then pull and run.
RPC with TCP in Node.js
First, prepare node project environment and install node.js thrift package.
npm init -y
npm install thrift
Then prepare thrift definition IDLs file hello.thrift
.
service helloSvc {
string getMessage(1: string name)
}
As you can see there, we only define a simple service with a method. This is only an interface. Later we will implement this method on server side and call this method from client side.
Then let's use thrift compiler to generate code from the IDLs file.
thrift -gen js:node hello.thrift
Then we should have a file structure as below. The generated code is stored in the gen-nodejs
folder.
% tree -I node_modules
.
├── gen-nodejs
│ ├── helloSvc.js
│ └── hello_types.js
├── hello.thrift
├── package-lock.json
└── package.json
1 directory, 5 files
The first file is helloSvc.js
. It contains the logic for thrift client and server. It is pretty long, so I remove the function body then we can see the structure clearer.
//
// Autogenerated by Thrift Compiler (0.17.0)
//
// DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
//
"use strict";
var thrift = require('thrift');
var Thrift = thrift.Thrift;
var Q = thrift.Q;
var Int64 = require('node-int64');
var ttypes = require('./hello_types');
//HELPER FUNCTIONS AND STRUCTURES
var helloSvc_getMessage_args = function(args) {
// ...
};
helloSvc_getMessage_args.prototype = {};
helloSvc_getMessage_args.prototype.read = function(input) {
// ...
};
helloSvc_getMessage_args.prototype.write = function(output) {
// ...
};
var helloSvc_getMessage_result = function(args) {
// ...
};
helloSvc_getMessage_result.prototype = {};
helloSvc_getMessage_result.prototype.read = function(input) {
// ...
};
helloSvc_getMessage_result.prototype.write = function(output) {
// ...
};
var helloSvcClient = exports.Client = function(output, pClass) {
this.output = output;
this.pClass = pClass;
this._seqid = 0;
this._reqs = {};
};
helloSvcClient.prototype = {};
helloSvcClient.prototype.seqid = function() { return this._seqid; };
helloSvcClient.prototype.new_seqid = function() { return this._seqid += 1; };
helloSvcClient.prototype.getMessage = function(name, callback) {
// ...
};
helloSvcClient.prototype.send_getMessage = function(name) {
// ...
};
helloSvcClient.prototype.recv_getMessage = function(input,mtype,rseqid) {
// ...
};
var helloSvcProcessor = exports.Processor = function(handler) {
this._handler = handler;
};
helloSvcProcessor.prototype.process = function(input, output) {
// ...
};
helloSvcProcessor.prototype.process_getMessage = function(seqid, input, output) {
// ...
};
The second file is hello_types.js
, which should contains the user defined types. It is empty now, because we didn's define any user types.
//
// Autogenerated by Thrift Compiler (0.17.0)
//
// DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
//
"use strict";
var thrift = require('thrift');
var Thrift = thrift.Thrift;
var Q = thrift.Q;
var Int64 = require('node-int64');
var ttypes = module.exports = {};
Now is the time to write server code helloServer.js
.
var thrift = require('thrift');
var helloSvc = require('./gen-nodejs/helloSvc.js');
// here we implement the interface
var helloHandler = {
getMessage: function (name, result) {
console.log("Received: " + name);
result(null, "Hello " + name);
}
};
// here we define server configs
var serverOpt = {
// we can choose TBinaryProtocol, TCompactProtocol, TJSONProtocol
protocol: thrift.TBinaryProtocol,
transport: thrift.TBufferedTransport
}
var port = 8585;
// createServer will start a TCP server
thrift.createServer(helloSvc, helloHandler, serverOpt)
.on('error', function(error) { console.log(error); })
.listen(port);
console.log("Thrift Server running on port: " + port);
And then we start it.
node helloServer.js
Then let's write the client code helloClient.js
.
var thrift = require('thrift');
var helloSvc = require('./gen-nodejs/helloSvc.js');
var connection = thrift.createConnection('localhost', 8585, {
transport: thrift.TBufferedTransport,
protocol: thrift.TBinaryProtocol
}).on('error', function(error) {
console.log(error);
}).on("connect", function() {
var client = thrift.createClient(helloSvc, connection);
client.getMessage("world", function(error, result) {
console.log("Msg from server: " + result);
connection.end();
});
});
As you can see, we also need to require the generated code. Then we start a TCP connection to the server. After the connection established, we can call the getMessage
function just as if it is a local function. Now run
% node helloClient.js
Msg from server: Hello world
RPC with HTTP on Web
The above example uses TCP for RPC. It is a typical method of communication between backends. But we can't send raw TCP on web, we need to use HTTP protocol. Thrift also supports it.
Let's first start from server side. We will use the same IDLs file, so the code generation part for Node.js is the same. Now let's write server code.
var thrift = require('thrift');
var helloSvc = require('./gen-nodejs/helloSvc');
var helloHandler = {
counter: 0,
getMessage: function(name, result) {
this.counter++;
var msg = "" + this.counter + ") Hello " + name + "!";
console.log(msg);
result(null, msg);
}
}
var helloSvcOpt = {
handler: helloHandler,
processor: helloSvc,
protocol: thrift.TJSONProtocol,
transport: thrift.TBufferedTransport
};
var serverOpt = {
files: ".",
services: {
"/hello": helloSvcOpt
},
cors: {
"*": true
}
}
var port = 9099;
thrift.createWebServer(serverOpt).listen(port);
console.log("Http/Thrift Server running on port: " + port);
Two difference we need to see:
- we use TJSONProtocol here, so we can see the message from browser console
- we use createWebServer, so the HTTP protocol will be used
- cors is set
Then let's see the client side code. Since client side code should run in browser, so we need to generate code for this environment.
thrift -gen js hello.thrift
As you can see, the same IDLs file is used, and we should get code generated in gen-js
folder.
Then the whole client code is in one html file as below.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hello Thrift</title>
</head>
<body>
Name:
<input type="text" id="name_in">
<input type="button" id="get_msg" value="Get Message" >
<div id="output"></div>
<!-- file from here: https://github.com/apache/thrift/tree/master/lib/js/src -->
<script src="./thrift.js"></script>
<!-- generated code for js -->
<script src="gen-js/helloSvc.js"></script>
<script>
(function() {
var transport = new Thrift.TXHRTransport("http://localhost:9099/hello");
var protocol = new Thrift.TJSONProtocol(transport);
var client = new helloSvcClient(protocol);
var nameElement = document.getElementById("name_in");
var outputElement = document.getElementById("output");
document.getElementById("get_msg")
.addEventListener("click", function(){
outputElement.innerHTML = client.getMessage(nameElement.value);
});
})();
</script>
</body>
</html>
Run this html in a server, click the Get Message
button, we should see the result displayed properly.