Thrift入门教程
Thrift最初由Facebook研发,主要用于各个服务之间的RPC通信,支持跨语言,常用的语言比如C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml都支持。Thrift是一个典型的CS(客户端/服务端)结构,客户端和服务端可以使用不同的语言开发。既然客户端和服务端能使用不同的语言开发,那么一定就要有一种中间语言来关联客户端和服务端的语言,没错,这种语言就是IDL(Interface Description Language)。
Thrift IDL
本节介绍Thrift的接口定义语言,Thrift IDL支持的数据类型包含:
基本类型
thrift不支持无符号类型,因为很多编程语言不存在无符号类型,比如java
bool:布尔值,true 或 false,对应 Java 的 boolean
byte: 有符号字节,对应Java的byte
i16: 16位有符号整数,对应Java的short
i32: 32位有符号整数,对应Java的int
i64: 64位有符号整数,对应Java的long
double: 64位浮点数,对应Java的double
string: 未知编码文本或二进制字符串,对应Java的String
容器类型
集合中的元素可以是除了service之外的任何类型,包括exception。
list: 一系列由T类型的数据组成的有序列表,元素可以重复
set: 一系列由T类型的数据组成的无序集合,元素不可重复
map:一个字典结构,key为K类型,value为V类型
结构体(struct)
就像C语言一样,thrift也支持struct类型,目的就是将一些数据聚合在一起,方便传输管理。在Java 中是一个JavaBean。成员是被正整数编号过的,其中的编号使不能重复的,这个是为了在传输过程中编码使用。struct的定义形式如下:
struct People {
1: string name;
2: i32 age;
3: string sex;
}
字段会有optional和required之分,但是如果不指定则为无类型。
1.无类型可以不填充值,但是在序列化传输的时候也会序列化进去,序列化之后值为对应数据类型的默认值:
(1)i32类型,在服务端使用isSetXxx()方法时,无论有没有填充结果返回true,服务端获取对应值为0 。
(2)string类型,如果没有填充值,在服务端使用isSetXxx()方法时结果返回false,在服务端获取对应值为null;如果填充null值,在服务端使用isSetXxx()方法时结果返回false,在服务端获取对应值为null。
2.optional是不填充则不序列化,如果没有填充值,在服务端使用isSetXxx()方法时结果返回false,获取该值时结果为对应数据类型的默认值:
(1)string类型如果填充结果为null,在服务端使用isSetXxx()方法时结果返回false,服务端获取结果为null;
(2)i32类型如果填充结果为0,在服务端使用isSetXxx()方法时结果返回true,服务端获取结果为0
3.required是必须填充也必须序列化,如果不填充该字段则会报错。
同一文件可以定义多个struct,也可以定义在不同的文件,进行include引入。每个字段可以设置默认值。
枚举(enum)
枚举的定义形式和Java的Enum定义差不多,例如:
enum Sex {
MALE,
FEMALE
}
异常(exception)
thrift支持自定义exception,规则和struct一样,所定义的异常会继承对应语言的异常基类,例如java,就会继承 java.lang.Exception。如下:
exception RequestException {
1: i32 code;
2: string reason;
}
服务(service)
thrift定义服务相当于Java中创建Interface一样,创建的service经过代码生成命令之后就会生成客户端和服务端的框架代码。定义形式如下:
service HelloWordService {
// service中定义的函数,相当于Java interface中定义的函数
string doAction(1: string name, 2: i32 age);
}
类型定义
thrift支持类似C++一样的typedef定义,比如:
typedef i32 Integer
typedef i64 Long
注意,末尾没有逗号或者分号
常量(const)
thrift也支持常量定义,使用const关键字,例如:
const i32 MAX_RETRIES_TIME = 10
const string MY_WEBSITE = "http://qifuguang.me";
末尾的分号是可选的,可有可无,并且支持16进制赋值
命名空间
thrift的命名空间相当于Java中的package的意思,主要目的是组织代码。thrift使用关键字namespace定义命名空间,例如:
namespace java com.winwill.thrift
格式是:namespace 语言名 路径
注意末尾不能有分号。
文件包含
thrift也支持文件包含,相当于C/C++中的include,Java中的import。使用关键字include定义,例如:
include "global.thrift"
注释
thrift注释方式支持shell风格的注释,支持C/C++风格的注释,即#和//开头的语句都单当做注释,/**/包裹的语句也是注释。
生成代码
知道了怎么定义thirtf文件之后,我们需要用定义好的thrift文件生成我们需要的目标语言的源码,本文以生成java源码为例。假设现在定义了如下一个thrift文件:Test.thrift
namespace java com.zl.thrift
enum RequestType {
SAY_HELLO, //问好
QUERY_TIME, //询问时间
}
struct Request {
1: required RequestType type; // 请求的类型,必选
2: string name; // 发起请求的人的名字
3: string oldName; // 发起请求的人的名字
4: i32 age; // 发起请求的人的年龄
5: optional string address; //发起请求人的地址
6: optional i32 account //发起请求人的邮编
}
exception RequestException {
1: required i32 code;
2: optional string reason;
}
// 服务名
service HelloWordService {
string doAction(1: Request request) throws (1:RequestException qe); // 可能抛出异常。
}
在终端运行如下命令(将thrift-0.9.2.exe与.thrift文件放入同一个路径下,进入CMD执行命令):
thrift-0.9.2 --gen java Test.thrift
则在当前目录会生成一个gen-java目录,该目录下会按照namespace定义的路径名一次一层层生成文件夹,到gen-java\com\zl\thrif\目录下可以看到生成的4个Java类
可以看到,thrift文件中定义的enum,struct,exception,service都相应地生成了一个Java类,这就是能支持Java语言的基本的框架代码。
服务端实现
代码生成这一步已经将接口代码生成了,现在需要做的是实现HelloWordService的具体逻辑,实现的方式就是创建一个Java类,implements com.winwill.thrift.HelloWordService.Iface,例如:
package com.zl.thrift;
import org.apache.commons.lang.StringUtils;
import org.apache.thrift.TException;
import java.util.Date;
public class HelloWordServiceImpl implements com.zl.thrift.HelloWordService.Iface {
// 实现这个方法完成具体的逻辑。
public String doAction(com.zl.thrift.Request request) throws com.zl.thrift.RequestException, TException {
System.out.println("Get request: " + request);
if (StringUtils.isBlank(request.getName()) || request.getType() == null) {
throw new com.zl.thrift.RequestException();
}
StringBuilder result = new StringBuilder();
if (request.getType() == com.zl.thrift.RequestType.SAY_HELLO) {
result.append("isSetName:").append(request.isSetName()).append("\n");
result.append("isSetOldName:").append(request.isSetOldName()).append("\n");
result.append("isSetAge:").append(request.isSetAge()).append("\n");
result.append("isSetAddress:").append(request.isSetAddress()).append("\n");
result.append("isSetAccount:").append(request.isSetAccount()).append("\n");
result.append("----------").append("\n");
result.append("Name:").append(request.getName())
.append(", OldName:").append(request.getOldName())
.append(", Age:").append(request.getAge())
.append(", Address:").append(request.getAddress())
.append(", Account:").append(request.getAccount());
} else {
result.append("Now is ").append(new Date().toLocaleString());
}
System.out.println(result.toString());
return result.toString();
}
}
服务端启动服务
上面这个就是服务端的具体实现类,现在需要启动这个服务,所以需要一个启动类,启动类的代码如下:
代码主要实现需要做:
- 实例化服务端,并且关联服务的实现类HelloWordServiceImpl
- 创建server类,关联协议工厂实例、传送方式实例(该实例中设置服务端口)、服务端实例
package com.zl.thrift;
import org.apache.thrift.protocol.TCompactProtocol;
import org.apache.thrift.protocol.TProtocolFactory;
import org.apache.thrift.server.TServer;
import org.apache.thrift.server.TThreadPoolServer;
import org.apache.thrift.transport.TServerSocket;
public class ServerTest {
private final static int port = 7456;
public static void main(String[] args) throws Exception {
/** 创建传输方式实例,并设置监听端口 */
TServerSocket tServerSocket = new TServerSocket(port);
/**
* 实例化服务端对象,并关联Thrift文件中service具体实现类
*/
HelloWordService.Processor<HelloWordServiceImpl> helloWordServiceProcessor =
new HelloWordService.Processor<>(new HelloWordServiceImpl());
/** 采用TCompactProtocol传输协议,服务端和客户端协议使用必须一致
* 可选的协议有:TBinaryProtocol/TJSONProtocol/TSimpleJSONProtocol/TTupleProtocol
* 其中TTupleProtocol是TCompactProtocol的子类,可以根据自己的业务选择对应的协议
*/
TProtocolFactory pf = new TCompactProtocol.Factory();
/**
* 创建server实例,并设置传输方式,传输协议,服务端实例
*/
TServer server =
new TThreadPoolServer(new TThreadPoolServer.Args(tServerSocket).protocolFactory(pf).processor(helloWordServiceProcessor));
System.out.println("server started ,port is " + port);
server.serve();
}
}
运行之后看到控制台的输出为:
server started ,port is 7456
客户端请求
现在服务已经启动,可以通过客户端向服务端发送请求了,客户端的代码如下:
代码主要的完成的工作为:
1、创建客户端实例,创建客户端需要用到传输协议(传输协议中需要关联传输方式)和传输方式实例(传输方式实例中关联服务端ip和port)
2、使用客户端实例取调用服务端的方法
package com.zl.thrift;
import org.apache.thrift.protocol.TCompactProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
public class ClientTest {
public static void main(String[] args) throws Exception {
/* 创建传输方式实例,设置服务端ip和port */
TTransport transport = new TSocket("localhost", 7456);
/* 创建传输协议实例,并设置传输方式 */
TProtocol protocol = new TCompactProtocol(transport);
/* 创建client,关联传输协议 */
com.zl.thrift.HelloWordService.Client client = new com.zl.thrift.HelloWordService.Client(protocol);
/* 建立连接 */
transport.open();
// 第一种请求类型
com.zl.thrift.Request request = new com.zl.thrift.Request();
request.setType(com.zl.thrift.RequestType.SAY_HELLO);
request.setName("zhangsan");
// 通过客户端实例调用服务端实现类中的方法
System.out.println(client.doAction(request));
}
}
运行客户端代码,得到结果:
Received 1
isSetName:true
isSetOldName:false
isSetAge:true
isSetAddress:false
isSetAccount:false
----------
Name:zhangsan, OldName:null, Age:0, Address:null, Account:0并且此时,服务端会有请求日志:
Get request: Request(type:SAY_HELLO, name:zhangsan, oldName:null, age:0)
isSetName:true
isSetOldName:false
isSetAge:true
isSetAddress:false
isSetAccount:false
----------
Name:zhangsan, OldName:null, Age:0, Address:null, Account:0
可以看到,客户端成功将请求发到了服务端,服务端成功地将请求结果返回给客户端,整个通信过程完成。