前言:html
目前流行的服务调用方式有不少种,例如基于 SOAP 消息格式的 Web Service,基于 JSON 消息格式的 RESTful 服务等。其中所用到的数据传输方式包括 XML,JSON 等,然而 XML 相对体积太大,传输效率低,JSON 体积较小,新颖,但还不够完善。本文将介绍由 Facebook 开发的远程服务调用框架 Apache Thrift,它采用接口描述语言定义并建立服务,支持可扩展的跨语言服务开发,所包含的代码生成引擎能够在多种语言中,如 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, Smalltalk 等建立高效的、无缝的服务,其传输数据采用二进制格式,相对 XML 和 JSON 体积更小,对于高并发、大数据量和多语言的环境更有优点。本文将详细介绍 Thrift 的使用,而且提供丰富的实例代码加以解释说明,帮助使用者快速构建服务。java
本文首先介绍一个简单的 Thrift 实现实例,使读者可以快速直观地了解什么是 Thrift 以及如何使用 Thrift 构建服务。web
建立一个简单的服务 Hello。首先根据 Thrift 的语法规范编写脚本文件 Hello.thrift,代码以下:apache
清单 1. Hello.thrift namespace java service.demo service Hello{ string helloString(1:string para) i32 helloInt(1:i32 para) bool helloBoolean(1:bool para) void helloVoid() string helloNull() } |
其中定义了服务 Hello 的五个方法,每一个方法包含一个方法名,参数列表和返回类型。每一个参数包括参数序号,参数类型以及参数名。 Thrift 是对 IDL(Interface Definition Language) 描述性语言的一种具体实现。所以,以上的服务描述文件使用 IDL 语法编写。使用 Thrift 工具编译 Hello.thrift,就会生成相应的 Hello.java 文件。该文件包含了在 Hello.thrift 文件中描述的服务 Hello 的接口定义,即 Hello.Iface 接口,以及服务调用的底层通讯细节,包括客户端的调用逻辑 Hello.Client 以及服务器端的处理逻辑 Hello.Processor,用于构建客户端和服务器端的功能。编程
建立 HelloServiceImpl.java 文件并实现 Hello.java 文件中的 Hello.Iface 接口,代码以下:api
清单 2. HelloServiceImpl.java package service.demo; import org.apache.thrift.TException; public class HelloServiceImpl implements Hello.Iface { @Override public boolean helloBoolean(boolean para) throws TException { return para; } @Override public int helloInt(int para) throws TException { try { Thread.sleep(20000); } catch (InterruptedException e) { e.printStackTrace(); } return para; } @Override public String helloNull() throws TException { return null; } @Override public String helloString(String para) throws TException { return para; } @Override public void helloVoid() throws TException { System.out.println("Hello World"); } } |
建立服务器端实现代码,将 HelloServiceImpl 做为具体的处理器传递给 Thrift 服务器,代码以下:服务器
清单 3. HelloServiceServer.java package service.server; import org.apache.thrift.TProcessor; import org.apache.thrift.protocol.TBinaryProtocol; import org.apache.thrift.protocol.TBinaryProtocol.Factory; import org.apache.thrift.server.TServer; import org.apache.thrift.server.TThreadPoolServer; import org.apache.thrift.transport.TServerSocket; import org.apache.thrift.transport.TTransportException; import service.demo.Hello; import service.demo.HelloServiceImpl; public class HelloServiceServer { /** * 启动 Thrift 服务器 * @param args */ public static void main(String[] args) { try { // 设置服务端口为 7911 TServerSocket serverTransport = new TServerSocket(7911); // 设置协议工厂为 TBinaryProtocol.Factory Factory proFactory = new TBinaryProtocol.Factory(); // 关联处理器与 Hello 服务的实现 TProcessor processor = new Hello.Processor(new HelloServiceImpl()); TServer server = new TThreadPoolServer(processor, serverTransport, proFactory); System.out.println("Start server on port 7911..."); server.serve(); } catch (TTransportException e) { e.printStackTrace(); } } } |
建立客户端实现代码,调用 Hello.client 访问服务端的逻辑实现,代码以下:多线程
清单 4. HelloServiceClient.java package service.client; import org.apache.thrift.TException; import org.apache.thrift.protocol.TBinaryProtocol; import org.apache.thrift.protocol.TProtocol; import org.apache.thrift.transport.TSocket; import org.apache.thrift.transport.TTransport; import org.apache.thrift.transport.TTransportException; import service.demo.Hello; public class HelloServiceClient { /** * 调用 Hello 服务 * @param args */ public static void main(String[] args) { try { // 设置调用的服务地址为本地,端口为 7911 TTransport transport = new TSocket("localhost", 7911); transport.open(); // 设置传输协议为 TBinaryProtocol TProtocol protocol = new TBinaryProtocol(transport); Hello.Client client = new Hello.Client(protocol); // 调用服务的 helloVoid 方法 client.helloVoid(); transport.close(); } catch (TTransportException e) { e.printStackTrace(); } catch (TException e) { e.printStackTrace(); } } } |
代码编写完后运行服务器,再启动客户端调用服务 Hello 的方法 helloVoid,在服务器端的控制台窗口输出“Hello World”(helloVoid 方法实如今控制台打印字符串,没有返回值,因此客户端调用方法后没有返回值输出,读者能够本身尝试其余有返回值方法的调用,其结果能够打印在客户端的控制台窗口 )。架构
Thrift 包含一个完整的堆栈结构用于构建客户端和服务器端。下图描绘了 Thrift 的总体架构。并发
图1. 架构图如图所示,图中黄色部分是用户实现的业务逻辑,褐色部分是根据 Thrift 定义的服务接口描述文件生成的客户端和服务器端代码框架,红色部分是根据 Thrift 文件生成代码实现数据的读写操做。红色部分如下是 Thrift 的传输体系、协议以及底层 I/O 通讯,使用 Thrift 能够很方便的定义一个服务而且选择不一样的传输协议和传输层而不用从新生成代码。
Thrift 服务器包含用于绑定协议和传输层的基础架构,它提供阻塞、非阻塞、单线程和多线程的模式运行在服务器上,能够配合服务器 / 容器一块儿运行,能够和现有的 J2EE 服务器 /Web 容器无缝的结合。
服务端和客户端具体的调用流程以下:
图 2. Server 端启动、服务时序图( 查看大图)该图所示是 HelloServiceServer 启动的过程以及服务被客户端调用时,服务器的响应过程。从图中咱们能够看到,程序调用了 TThreadPoolServer 的 serve 方法后,server 进入阻塞监听状态,其阻塞在 TServerSocket 的 accept 方法上。当接收到来自客户端的消息后,服务器发起一个新线程处理这个消息请求,原线程再次进入阻塞状态。在新线程中,服务器经过 TBinaryProtocol 协议读取消息内容,调用 HelloServiceImpl 的 helloVoid 方法,并将结果写入 helloVoid_result 中传回客户端。
图 3. Client 端调用服务时序图( 查看大图)该图所示是 HelloServiceClient 调用服务的过程以及接收到服务器端的返回值后处理结果的过程。从图中咱们能够看到,程序调用了 Hello.Client 的 helloVoid 方法,在 helloVoid 方法中,经过 send_helloVoid 方法发送对服务的调用请求,经过 recv_helloVoid 方法接收服务处理请求后返回的结果。
Thrift 脚本可定义的数据类型包括如下几种类型:
Thrift 可让用户选择客户端与服务端之间传输通讯协议的类别,在传输协议上整体划分为文本 (text) 和二进制 (binary) 传输协议,为节约带宽,提升传输效率,通常状况下使用二进制类型的传输协议为多数,有时还会使用基于文本类型的协议,这须要根据项目 / 产品中的实际需求。经常使用协议有如下几种:
使用方法如清单 3 和清单 4 所示。
构建 TCompactProtocol 协议的服务器和客户端只需替换清单 3 和清单 4 中 TBinaryProtocol 协议部分便可,替换成以下代码:
TCompactProtocol.Factory proFactory = new TCompactProtocol.Factory(); |
TCompactProtocol protocol = new TCompactProtocol(transport); |
构建 TJSONProtocol 协议的服务器和客户端只需替换清单 3 和清单 4 中 TBinaryProtocol 协议部分便可,替换成以下代码:
TJSONProtocol.Factory proFactory = new TJSONProtocol.Factory(); |
TJSONProtocol protocol = new TJSONProtocol(transport); |
经常使用的传输层有如下几种:
使用方法如清单 4 所示。
若使用 TFramedTransport 传输层,其服务器必须修改成非阻塞的服务类型,客户端只需替换清单 4 中 TTransport 部分,代码以下,清单 9 中 TNonblockingServerTransport 类是构建非阻塞 socket 的抽象类,TNonblockingServerSocket 类继承 TNonblockingServerTransport
TNonblockingServerTransport serverTransport; serverTransport = new TNonblockingServerSocket(10005); Hello.Processor processor = new Hello.Processor(new HelloServiceImpl()); TServer server = new TNonblockingServer(processor, serverTransport); System.out.println("Start server on port 10005 ..."); server.serve(); |
TTransport transport = new TFramedTransport(new TSocket("localhost", 10005)); |
使用方法请参考 Thrift 异步客户端构建
常见的服务端类型有如下几种:
代码以下:
TServerSocket serverTransport = new TServerSocket(7911); TProcessor processor = new Hello.Processor(new HelloServiceImpl()); TServer server = new TSimpleServer(processor, serverTransport); System.out.println("Start server on port 7911..."); server.serve(); |
客户端的构建方式可参考清单 4。
使用方法如清单 3 所示。
使用方法请参考 Thrift 异步客户端构建
Thrift 提供非阻塞的调用方式,可构建异步客户端。在这种方式中,Thrift 提供了新的类 TAsyncClientManager 用于管理客户端的请求,在一个线程上追踪请求和响应,同时经过接口 AsyncClient 传递标准的参数和 callback 对象,服务调用完成后,callback 提供了处理调用结果和异常的方法。
首先咱们看 callback 的实现:
清单 12.CallBack 的实现:MethodCallback.java package service.callback; import org.apache.thrift.async.AsyncMethodCallback; public class MethodCallback implements AsyncMethodCallback { Object response = null; public Object getResult() { // 返回结果值 return this.response; } // 处理服务返回的结果值 @Override public void onComplete(Object response) { this.response = response; } // 处理调用服务过程当中出现的异常 @Override public void onError(Throwable throwable) { } } |
如代码所示,onComplete 方法接收服务处理后的结果,此处咱们将结果 response 直接赋值给 callback 的私有属性 response。onError 方法接收服务处理过程当中抛出的异常,此处未对异常进行处理。
建立非阻塞服务器端实现代码,将 HelloServiceImpl 做为具体的处理器传递给异步 Thrift 服务器,代码以下:
清单 13.HelloServiceAsyncServer.java package service.server; import org.apache.thrift.server.TNonblockingServer; import org.apache.thrift.server.TServer; import org.apache.thrift.transport.TNonblockingServerSocket; import org.apache.thrift.transport.TNonblockingServerTransport; import org.apache.thrift.transport.TTransportException; import service.demo.Hello; import service.demo.HelloServiceImpl; public class HelloServiceAsyncServer { /** * 启动 Thrift 异步服务器 * @param args */ public static void main(String[] args) { TNonblockingServerTransport serverTransport; try { serverTransport = new TNonblockingServerSocket(10005); Hello.Processor processor = new Hello.Processor( new HelloServiceImpl()); TServer server = new TNonblockingServer(processor, serverTransport); System.out.println("Start server on port 10005 ..."); server.serve(); } catch (TTransportException e) { e.printStackTrace(); } } } |
HelloServiceAsyncServer 经过 java.nio.channels.ServerSocketChannel 建立非阻塞的服务器端等待客户端的链接。
建立异步客户端实现代码,调用 Hello.AsyncClient 访问服务端的逻辑实现,将 MethodCallback 对象做为参数传入调用方法中,代码以下:
清单 14.HelloServiceAsyncClient.java package service.client; import java.io.IOException; import org.apache.thrift.async.AsyncMethodCallback; import org.apache.thrift.async.TAsyncClientManager; import org.apache.thrift.protocol.TBinaryProtocol; import org.apache.thrift.protocol.TProtocolFactory; import org.apache.thrift.transport.TNonblockingSocket; import org.apache.thrift.transport.TNonblockingTransport; import service.callback.MethodCallback; import service.demo.Hello; public class HelloServiceAsyncClient { /** * 调用 Hello 服务 * @param args */ public static void main(String[] args) throws Exception { try { TAsyncClientManager clientManager = new TAsyncClientManager(); TNonblockingTransport transport = new TNonblockingSocket( "localhost", 10005); TProtocolFactory protocol = new TBinaryProtocol.Factory(); Hello.AsyncClient asyncClient = new Hello.AsyncClient(protocol, clientManager, transport); System.out.println("Client calls ....."); MethodCallback callBack = new MethodCallback(); asyncClient.helloString("Hello World", callBack); Object res = callBack.getResult(); while (res == null) { res = callBack.getResult(); } System.out.println(((Hello.AsyncClient.helloString_call) res) .getResult()); } catch (IOException e) { e.printStackTrace(); } } } |
HelloServiceAsyncClient 经过 java.nio.channels.Socketchannel 建立异步客户端与服务器创建链接。在本文中异步客户端经过如下的循环代码实现了同步效果,读者可去除这部分代码后再运行对比。
清单 15. 异步客户端实现同步效果代码段 Object res = callBack.getResult(); // 等待服务调用后的返回结果 while (res == null) { res = callBack.getResult(); } |
经过与清单 9 和清单 10 的代码比较,咱们能够构建一个 TNonblockingServer 服务类型的服务端,在客户端构建一个 TFramedTransport 传输层的同步客户端和一个 TNonblockingTransport 传输层的异步客户端,那么一个服务就能够经过一个 socket 端口提供两种不一样的调用方式。有兴趣的读者能够尝试一下。
咱们在对服务的某个方法调用时,有时会出现该方法返回 null 值的状况,在 Thrift 中,直接调用一个返回 null 值的方法会抛出 TApplicationException 异常。在清单 2 中,HelloServiceImpl 里实现了 helloNull 方法,返回 null 值,咱们在 HelloServiceClient.java 中加入调用该方法的代码,出现以下图所示的异常:
图 4. TApplicationException 异常为了处理返回 null 值状况,咱们要捕获该异常,并进行相应的处理,具体客户端代码实现以下:
清单 16. 处理服务返回值为 null 的代码 package service.client; import org.apache.thrift.TApplicationException; import org.apache.thrift.TException; import org.apache.thrift.protocol.TBinaryProtocol; import org.apache.thrift.protocol.TProtocol; import org.apache.thrift.transport.TSocket; import org.apache.thrift.transport.TTransport; import org.apache.thrift.transport.TTransportException; import service.demo.Hello; public class HelloServiceClient { /** * 调用 Hello 服务,并处理 null 值问题 * @param args */ public static void main(String[] args) { try { TTransport transport = new TSocket("localhost", 7911); transport.open(); TProtocol protocol = new TBinaryProtocol(transport); Hello.Client client = new Hello.Client(protocol); System.out.println(client.helloNull()); transport.close(); } catch (TTransportException e) { e.printStackTrace(); } catch (TException e) { if (e instanceof TApplicationException && ((TApplicationException) e).getType() == TApplicationException.MISSING_RESULT) { System.out.println("The result of helloNull function is NULL"); } } } } |
调用 helloNull 方法后,会抛出 TApplicationException 异常,而且异常种类为 MISSING_RESULT,本段代码显示,捕获该异常后,直接在控制台打印“The result of helloNull function is NULL”信息。
Apache Thrift 的官方网站为:http://thrift.apache.org/tutorial/,具体安装步骤以下:
基于 Apache Thrift 框架生成的服务包括客户端和服务器端,具体的部署模式以下所示:
图 5. 部署图从图中咱们能够看到,客户端和服务器端部署时,须要用到公共的 jar 包和 java 文件,如图“Common file”区域,其中 Hello.java 由 Hello.thrift 编译而来。在服务器端,服务必须实现 Hello.Iface 接口,同时要包括服务器的启动代码 HelloServiceServer.java。在客户端,包括客户端调用服务的代码 HelloServiceClient.java。客户端和服务器经过 Hello.java 提供的 API 实现远程服务调用。
本文介绍了 Apache Thrift 的安装部署和架构,并经过大量实例介绍了在不一样状况下如何使用 Apache Thrift 来构建服务,同时着重介绍了 Thrift 异步客户端的构建,但愿能给读者带来一些帮助。
学习
Apache Thrift 跨语言服务开发框架(推荐阅读)