目录
1 RPC及Thrift简介
- RPC(Remote Procedure Call Protocol,远程过程调用协议):是一种通过网络从远程计算机程序上请求服务,不需要了解底层网络技术的协议;
- RPC使得程序能够像访问本地系统资源一样访问远端系统资源;
- 关键技术包括:通信协议、序列化、接口描述、服务框架、性能和语言支持等;
- Thrift是用于实现RPC通信的一种框架,支持跨语言,包括C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, OCaml, Delphi等;
- Thrift是一种典型的CS(客户端/服务端)结构,客户端和服务端可以使用不同的语言开发。既然客户端和服务端能使用不同的语言开发,就一定要有一种中间语言来关联客户端和服务端的语言,这种语言就是IDL(Interface Description Language);
- Thrift结合了功能强大的软件堆栈和代码生成引擎,通过建立thrift文件来定义数据类型和服务接口,以作为输入文件,编译器生成代码用来方便地实现RPC客户端和服务器之间的通信;
- Thrift 是完全静态化的,当数据结构发生变化时,必须重新进行编辑IDL文件、代码生成再编译载入的流程。Thrift 适用于搭建大型数据交换及存储的通用工具,在大型系统中的内部数据传输上相对于 JSON 和 XML 无论在性能、传输大小上有明显的优势。
2 Thrift工作原理
- 传输使用socket,数据以特定格式发送,接收方进行解析;
- 定义thrift IDL文件,由thrift IDL文件生成双方语言的接口和model,在生成的接口和model中会有解码编码的代码;
3 Thrift整体架构
- 黄色部分是用户实现的业务逻辑;
- 褐色部分是根据Thrift定义的服务接口描述文件生成的客户端和服务器端代码框架;
- 红色部分是根据Thrift文件生成代码实现数据的读写操作;
- 红色部分以下是Thrift的协议、传输体系以及底层的IO通信,使用thrift可以很方便的定义一个服务并且选择不同的传输协议和传输层;
4 Thrift 三大重要组件:Protocol、Transport 和 Server
Protocol 定义了消息是怎样序列化的;Transport 定义了消息是怎样在客户端和服务器端之间通信的;Server 用于从Transport 接收序列化的消息,根据 Protocol 反序列化之,调用用户定义的消息处理器,并序列化消息处理器的响应,然后再将它们写回 Transport。
4.1 传输方式Transport
- TSocket:阻塞型socket,用于客户端,采用系统函数read和write进行读写数据;
- TNonblockingServerSocket:非阻塞型socket,用于服务端,accept到的socket类型都是TSocket;
- TBufferedTransport和TFramedTransport:非阻塞有缓存,均继承TBufferBase,调用下一层TTransport类进行读写操作,结构非常相似。但TFramedTransport以帧为传输单位,帧结构为:4字节+传输字符串,4字节用来存储传输字符串的长度,该字符串才是真正需要传输的数据,因此TFramedTransport每传一帧都要比TBufferedTransport和TSocket多传4字节;
- TMemoryBuffer:继承TBufferBase,用于程序内部通信用,不涉及任何网络I/O;
- TFileTransport:直接继承TTransport,用于写数据到文件;
- TFDTransport:简单地写数据到文件和从文件读数据,其read和write函数直接调用系统函数read和write;
- TSimpleFileTransport:继承 TFDTransport,没有添加任何成员函数和成员变量,不同的是构造函数的参数和在 TSimpleFileTransport 构造函数里对父类进行了初始化(打开指定文件并将fd传给父类和设置父类的close_policy为CLOSE_ON_DESTROY);
- TZlibTransport:和TBufferedTransport 和 TFramedTransport一样,调用下一层 TTransport 类进行读写操作。它采用<zlib.h>提供的 zlib 压缩和解压缩库函数来进行压解缩,写时先压缩再调用底层 TTransport 类发送数据,读时先调用 TTransport 类接收数据再进行解压,最后供上层处理;
- TSSLSocket:继承 TSocket,阻塞型 socket,用于客户端。采用 openssl 的接口进行读写数据;
- TSSLServerSocket:继承 TServerSocket,非阻塞型 socket, 用于服务端。accecpt 到的 socket 类型都是 TSSLSocket 类型;
- THttpClient 和 THttpServer:基于 Http1.1 协议,均继承THttpTransport,其中 THttpClient 用于客户端,THttpServer 用于服务器端。两者都调用下一层 TTransport 类进行读写操作,均用到TMemoryBuffer 作为读写缓存,只有调用 flush() 函数才会将真正调用网络 I/O 接口发送数据;
TTransport 是所有 Transport 类的父类,为上层提供了统一的接口而且通过 TTransport 即可访问各个子类不同实现,类似多态。
4.2 传输协议Protocol
Thrift传输协议总体上可划分为二进制(binary)和文本(text)传输协议两大类,一般在生产环境中使用二进制类型的传输协议(相比于文本和JSON传输效率更高)。
- TBinaryProtocol:使用二进制编码格式进行数据传输,Thrift的默认协议;
- TCompactProtocol:使用高效率的、密集的二进制编码格式进行数据传输,推荐使用;
- TJSONProtocol:使用JSON数据编码格式进行数据传输;
- TSimpleJSONProtocol:使用JSON只写的数据编码格式进行数据传输,适用于通过脚本语言解析;
- TDebugProtocol:使用text编码格式进行数据传输,可读性强,常用于编码人员测试;
4.3 服务模型Server
4.3.1 TSimpleServer模式
- TSimpleServer的工作模式下只有一个工作线程,循环监听新请求的到来并完成对请求的处理,它只是在简单的演示时候使用;
- TSimpleServer的工作模式采用最简单的阻塞IO,实现方法简洁明了,便于理解,但是一次只能接收和处理一个socket连接,效率比较低,主要用于演示Thrift的工作过程,在实际开发过程中很少用到它。
4.3.2 TNonblockingServer模式
TNonblockingServer也是单线程工作,但是该模式采用NIO的方式,所有的socket都被注册到selector中,在一个线程中通过seletor循环监控所有的socket;每次selector结束时,处理所有的处于就绪状态的socket:对于监听socket产生一个新业务socket并将其注册到selector中,对于有数据到来的socket进行数据读取操作,对于有数据发送的socket进行数据发送操作。
TNonblockingServer模式优点:
相比于TSimpleServer效率提升主要体现在IO多路复用上,TNonblockingServer采用非阻塞IO,同时监控多个socket的状态变化。
TNonblockingServer模式缺点:
TNonblockingServer模式在业务处理上还是采用单线程顺序来完成,在业务处理比较复杂、耗时的时候,例如某些接口函数需要读取数据库执行时间较长,此时该模式效率也不高,因为多个调用请求任务依然是一个接一个顺序执行。
4.3.3 THsHaServer模式(半同步半异步)
THsHaServer类是TNonblockingServer类的子类,TNonblockingServer模式中,采用一个线程来完成对所有socket的监听和业务处理,造成了效率的低下,THsHaServer模式的引入则是部分解决了这些问题。THsHaServer模式中,引入一个线程池来专门进行业务处理。
THsHaServer的优点:
与TNonblockingServer模式相比,THsHaServer在完成数据读取之后,将业务处理过程交由一个线程池来完成,主线程直接返回进行下一次循环操作,效率大大提升。
THsHaServer的缺点:
主线程需要完成对所有socket的监听以及数据读写的工作,当并发请求数较多,且发送数据量较大时,监听socket上新连接请求不能被及时接收。
4.3.4 TThreadPoolServer模式
TThreadPoolServer模式采用阻塞socket方式工作,,主线程负责阻塞式监听“监听socket”中是否有新socket到来,业务处理交由一个线程池来处理。
TThreadPoolServer模式优点:
线程池模式中,数据读取和业务处理都交由线程池完成,主线程只负责监听新连接,因此在并发量较大时新连接也能够被及时接收。线程池模式比较适合服务器端能预知最多有多少个客户端并发的情况,这时每个请求都能被业务线程池及时处理,性能也非常高。
TThreadPoolServer模式缺点:
线程池模式的处理能力受限于线程池的工作能力,当并发请求数大于线程池中的线程数时,新请求也只能排队等待。
4.3.5 TThreadedSelectorServer模式
TThreadedSelectorServer模式是目前Thrift提供的最高级的模式,它内部有如下几个部分构成:
- 一个AcceptThread线程对象,专门用于处理监听socket上的新连接;
- 若干个SelectorThread对象专门用于处理业务socket的网络I/O操作,所有网络数据的读写均是由这些线程来完成;
- 一个负载均衡器SelectorThreadLoadBalancer对象,主要用于AcceptThread线程接收到一个新socket连接请求时,决定将这个新连接请求分配给哪个SelectorThread线程。
- 一个ExecutorService类型的工作线程池,在SelectorThread线程中,监听到业务socket中有调用请求过来,则将请求读取之后,交给ExecutorService线程池中的线程完成此次调用的具体执行;
TThreadedSelectorServer模式中有一个专门的线程AcceptThread用于处理新连接请求,因此能够及时响应大量并发连接请求;另外它将网络I/O操作分散到多个SelectorThread线程中来完成,因此能够快速对网络I/O进行读写操作,能够很好地应对网络I/O较多的情况;TThreadedSelectorServer对于大部分应用场景性能都不会差,因此,如果实在不知道选择哪种工作模式,使用TThreadedSelectorServer就可以。
5 Thrift IDL文件
5.1 数据类型
- bool:Boolean, one byte
- i8(byte):Signed 8-bit integer
- i16:Signed 16-bit integer
- i32:Signed 32-bit integer
- i64:Signed 64-bit integer
- double:64-bit floating point value
- string:String
- binary:byte[]
- map<t1,t2>:Map from one type to another
- list<t1>:Ordered list of one type
- set<t1>:Set of unique elements of one type
5.2 结构体(struct)
- 由字段组成,每个字段都有一个整数标识符、一个类型、一个符号名和一个可选的默认值;
- 字段可以声明为"optional",这样可以确保在未设置字段的情况下,它们不会出现在序列化输出中;
例
struct Work {
1: i32 num1 = 0,
2: i32 num2,
3: Operation op,
4: optional string comment
}
5.3 枚举(enum)
可以定义枚举类型,都是32位整数。
例
enum Operation {
ADD = 1,
SUBTRACT = 2,
MULTIPLY = 3,
DIVIDE = 4
}
5.4 异常(exception)
可以自定义异常,规则与struct一样
例
exception InvalidOperation {
1: i32 whatOp,
2: string why
}
5.5 服务(service)
Thrift中的service就相当于Java中的Interface,里面包含若干空方法,还可以使用extends继承其他service。
例
service Calculator extends shared.SharedService {
void ping(),
i32 add(1:i32 num1, 2:i32 num2),
i32 calculate(1:i32 logid, 2:Work w) throws (1:InvalidOperation ouch),
/**
* This method has a oneway modifier. That means the client only makes
* a request and does not listen for any response at all. Oneway methods
* must be void.
*/
oneway void zip()
}
5.6 类型定义
Thrift支持类似C语言一样的typedef定义:
例
typedef i32 int
typedef i64 long
5.7 常量
Thrift使用const关键字支持常量定义,复杂类型和结构使用JSON符号指定。
const i32 INT_CONST = 1234; // 1
const map<string,string> MAP_CONST = {"hello": "world", "goodnight": "moon"}
5.8 命名空间
- Thrift的命名空间相当于Java中的package,使用namespace来定义,主要目的是组织代码;
- 格式:namespace 语言名 路径
例
namespace java com.example.project
翻译成Java即:
package com.example.project
5.9 文件包含
Thrift中的文件包含相当于Java中的import,使用关键字include定义。
include "tweet.thrift" //文件名必须加引号,最后没有分号
struct TweetSearchResult {
1: list<tweet.Tweet> tweets; //文件名tweet作为前缀
}
5.10 注释
支持shell风格以及C++/Java风格的注释,即#、//、/* */都可以。
# This is a valid comment.
/*
* This is a multi-line comment.
* Just like in C.
*/
// C++/Java style single-line comments work just as well.
5.11 可选与必选
Thrift提供两个关键字required、optional,分别表示对应的字段是必填的还是可选的。
例
struct People {
1: required string name,
2: optional i32 age
}
6 案例
6.1 定义thrift文件
- shared.thrift
namespace java shared
namespace py shared
struct SharedStruct {
1: i32 key
2: string value
}
service SharedService {
SharedStruct getStruct(1: i32 key)
}
- tutorial.thrift
include "shared.thrift"
namespace java tutorial
namespace py tutorial
enum Operation {
ADD = 1,
SUBTRACT = 2,
MULTIPLY = 3,
DIVIDE = 4
}
struct Work {
1: i32 num1 = 0,
2: i32 num2,
3: Operation op,
4: optional string comment,
}
exception InvalidOperation {
1: i32 whatOp,
2: string why
}
service Calculator extends shared.SharedService {
void ping(),
i32 add(1:i32 num1, 2:i32 num2),
i32 calculate(1:i32 logid, 2:Work w) throws (1:InvalidOperation ouch),
/**
* This method has a oneway modifier. That means the client only makes
* a request and does not listen for any response at all. Oneway methods
* must be void.
*/
oneway void zip()
}
6.2 生成代码
- 在定义好thrift文件后,使用命令行生成我们需要的目标语言的源码;
- cd到thrift文件所在目录,使用命令
thrift --gen java shared.thrift
、thrift --gen java tutorial.thrift
、thrift --gen py shared.thrift
、thrift --gen py tutorial.thrift
(需要提前安装好thrift编译器);
生成代码如下所示
6.3 创建CalculatorHandler.java实现Calculator.Iface接口
- CalculatorHandler.java
import tutorial.*;
import shared.*;
import java.util.HashMap;
public class CalculatorHandler implements Calculator.Iface {
private HashMap<Integer,SharedStruct> log;
public CalculatorHandler() {
log = new HashMap<Integer, SharedStruct>();
}
public void ping() {
System.out.println("ping()");
}
public int add(int n1, int n2) {
System.out.println("add(" + n1 + "," + n2 + ")");
return n1 + n2;
}
public int calculate(int logid, Work work) throws InvalidOperation {
System.out.println("calculate(" + logid + ", {" + work.op + "," + work.num1 + "," + work.num2 + "})");
int val = 0;
switch (work.op) {
case ADD:
val = work.num1 + work.num2;
break;
case SUBTRACT:
val = work.num1 - work.num2;
break;
case MULTIPLY:
val = work.num1 * work.num2;
break;
case DIVIDE:
if (work.num2 == 0) {
InvalidOperation io = new InvalidOperation();
io.whatOp = work.op.getValue();
io.why = "Cannot divide by 0";
throw io;
}
val = work.num1 / work.num2;
break;
default:
InvalidOperation io = new InvalidOperation();
io.whatOp = work.op.getValue();
io.why = "Unknown operation";
throw io;
}
SharedStruct entry = new SharedStruct();
entry.key = logid;
entry.value = Integer.toString(val);
log.put(logid, entry);
return val;
}
public SharedStruct getStruct(int key) {
System.out.println("getStruct(" + key + ")");
return log.get(key);
}
public void zip() {
System.out.println("zip()");
}
}
6.4 创建JavaServer.java实现服务端
- JavaServer.java
import org.apache.thrift.TProcessorFactory;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.server.TServer;
import org.apache.thrift.server.TThreadedSelectorServer;
import org.apache.thrift.transport.TNonblockingServerSocket;
import org.apache.thrift.transport.layered.TFramedTransport;
import tutorial.*;
public class JavaServer {
public static CalculatorHandler handler;
public static Calculator.Processor processor;
public static void main(String [] args) {
try {
handler = new CalculatorHandler();
processor = new Calculator.Processor(handler);
TNonblockingServerSocket serverTransport = new TNonblockingServerSocket(9090);
TThreadedSelectorServer.Args arg = new TThreadedSelectorServer.Args(serverTransport);
arg.protocolFactory(new TBinaryProtocol.Factory());
arg.transportFactory(new TFramedTransport.Factory());
arg.processorFactory(new TProcessorFactory(processor));
TServer server = new TThreadedSelectorServer(arg);
System.out.println("Starting the server...");
server.serve();
} catch (Exception x) {
x.printStackTrace();
}
}
}
6.5 创建JavaClient.java实现客户端
- JavaClient.java
import org.apache.thrift.transport.layered.TFramedTransport;
import org.apache.thrift.TException;
import org.apache.thrift.transport.TTransport;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import tutorial.*;
import shared.*;
public class JavaClient {
public static void main(String [] args) {
try {
TTransport transport = new TFramedTransport(new TSocket("localhost", 9090));
TProtocol protocol = new TBinaryProtocol(transport);
Calculator.Client client = new Calculator.Client(protocol);
transport.open();
perform(client);
transport.close();
} catch (TException x) {
x.printStackTrace();
}
}
private static void perform(Calculator.Client client) throws TException
{
client.ping();
System.out.println("ping()");
int sum = client.add(1,1);
System.out.println("1+1=" + sum);
Work work = new Work();
work.op = Operation.DIVIDE;
work.num1 = 1;
work.num2 = 0;
try {
int quotient = client.calculate(1, work);
System.out.println("Whoa we can divide by 0");
} catch (InvalidOperation io) {
System.out.println("Invalid operation: " + io.why);
}
work.op = Operation.SUBTRACT;
work.num1 = 15;
work.num2 = 10;
try {
int diff = client.calculate(1, work);
System.out.println("15-10=" + diff);
} catch (InvalidOperation io) {
System.out.println("Invalid operation: " + io.why);
}
SharedStruct log = client.getStruct(1);
System.out.println("Check log: " + log.value);
}
}
6.6 运行测试
先运行JavaServer.java创建Server并启动,然后运行JavaClient.java创建Client并进行远程调用,运行结果如下:
JavaClient
ping()
1+1=2
Invalid operation: Cannot divide by 0
15-10=5
Check log: 5
JavaServer
Starting the server...
ping()
add(1,1)
calculate(1, {DIVIDE,1,0})
calculate(1, {SUBTRACT,15,10})
getStruct(1)
链接: Python服务端源码解析.