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 部分

语法组成

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

IDL (Field)基本类型

  • 基本类型的语法格式
    1
    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的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

语法格式

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

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

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

1
2
3
4
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' }

异常类型

语法

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

例子

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

1
2
3
4
5
6
7
exception Error {
1: required i8 Code,
2: string Msg,
}
service ExampleService {
string GetName() throws (1: Error err),
}

枚举类型

语法:

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

例子

1
2
3
4
5
6
7
8
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

语法

Typedef ::= 'typedef' DefinitionType Identifier

例子

1
2
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 语法定义

语法

Include ::= 'include' Literal

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

例子

  • base.thrift
    1
    2
    3
    4
    5
    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 在不同语言中会有不一样的表现.

参考



支付宝打赏 微信打赏

赞赏一下