以前工作中也用过一些远程服务调用,比如Webservice,PHP中常用的SOAP,XML-RPC,还有jsonp和restful之类的。最近公司工作中,用到了thrift这种远程服务调用框架,总结和记录下。
Thrift 是 Facebook 实现的一种高效的、支持多种编程语言的远程服务调用的框架。它通过一个中间语言IDL(接口定义语言)来定义RPC的接口和数据类型,然后通过一个编译器生成不同语言的代码并由生成的代码负责RPC协议层和传输层的实现。
Thrift的架构图
黄色是用户实现的业务逻辑,褐色部分是根据 Thrift定义的服务接口描述文件生成的客户端和服务器端代码框架,红色部分是根据 Thrift 文件生成代码实现数据的读写操作。红色部分以下是Thrift的传输体系、协议以及底层 I/O 通信,使用 Thrift 可以很方便的定义一个服务并且选择不同的传输协议和传输层而不用重新生成代码。用户在Thirft描述文件中声明自己的服务,这些服务经过编译后会生成相应语言的代码文件,然后用户实现服务(客户端调用服务,服务器端提服务)。其中protocol(协议层, 定义数据传输格式,可以为二进制或者XML等)和transport(传输层,定义数据传输方式,可以为TCP/IP传输,内存共享或者文件共享等)被用作运行时库。
传输协议
在传输协议上总体划分为文本和二进制 ,为节约带宽,提高传输效率,一般情况下使用二进制类型的传输协议为多数.
- TBinaryProtocol — 二进制编码格式进行数据传输
- TCompactProtocol — 高效率的、密集的二进制编码格式进行数据传输
- TJSONProtocol — 使用 JSON 的数据编码协议进行数据传输
- TSimpleJSONProtocol — 只提供 JSON 只写的协议,适用于通过脚本语言解析
- TDebugProtocol – 使用易懂的可读的文本格式,以便于debug
数据传输
- TSocket — 使用阻塞式 I/O 进行传输,是最常见的模式
- TFramedTransport — 使用非阻塞方式,按块的大小进行传输
- TNonblockingTransport — 使用非阻塞方式,用于构建异步客户端
- TMemoryTransport – 将内存用于I/O
- TZlibTransport – 使用zlib进行压缩, 与其他传输方式联合使用
- TFileTransport – 以文件形式进行传输
服务端类型
- TSimpleServer — 单线程服务器端使用标准的阻塞式 I/O
- TThreadPoolServer —— 多线程服务器端使用标准的阻塞式 I/O
- TNonblockingServer —— 多线程服务器端使用非阻塞式 I/O
安装
具体安装步骤可以参照官方的 Building From Source 和 index of install
Thrift部署服务
一般的步骤为:编写服务说明,保存到.thrift文件->编译.thrift文件,生成相应的语言源代码->编写client端和server端代码。
语法
数据类型
Thrift 脚本可定义的数据类型包括以下几种类型:
基本类型:
bool:布尔值,true 或 false
byte:8 位有符号整数
i16:16 位有符号整数
i32:32 位有符号整数
i64:64 位有符号整数
double:64 位浮点数
string:未知编码文本或二进制字符串
结构体类型:
struct:定义公共的对象,类似于 C 语言中的结构体定义
容器类型:
List:一系列t1类型的元素组成的有序表,元素可以重复
Set:一系列t1类型的元素组成的无序表,元素唯一
Map<t1,t2>:key/value对(key的类型是t1且key唯一,value类型是t2)
异常类型:
exception 异常在语法和功能上类似于结构体,它在语义上不同于结构体—当定义一个RPC服务时,开发者可能需要声明一个远程方法抛出一个异常。
服务类型:
service:对应服务的类
注释
Thrfit支持shell注释风格,C/C++语言中单行或者多行注释风格
1 |
# This is a valid comment. |
4 |
* This is a multi-line comment. |
7 |
// C++/Java style single-line comments work just as well. |
命名空间
Thrift中的命名空间同C++中的namespace和java中的package类似,它们均提供了一种组织(隔离)代码的方式。因为每种语言均有自己的命名空间定义方式(如python中有module),thrift允许开发者针对特定语言定义namespace:
1 |
namespace cpp com.example.project |
2 |
namespace java com.example.project |
文件包含
Thrift允许thrift文件包含,用户需要使用thrift文件名作为前缀访问被包含的对象
定义常量
Thrift允许用户定义常量,复杂的类型和结构体可使用JSON形式表示
1 |
const i32 INT_CONST = 1234; |
2 |
const map<string,string> MAP_CONST = {"hello": "world", "goodnight": "moon"} |
定义结构体
03 |
1: required i32 userId; // 每个域有一个唯一的,正整数标识符 |
05 |
2: required string userName; // 每个域可以标识为required或者optional(也可以不注明) |
07 |
3: required string text; |
09 |
4: optional Location loc; // 结构体可以包含其他结构体 |
11 |
16: optional string language = "english" // 域可以有缺省值 |
15 |
struct Location { // 一个thrift中可定义多个结构体,并存在引用关系 |
17 |
1: required double latitude; |
19 |
2: required double longitude; |
定义服务
Thrift编译器会根据选择的目标语言为server产生服务接口代码,为client产生桩代码
01 |
//“Twitter”与“{”之间需要有空格!!! |
04 |
// 方法定义方式类似于C语言中的方式,它有一个返回值,一系列参数和可选的异常 |
06 |
// 列表. 注意,参数列表和异常列表定义方式与结构体中域定义方式一致. |
08 |
void ping(), // 函数定义可以使用逗号或者分号标识结束 |
10 |
bool postTweet(1:Tweet tweet); // 参数可以是基本类型或者结构体,参数是只读的(const),不可以作为返回值!!! |
12 |
TweetSearchResult searchTweets(1:string query); // 返回值可以是基本类型或者结构体 |
14 |
// ”oneway”标识符表示client发出请求后不必等待回复(非阻塞)直接进行下面的操作, |
16 |
// ”oneway”方法的返回值必须是void |
18 |
oneway void zip() // 返回值可以是void |
函数中参数列表的定义方式与struct完全一样,Service支持继承,一个service可使用extends关键字继承另一个service
实例
直接用官方的tutorial,服务端用python,客户端用php
shared.thrift
tutorial.thrift
服务端
2 |
thrift -r --gen py tutorial.thrift |
新建 server.py
02 |
sys.path.append( 'gen-py' ) |
04 |
from tutorial import Calculator |
05 |
from tutorial.ttypes import * |
07 |
from shared.ttypes import SharedStruct |
09 |
from thrift.transport import TSocket |
10 |
from thrift.transport import TTransport |
11 |
from thrift.protocol import TBinaryProtocol |
12 |
from thrift.server import TServer |
14 |
class CalculatorHandler: |
21 |
def add( self , n1, n2): |
22 |
print 'add(%d,%d)' % (n1, n2) |
25 |
def calculate( self , logid, work): |
26 |
print 'calculate(%d, %r)' % (logid, work) |
28 |
if work.op = = Operation.ADD: |
29 |
val = work.num1 + work.num2 |
30 |
elif work.op = = Operation.SUBTRACT: |
31 |
val = work.num1 - work.num2 |
32 |
elif work.op = = Operation.MULTIPLY: |
33 |
val = work.num1 * work.num2 |
34 |
elif work.op = = Operation.DIVIDE: |
36 |
x = InvalidOperation() |
38 |
x.why = 'Cannot divide by 0' |
40 |
val = work.num1 / work.num2 |
42 |
x = InvalidOperation() |
44 |
x.why = 'Invalid operation' |
49 |
log.value = '%d' % (val) |
54 |
def getStruct( self , key): |
55 |
print 'getStruct(%d)' % (key) |
61 |
handler = CalculatorHandler() |
62 |
processor = Calculator.Processor(handler) |
63 |
transport = TSocket.TServerSocket(port = 9090 ) |
64 |
tfactory = TTransport.TBufferedTransportFactory() |
65 |
pfactory = TBinaryProtocol.TBinaryProtocolFactory() |
67 |
server = TServer.TSimpleServer(processor, transport, tfactory, pfactory) |
73 |
print 'Starting the server...' |
客户端
拷贝thrift包中的 lib/php/lib 到当前目录下
1 |
thrift -r --gen php tutorial.thrift |
新建 client.php
03 |
namespace tutorialphp; |
05 |
error_reporting (E_ALL); |
07 |
require_once __DIR__. '/lib/Thrift/ClassLoader/ThriftClassLoader.php' ; |
09 |
use ThriftClassLoaderThriftClassLoader; |
11 |
$GEN_DIR = realpath (dirname( __FILE__ ). '/' ). '/gen-php' ; |
13 |
$loader = new ThriftClassLoader(); |
14 |
$loader ->registerNamespace( 'Thrift' , __DIR__ . '/lib' ); |
15 |
$loader ->registerDefinition( 'shared' , $GEN_DIR ); |
16 |
$loader ->registerDefinition( 'tutorial' , $GEN_DIR ); |
19 |
use ThriftProtocolTBinaryProtocol; |
20 |
use ThriftTransportTSocket; |
21 |
use ThriftTransportTHttpClient; |
22 |
use ThriftTransportTBufferedTransport; |
23 |
use ThriftExceptionTException; |
27 |
if ( array_search ( '--http' , $argv )) { |
28 |
$socket = new THttpClient( '127.0.0.1' , 8080, '/php/PhpServer.php' ); |
30 |
$socket = new TSocket( '127.0.0.1' , 9090); |
32 |
$transport = new TBufferedTransport( $socket , 1024, 1024); |
33 |
$protocol = new TBinaryProtocol( $transport ); |
34 |
$client = new utorialCalculatorClient( $protocol ); |
41 |
$sum = $client ->add(1,1); |
44 |
$work = new utorialWork(); |
46 |
$work ->op = utorialOperation::DIVIDE; |
51 |
$client ->calculate(1, $work ); |
52 |
print "Whoa! We can divide by zero?
" ; |
53 |
} catch ( utorialInvalidOperation $io ) { |
54 |
print "InvalidOperation: $io->why
" ; |
57 |
$work ->op = utorialOperation::SUBTRACT; |
60 |
$diff = $client ->calculate(1, $work ); |
63 |
$log = $client ->getStruct(1); |
64 |
print "Log: $log->value
" ; |
68 |
} catch (TException $tx ) { |
69 |
print 'TException: ' . $tx ->getMessage(). "
" ; |
运行结果
1 |
ryan@localhost-6:~/WebRoot/ test /thrift-blog$ python server.py |
5 |
calculate(1, Work(comment=None, num1=1, num2=0, op =4)) |
6 |
calculate(1, Work(comment=None, num1=15, num2=10, op =2)) |
1 |
ryan@localhost-6:~/WebRoot/ test /thrift-client$ php client.php |
4 |
InvalidOperation: Cannot divide by 0 |
参考:
《Apache Thrift – 可伸缩的跨语言服务开发框架》
《Thirft框架介绍》