使用Thrift来实现跨语言的方法调用
有时遇到10多年历史的C++写的老代码,对于不熟悉C++开发的团队来说,最好的方式是不去改它。但是,你却有需求从Web(比如PHP的站点)来调用老代码的库。怎么办?传统的方式是用COM组件,但这就限制在Windows平台上了。要做到完全跨平台,跨各种语言。这里介绍一个工具,就是Apache Thrift,最初由Facebook开发,后来归入Apache基金会。
本文会介绍Python和PHP如何同C之间交互,其他语言读者可以自行去扩展。文中使用的环境是操作系统Ubuntu 18.04,PHP7.2,Python 2.7.17和3.6.9,以及gcc/g++ 7.5,对应Thrift版本0.13。其他的版本可能编译过程会遇到不同的问题,比如我在CentOS 7.6上,编译Thrift 0.9.3版本很顺利,但0.13版本就各种问题。
编译安装Thrift
- 在安装Thrift前,先确保你安装了下列环境C, PHP和Python环境
$ sudo apt update
$ sudo apt install gcc g++ openssl libboost-all-dev
$ sudo apt install php php-dev php-cli php-gd php-mbstring php-xml php-pear phpunit
$ sudo apt install python python3 python-dev python3-dev libpython3-all-dev
- PHP Composer必须安装
$ php -r "copy('https://install.phpcomposer.com/installer', 'composer-setup.php');"
$ sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer
- 下载源码,可以去官网下载,或者到Github克隆最新的代码。
$ git clone https://github.com/apache/thrift.git
- 进入Thrift源码目录,本文放在”/opt/thrift-0.13/“下,开始编译安装
$ cd /opt/thrift-0.13
$ ./configure --without-go --without-java --without-ruby
$ make
$ sudo make install
默认编译会安装所有语言的库,本例为了简便,把java,go和ruby的编译去掉了。如果需要,可以在configure
时将相应的”–without-xxx”选项去掉。
- 安装完毕后,查看下Thrift版本
$ thrift --version
成功的话,你会看到”Thrift version 0.13.0”字样。
- 检查下C++的动态库有没有生成,位置是在”/opt/thrift-0.13/lib/cpp/.libs/libthrift.so”,如果没有的话,很可能是少安装了一些包。切到C++ lib目录下,通过make来检查下。
$ cd /opt/thrift-0.13/lib/cpp
$ make
另外,需要把编译好的C++的类库目录加到动态库目录下,可以将其写入”~/.bashrc”
$ export LD_LIBRARY_PATH=/opt/thrift-0.13/lib/cpp/.libs:$LD_LIBRARY_PATH
编写接口文件
Thrift的核心就是通过接口文件,来生成各语言的代码,接口文件以”*.thrift”命名。代码生成完,被调用方要编写服务端代码,其本质就是通过Thrift库监听一个Socket端口;而调用方编写客户端代码,同样通过Thrift库调用服务端的Socket端口,实现RPC调用。
我们到”/opt/thrift-0.13/tutorial”目录下,创建接口文件”tester.thrift”,内容如下,说明都放在注释里了。
// 定义命名空间
namespace php mytest
namespace py mytest
namespace cpp mytest
typedef i32 Int // 类型名称映射,将32位整型映射位Int类型
// 定义服务接口,接口中提供两个方法。
service Tester
{
Int add(1:Int num1, 2:Int num2), // 整数加法
string merge(1:string str1, 2:string str2), // 字符串连接
}
- 生成各语言的代码
$ thrift -r --gen php tester.thrift
$ thrift -r --gen py tester.thrift
$ thrift -r --gen cpp tester.thrift
注意,这里不生成PHP服务端的代码,要生成的话,需要用thrift --gen php:server tester.thrift
来指明,本文不演示PHP服务端的代码。
- 执行成功后,你会看到在”/opt/thrift-0.13/tutorial”目录下自动创建了三个目录:”gen-php”,”gen-py”和”gen-cpp”。接下来,我们分别写各个语言的服务端和客户端代码。
编写Python Client和Server
将”/opt/thrift-0.13/tutorial/py”目录下的”PythonServer.py”和”PythonClient.py”复制到刚才生成的”/opt/thrift-0.13/tutorial/gen-py”目录下,并把文件改名为”TesterServer.py”和”TesterClient.py”。
先写服务端代码,打开”TesterServer.py”,大部分代码这里都有了,你要做的是把包名和地址改改,另外把
CalculatorHandler
改为本例的TesterHandler
,并将add
和merge
两个方法实现了。具体代码如下,改动部分我都加了# Changed
注释
import glob
import sys
sys.path.append('../gen-py') # Changed
sys.path.insert(0, glob.glob('../../lib/py/build/lib*')[0])
from mytest import Tester # Changed
from mytest.ttypes import * # Changed
from thrift.transport import TSocket
from thrift.transport import TTransport
from thrift.protocol import TBinaryProtocol
from thrift.server import TServer
# Changed
class TesterHandler:
def __init__(self):
self.log = {}
def add(self, num1, num2):
print('add(%d, %d) is called' % (num1, num2))
return num1 + num2
def merge(self, str1, str2):
print('merge(%s, %s) is called' % (str1, str2))
return str1 + str2
if __name__ == '__main__':
handler = TesterHandler() # Changed
processor = Tester.Processor(handler) # Changed
transport = TSocket.TServerSocket(host='127.0.0.1', port=9090)
tfactory = TTransport.TBufferedTransportFactory()
pfactory = TBinaryProtocol.TBinaryProtocolFactory()
server = TServer.TSimpleServer(processor, transport, tfactory, pfactory)
print('Starting the server...')
server.serve()
print('done.')
- 再编写客户端代码,打开”TesterClient.py”,也是将包名和地址改改,另外把调用服务端的方法改了。具体代码如下,改动部分我也加了
# Changed
注释
import sys
import glob
sys.path.append('../gen-py') # Changed
sys.path.insert(0, glob.glob('../../lib/py/build/lib*')[0])
from mytest import Tester # Changed
from mytest.ttypes import * # Changed
from thrift import Thrift
from thrift.transport import TSocket
from thrift.transport import TTransport
from thrift.protocol import TBinaryProtocol
def main():
transport = TSocket.TSocket('localhost', 9090)
transport = TTransport.TBufferedTransport(transport)
protocol = TBinaryProtocol.TBinaryProtocol(transport)
client = Tester.Client(protocol)
transport.open()
sum_ = client.add(3, 4)
print('3 + 4 = %d' % sum_)
# Changed
str_ = client.merge('Hello ', 'Python')
print('Hello + Python = %s' % str_)
transport.close()
if __name__ == '__main__':
try:
main()
except Thrift.TException as tx:
print('%s' % tx.message)
- 到”/opt/thrift-0.13/tutorial/gen-py”目录下,让我们启动服务端Socket
$ python TesterServer.py
成功的话,你可以看到”Starting the server…“字样,此时9090接口已经开始被监听。
- 再让我们调用客户端
$ python TesterClent.py
此时,在客户端你可以看到
3 + 4 = 7
Hello + Python = Hello Python
而在服务端控制台,你也可以看到
add(3, 4) is called
merge(Hello , Python) is called
恭喜你程序跑通了。你可以换Python3试试,这份代码是兼容的。
编写C++ Client和Server
将”/opt/thrift-0.13/tutorial/cpp”目录下的”CppServer.cpp”和”CppClient.cpp”复制到刚才生成的”/opt/thrift-0.13/tutorial/gen-cpp”目录下,并把文件改名为”TesterServer.cpp”和”TesterClient.cpp”。
先写服务端代码,打开”TesterServer.cpp”,同Python部分一样,大部分代码都不用改,你要做的是改变包名和类名,并把
TesterHandler
和TesterCloneFactory
实现了。具体代码如下,改动部分我加了// Changed
注释
#include <thrift/concurrency/ThreadManager.h>
#include <thrift/concurrency/ThreadFactory.h>
#include <thrift/protocol/TBinaryProtocol.h>
#include <thrift/server/TSimpleServer.h>
#include <thrift/server/TThreadPoolServer.h>
#include <thrift/server/TThreadedServer.h>
#include <thrift/transport/TServerSocket.h>
#include <thrift/transport/TSocket.h>
#include <thrift/transport/TTransportUtils.h>
#include <thrift/TToString.h>
#include <iostream>
#include <stdexcept>
#include <sstream>
#include "../gen-cpp/Tester.h" // Changed
using namespace std;
using namespace apache::thrift;
using namespace apache::thrift::concurrency;
using namespace apache::thrift::protocol;
using namespace apache::thrift::transport;
using namespace apache::thrift::server;
using namespace mytest; // Changed
// Changed
class TesterHandler : public TesterIf {
public:
TesterHandler() = default;
Int add(const Int num1, const Int num2) override {
cout << "add(" << num1 << ", " << num2 << ") is called" << endl;
return num1 + num2;
}
void merge(std::string& _return, const std::string& str1, const std::string& str2) override {
cout << "merge(" << str1 << ", " << str2 << ") is called" << endl;
char buffer[str1.length() + str2.length() + 1];
sprintf(buffer, "%s%s", str1.c_str(), str2.c_str());
_return = buffer;
return;
}
};
// Changed
class TesterCloneFactory : virtual public TesterIfFactory {
public:
~TesterCloneFactory() override = default;
TesterIf* getHandler(const ::apache::thrift::TConnectionInfo& connInfo) override
{
std::shared_ptr<TSocket> sock = std::dynamic_pointer_cast<TSocket>(connInfo.transport);
cout << "Incoming connection\n";
cout << "\tSocketInfo: " << sock->getSocketInfo() << "\n";
cout << "\tPeerHost: " << sock->getPeerHost() << "\n";
cout << "\tPeerAddress: " << sock->getPeerAddress() << "\n";
cout << "\tPeerPort: " << sock->getPeerPort() << "\n";
return new TesterHandler;
}
void releaseHandler( ::mytest::TesterIf* handler) override {
delete handler;
}
};
int main() {
TThreadedServer server(
std::make_shared<TesterProcessorFactory>(std::make_shared<TesterCloneFactory>()), // Changed
std::make_shared<TServerSocket>(9090), //port
std::make_shared<TBufferedTransportFactory>(),
std::make_shared<TBinaryProtocolFactory>());
cout << "Starting the server..." << endl;
server.serve();
cout << "Done." << endl;
return 0;
}
- 再编写客户端代码,打开”TesterClient.cpp”,也是将包名和地址改改,另外把调用服务端的方法改了。具体代码如下,改动部分我也加了
// Changed
注释
#include <iostream>
#include <thrift/protocol/TBinaryProtocol.h>
#include <thrift/transport/TSocket.h>
#include <thrift/transport/TTransportUtils.h>
#include "../gen-cpp/Tester.h" // Changed
using namespace std;
using namespace apache::thrift;
using namespace apache::thrift::protocol;
using namespace apache::thrift::transport;
using namespace mytest; // Changed
int main() {
std::shared_ptr<TTransport> socket(new TSocket("localhost", 9090));
std::shared_ptr<TTransport> transport(new TBufferedTransport(socket));
std::shared_ptr<TProtocol> protocol(new TBinaryProtocol(transport));
TesterClient client(protocol); // Changed
try {
transport->open();
cout << "5 + 6 = " << client.add(5, 6) << endl;
// Changed
std::string result;
client.merge(result, "Hello ", "C++");
cout << "Hello + C++ = " << result << endl;
transport->close();
} catch (TException& tx) {
cout << "ERROR: " << tx.what() << endl;
}
}
- 开始编译客户端和服务端程序
$ g++ -o TesterServer tester_constants.cpp tester_types.cpp Tester.cpp TesterServer.cpp -lthrift
$ g++ -o TesterClient tester_constants.cpp tester_types.cpp Tester.cpp TesterClient.cpp -lthrift
注意这里-lthrift
说明要连接libthrift.so
动态库,上文编译安装Thrift部分要加”LD_LIBRARY_PATH”就是为了找到该库。
- 在”/opt/thrift-0.13/tutorial/gen-cpp”目录下,让我们启动服务端程序
$ ./TesterServer
- 在同一目录下,启动客户端
$ ./TesterClient
是不是得到跟上文Python一样的结果啊?你可以用C++客户端调Python的服务端,或者Python客户端调C++的服务端试试。是不是完全相通?很神奇吧!
编写PHP Client
最后让我们实现PHP客户端,因为PHP服务端不是Socket,而是通过HTTP实现的,这里我们就不试了。
将”/opt/thrift-0.13/tutorial/php”目录下的”PhpServer.php”和”PhpClient.php”复制到刚才生成的”/opt/thrift-0.13/tutorial/gen-php”目录下,并把文件改名为”TesterServer.php”和”TesterClient.php”。
编写客户端代码,打开”TesterClient.php”,同样将包名和地址改改,另外把调用服务端的方法改了。具体代码如下,改动部分我加了
// Changed
注释
<?php
namespace mytest\php; // Changed
error_reporting(E_ALL);
require_once __DIR__.'/../../vendor/autoload.php';
use Thrift\ClassLoader\ThriftClassLoader;
$GEN_DIR = realpath(dirname(__FILE__).'/..').'/gen-php';
$loader = new ThriftClassLoader();
$loader->registerNamespace('Thrift', __DIR__ . '/../../lib/php/lib');
$loader->registerNamespace('mytest', $GEN_DIR); // Changed
$loader->register();
use Thrift\Protocol\TBinaryProtocol;
use Thrift\Transport\TSocket;
use Thrift\Transport\THttpClient;
use Thrift\Transport\TBufferedTransport;
use Thrift\Exception\TException;
try {
if (array_search('--http', $argv)) {
$socket = new THttpClient('localhost', 8080, '/php/PhpServer.php');
} else {
$socket = new TSocket('localhost', 9090);
}
$transport = new TBufferedTransport($socket, 1024, 1024);
$protocol = new TBinaryProtocol($transport);
$client = new \mytest\TesterClient($protocol); // Changed
$transport->open();
$sum = $client->add(7, 8);
print "7 + 8 = $sum\n";
// Changed
$result = $client->merge('Hello ', 'PHP');
print "Hello + PHP = $result\n";
$transport->close();
} catch (TException $tx) {
print 'TException: '.$tx->getMessage()."\n";
}
?>
- 启动刚才Python或C++的服务端,然后我们调用此PHP客户端
$ php ./TesterClient.php
有没有看到同样的输出?
本文只是对Thrift做了最简单的入门介绍,Thrift可以用来实现各种阻塞或非阻塞的服务端程序,并支持大规模的跨语言服务开发。想要深入了解,还需仔细阅读官网的文档和源码库。
本篇中的示例代码可以在这里下载。