Thrift & IDL 介绍

15,944 阅读12分钟

Thrift & IDL

本文主要是对官方 IDL 文档的一个翻译,官方文档是按照一种有全貌到细节的方式对 thrift 的 IDL 进行描述的,而且文档中直接使用正则来描述语法,所以对于对正则文法不了解的同学来说,读起来确实有些吃力,这也是本文的一个目标:以一种简单易懂的方式介绍 Thrift IDL 的语法

官方文档的正则文法中使用 ' 单引号括起来的内容表示终结符,通常也就是语言中的关键字,没有被括起来的就是非终结符,非终结符最终会被推导为终结符,然后结束。

Thrift

Thrift 是一个提供可扩展,跨语言的服务开发框架,通过其强大的代码生成器,可以和 C++, Java, Python, PHP, Ruby, Erlang, Haskell, C#, Cocoa, Javascript, Node.js, Smalltalk, OCaml, Golang 等多种语言高效且无缝的工作。

Thrift 最初是由 Facebook 作为内部项目开发使用,于 2007.04 开源,2008.05 进入 Apache 孵化器,并于 2010.11 成为 Apache 顶级项目(Top-Level Project, TLP), 至今已有 10+ 年,thrift 功能强大,使用二进制进行传输,速度更快。

IDL(Interface Description Language)

IDL 是 Thrift 的核心,也是 Thrift 编译器直接操作的源码,既然 IDL 是一门语言,那么就有他自己的语法,下面的章节,我们就着重介绍 IDL 的语法

关键字(keywords) & 标识符(Identifer)

IDL 中的关键字如下:

include, cpp_include, namespace, const, typedef, enum, senum, struct, union, exception, service, extends, required, optional, oneway, void, throws, bool, byte, i8, i16, i32, i64, double, string, binary, slist, map, set, list, cpp_type

一些 Facebook 内部才使用的关键字没有列出来,因为非 fb 的服务不推荐使用,如果有对 thrift 编译器进行扩展,建议定义与所属组织或业务相关的关键字

IDL 中标识符用来定义结构名,变量名,服务名,等等,其规则和其他语言一致:

官方文档中的定义如下:

Identifier ::= ( Letter | '_' ) ( Letter | Digit | '.' | '_' )*

即合法的标识符满足以下条件:

  1. 标识符只能由字母,数字,_(under score), .(dot)组成
  2. 只能以字母,_ 开头

其中 Letter 和 Digit 的定义如下:

Letter ::= [ 'A'-'Z' ] | [ 'a'-'z' ]
Digit  ::= [ '0'-'9' ]

翻译过来就是:

  • 字母只能是字母表中的大写 'A' - 'Z' 以及小写 'a' - 'z'
  • 数字只能是 0 - 9

基础类型

语法定义如下:

BaseType ::= 'bool' | 'byte' | 'i8' | 'i16' | 'i32' | 'i64' | 'double' | 'string' | 'binary' | 'slist'

即 IDL 中的基础类型有 10 种(当前 Thrift 版本为 0.13.0,可能随着后续的迭代,可能会引入新的类型,而且项目中使用的版本低于当前版本的,需要注意兼容)

  • bool 布尔型,false | true
  • byte byte
  • i8 int8
  • i16 int16
  • i32 int32
  • i64 int64
  • double 双精度浮点型
  • string 字符串
  • binary 二进制 byte[]
  • slist

容器类型

官方定义如下:

ContainerType ::= MapType | SetType | ListType
MapType       ::= 'map' CppType? '<' FieldType ',' FieldType '>'
SetType       ::= 'set' CppType? '<' FieldType '>'
ListType      ::= 'list' '<' FieldType '>' CppType?
FieldType     ::= BaseType | ContainerType | Identifier
CppType       ::= 'cpp_type' Literal
Literal       ::= ('"' [^"]* '"') | ("'" [^']* "'")

先解释 Literal, 即字面量,在 IDL 中,字面量就是使用单引号 ' 或双引号 " 括起来的字符串

CppType 就是使用 cpp_type 声明的类型

cpp_type 'test'

cpp_type "test"

CppType 的用法暂时不太清楚

FieldType 字段类型,在接下来的介绍中,这个类型会有比较多的类型,从官方定义来看,FieldType 可以是基础类型,容器类型或者合法标识符,这里的合法标识符就是下文中通过 typedef, enum, struct 等关键中声明的类型,所以以下都是合法的容器类型声明

// Map 简单的 key-value 对,key 不可以重复,`<keyType, valueType>` 用来指定 key 和 value 的类型
// 简单的 map 类型
map <string, i8>

// 嵌套 map 类型
map <map <string, i32>, i64>

// 更多的嵌套可能是这样的
map <string, map<string, string>>
map <string, set<i32>>

// Set 不重复的数据集合,`<type>` 指定集合中元素的类型
set <string>
set <map <string, bool>>

// List 类似数组,元素可重复, `<type>` 指定集合中元素的类型
list <i64>
list <set <string>>
list <map<string, set<i8>>>

IDL 定义或者叫声明

IDL 是用来进行接口描述的,描述文件通常以 .thrift 作为扩展类型,IDL 的关键字,基本数据类型我们已经了解了,那到底是怎么使用这些关键字以及基本的数据类型进行接口描述的呢?我们可以先看一个 demo 文件,然后围绕这个 thrift 文件来进行讲解。

fb303.thrift

namespace java com.facebook.fb303
namespace cpp facebook.fb303
namespace perl Facebook.FB303
namespace netstd Facebook.FB303.Test

enum fb_status {
  DEAD = 0,
  STARTING = 1,
  ALIVE = 2,
  STOPPING = 3,
  STOPPED = 4,
  WARNING = 5,
}

service FacebookService {
  string getName(),
  string getVersion(),
  fb_status getStatus(),
  string getStatusDetails(),
  map<string, i64> getCounters(),
  i64 getCounter(1: string key),
  void setOption(1: string key, 2: string value),
  string getOption(1: string key),
  map<string, string> getOptions(),
  string getCpuProfile(1: i32 profileDurationInSec),
  i64 aliveSince(),
  oneway void reinitialize(),
  oneway void shutdown(),
}

上述是 fb 提供的 demo 文件,相对来说比较简单,并没有涵盖所有的功能,从上述的示例中,我们已经可以看到一些前面介绍的关键字以及基础类型,如果还不能完全理解,也不要着急,接下来就会对 IDL 中的所有功能进行介绍

thrift 文件的组成

语法定义:

Document   ::= Header* Definition*
Header     ::= Include | CppInclude | Namespace
Definition ::= Const | Typedef | Enum | Senum | Struct | Union | Exception | Service

从上述的语法定义中我们会返现又有很多新的定义,现在先不要着急,下文会依次介绍到。

首先一个 thrift 文件就是一个 Document, 而一个 thrift 文件又是由若干 Header 和 Definition 组成的,当然也可以是一个空文件(这时候 thrift compiler 不会生成任何文件)

接下来我们再介绍下 Header, 上述示例中

namespace java com.facebook.fb303
namespace cpp facebook.fb303
namespace perl Facebook.FB303
namespace netstd Facebook.FB303.Test

这部分就是 4 个 Header, 他定义了 4 个 Namespace

Header

从语法定义看,Header 可以是 Include, CppInclude, Namespace, 接下来,我们依次进行介绍。

Include 语法定义

Include ::= 'include' Literal

从语法规则上看,Include 由 include 关键字 + thrift 文件路径组成。

在真正的业务开发中,我们不可能把所有的服务都定义到一个文件中,通常会根据业务模块进行拆分,然后将这些服务 include 到一个入口文件中,然后在最终服务发布上线的时候,thrift 编译器只需要编译入口文件,就能将所有引入的文件都生成对应的代码,而且 include 进来的文件中定义的内容都是可见的。比如:

base.thrift

namespace go base

struct Base {
  ...
}

example.thrift

include 'base.thrift'

// 这时,我们就可以引用从 base.thrift 导入的内容了
struct Example {
  1: base.Base ExampleBase
}

CppInclude 语法定义

CppInclude ::= 'cpp_include' Literal

CppInclude 主要是用来为当前的 thrift 文件生成的代码中添加一个自定义的 C++ 引入声明

目前没有使用场景,不做过多陈述

Namespace 语法定义

Namespace ::= ( 'namespace' ( NamespaceScope Identifier ) )
NamespaceScope  ::=  '*' | 'c_glib' | 'cpp' | 'csharp' | 'delphi' | 'go' | 'java' | 'js' | 'lua' | 'netcore' | 'perl' | 'php' | 'py' | 'py.twisted' | 'rb' | 'st' | 'xsd'

Namespace 用来声明使用哪种语言来处理当前 thrift 文件中定义的各种类型,NamespaceScope 就是各种语言的标识,也可以指定为通配符 * 标识,标识 thrift 文件中的定义适用于所有的语言。除此之外 Namespace 还有一个作用就是避免不同 Identifier 定义的命名冲突。

上述示例中有 4 条 Namespace 声明语句,标识当前的 thrift 文件适用于 java, cpp, prel, netstd。

NamespaceScope 后面紧跟着的 Identifier 在不同语言中会有不一样的表现,比如:

namespace go tutorial.thrift.example  // 生成的代码会是一个目录结构 `tutorial/thrift/example/*.go` 其中每个 go 文件都会包含 `package example` 表示属于同一个 package

Definition

Definition 的语法定义如下:

Definition ::= Const | Typedef | Enum | Senum | Struct | Union | Exception | Service

从语法上看,Definition 可以是常量(Const), 类型定义(Typedef),枚举值(Enum),Senum, 结构体(Struct), Union, 异常(Exception), 服务(Service),这些定义就是组成 Thrift 文件的核心内容,我们一一进行介绍。

Senum, Slist 已经不再推荐使用,下文也不再介绍

Const & ConstValue 常量 & 常量值

常量的语法定义

Const          ::= 'const' FieldType Identifier '=' ConstValue ListSeparator?
ConstValue     ::= IntConstant | DoubleConstant | Literal | Identifier | ConstList | ConstMap
IntConstant    ::= ('+' | '-')? Digit+
DoubleConstant ::= ('+' | '-')? Digit* ('.' Digit+)? ( ('E' | 'e') IntConstant )?
ConstList      ::= '[' (ConstValue ListSeparator?)* ']'
ConstMap       ::= '{' (ConstValue ':' ConstValue ListSeparator?)* '}'
ListSeparator  ::= ',' | ';'

先说明下 ListSeparator, 这个分隔符就好比 Java 中一句话结束后的 ;,在 IDL 中分隔符可以是 , 或者 ; 而且大部分情况下可以忽略不写

IDL 中通过 const 关键字进行声明

const string testConst = 'hello,thrift'; // `;` 可以替换为 `,` 也可以不写

常量声明语句中 = 后面的内容就是常量值,IDL 中的合法常量值如下:

// int 类型常量,和 js 中的 number 字面量是一个意思
const i8 count = 100  // 可以是正数,负数(如:-2)

// doubule 类型
const double money = '13.14'   // 同样可正可负
const double rate = 1.2e-5     // 可以使用科学计数法,表示 0.000012
const double salary = 3.5e8    // 表示 350000000

// 常量 list, 类似 js 中的数组字面量
const list<string> names = [ 'tom', 'joney', 'catiy' ]  // 当然 `,` 可以替换成 ';', 也可以不写

// 常量 map, 类似 js 中的对象字面量
const map<string, string> = { 'name': 'johnson', 'age': '20' }

Note: 没有 ConstSet

Typedef 类型定义

看到这里,有没有一种 C 的既视感,没错 IDL 也可以定义类型,语法如下:

Typedef ::= 'typedef' DefinitionType Identifier

这个没啥可介绍的,举个🌰视之:

typedef i8 int8  // 这里把 i8 去个别名 int8, 在后面的定义中就可以使用了

const int8 count = 100 // 等价于 const i8 count = 100

Enum 枚举值

语法定义如下:

Enum ::= 'enum' Identifier '{' (Identifier ('=' IntConstant)? ListSeparator?)* '}'

上述示例中的

enum fb_status {
  DEAD = 0,
  STARTING = 1,
  ALIVE = 2,
  STOPPING = 3,
  STOPPED = 4,
  WARNING = 5,
}

从语法定义来看,('=' IntConstant)? 是一个可选项,也就是说我们可以不用指定值,默认就是从 0 开始递增,如果要指定,那就必须是一个整型常量,如果后续没有再指定,则从第一个指定的整型值开始进行递增. 所以上述示例中的枚举值,也可以写成如下:

enum fb_status {
  DEAD,
  STARTING,
  ALIVE,
  STOPPING,
  STOPPED,
  WARNING,
}

Struct 结构体

结构体可能是最常用的类型定义语句了,语法定义如下:

Struct     ::= 'struct' Identifier 'xsd_all'? '{' Field* '}'
Field      ::=  FieldID? FieldReq? FieldType Identifier ('=' ConstValue)? XsdFieldOptions ListSeparator?
FieldID    ::=  IntConstant ':'
FieldReq   ::=  'required' | 'optional'

xsd_all 是 Facebook 内部的字段,直接忽略,就算你写了,也不会有啥影响

Field 中的 XsdFeildOptions 也是 Facebook 内部字段,直接忽略

从语法定义看,一个 Struct 定义的核心是 Field 字段,而且每个字段的名字在一个 Struct 内要确保是唯一的,Struct 不能继承,但是可以嵌套使用,即可以作为 struct 字段的类型。

接下来就仔细看下 Field 的定义,一个合法的 Field 只需要哟 FieldType 和对应的 Identifier 就可以了,但是通常我们都会加上 FieldId 和 ListSeparator, 而 FieldReq 则视情况而定。

FieldId 必须是整型常量加 : 组成。

FieldReq 则用来标识该字段是必填还是选填,分别是 required, optional 这两个关键字,都不指定,就是默认情况,默认情况可以理解为是 requiredoptional 的混合版,理论上,字段通常是会被序列化的,除非该字段是 thrift 无法传输的内容,那么这个字段就会被忽略掉。

而且,我们可以为字段指定默认值,默认值只能是 ConstValue

struct BaseExample {
  1: required string name,  // 标识为必填
  2: i8 sex = 1,            // 指定默认值
}

struct Example {
  1: i8 age,
  255: BaseExample base,    // 嵌套使用 BaseExample
}

Union

Union 语法定义除了关键字不一样外,基本与 Struct 相同,只不过语义上是有很大差别的,Union 可以定义这样一个结构体,结构体中的字段只要有要给被赋予合法值,就可以被 thrift 传输,而且 Union 结构中的字段默认就是 optional 的,不能使用 required 声明,写了也没意义,其语法定义如下:

Union ::= 'union' Identifier 'xsd_all'? '{' Field* '}'

可以想象这么一个场景,我们收集用户的信息,只要用户填写了手机号或者邮箱中的一个就可以了,这时候我们就可以使用 Union 结构来标识这个类型

union UserInfo {
  1: string phone,
  2: string email
}

Exception 异常定义

Exception 语法定义与 Struct 相似,但是这个类型通常适合目标语言的异常处理机制配合使用的,语法定义如下:

Exception ::= 'exception' Identifier '{' Field* '}'

再举个 🌰 看看是怎么用的, 通常在 Function 中和 throws 配合使用

exception Error {
  1: required i8 Code,
  2: string Msg,
}

service ExampleService {
  string GetName() throws (1: Error err),
}

Service 服务定义

终于到了最后一个非常核心的概念了,Service, 上面的小节中介绍的所有内容都是用来服务 Service 的,Service 提供了我们要暴露的接口,而且,Service 是可以被继承的,Service A 继承了 Service B, A 除了提供自己定义的接口外,他还提供了从 B 继承来的接口。语法定义如下:

Service      ::= 'service' Identifier ( 'extends' Identifier )? '{' Function* '}'
Function     ::= 'oneway'? FunctionType Identifier '(' Field* ')' Throws? ListSeparator?
FunctionType ::= FieldType | 'void'
Throws       ::= 'throws' '(' Field* ')'

从语法定义,我们可以看到 Service 的核心就是 Function 的定义,都到这里了,话不多说,直接拿 🌰 来解释:

service ExampleService {
  oneway void GetName(1: string UserId),
  void GetAge(1: string UserId) throws (1: Error err),
}

oneway 是一个关键字,从字面上,我们就可以了解到,他是单向的,怎么理解呢?非 oneway 修饰的 function 是应答式,即 req-resp, 客户端发请求,服务端返回响应,被 oneway 修饰后的函数,则意味着,客户端只是会发起,无须关注返回,服务端也不会响应,与 void 的区别是 void 类型的方法还可以返回异常。

FunctionType 是任何合法的 FieldType 或者 void 关键字,表示无返回类型

Throws 顾名思义,参考上述示例即可。

Thrift 中 IDL 语法即常规使用就介绍完了,更多阅读可以参考 References

References