1、概述
通过上一篇文章《架构设计:系统间通信(10)——RPC的基本概念》的介绍,相信读者已经理解了基本的RPC概念。为了加深这个理解,后面几篇文章我将详细讲解一款典型的RPC规范的实现Apache Thrift。Apache Thrift的介绍一共分为三篇文章,上篇讲解Apache Thrift的基本使用;中篇讲解Apache Thrift的工作原理(主要围绕Apache Thrift使用的消息格式封装、支持的网络IO模型和它的客户端请求处理方式);下篇对Apache Thrift的不足进行分析,并基于Apache Thrift实现一个自己设计的RPC服务治理的管理方案。这样对我们后续理解Dubbo的服务治理方式会有很好的帮助作用。
2、基本知识
Thrift最初由facebook开发用做系统内各语言之间的RPC框架 。2007年由facebook贡献到apache基金 ,08年5月进入apache孵化器 ,称为Apache Thrift。和其他RPC实现相比,Apache Thrift主要的有点是:支持的语言多(C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, Smalltalk等多种语言)、并发性能高(还记得上篇文章中,我们提到的影响RPC性能的几个关键点吗?)。
为了支持多种语言,Apache Thrift有一套自己的接口定义语言,并且通过Apache Thrift的代码生成程序,能够生成各种编程语言的代码。这样是保证各种语言进行通讯的前提条件。为了能够实现简单的Apache Thrift实例,首先我们就需要讲解一下Apache Thrift的IDL。
2-1、Thrift代码生成程序安装
如果您是在windows环境下运行进行Apache Thrift的试验,那么您无需安装任何工具,直接下载Apache Thrift在windows下的代码生成程序http://www.apache.org/dyn/closer.cgi?path=/thrift/0.9.3/thrift-0.9.3.exe(在这篇文章写作时,使用的是Apache Thrift的0.9.3版本);如果您运行在Linux系统下,那么下载http://www.apache.org/dyn/closer.cgi?path=/thrift/0.9.3/thrift-0.9.3.tar.gz,并进行编译、安装(过程很简单,这里就不再赘述了)。安装后记得添加运行位置到环境变量中。
2-2、IDL格式概要
以下是一个简单的IDL文件定义:
# 命名空间的定义 注意‘java’的关键字
namespace java testThrift.iface
# 结构体定义
struct Request {
1:required string paramJSON;
2:required string serviceName;
}
# 另一个结构体定义
struct Reponse {
1:required RESCODE responeCode;
2:required string responseJSON;
}
# 异常描述定义
exception ServiceException {
1:required EXCCODE exceptionCode;
2:required string exceptionMess;
}
# 枚举定义
enum RESCODE {
_200=200;
_500=500;
_400=400;
}
# 另一个枚举
enum EXCCODE {
PARAMNOTFOUND = 2001;
SERVICENOTFOUND = 2002;
}
# 服务定义
service HelloWorldService {
Reponse send(1:Request request) throws (1:ServiceException e);
}
以上IDL文件是可以直接用来生成各种语言的代码的。下面给出常用的各种不同语言的代码生成命令:
# 生成java
thrift-0.9.3 -gen java ./demoHello.thrift
# 生成c++
thrift-0.9.3 -gen cpp ./demoHello.thrift
# 生成php
thrift-0.9.3 -gen php ./demoHello.thrift
# 生成node.js
thrift-0.9.3 -gen js:node ./demoHello.thrift
# 生成c#
thrift-0.9.3 -gen csharp ./demoHello.thrift
# 您可以通过以下命令查看生成命令的格式
thrift-0.9.3 -help
2-2-1、基本类型
基本类型就是:不管哪一种语言,都支持的数据形式表现。Apache Thrift中支持以下几种基本类型:
- bool: 布尔值 (true or false), one byte
- byte: 有符号字节
- i16: 16位有符号整型
- i32: 32位有符号整型
- i64: 64位有符号整型
- double: 64位浮点型
- string: 字符串/字符数组
- binary: 二进制数据(在java中表现为java.nio.ByteBuffer)
2-2-2、struct结构
在面向对象语言中,表现为“类定义”;在弱类型语言、动态语言中,表现为“结构/结构体”。定义格式如下:
struct <结构体名称> {
<序号>:[字段性质] <字段类型> <字段名称> [= <默认值>] [;|,]
}
实例:
struct Request {
1:required binary paramJSON;
2:required string serviceName
3:optional i32 field1 = 0;
4:optional i64 field2,
5: list<map<string , string>> fields3
}
- 结构体名称:可以按照您的业务需求,给定不同的名称(区分大小写)。但是要注意,一组IDL定义文件中结构体名称不能重复,且不能使用IDL已经占用的关键字(例如required 、struct 等单词)。
- 序号:序号非常重要。正整数,按照顺序排列使用。这个属性在Apache Thrift进行序列化的时候被使用。
- 字段性质:包括两种关键字:required 和 optional,如果您不指定,那么系统会默认为required。required表示这个字段必须有值,并且Apache Thrift在进行序列化时,这个字段都会被序列化;optional表示这个字段不一定有值,且Apache Thrift在进行序列化时,这个字段只有有值的情况下才会被序列化。
- 字段类型:在struct中,字段类型可以是某一个基础类型,也可以是某一个之前定义好的struct,还可以是某种Apache Thrift支持的容器(set、map、list),还可以是定义好的枚举。字段的类型是必须指定的。
- 字段名称:字段名称区分大小写,不能重复,且不能使用IDL已经占用的关键字(例如required 、struct 等单词)。
- 默认值:您可以为某一个字段指定默认值(也可以不指定)。
- 结束符:在struct中,支持两种结束符,您可以使用“;”或者“,”。当然您也可以不使用结束符(Apache Thrift代码生成程序,会自己识别到)
2-2-3、containers集合/容器
Apache Thrift支持三种类型的容器,容器在各种编程语言中普遍存在:
- list< T >:有序列表(JAVA中表现为ArrayList),T可以是某种基础类型,也可以是某一个之前定义好的struct,还可以是某种Apache Thrift支持的容器(set、map、list),还可以是定义好的枚举。有序列表中的元素允许重复。
- set< T >:无序元素集合(JAVA中表现为HashSet),T可以是某种基础类型,也可以是某一个之前定义好的struct,还可以是某种Apache Thrift支持的容器(set、map、list),还可以是定义好的枚举。无序元素集合中的元素不允许重复,一旦重复后一个元素将覆盖前一个元素。
- map
2-2-4、enmu枚举
enum <枚举名称> {
<枚举字段名> = <枚举值>[;|,]
}
示例如下:
enum RESCODE {
_200=200;
_500=500;
_400=400;
}
2-2-5、常量定义
Apache Thrift允许定义常量。常量的关键字为“const”,常量的类型可以是Apache Thrift的基础类型,也可以是某一个之前定义好的struct,还可以是某种Apache Thrift支持的容器(set、map、list),还可以是定义好的枚举。示例如下:
const i32 MY_INT_CONST = 111111;
const i64 MY_LONG_CONST = 11111122222222333333334444444;
const RESCODE MY_RESCODE = RESCODE._200;
2-2-6、exception 异常
Apache Thrift的exception,主要在定义服务接口时使用。其定义方式类似于struct(您可以理解成,把struct关键字换成exception关键字即可),示例如下:
exception ServiceException {
1:required EXCCODE exceptionCode;
2:required string exceptionMess;
}
2-2-7、service 服务接口
Apache Thrift中最重要的IDL定义之一。在后续的代码生成阶段,通过IDL定义的这些服务将构成Apache Thrift客户端调用Apache Thrift服务端的基本远端过程。service服务接口的定义形式如下所示:
service <服务名称> {
<void | 返回指类型> <服务方法名>([<入参序号>:[required | optional] <参数类型> <参数名> ...]) [throws ([<异常序号>:[required | optional] <异常类型> <异常参数名>...])]
}
- 服务名称:服务名可以按照您的业务需求自行制定,注意服务名是区分大小写的。IDL中服务名称只有两个限制,就是不能重复使用相同的名称,不能使用IDL已经占用的关键字(例如required 、struct 等单词)。
- 返回值类型:如果这个调用方法没有返回类型,那么可以关键字“void”; 可以是Apache Thrift的基础类型,也可以是某一个之前定义好的struct,还可以是某种Apache Thrift支持的容器(set、map、list),还可以是定义好的枚举。
- 服务方法名:服务方法名可以根据您的业务需求自定制定,注意区分大小写。在同一个服务中, 不能重复使用一个服务方法名命名多个方法(一定要注意) ,不能使用IDL已经占用的关键字。
- 服务方法参数:<入参序号>:[required | optional] <参数类型> <参数名>。注意和struct中的字段定义相似,可以指定required或者optional;如果不指定则系统默认为required 。如果一个服务方法中有多个参数名,那么这些参数名称不能重复。
- 服务方法异常:throws ([<异常序号>:[required | optional] <异常类型> <异常参数名>。throws关键字是服务方法异常定义的开始点。在throws关键字后面,可以定义1个或者多个不同的异常类型。
Apache Thrift服务定义的示例如下:
service HelloWorldService {
Reponse send(1:Request request) throws (1:ServiceException e);
}
2-2-8、namespace命名空间
Apache Thrift支持为不同语言制定不同的命名空间:
namespace java testThrift.iface
namespace php testThrift.iface
namespace cpp testThrift.iface
2-2-9、注释
Apache Thrift 支持多种风格的注释。这是为了适应不同语言背景的开发者:
/*
* 注释方式1:
**/
// 注释方式2
# 注释方式3
2-2-10、include关键字
如果您的整个工程中有多个IDL定义文件(IDL定义文件的文件名可以随便取)。那么您可以使用include关键字,在IDL定义文件A中,引入一个其他的IDL文件:
include "other.thrift"
请注意,一定使用双引号(不要用成中文的双引号咯),并且不使用“;”或者“,”结束符。
以上就是IDL基本的语法了,由于篇幅原因不可能把每种语法、每一个细节都讲到,但是以上的语法要点已经足够您编辑一个适应业务的,灵活的IDL定义了。如果您需要了解更详细的Thrift IDL语法,可以参考官方文档的讲述:http://thrift.apache.org/docs/idl
2-3、最简单的Thrift代码
- 定义Thrift中业务接口HelloWorldService.Iface的实现:
package testThrift.impl;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.thrift.TException;
import testThrift.iface.HelloWorldService.Iface;
import testThrift.iface.RESCODE;
import testThrift.iface.Reponse;
import testThrift.iface.Request;
/**
* 我们定义了一个HelloWorldService.Iface接口的具体实现。<br>
* 注意,这个父级接口:HelloWorldService.Iface,是由thrift的代码生成工具生成的<br>
* 要运行这段代码,请导入maven-log4j的支持。否则修改LOGGER.info方法
* @author yinwenjie
*/
public class HelloWorldServiceImpl implements Iface {
/**
* 日志
*/
private static final Log LOGGER = LogFactory.getLog(HelloWorldServiceImpl.class);
/**
* 在接口定义中,只有一个方法需要实现。<br>
* HelloWorldServiceImpl.send(Request request) throws TException <br>
* 您可以理解成这个接口的方法接受客户端的一个Request对象,并且在处理完成后向客户端返回一个Reponse对象<br>
* Request对象和Reponse对象都是由IDL定义的结构,并通过“代码生成工具”生成相应的JAVA代码。
*/
@Override
public Reponse send(Request request) throws TException {
/*
* 这里就是进行具体的业务处理了。
* */
String json = request.getParamJSON();
String serviceName = request.getServiceName();
HelloWorldServiceImpl.LOGGER.info("得到的json:" + json + " ;得到的serviceName: " + serviceName);
// 构造返回信息
Reponse response = new Reponse();
response.setResponeCode(RESCODE._200);
response.setResponseJSON("{\"user\":\"yinwenjie\"}");
return response;
}
}
各位可以看到,上面一段代码中具体业务和过程和普通的业务代码没有任何区别。 甚至这段代码的实现都不知道自己将被Apache Thrift框架中的客户端调用 。
- 然后我们开始书写Apache Thrift的服务器端代码:
package testThrift.man;
import java.util.concurrent.Executors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.log4j.BasicConfigurator;
import org.apache.thrift.TProcessor;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.server.TThreadPoolServer;
import org.apache.thrift.server.TThreadPoolServer.Args;
import org.apache.thrift.transport.TServerSocket;
import testThrift.iface.HelloWorldService;
import testThrift.iface.HelloWorldService.Iface;
import testThrift.impl.HelloWorldServiceImpl;
public class HelloBoServerDemo {
static {
BasicConfigurator.configure();
}
/**
* 日志
*/
private static final Log LOGGER =LogFactory.getLog(HelloBoServerDemo.class);
public static final int SERVER_PORT = 9111;
public void startServer() {
try {
HelloBoServerDemo.LOGGER.info("看到这句就说明thrift服务端准备工作 ....");
// 服务执行控制器(只要是调度服务的具体实现该如何运行)
TProcessor tprocessor = new HelloWorldService.Processor<Iface>(new HelloWorldServiceImpl());
// 基于阻塞式同步IO模型的Thrift服务,正式生产环境不建议用这个
TServerSocket serverTransport = new TServerSocket(HelloBoServerDemo.SERVER_PORT);
// 为这个服务器设置对应的IO网络模型、设置使用的消息格式封装、设置线程池参数
Args tArgs = new Args(serverTransport);
tArgs.processor(tprocessor);
tArgs.protocolFactory(new TBinaryProtocol.Factory());
tArgs.executorService(Executors.newFixedThreadPool(100));
// 启动这个thrift服务
TThreadPoolServer server = new TThreadPoolServer(tArgs);
server.serve();
} catch (Exception e) {
HelloBoServerDemo.LOGGER.error(e);
}
}
/**
* @param args
*/
public static void main(String[] args) {
HelloBoServerDemo server = new HelloBoServerDemo();
server.startServer();
}
}
以上的代码有几点需要说明:
- TBinaryProtocol:这个类代码Apache Thrift特有的一种二进制描述格式。它的特点是传输单位数据量所使用的传输量更少。Apache Thrift还支持多种数据格式,例如我们熟悉的JSON格式。后文我们将详细介绍Apache Thrift中的数据格式。
- tArgs.executorService():是不是觉得这个executorService很熟悉,是的这个就是JAVA JDK 1.5+ 后java.util.concurrent包提供的异步任务调度服务接口,Java标准线程池ThreadPoolExecutor就是它的一个实现。
- server.serve(),由于是使用的同步阻塞式网络IO模型,所以这个应用程序的主线程执行到这句话以后就会保持阻塞状态了。不过下层网络状态不出现错误,这个线程就会一直停在这里。
另外,同HelloWorldServiceImpl 类中的代码,请使用Log4j。如果您的测试工程里面没有Log4j,请改用System.out。
- 接下来我们进行最简单的Apache Thrift Client的代码编写:
package testThrift.client;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.log4j.BasicConfigurator;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TSocket;
import testThrift.iface.HelloWorldService;
import testThrift.iface.Reponse;
import testThrift.iface.Request;
/**
* 同样是基于同步阻塞模型的thrift client。
* @author yinwenjie
*/
public class HelloClient {
static {
BasicConfigurator.configure();
}
/**
* 日志
*/
private static final Log LOGGER = LogFactory.getLog(HelloClient.class);
public static final void main(String[] args) throws Exception {
// 服务器所在的IP和端口
TSocket transport = new TSocket("127.0.0.1", 9111);
TProtocol protocol = new TBinaryProtocol(transport);
// 准备调用参数
Request request = new Request("{\"param\":\"field1\"}", "\\mySerivce\\queryService");
HelloWorldService.Client client = new HelloWorldService.Client(protocol);
// 准备传输
transport.open();
// 正式调用接口
Reponse reponse = client.send(request);
// 一定要记住关闭
transport.close();
HelloClient.LOGGER.info("response = " + reponse);
}
}
- Thrift客户端所使用的网络IO模型,必须要与Thrift服务器端所使用的网络IO模型一致。也就是说服务器端如果使用的是阻塞式同步IO模型,那么客户端就必须使用阻塞式同步IO模型。
- Thrift客户端所使用的消息封装格式,必须要与Thrift服务器端所使用的消息封装格式一直。也就是说服务器端如果使用的是二进制流的消息格式TBinaryProtocol,那么客户端就必须同样使用二进制刘的消息格式TBinaryProtocol。
- 其它的代码要么就是由IDL定义并由Thrift的代码生成工具生成;要么就不是重要的代码,所以为了节约篇幅就没有必要再贴出来了。以下是运行效果。
- 服务器端运行效果
- 服务器端收到客户端请求后,取出线程池中的线程进行运行
请注意服务器端在收到客户端请求后的运行方式:取出一条线程池中的线程,并且运行这个服务接口的具体实现。接下来我们马上介绍Apache Thrift的工作细节。
(接下文)