有时遇到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,并将addmerge两个方法实现了。具体代码如下,改动部分我都加了# 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部分一样,大部分代码都不用改,你要做的是改变包名和类名,并把TesterHandlerTesterCloneFactory实现了。具体代码如下,改动部分我加了// 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可以用来实现各种阻塞或非阻塞的服务端程序,并支持大规模的跨语言服务开发。想要深入了解,还需仔细阅读官网的文档源码库

本篇中的示例代码可以在这里下载