本文是阅读《Thrift: The Missing Guide》的笔记(好吧几乎就是翻译了)。
类型
支持预定义的基本类型、结构体、容器、异常和服务的定义等。
基本类型
bool
:一个字节byte
:有符号的一个字节i16
:16 位有符号整型i32
:32 位有符号整型i64
:64 位有符号整型double
:64 位浮点型数字binary
:A byte arraystring
:未知编码的文本,或者二进制字符串
Thrift 不支持无符号整型。
容器
list<t1>
:t1 类型的有序列表,元素可以重复set<t1>
:t1 类型的无序集合,元素唯一map<t1, t2>
:t1 是 keys,对应 values 是 t2。t1 唯一。
结构体和异常(Exceptions)
Thrift 的结构体类似于 C 的结构体。详见下文
Thrift 的异常(Exceptions,下同)在语法和功能上都和结构体类似。不过不用 struct 关键字声明,而用 exception 关键字声明。异常在语义上和结构体不同,定义 RPC 服务时,开发者可以声明一个抛出异常的远程方法。
服务
“服务”在语义上等同于面向对象程序里的“接口”。Thrift 编译器根据接口自动生成完整功能的客户端和服务端。
Typedefs
Thrift 支持 C/C++ 风格的 typedefs
typedef i32 MyInteger // 1
typedef Tweet ReTweet // 2
- 注意没有分号
- 结构体也可以用于 typedefs
枚举
举例如下:
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"
}
- C 风格枚举,默认初始值为 0
- 可以自定义赋值
- 16 进制数也支持
- 不需要分号
- 用于赋值是要写全名字
目前不支持嵌套枚举或者结构体。枚举常量必须在正数的 32 位整型范围内。
注释
shell 注释风格、C 多行注释风格、Java/C++ 单行注释风格都是支持的。
# This is a valid comment.
/*
* This is a multi-line comment.
* Just like in C.
*/
// C++/Java style single-line comments work just as well.
命名空间
命名空间可以用来组织或者分隔代码,避免命名污染。和 C++ 的命名空间或者 Java 的包是同样的概念。各种语言的相关机制是不同的,Thrift 可以对此定制。例子:
namespace cpp com.example.project // 1
namespace java com.example.project // 2
指定为 C++ 风格,将被翻译成
namespace com { namespace example { namespace project {
指定为 Java 风格,将被翻译成
com.example.project
Includes
可以通过 include 来引用其它 Thrift 文件。Thrift 在当前的目录下查找被 include 的文件,通过-I
编译可以指定其它目录为搜索路径。
使用时其它文件的内容时,需要添加该文件的名字作为前缀。
include "tweet.thrift" // 1
...
struct TweetSearchResult {
1: list<tweet.Tweet> tweets; // 2
}
- 用引号括起文件名,此处无序分号
- 使用时需要加上文件名做前缀,如
tweet.Tweet
常量
复杂类型和结构体常量的定义使用 JSON 格式。
const i32 INT_CONST = 1234; // 1
const map<string,string> MAP_CONST = {"hello": "world", "goodnight": "moon"}
- 16 进制数也是合法的
定义结构体
结构体由域组成,每个域由一个唯一的整型数字标号、一个类型、一个变量名 和一个可选的默认值构成。
struct Location { // 5
1: required double latitude;
2: required double longitude;
}
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
}
- 每个域都必须有一个唯一的,正值的整型标号。
- 域可以用 required 或者 optional 来标记。
- struct 可以嵌套
- 可以为域添加一个默认值
- 可以在同一个 Thrift 文件里面定义和引用多个结构体
每个域的整型标号用于序列化传输,信息在使用时,该标号不要改变。
标记为 required 的域必须进行赋值。标记为 optional 的域如果没有赋值,将不会被传输;如果有默认值,则它在被传输的时候会被赋予该默认值,除非你显式地重新赋值了。
struct 不能继承,也就是说不能用于扩展其它 structs。
警告
把一个域标记为 required 要谨慎。如果将来你决定弃用这个域,但是一些旧的组件还在使用这个域,那么你的消息可能会被对方认为不合法而进行丢弃或者别的处理。这时候你只能考虑采取一些很恶心的方法来做补救了。所以有人认为使用 required 域弊大于利。当然,到底咋用,看你咯。
定义服务
Thrift 编译器可以自动生成你指定地语言的服务器端接口代码和应用端桩代码。
service Twitter {
// 方法的定义看起来像是 C 代码。它有一个返回值,有参数列表,可能还会有异常列表。
// 注意参数列表和异常列表的语法和结构体里域的定义是一样的
void ping(), // 1
bool postTweet(1:Tweet tweet) throws (1:TwitterUnavailable unavailable), // 2
TweetSearchResult searchTweets(1:string query); // 3
// 'oneway' 声明表示,客户端只会发起请求,但是不会等待应答。
// 'oneway' 方法的返回值必须是 void
oneway void zip() // 4
}
- 方法定义可以用逗号或者分号结尾。
- 参数类型可以是原始类型,也可以是结构体。
- 返回值也是这样。
- 函数返回 void 时合法的。
函数的参数列表和异常列表,和结构体的组织方式是一样的。
服务支持继承,通过 extends 关键字,可以让一个服务继承另外一个服务。
提示
Thrift 不支持嵌套定义,也就是说你不能在一个结构体或者枚举里定义另一个结构体。但是,你可以在结构体/枚举里使用其它结构体。
生成代码
概念
Thrift 网络栈
+-------------------------------------------+
| Server |
| (single-threaded, event-driven etc) |
+-------------------------------------------+
| Processor |
| (compiler generated) |
+-------------------------------------------+
| Protocol |
| (JSON, compact etc) |
+-------------------------------------------+
| Transport |
| (raw TCP, HTTP etc) |
+-------------------------------------------+
Transport 层
Transport 层实现了对网络读写的简单抽象。
Transport 层提供以下方法:
open
close
read
write
flush
Thrift 还提供了 ServerTransport 接口来接受或者创建原始的 Transport 对象,作为对上述方法的补充。ServerTransport 主要用于服务器端。
open
listen
accept
close
对于大部分 Thrift 支持的语言,都有以下 transports 可用:
file
:从磁盘上对文件进行读写http
:对 http 操作
Protocol 层
Protocol 抽象层定义了如何从本机数据结构转换到网络传输序列的机制。所以本层管理着编码模式,并且负责将数据序列化(或者解序)。protocol 的一些例子有 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 支持的语言都可以使用如下的 protocols:
- binary:简单的二进制编码,域的长度和类型以字节形式跟在实际的域值后面
- compact:定义在 THRIFT-110 中
- json
Processor
Processor 提供了从 输入输出流中读写数据的功能。输入输出流以 Protocol 对象的形式体现。它的接口很简单:
interface TProcessor {
bool process(TProtocol in, TProtocol out) throws TException
}
服务端专用的 processor 实现由编译器生成。这个 Processor 使用输入 protocol 读入数据,交给用户定义的函数处理后,把结果通过输出 protocol 输出。
Server
Server 把上述所有层级都包含进来:
- 创建一个 transport
- 为 transport 创建输入输出 protocol
- 根据 protocol 创建 processor
- 等待连接并把连接移交给 processor
示例 IDL
namespace cpp thrift.example
enum TweetType {
TWEET,
RETWEET = 2,
DM = 0xa,
REPLY
}
struct Location {
1: required double latitude;
2: required double longitude;
}
struct Tweet {
1: required i32 userId;
2: required string userName;
3: required string text;
4: optional Location loc;
5: optional TweetType tweetType = TweetType.TWEET;
16: optional string language = "english";
}
typedef list<Tweet> TweetList
struct TweetSearchResult {
1: TweetList tweets;
}
exception TwitterUnavailable {
1: string message;
}
const i32 MAX_RESULTS = 100;
service Twitter {
void ping(),
bool postTweet(1:Tweet tweet) throws (1:TwitterUnavailable unavailable),
TweetSearchResult searchTweets(1:string query);
oneway void zip()
}
C++
生成的文件
- 所有常量都会写入一个单独的 .cpp/.h 文件组合中。
- 所有类型定义(枚举和结构体)都会写入另一个.cpp/.h 组合中
- 每一个 service 都会生成其独有的 .cpp/.h 组合。
比如对应上面的示例 IDL,生成的 C++ 文件为:
$ tree gen-cpp
|-- example_constants.cpp
|-- example_constants.h
|-- example_types.cpp
|-- example_types.h
|-- Twitter.cpp
|-- Twitter.h
`-- Twitter_server.skeleton.cpp
Thrift 类型和 C++ 类型的对应关系如下:
bool
:bool
binary
:std::string
byte
:int8_t
i16
:int16_t
i32
:int32_t
i64
:int64_t
double
:double
string
:std::string
list<t1>
:std::vector<t1>
set<t1>
:std::set<t1>
map<t1,t2>
:std::map<T1, T2>
(完,本文最新修改于 2016/4/27)