Thrift框架详解(二)

Thrift IDL 详解

Posted by Jason Lee on 2020-03-01

概诉

前一章节,我们基本入门,了解了 Thrift 的基本用法,这一节继续来分析 Thrift IDL的内容。
Thrift 采用IDL(Interface Definition Language)来定义通用的服务接口,然后通过 Thrift 提供的编译器,可以将服务接口编译成不同语言编写的代码,通过这个方式来实现跨语言的功能。

IDL定义的规范

IDL 结构规范

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

IDL的结构分为Header,Definition两个部分。
Head 只是的是协议头,主要包含三个关键字

  • Include
  • CppInclude
  • Namespace
    Thrift IDL 的 Definition为定义部分。

IDL 语法规范

Identifier

官方文档中的定义如下:
Identifier ::= ( Letter | '_' ) ( Letter | Digit | '.' | '_' )*

复制代码即合法的标识符满足以下条件:

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

IDL Definition 部分

语法组成

::
1
2
3
4
5

## IDL (Field)基本类型
+ 基本类型的语法格式
```java
FieldID? FieldReq? FieldType Identifier ('=' ConstValue)? XsdFieldOptions ListSeparator?

FieldID 为类型的展位符号 通常用int

FieldType

关键字 类型 对应Java中的类型
bool 布尔值 对应Java中的boolean
byte 有符号字节 对应Java中的byte
i16 16位有符号整型 对应Java中的short
i32 32位有符号整型 对应Java中的int
i64 64位有符号整型 对应Java中的long
double 64位浮点型 对应Java中的double
string 字符串 对应Java中的String
binary Blob 类型 对应Java中的byte[]
slist

可以是基础类型,容器类型或者合法标识符,这里的合法标识符就是下文中通过 typedef, enum, struct 等关键中声明的类型。

FiledReq

  • required:

    1. 写:必须字段始终写入,并且应该设置这些字段。
    2. 读:必须字段始终读取,并且它们将包含在输入流中
    3. 默认值:始终写入

    注意:如果一个必须字段在读的时候丢失,则会抛出异常或返回错误,所以在版本控制的时候,要严格控制字段的必选和可选,必须字段如果被删或者改为可选,那将会造成版本不兼容。

  • optional:
    1、写:可选字段仅在设置时写入
    2、读:可选字段可能是也可能不是输入流的一部分
    3、默认值:在设置了isset标志时写入

    Thrift使用所谓的“isset”标志来指示是否设置了特定的可选字段, 仅设置了此标志的字段会写入,相反,仅在从输入流中读取字段值时才设置该标志。

  • default:
    1、写:理论上总是写入,但是有一些特例
    2、读:跟optional一样
    3、默认值:可能不会写入

    默认类型是required和optional的结合,可选输入(读),必须输出(写)。

    1
    2
    3
    4
    5
    6

    ### 例子

    ```java
    1: required string name,
    2: required i16 age = 0;

Container (容器)

有3种可用容器类型:

  • list: 元素类型为t的有序表,容许元素重复。对应c++的vector,java的ArrayList或者其他语言的数组

  • set: 元素类型为t的无序表,不容许元素重复。对应c++中的set,java中的HashSet,python中的set,php中没有set,则转换为list类型了

  • map<t, t>: 键类型为t,值类型为t的kv对,键不容许重复。对用c++中的map, Java的HashMap, PHP 对应 array, Python/Ruby 的dictionary

例子:

1
2
3
7: required list<User> friends,
8: optional map<i32, User> mapUser;
9: optional set<User> setUser;

struct结构体

struct语法

1
2
3
4
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 就可以了,但是通常我们都会加上 FieldIdListSeparator, 而 FieldReq 则视情况而定。
FieldId 必须是整型常量加 : 组成。

struct约束:

  1. struct不能继承,但是可以嵌套,不能嵌套自己。
  2. 其成员都是有明确类型
  3. 成员是被正整数编号过的,其中的编号使不能重复的,这个是为了在传输过程中编码使用。
  4. 成员分割符可以是逗号(,)或是分号(;),而且可以混用
  5. 字段会有optional和required之分和protobuf一样,但是如果不指定则为无类型–可以不填充该值,但是在序列化传输的时候也会序列化进去,optional是不填充则部序列化,required是必须填充也必须序列化。
  6. 每个字段可以设置默认值
  7. 同一文件可以定义多个struct,也可以定义在不同的文件,进行include引入。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Friends {
1: required i32 No,
}

struct User {
1: required string name,
2: required i16 age = 0;
3: required bool gender,
4: required i32 No,
5: required i64 createTime,
6: required double grade,
7: required list<Friends> friends,
8: optional map<i32, Friends> mapUser;
9: optional set<Friends> setUser;
//...
}

Union

语法格式

::
1
2
3
4
5
6
7
8
9

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

可以想象这么一个场景,我们收集用户的信息,只要用户填写了手机号或者邮箱中的一个就可以了,这时候我们就可以使用 `Union` 结构来标识这个类型
```java
union UserInfo {
1: string phone,
2: string email
}

常量类型

语法

1
2
3
4
5
6
7
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 关键字进行声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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' }

异常类型

语法

::
1
2
3
4
5
6
7
8
9
10
### 例子
Exception 语法定义与 Struct 相似,但是这个类型通常适合目标语言的异常处理机制配合使用的
```java
exception Error {
1: required i8 Code,
2: string Msg,
}
service ExampleService {
string GetName() throws (1: Error err),
}

枚举类型

语法:

::
1
2
3
4
5
6
7
8
9
10
### 例子
```java
enum fb_status {
DEAD = 0,
STARTING = 1,
ALIVE = 2,
STOPPING = 3,
STOPPED = 4,
WARNING = 5,
}

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

1
2
3
4
5
6
7
8
enum fb_status {
DEAD,
STARTING,
ALIVE,
STOPPING,
STOPPED,
WARNING,
}

Typedef

语法

::
1
2
3
4
### 例子
```java
typedef i8 int8 // 这里把 i8 去个别名 int8, 在后面的定义中就可以使用了
const int8 count = 100 // 等价于 const i8 count = 100

Services类型

语法

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

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

复制代码从语法定义,我们可以看到 Service 的核心就是 Function 的定义

例子

1
2
3
4
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 顾名思义,参考上述示例即可。

IDL Head 部分

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

Include 语法定义

语法

::
1
2
3
4
5
6
7
8
9
10
11
12

复制代码从语法规则上看,Include 由 include 关键字 + thrift 文件路径组成。
在真正的业务开发中,我们不可能把所有的服务都定义到一个文件中,通常会根据业务模块进行拆分,然后将这些服务 include 到一个入口文件中,然后在最终服务发布上线的时候,thrift 编译器只需要编译入口文件,就能将所有引入的文件都生成对应的代码,而且 include 进来的文件中定义的内容都是可见的

### 例子
+ base.thrift
```java
namespace go base

struct Base {
...
}
  • example.thrift
1
2
3
4
5
6
include 'base.thrift'

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

CppInclude

语法

复制代码CppInclude 语法定义
CppInclude ::= ‘cpp_include’ Literal
复制代码CppInclude 主要是用来为当前的 thrift 文件生成的代码中添加一个自定义的 C++ 引入声明 目前没有使用场景,不做过多陈述

Namespace

语法

1
2
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 定义的命名冲突。

例子

1
namespace java com.facebook.fb303

上述示例中 Namespace 声明语句,标识当前的 thrift 文件适用于 java
NamespaceScope 后面紧跟着的 Identifier 在不同语言中会有不一样的表现.

参考



支付宝打赏 微信打赏

赞赏一下