经过上一篇文章《架构设计:系统间通讯(10)——RPC的基本概念》的介绍,相信读者已经理解了基本的RPC概念。为了加深这个理解,后面几篇文章我将详细讲解一款典型的RPC规范的实现Apache Thrift。Apache Thrift的介绍一共分为三篇文章,上篇讲解Apache Thrift的基本使用;中篇讲解Apache Thrift的工做原理(主要围绕Apache Thrift使用的消息格式封装、支持的网络IO模型和它的客户端请求处理方式);下篇对Apache Thrift的不足进行分析,并基于Apache Thrift实现一个本身设计的RPC服务治理的管理方案。这样对咱们后续理解Dubbo的服务治理方式会有很好的帮助做用。php
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性能的几个关键点吗?)。java
为了支持多种语言,Apache Thrift有一套本身的接口定义语言,而且经过Apache Thrift的代码生成程序,可以生成各类编程语言的代码。这样是保证各类语言进行通信的前提条件。为了可以实现简单的Apache Thrift实例,首先咱们就须要讲解一下Apache Thrift的IDL。node
若是您是在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,并进行编译、安装(过程很简单,这里就再也不赘述了)。安装后记得添加运行位置到环境变量中。c++
如下是一个简单的IDL文件定义:apache
# 命名空间的定义 注意‘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); }1234567891011121314151617181920212223242526272829303132333435363738
以上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 -help1234567891011121314151617
基本类型就是:无论哪种语言,都支持的数据形式表现。Apache Thrift中支持如下几种基本类型:json
bool: 布尔值 (true or false), one bytec#
byte: 有符号字节windows
i16: 16位有符号整型数组
i32: 32位有符号整型
i64: 64位有符号整型
double: 64位浮点型
string: 字符串/字符数组
binary: 二进制数据(在java中表现为java.nio.ByteBuffer)
在面向对象语言中,表现为“类定义”;在弱类型语言、动态语言中,表现为“结构/结构体”。定义格式以下:
struct <结构体名称> { <序号>:[字段性质] <字段类型> <字段名称> [= <默认值>] [;|,] }1234
实例:
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 }1234567
结构体名称:能够按照您的业务需求,给定不一样的名称(区分大小写)。可是要注意,一组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代码生成程序,会本身识别到)
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
enum <枚举名称> { <枚举字段名> = <枚举值>[;|,] }123
示例以下:
enum RESCODE { _200=200; _500=500; _400=400; }12345
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;12345
Apache Thrift的exception,主要在定义服务接口时使用。其定义方式相似于struct(您能够理解成,把struct关键字换成exception关键字便可),示例以下:
exception ServiceException { 1:required EXCCODE exceptionCode; 2:required string exceptionMess; }1234
Apache Thrift中最重要的IDL定义之一。在后续的代码生成阶段,经过IDL定义的这些服务将构成Apache Thrift客户端调用Apache Thrift服务端的基本远端过程。service服务接口的定义形式以下所示:
service <服务名称> { <void | 返回指类型> <服务方法名>([<入参序号>:[required | optional] <参数类型> <参数名> ...]) [throws ([<异常序号>:[required | optional] <异常类型> <异常参数名>...])] }123
服务名称:服务名能够按照您的业务需求自行制定,注意服务名是区分大小写的。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); }123
Apache Thrift支持为不一样语言制定不一样的命名空间:
namespace java testThrift.iface namespace php testThrift.iface namespace cpp testThrift.iface12345
Apache Thrift 支持多种风格的注释。这是为了适应不一样语言背景的开发者:
/* * 注释方式1: **/ // 注释方式2 # 注释方式3 1234567
若是您的整个工程中有多个IDL定义文件(IDL定义文件的文件名能够随便取)。那么您可使用include关键字,在IDL定义文件A中,引入一个其余的IDL文件:
include "other.thrift" 1
请注意,必定使用双引号(不要用成中文的双引号咯),而且不使用“;”或者“,”结束符。
以上就是IDL基本的语法了,因为篇幅缘由不可能把每种语法、每个细节都讲到,可是以上的语法要点已经足够您编辑一个适应业务的,灵活的IDL定义了。若是您须要了解更详细的Thrift IDL语法,能够参考官方文档的讲述:http://thrift.apache.org/docs/idl
定义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; } }123456789101112131415161718192021222324252627282930313233343536373839404142434445
各位能够看到,上面一段代码中具体业务和过程和普通的业务代码没有任何区别。甚至这段代码的实现都不知道本身将被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(); } }1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
以上的代码有几点须要说明:
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); } }1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
Thrift客户端所使用的网络IO模型,必需要与Thrift服务器端所使用的网络IO模型一致。也就是说服务器端若是使用的是阻塞式同步IO模型,那么客户端就必须使用阻塞式同步IO模型。
Thrift客户端所使用的消息封装格式,必需要与Thrift服务器端所使用的消息封装格式一直。也就是说服务器端若是使用的是二进制流的消息格式TBinaryProtocol,那么客户端就必须一样使用二进制刘的消息格式TBinaryProtocol。
其它的代码要么就是由IDL定义并由Thrift的代码生成工具生成;要么就不是重要的代码,因此为了节约篇幅就没有必要再贴出来了。如下是运行效果。
服务器端运行效果
服务器端收到客户端请求后,取出线程池中的线程进行运行
请注意服务器端在收到客户端请求后的运行方式:取出一条线程池中的线程,而且运行这个服务接口的具体实现。接下来咱们立刻介绍Apache Thrift的工做细节。
(接下文)