Thrift : the missing guide
Diwaker Gupta me@diwakergupta.info
Revision History
2012-01-20
适用于Thrift 0.6.0
潘飞(cnweike@gmail.com)翻译
1. 语言参考
1.1 类型
Thrift类型系统包含预先定义好的基本类型,用户自定义的结构体,容器类型,异常和服务定义。
基本类型
bool:一个布尔值(true或者false),一个字节
byte: 有符号字节
i16: 16位的有符号整数
i32: 32位的有符号整数、
i64: 64位的有符号整数
double:64位的浮点数
string: 未知编码的文本或者二进制字符串
注意Thrift不支持无符号整数,因为在Thrift很多要支持的语言中并没有直接原生类型。
容器
Thrift容器是强类型的容器,它可以映射到主流编程语言中最常用的容器。它们以Java泛型风格来做标明。有三种容器类型可用:
list<t1>: 一个类型为t1的有序的元素列表。可以包含重复值。
set<t1>: 一个类型为t1的无序的去重值得集合。
map<t1, t2>: 类型为t1的去重key到类型为t2的值得映射。
容器中的类型可以是任何合法的Thrift类型(包括结构体和异常),但是不包括服务。
结构体和异常
一个Thrift结构体概念上类似于一个C结构体——一种方便的将相关项目组合在一起的方式。结构体被翻译成面向对象语言中的类。
异常在语法和功能上等同于一个结构体,不同之处仅仅是它用exception关键字声明而不是用struct关键字。它们在语义上不同于结构体——当定义RPC服务的时候,开发者可以声明一个远程方法抛出一个异常。
关于定义结构体和异常的细节在后面的部分中。
服务
服务定义在语义上等同于在面向对象语言中定义一个interface(或者是一个纯虚类)。Thrift编译器生成功能完整的客户端和服务端框架,整个框架实现了这个接口。
定义服务的细节在后面的部分中。
1.2 Typedefs
Thrift支持C/C++风格的typedefs
typedef i32 MyInteger //1
typedef Tweet ReTweet //2
1 注意在尾部没有分号
2 结构体也可以用在typdefs中
1.3 枚举类型
当你定义一个消息类型的时候,你可能希望其中的一个字段只能从预先定义好的一些值中取值。比如,你想为每个Tweet定义一个tweetType字段,其中tweetType可以为TWEET,RETWEET,DM或者是REPLY。这可以通过在你的消息定义中加入一个枚举类型来轻松实现——一个枚举类型的字段的值只能是预先定义好的常量中的一个(如果你试图提供一个不同于这些值得值,解析器将视它为一个未知字段)。在下面的例子中我们已经将一个叫做TweetType的枚举值,并且给出了这个值所有可能的值;还有一个相同类型的字段:
Enum TweetType{
TWEET,// 1
RETWEET= 2, //2
DM= 0xa, // 3
REPLY
} //4
Struct Tweet{
1:required i32 userId;
2:required string username;
3:required string text;
4:optional Location loc;
5:optional TweetType tweetType = TweetType.TWEET // 5
16:optional string language = “english”
}
1 枚举类型以C语言风格来指定。编译器默认赋的初始值是0.
2 当然,你也可以提供一个指定的整形值来作为常数。
3 十六进制数也是可以接受的。
4 再一次注意没有尾部的分号。
5 当赋默认值的时候,要使用常量的全名。
注意,不同于Protocl Buffers,Thrift还不支持嵌套的枚举类型。
枚举类型常量必须在32位常量的范围之内。
1.4 注释
Thrift支持shell风格,C风格,JAVA/C++风格的单行和多行注释。
# This is a valid comment
/*
*This is a multi-liine comment.
*just like in C.
*/
// C++/Java style single-line commets work just as well
1.5 名称空间
Thrift中的名称空间类似于C++中的名称空间或者是Java中的包——它们提供了组织你的代码的一种便利的方式。名称空间也可以防止类型定义之间的冲突。
因为每种语言都有自己的类似于包的机制(比如Python有模块),Thrift允许你自定义每种语言的名称空间行为:
namespace cppcom.example.project //1
namespace java com.example.project //2
1 翻译成namespace com { namespace example {namespace project {
2 翻译成 package com.example.project
1.6 Includes
为了便于维护通常将Thrift的定义文件分到不同的定义文件中去,使得能够重用和优化模块机制/组织。Thrift允许文件爱你包含其它Thrift文件。被包含的文件会被从当前目录下查找,也会从-I编译参数指定的路径中搜索。
被包含进来的对象通过Thrift文件的名字作为前缀来访问。
Include “tweet.thrift”//1
…
StructTweetSearchResult{
1: list<tweet.Tweet> tweets;//2
}
1 文件名必须被引起来;再一次注意分号的缺席。
2 注意tweet前缀。
1.7 常量
Thrift允许你定义跨语言的常量。复杂的类型和结构体通过JSON记法来指定。
const i32 INT_CONST = 1234; //1
const map<string, string>MAP_CONST = {“hello”:”world”, “goodnight”:”moon”}
1 分号是可选的;十六进制的数也是可以的
1.8 定义结构体
结构体(在某些系统中也被叫做消息)是ThriftIDL中的基本构建块。一个结构体由字段来构成;每个字段有一个唯一的整形标识,一个类型,一个名字和一个可选的默认值。
看一个简单的例子。比如你想构建一个类似于Twitter的服务。下面就是我们怎么来定一个Tweet:
struct Tweet{
1: required i32 userId; //1
2: required string username; //2
3: required string text;
4: optional Location loc; //3
16: optional string language = “english”//4
}
Struct Location{//5
1: required double latitude;
2: required double longitude;
}
1 每个字段都必须有一个唯一的,正的整形标识
2 字段必须指定为required或者是optional
3 结构体可以包含其他结构体
4 你可以为一个字段指定一个可选的默认值
5 多个结构体可以在一个相同的Thrift文件中定义并引用
正如你所见,消息定义中每个字段都有一个数字标识。这些标识被以一定的策略使用,一旦定义就不能更改。
字段可以被标记为required或者是optional,它们的意义都很清晰,是为了更好地组织结构体。比如,Thrift会在required字段没有在结构体中设置的时候报错。如果一个optional的字段没有在结构体中指定,那么在策略背后讲不会被序列化。如果一个optional的字段被指定了一个默认值,那么在解析这个结构体,并且这个字段没有被设定的情况下,这个字段将会被设置为默认值。
不同于服务,结构体不支持继承,也就是说,一个结构体不能扩展另一个结构体。
警告:
你应该非常小心地将一个字段标记为required。如果在某些情况下你想停止写或者发送一个required字段,将它转变为一个optional字段将是一个文倜——旧的读取这可能会因为这个字段的缺失,认为这个消息不完整,并且在没有授意的情况下拒绝或者丢弃它们。你应该思考应用相关的个性化的验证路径。又人已经得出结论,使用required弊大于利;他们倾向于只使用optional并重复。然而,这种观点并不是普遍的。
1.9 定义服务
已经有几个流行的序列化/反序列化框架(像是ProtocolBuffers),几乎没有框架提供跨语言的基于RPC的服务支持。这是Thrift的主要吸引点之一。
把服务定义想象成Java接口——你需要为方法提供名称和签名。作为一个可选项,一个服务可以扩展另外一个服务。
Thrift编译器将生成你所选择语言的服务接口代码(为服务端)和框架(为客户端)。Thrift自带了大多数语言的RPC库,你可以用之来运行你的客户端和服务端。
service Twitter{
// A method definition looks like C code. It has a return type, arguments,
// and optionally a list of exceptionsthat is may throw. Note that argument
// lists and exceptions list arespecified using the exact same syntax as field
// lists in structs.
void ping(), // 1
bool postTweet(1: Tweet tweet); //2
TweetSearchResult searchTweets(1:string query);//3
// The oneway modifier indicates thatthe client only makes a request and
// does not wait for any response atall. Oneway methods MUST be void.
Oneway void zip() //4
}
1 很操蛋,方法定义可以使用逗号,也可以使用分号来终结
2 参数可以是原生类型,也可以是结构体
3 也可以作为返回类型
4 void是一个合法的函数返回类型
注意,函数的参数列表(和异常列表)的指定非常像结构体的定义。
服务支持继承:一个服务可以通过extends关键字来继承其它服务。
重要:
在写本文时,Thrift不支持嵌套类型定义。也就是说,你不能在一个结构体(或者一个枚举类型)内定义一个结构体;当然,你可以在其它的结构体中使用结构体或者枚举。
2 生成的代码
这一部分包含了Thrift为不同目标语言生成的代码使用的文档。我们一开始先介绍常用的概念——这些能说明生成的代码是怎样组织的,并且希望能懂得怎样更高效地使用它。
2.1 概念
这里是一个Thrift网络栈的图片:
Transport
传输层为读写网络提供了一个简单的抽象。这将Thrift底层的传输和系统的其它部分(比如序列化,反序列化)解耦。
这里是Transport接口暴露的一些方法:
open
close
read
write
flush
除了上面的Transport接口,Thrift也使用ServerTransport接口用来接受或者创建原生的transport对象。正如名字所暗示的,ServerTransport主要被用在服务端为进来的连接(connections)创建新的transport对象。
open
listen
accept
close
这里是Thrift支持的主要语言的一些可用的tranports
file:读写磁盘上的一个文件
http:正如名字所指的
协议
协议抽象定义了从内存数据映射到线性数据的机制。换句话说,协议指定了怎样使用底层的transport来编码/解码。所以协议实现控制了编码模式并且对序列化/反序列化负责。一些协议的例子包含JSON,XML,纯文本,协议二进制数据等等。
这里是Protocol接口:
writeMessageBegin(name,type, seq)
writeMessageEnd()
writeStructBegin(name)
writeStructEnd()
writeFieldBegin(name,type, id)
writeFieldEnd()
writeFieldStop()
writeMapBegin(ktype,vtype, size)
writeMapEnd()
writeListBegin(etype,size)
writeListEnd()
writeSetBegin(etype,size)
writeSetEnd()
writeBool(bool)
writeByte(byte)
writeI16(i16)
writeI32(i32)
writeI64(i64)
writeDouble(double)
writeString(string)
name, type, seq =readMessageBegin()
readMessageEnd()
name = readStructBegin()
readStructEnd()
name, type, id = readFieldBegin()
readFieldEnd()
k, v, size = readMapBegin()
readMapEnd()
etype, size = readListBegin()
readListEnd()
etype, size = readSetBegin()
readSetEnd()
bool = readBool()
byte = readByte()
i16 = readI16()
i32 = readI32()
i64 = readI64()
double = readDouble()
string = readString()
Thrift协议在设计上是面向流的。没有必要有任何显示的帧。比如,在我们序列化之前,没有必要知道一个列表的元素的一个字符串或者数字的长度。
下面是Thrift支持的大多数语言可用的协议:
二进制:相当简单的二进制编码——一个字段的类型和长度被编码为字节,后面跟着真实的数据。
Compact: https://issues.apache.org/jira/browse/THRIFT-110
JSON
处理器(Processor)
一个Processor封装了从输入流中读取数据和向输出流中写入数据的能力。输入和输出流通过协议对象来表示。Processor接口非常简单:
interface TProcessor{
bool process(TProtocolin, TProtocol out) throws TException
}
服务相关的处理器实现是编译器产生的。处理器主要负责从线中读取数据(使用输入协议),代理handler(用户实现)的处理,并且将应答写回到线中(使用输出协议)。
服务器(Server)
一个服务器将上面涉及的所有特性汇集到一起:
创建一个transport
为这个transport创建一个输入/输出协议
基于输入/输出协议创建一个处理器(processor)
等待进来的连接并且将它们交给处理器
下面我们讨论为某种语言生成的代码。除非被提及,否则下面的部分都是用下面的Thrift明细:
namespace cpp thrift.example
namespace java thrift.example
enum TweetType{
Tweet,
RETWEET= 2,
DM= 0xa,
REPLY
}
struct Location {
1: required doublelatitude;
2: required doublelongitude;
}
struct Tweet {
1: required i32userId;
2: required stringuserName;
3: required stringtext;
4: optional Locationloc;
5: optionalTweetType tweetType = TweetType.TWEET;
16: optional stringlanguage = "english";
}
typedeflist<Tweet> TweetList
structTweetSearchResult {
1: TweetList tweets;
}
const i32MAX_RESULTS = 100;
service Twitter {
void ping(),
boolpostTweet(1:Tweet tweet);
TweetSearchResultsearchTweets(1:string query);
oneway void zip()
}
嵌套的结构体是怎会 被初始化的呢?
在先前的部分中,我们看到Thrift是怎样允许结构体包含其它结构体的(当然不能嵌套定义!),在大多数的面向对象或者是动态语言中,结构体映射到对象,那么知道Thrift是怎样初始化嵌套结构体是有益的。一个可用的方法是将嵌套的结构体看成是指针或者引用,并且在显示地设定它们之前被初始化为NULL。
不幸的是,对于很多语言,Thrift使用值传递模型。作为一个具体的例子,考虑上面的Tweet例子产生的C++代码:
...
int32_t userId;
std::stringuserName;
std::string text;
Location loc;
TweetType::typetweetType;
std::stringlanguage;
...
正如你所看到的,嵌套的Location结构体是完全分配内联的。因为Location是可选的,代码使用内部的__isset标志来确定这个字段是不是被用户设定过。
这可以能导致一些令人惊讶的和直观的行为:
在一些语言中,初始化的时候,每个子结构体都要完整分配,导致内存使用可能比你想象的要打,特别是带有许多未设定字段的复杂结构体。
服务方法的参数和返回值可能不是“optional”的,并且你不能在任何动态语言中赋予或者返回null。所以要从方法中返回“no value”结果,你必须声明一个外壳结构体,包含一个包含这个值的可选字段,在没有设定那个字段的情况下返回这个外壳结构体。
传输层可以监视方法从旧的服务定义的调用,调用过程中缺失参数。所以,如果原先的服务包含一个方法postTweet(1: Tweet tweet)并且后来的版本中改为postTweet(1:Tweet tweet, 2:string group),然后一个旧的客户端调用了前面的方法将导致一个新的服务端接收到了一个新参数未被设置的调用。如果新的服务端是Java写的,比如,那么,你的新参数可能真实接受到一个null值。并且你可以在IDL中不声明一个参数是可空的。
2.2 Java
一个文件(Constants.java)包含所有的常量定义
每个结构体,枚举类型和服务都各有一个文件
$ tree gen-java
`-- thrift
`-- example
|-- Constants.java
|-- Location.java
|-- Tweet.java
|-- TweetSearchResult.java
|-- TweetType.java
`-- Twitter.java
名称习惯
因为Thrift编译器不强制使用任何命名习惯,建议遵守标准的命名习惯,否则你可能遇到一些令你惊讶的问题。比如,如果你有一个命名为TweetSearchResults的结构体(注意驼峰写法)其中包含一个命名为tweetSearchResults(像先前的结构体)的类。这显然在Java下不能编译。
类型
Thrift映射了Java基本类型和容器类型:
• bool: boolean
• binary: byte[]
• byte: byte
• i16: short
• i32: int
• i64: long
• double: double
• string: String
• list<t1>: List<t1>
• set<t1>: Set<t1>
• map<t1,t2>: Map<t1,t2>
正如你所看到的,映射是直观的并且大部分是一对一的。这并不意外,因为Thrift项目开始的时候,Java就是主要的目标语言之一。
Typedefs
Java语言没有对typedefs的原生支持。所以Thrift的Java代码生成器有一个typedef声明,它仅仅是替换为原来的来行。也就是,即使你可能已经typdefed TypeA到TypeB了,在产生的Java代码中,所有引用TypeB的地方都被TypeA代替。
考虑上面的IDL。生成的代码中为TweetSearchResults类型的tweets的声明仅仅就是简单的publicList<Tweets> tweets。
枚举类型
Thrift枚举类型映射到Java的enum类型。你可以通过getValue(TEnum接口)方法来获取到一个枚举的数字值。除此之外,编译器生成了findByValue方法包含了根据数字值的枚举。这比用Java枚举类型原来的特性更健壮。
常量
Thrift将所有定义的常量放在一个叫做Constants的类中,它们都声明为public static final的成员。任何原生类型常量都是支持的。
引入你的常量
如果你有多个Thrift文件(在同样的名称空间中)都常量定义,Thrift编译器将用后来的的定义覆盖Constants.java。你必须或者定义你的常量在一个单独的文件中,或者是对一个包含了所有其它文件的单个文件调用Thrift编译器。
2.3 2.4 对于C++部分的描述略过。
3 最佳实践
3.1 版本化/兼容性
协议随着时间不断演进。如果一个已经存在的消息类型已经不能满足你的需求——比如,你希望这个消息格式有一个附加的字段——但是你还是想用旧格式产生的代码,必要担心!能非常简单做到更新你的消息类型而不损害现有的代码。仅仅记住下面的规则:
1 不要修改已存在的字段的数字标识
2 你添加的任何字段都应该是可选的。这意味着任何使用你的旧的消息格式的消息可以被新产生的代码理解,因为它们不会缺少任何required的元素。你应该为新加入的字段赋予合适的默认值,所以你的新代码可以和旧代码产生的消息交互。类似的,新代码产生的消息可以被旧代码理解:旧的二进制就是简单地忽略新的字段。然而,未知的字段并咩有被舍弃,如果这个消息后来被序列化,未知字段也会随着序列化——所以,如果这个消息被传递给新的代码,新的字段也是可用的
3 不必须的字段可以被移除,只要你的这些数字表示不在新的消息类型中出现即可(也许重命名这个字段会更好些,或者加一个OBSOLETE_前缀,这样你的thrift文件的使用者就不会意外地重复使用这个值了)。
4 改变默认值通常是OK的,只要你记得那个默认值不会通过线型参数传递。所以,如果一个程序收到一个消息,其中某个字段没有被设定,程序将看到协议中定义的这个字段的默认值。它不会看到发送者代码中定义的默认值。
5 资源
• Thrift whitepaper[http://thrift.apache.org/static/thrift-20070401.pdf]
• Thrift Tutorial[http://wiki.apache.org/thrift/Tutorial]
• Thrift Wiki [http://wiki.apache.org/thrift]
• Protocol Buffers[http://code.google.com/apis/protocolbuffers/docs/overview.html]