thrift
RPC基本原理
RPC(Remote Procedure Call),远程过程调用,大部分的RPC框架都遵循如下三个开发步骤:
- 定义一个接口说明文件:描述了对象(结构体)、对象成员、接口方法等一系列信息;
- 通过RPC框架所提供的编译器,将接口说明文件编译成具体的语言文件;
- 在客户端和服务器端分别引入RPC编译器所生成的文件,即可像调用本地方法一样调用服务端代码。
RPC通信过程如下图所示:
通信过程包括以下几个步骤:
- 客户过程以正常方式调用客户桩(client stub,一段代码);
- 客户桩生成一个消息,然后调用本地操作系统;
- 客户端操作系统将消息发送给远程操作系统;
- 远程操作系统将消息交给服务器桩(server stub,一段代码);
- 服务器桩将参数提取出来,然后调用服务器过程;
- 服务器执行要求的操作,操作完成后将结果返回给服务器桩;
- 服务器桩将结果打包成一个消息,然后调用本地操作系统;
- 服务器操作系统将含有结果的消息发送回客户端操作系统;
- 客户端操作系统将消息交给客户桩;
- 客户桩将结果从从消息中提取出来,返回给调用它的客户过程
所有这些步骤的效果是,将客户过程对客户桩发出的本地调用转换成对服务器过程的本地调用,而客户端和服务器都不会意识到有中间步骤的存在。
这个时候,你可能会想,既然是调用另一台机器的服务,使用 RESTful API 也可以实现啊,为什么要选择 RPC 呢?我们可以从两个方面对比:
- 资源粒度。RPC 就像本地方法调用,RESTful API 每一次添加接口都可能需要额外地组织开放接口的数据,这相当于在应用视图中再写了一次方法调用,而且它还需要维护开发接口的资源粒度、权限等;
- 流量消耗。RESTful API 在应用层使用 HTTP 协议,哪怕使用轻型、高效、传输效率高的 JSON 也会消耗较大的流量,而 RPC 传输既可以使用 TCP 也可以使用 UDP,而且协议一般使用二制度编码,大大降低了数据的大小,减少流量消耗。
对接异构第三方服务时,通常使用 HTPP/RESTful 等公有协议,对于内部的服务调用,应用选择性能更高的二进制私有协议。
Thrift架构
thrift主要用于各个服务之间的RPC通信,支持跨语言。thrift是一个典型的CS结构,客户端和服务端可以使用不同的语言开发,thrift通过IDL(Interface Description Language)来关联客户端和服务端。thrift的整体架构图如下图所示:
图中Your Code是用户实现的业务逻辑,接下来的 FooService.Client和 Foo.write()/read()是thrift根据IDL生成的客户端和服务端的代码,对应于RPC中Client stub和Server stub。TProtocol 用来对数据进行序列化与反序列化,具体方法包括二进制,JSON 或者 Apache Thrift 定义的格式。TTransport 提供数据传输功能,使用 Apache Thrift 可以方便地定义一个服务并选择不同的传输协议。
如下图所示为thrift的网络栈结构
thirft使用socket进行数据传输,数据以特定的格式发送,接收方进行解析。我们定义好thrift的IDL文件后,就可以使用thrift的编译器来生成双方语言的接口、model,在生成的model以及接口代码中会有解码编码的代码。
TTransport层
代表thrift的数据传输方式,thrift定义了如下几种常用数据传输方式
- TSocket: 阻塞式socket;
- TFramedTransport: 以frame为单位进行传输,非阻塞式服务中使用;
- TFileTransport: 以文件形式进行传输;
TProtocol层
代表thrift客户端和服务端之间传输数据的协议,通俗来讲就是客户端和服务端之间传输数据的格式(例如json等),thrift定义了如下几种常见的格式
- TBinaryProtocol: 二进制格式;
- TCompactProtocol: 压缩格式;
- TJSONProtocol: JSON格式;
- TSimpleJSONProtocol: 提供只写的JSON协议;
thrift支持的Server模型
thrift主要支持以下几种服务模型
- TSimpleServer: 简单的单线程服务模型,常用于测试;
- TThreadPoolServer: 多线程服务模型,使用标准的阻塞式IO;
- TNonBlockingServer: 多线程服务模型,使用非阻塞式IO(需要使用TFramedTransport数据传输方式);
- THsHaServer: THsHa引入了线程池去处理,其模型读写任务放到线程池去处理,Half-sync/Half-async处理模式,Half-async是在处理IO事件上(accept/read/write io),Half-sync用于handler对rpc的同步处理;
thrift IDL文件
thrift IDL不支持无符号的数据类型,因为很多编程语言中不存在无符号类型,thrift支持一下几种基本的数据类型
- byte: 有符号字节
- i16: 16位有符号整数
- i32: 32位有符号整数
- i64: 63位有符号整数
- double: 64位浮点数
- string: 字符串
此外thrift还支持以下容器类型:
- list: 一系列由T类型的数据组成的有序列表,元素可以重复;
- set: 一系列由T类型的数据组成的无序集合,元素不可重复;
- map: 一个字典结构,Key为K类型,Value为V类型,相当于java中的HashMap;
- thrift容器中元素的类型可以是除了service之外的任何类型,包括exception
thirft支持struct类型,目的就是讲一些数据聚合在一起,方便传输管理,struct定义形式如下:
struct People {
1:string name;
2:i32 age;
3:string gender;
}
thrift支持枚举类型,定义形式如下
enum Gender {
MALE,
FEMALE
}
thrift支持自定义异常类型exception,异常定义形式如下:
exception RequestException {
1:i32 code;
2:string reason;
}
thrift定义服务相当于Java中创建接口一样,创建的service经过代码生thrift代码生成工具编译后就会生成客户端和服务端的框架代码,service的定义形式如下:
service HelloWorldService {
// service中可以定义若干个服务,相当于Java Interface中定义的方法
string doAction(1:string name, 2:i32 age);
}
thrift支持给类型定义别名,如下所示:
typedef i32 int
typedef i64 long
thrift也支持常量的定义,使用const
关键字:
const i32 MAX_RETRIES_TIME = 10;
const string MY_WEBSITE = "http://facebook.com";
thrift支持命名空间,命名空间相当于Java中的package,主要用于组织代码,thrift使用关键字namespace
定义命名空间,格式是namespace 语言名 路径
,如下示例所示:
namespace java com.test.thrift.demo
thrift也支持文件包含,相当于CPP中的include
,Java中的import
,使用关键字include
:
include "global.thrift"
#
、//
、/**/
都可以作为thrift文件中的注释。
thrift提供两个关键字required
和optional
,分别用于表示对应的字段是必填的还是可选的(推荐尽量使用optional
),如下所示:
struct People {
1:required string name;
2:optional i32 age;
}
thrift应用示例
本示例中我们使用java编写thrift的服务端程序,使用python编写thrift的客户端程序。
首先定义thrift IDL文件
// data.thrift
namespace java thrift.generated
namespace py py.thrift.generated
typedef i16 short
typedef i32 int
typedef i64 long
typedef bool boolean
typedef string String
// struct关键字用于定义结构体,相当于面向对象编程语言中的类
struct Person {
// 相当于定义类中的成员,并生成相应的get和set方法,optional表示username这个成员可以没有
1: optional String username,
2: optional int age,
3: optional boolean married
}
// 定义一个异常类型,用于接口中可能抛出的异常
exception DataException {
1: optional String message,
2: optional String callStack,
3: optional String date
}
// 定义服务接口
service PersonService {
Person getPersonByUsername(1: required String username) throws (1: DataException data),
void savePerson(1: required Person person)
}
执行thrift --gen java src/thrift/data.thrift
生成对应的java
代码,并引入到Java工程当中,代码结构如下图所示
编写Java服务端代码,data.thrift
的service中定义了两个服务,我们需要定义相应服务的实现类(相当于handler),如下所示:
import thrift.generated.DataException;
import thrift.generated.Person;
import thrift.generated.PersonService;
public class PersonServiceImpl implements PersonService.Iface {
@Override
public Person getPersonByUsername(String username) throws DataException {
System.out.println("Got Client Param: " + username);
return new Person().setUsername(username).setAge(20).setMarried(false);
}
@Override
public void savePerson(Person person) throws DataException {
System.out.println("Got Client Param:");
System.out.println(person.username);
System.out.println(person.age);
System.out.println(person.married);
}
}
同时我们需要借助thrift为我们提供的类库实现一个服务器来监听rpc请求,代码如下所示:
import org.apache.thrift.TProcessorFactory;
import org.apache.thrift.protocol.TCompactProtocol;
import org.apache.thrift.server.THsHaServer;
import org.apache.thrift.server.TServer;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TNonblockingServerSocket;
import thrift.generated.PersonService;
public class ThriftServer {
public static void main(String[] args) throws Exception {
// 定义服务器使用的socket类型
TNonblockingServerSocket tNonblockingServerSocket = new TNonblockingServerSocket(8899);
// 创建服务器参数
THsHaServer.Args arg = new THsHaServer.Args(tNonblockingServerSocket).minWorkerThreads(2).maxWorkerThreads(4);
// 请求处理器
PersonService.Processor<PersonServiceImpl> processor = new PersonService.Processor<>(new PersonServiceImpl());
// 配置传输数据的格式
arg.protocolFactory(new TCompactProtocol.Factory());
// 配置数据传输的方式
arg.transportFactory(new TFramedTransport.Factory());
// 配置处理器用来处理rpc请求
arg.processorFactory(new TProcessorFactory(processor));
// 本示例中使用半同步半异步方式的服务器模型
TServer server = new THsHaServer(arg);
System.out.println("Thrift Server Started!");
// 启动服务
server.serve();
}
}
编写python客户端,执行thrift --gen py src/thrift/data.thrift
生成对应的python
代码,并引入到python工程当中,代码结构如下图所示
python客户端代码如下所示:
# -*- coding:utf-8 -*-
__author__ = 'kpzhang'
from py.thrift.generated import PersonService
from py.thrift.generated import ttypes
from thrift import Thrift
from thrift.transport import TSocket
from thrift.transport import TTransport
from thrift.protocol import TCompactProtocol
import sys
reload(sys)
sys.setdefaultencoding('utf8')
try:
tSocket = TSocket.TSocket("localhost", 8899)
tSocket.setTimeout(900)
transport = TTransport.TFramedTransport(tSocket)
protocol = TCompactProtocol.TCompactProtocol(transport)
client = PersonService.Client(protocol)
transport.open()
person = client.getPersonByUsername("张三")
print person.username
print person.age
print person.married
print '---------------------'
newPerson = ttypes.Person();
newPerson.username = "李四"
newPerson.age = 30
newPerson.married = True
client.savePerson(newPerson)
transport.close()
except Thrift.TException, tx:
print '%s' % tx.message
客户端可以向调用本地的方法一样调用服务端的方法。