简单练习

代码下载地址:

https://gitee.com/renCaiGe/demo/repository/archive/thrift-demo

基于windows实现

配置环境变量

下载thrift-0.10.0.exe,可以根据实际情况选择其他版本

http://archive.apache.org/dist/thrift/0.10.0/thrift-0.10.0.exe

配置path

1、将文件thrift-0.10.0.exe复制到 D:\installSoft\thrift文件夹,

2、重命名为thrift.exe;在环境变量

3、path中添加 路径:D:\installSoft\thrift

Java语言代码

新建pom项目,添加thrift插件

  1. <dependency>
  2. <groupId>org.apache.thrift</groupId>
  3. <artifactId>libthrift</artifactId>
  4. <version>0.10.0</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.slf4j</groupId>
  8. <artifactId>slf4j-api</artifactId>
  9. <version>1.7.5</version>
  10. </dependency>
  11. <dependency>
  12. <groupId>org.slf4j</groupId>
  13. <artifactId>slf4j-simple</artifactId>
  14. <version>1.7.5</version>
  15. </dependency>

HelloService.thrift

  1. namespace java com.thrift.demo.service
  2. service HelloService{
  3. string sayHello(1:string username)
  4. }

在windows 系统cmd执行命令

  1. thrift --gen java HelloService.thrift
  2. 》》生成 HelloService.java

生成对应语言编译文件

thrift —gen 语言 文件名称,如Python:thrift —gen py HelloService.thrift

HelloService.java

  1. /**
  2. * Autogenerated by Thrift Compiler (0.10.0)
  3. * <p>
  4. * DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
  5. *
  6. * @generated
  7. */
  8. package com.thrift.demo.service;
  9. @SuppressWarnings({"cast", "rawtypes", "serial", "unchecked", "unused"})
  10. @javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.10.0)", date = "2021-09-20")
  11. public class HelloService {
  12. public interface Iface {
  13. public java.lang.String sayHello(java.lang.String username) throws org.apache.thrift.TException;
  14. }
  15. ...

HelloWorldImpl.java

  1. package com.thrift.demo.service.impl;
  2. import com.thrift.demo.service.HelloService;
  3. import org.apache.thrift.TException;
  4. /**
  5. * @author bridge
  6. * @Date 2021/09/22/9:17
  7. */
  8. public class HelloWorldImpl implements HelloService.Iface {
  9. public HelloWorldImpl() {
  10. }
  11. @Override
  12. public String sayHello(String username) throws TException {
  13. return "hello thrift "+username;
  14. }
  15. }

HelloClient.java

  1. package com.thrift.client;
  2. import com.thrift.demo.service.HelloService;
  3. import org.apache.thrift.protocol.TBinaryProtocol;
  4. import org.apache.thrift.protocol.TProtocol;
  5. import org.apache.thrift.transport.TSocket;
  6. import org.apache.thrift.transport.TTransport;
  7. /**
  8. * @author bridge
  9. * @Date 2021/09/22/9:23
  10. */
  11. public class HelloClient {
  12. public static final int SERVER_PORT = 7099;
  13. public static final String SERVER_IP = "localhost";
  14. public void startClient(String username) {
  15. TTransport tTransport = null;
  16. try {
  17. tTransport = new TSocket(SERVER_IP, SERVER_PORT);
  18. //协议要和服务端一致
  19. TProtocol protocol = new TBinaryProtocol(tTransport);
  20. HelloService.Client client = new HelloService.Client(protocol);
  21. tTransport.open();
  22. String result = client.sayHello(username);
  23. System.out.println("Thrift client result=" + result);
  24. } catch (Exception e) {
  25. e.printStackTrace();
  26. }
  27. }
  28. public static void main(String[] args) {
  29. HelloClient client = new HelloClient();
  30. client.startClient("bridge");
  31. }
  32. }

HelloServer.java

  1. package com.thrift.server;
  2. import com.thrift.demo.service.HelloService;
  3. import com.thrift.demo.service.impl.HelloWorldImpl;
  4. import org.apache.thrift.TProcessor;
  5. import org.apache.thrift.protocol.TBinaryProtocol;
  6. import org.apache.thrift.server.TServer;
  7. import org.apache.thrift.server.TSimpleServer;
  8. import org.apache.thrift.transport.TServerSocket;
  9. /**
  10. * @author bridge
  11. * @Date 2021/09/22/9:21
  12. */
  13. public class HelloServer {
  14. public final static int SERVER_PORT = 7099;
  15. private static String SERVER_IP = "localhost";
  16. public void startServer() {
  17. try {
  18. System.out.println("HelloWorld Server start...");
  19. TServerSocket serverTransport = new TServerSocket(SERVER_PORT);
  20. TServer.Args args = new TServer.Args(serverTransport);
  21. TProcessor process = new HelloService.Processor(new HelloWorldImpl());
  22. TBinaryProtocol.Factory portFactory = new TBinaryProtocol.Factory(true, true);
  23. args.processor(process);
  24. args.protocolFactory(portFactory);
  25. TServer server = new TSimpleServer(args);
  26. server.serve();
  27. } catch (Exception e) {
  28. System.out.println("Server start error");
  29. e.printStackTrace();
  30. }
  31. }
  32. public static void main(String[] args) {
  33. HelloServer server = new HelloServer();
  34. server.startServer();
  35. }
  36. }

参考文章

https://blog.csdn.net/houjixin/article/details/42778335

参考视频

https://www.bilibili.com/video/BV1j44y1q7fy?from=search&seid=4708551372363609018&spm_id_from=333.337.0.0

P1从27:30开始观看,正式进入Thrift讲解

Thrift介绍与用法

一、 Thrift简单介绍

1.1、 Thrift是什么?能做什么?

Thrift是Facebook于2007年开发的跨语言的rpc服框架,提供多语言的编译功能,并提供多种服务器工作模式;用户通过Thrift的IDL(接口定义语言)来描述接口函数及数据类型,然后通过Thrift的编译环境生成各种语言类型的接口文件,用户可以根据自己的需要采用不同的语言开发客户端代码和服务器端代码。

例如,我想开发一个快速计算的RPC服务,它主要通过接口函数getInt对外提供服务,这个RPC服务的getInt函数使用用户传入的参数,经过复杂的计算,计算出一个整形值返回给用户;服务器端使用java语言开发,而调用客户端可以是java、c、python等语言开发的程序,在这种应用场景下,我们只需要使用Thrift的IDL描述一下getInt函数(以.thrift为后缀的文件),然后使用Thrift的多语言编译功能,将这个IDL文件编译成C、java、python几种语言对应的“特定语言接口文件”(每种语言只需要一条简单的命令即可编译完成),这样拿到对应语言的“特定语言接口文件”之后,就可以开发客户端和服务器端的代码了,开发过程中只要接口不变,客户端和服务器端的开发可以独立的进行。

Thrift为服务器端程序提供了很多的工作模式,例如:线程池模型、非阻塞模型等等,可以根据自己的实际应用场景选择一种工作模式高效地对外提供服务;

1.2、 Thrift的相关网址和资料:

(1) Thrift的官方网站:http://thrift.apache.org/

(2) Thrift官方下载地址:http://thrift.apache.org/download

(3) Thrift官方的IDL示例文件(自己写IDL文件时可以此为参考):

https://git-wip-us.apache.org/repos/asf?p=thrift.git;a=blob_plain;f=test/ThriftTest.thrift;hb=HEAD

(4) 各种环境下搭建Thrift的方法:

http://thrift.apache.org/docs/install/

该页面中共提供了CentOS\Ubuntu\OS X\Windows几种环境下的搭建Thrift环境的方法。

二、 Thrift的使用

Thrift提供跨语言的服务框架,这种跨语言主要体现在它对多种语言的编译功能的支持,用户只需要使用IDL描述好接口函数,只需要一条简单的命令,Thrift就能够把按照IDL格式描述的接口文件翻译成各种语言版本。其实,说搭建Thrift环境的时候,实际上最麻烦的就是搭建Thrift的编译环境,Thrift的编译和通常的编译一样经过词法分析、语法分析等等最终生成对应语言的源码文件,为了能够支持对各种语言的编译,你需要下载各种语言对应的编译时使用的包;

2.1、 搭建Thrift的编译环境

本节主要介绍如何搭建Unix编译环境,搭建时有以下要求:

基本要求:

G++、boost、lex、yacc

源码安装要求:

如果使用源码安装的方式,则还需要下列工具:

Autoconf、automake、libtool、pkg-config、lex和yacc的开发版、libssl-dev

语言要求:

搭建C++编译环境:boost、libevent、zlib

搭建java编译环境:jdk、ApacheAnt

具体搭建环境时可以参考“一”中所列官网的安装方法。

2.2、 搭建JAVA下Thrift开发环境

在java环境下开发thrift的客户端或者服务器程序非常简单,只需在工程文件中加上下面三个jar包(版本可能随时有更新):

libthrift-0.9.1.jar

slf4j-api-1.7.5.jar

slf4j-simple.jar

2.3、 编写IDL文件

使用Thrift开发程序,首先要做的事情就是使用IDL对接口进行描述, 然后再使用Thrift的多语言编译能力将接口的描述文件编译成对应语言的版本,本文中将IDL对接口的描述文件称为“Thrift文件”。

(1) 编写Thrift文件

使用IDL对接口进行描述的thrift文件命名一般都是以“.thrift”作为后缀:XXX.thrift,可以在该文件的开头为该文件加上命名空间限制,格式为:namespace语言 命名空间的名字;例如:

namespace javacom.test.service

IDL文件中对所有接口函数的描述都放在service中,service的名字可以自己指定,该名字也将被用作生成的特定语言接口文件的名字,接口函数需要对参数使用序号标号,除最后一个接口函数外,要以“,”结束对函数的描述。

例如,下面一个IDL描述的Thrift文件(该Thrift文件的文件名为:test_service.thrift)的全部内容:

  1. namespace java com.test.service
  2. include "thrift_datatype.thrift"
  3. service TestThriftService
  4. {
  5. /**
  6. *value 中存放两个字符串拼接之后的字符串
  7. */
  8. thrift_datatype.ResultStr getStr(1:string srcStr1, 2:string srcStr2),
  9. thrift_datatype.ResultInt getInt(1:i32 val)
  10. }

代码2.1

thrift_datatype.thrift 文件请看类型定义

这里的TestThriftService就被用作生成的特定语言的文件名,例如我想用该Thrift文件生成一个java版本的接口文件,那么生成的java文件名就是:TestThriftService.java。

(1) 编写IDL文件时需要注意的问题

[1]函数的参数要用数字依序标好,序号从1开始,形式为:“序号:参数名”;

[2]每个函数的最后要加上“,”,最后一个函数不加;

[3]在IDL中可以使用/……/添加注释

(2) IDL支持的数据类型

IDL大小写敏感,它共支持以下几种基本的数据类型:

[1]string, 字符串类型,注意是全部小写形式;例如:string aString

[2]i16, 16位整形类型,例如:i16 aI16Val;

[3]i32,32位整形类型,对应C/C++/java中的int类型;例如: I32 aIntVal

[4]i64,64位整形,对应C/C++/java中的long类型;例如:I64 aLongVal

[5]byte,8位的字符类型,对应C/C++中的char,java中的byte类型;例如:byte aByteVal

[6]bool, 布尔类型,对应C/C++中的bool,java中的boolean类型; 例如:bool aBoolVal

[7]double,双精度浮点类型,对应C/C++/java中的double类型;例如:double aDoubleVal

[8]void,空类型,对应C/C++/java中的void类型;该类型主要用作函数的返回值,例如:void testVoid(),

除上述基本类型外,ID还支持以下类型:

[1]map,map类型,例如,定义一个map对象:map newmap;

[2]set,集合类型,例如,定义set对象:set aSet;

[3]list,链表类型,例如,定义一个list对象:list aList;

(3) 在Thrift文件中自定义数据类型

在IDL中支持两种自定义类型:枚举类型和结构体类型,具体如下:

[1]enum, 枚举类型,例如,定义一个枚举类型:

  1. enum Numberz
  2. {
  3. ONE = 1,
  4. TWO,
  5. THREE,
  6. FIVE = 5,
  7. SIX,
  8. EIGHT = 8
  9. }

注意,枚举类型里没有序号

[2]struct,自定义结构体类型,在IDL中可以自己定义结构体,对应C中的struct,c++中的struct和class,java中的class。例如:

  1. struct TestV1 {
  2. 1: i32 begin_in_both,
  3. 3: string old_string,
  4. 12: i32 end_in_both
  5. }

注意,在struct定义结构体时需要对每个结构体成员用序号标识:“序号: ”。

(4) 定义类型别名

Thrift的IDL支持C/C++中类似typedef的功能,例如:

  1. typedef``i32 Integer
  2. 就可以为i32类型重新起个名字Integer

2.4、 生成Thrift服务接口文件

搭建Thrift编译环境之后,使用下面命令即可将IDL文件编译成对应语言的接口文件:

thrift —gen

例如:如果使用上面的thrift文件(见上面的代码2.1):test_service.thrift生成一个java语言的接口文件,则只需在搭建好thrift编译环境的机子上,执行如下命令即可:

thrift —gen java test_service.thrift

这里,我直接在test_service.thrift文件所在的目录下执行的命令,所以直接使用文件名即可(如图2.1的标号1所示),如果不在test_service.thrift所在的目录中,则需要具体指明该文件所在的路径。

图2.1

如图2.1 中标号2所示,生成的gen-java的目录,目录下面有com、test、service三级目录,这三级目录也是根据test_service.thrift文件中命名空间的名字:com.test.service生成的,进入目录之后可以看到生成的java语言的接口文件名为:TestThriftService.java,这个文件的名字也是根据test_service.thrift文件的service名字来生成的(见代码2.1)。

2.5、 编写服务器端的java代码

编写thrift服务器程序需要首先完成下面两步工作:

(1)先将2.2节中的三个jar包添加到工程里,如图2.2的标号2所示。

(2)将生成的java接口文件TestThriftService.java拷贝到自己的工程文件中,如图2.2的标号1所示。

图2.2

服务端程序需实现TestThriftService.Iface接口,在实现接口中完成自己要提供的服务,服务器端对服务接口实现的代码如下所示:

  1. package com.test.service;
  2. import org.apache.thrift.TException;
  3. public class TestThriftServiceImpl implements TestThriftService.Iface
  4. {
  5. @Override
  6. public String getStr(String srcStr1, String srcStr2) throws TException {
  7. long startTime = System.currentTimeMillis();
  8. String res = srcStr1 + srcStr2;
  9. long stopTime = System.currentTimeMillis();
  10. System.out.println("[getStr]time interval: " + (stopTime-startTime));
  11. return res;
  12. }
  13. @Override
  14. public int getInt(int val) throws TException {
  15. long startTime = System.currentTimeMillis();
  16. int res = val * 10;
  17. long stopTime = System.currentTimeMillis();
  18. System.out.println("[getInt]time interval: " + (stopTime-startTime));
  19. return res;
  20. }
  21. }

代码2.2

服务器端启动thrift服务框架的程序如下所示,在本例中服务器采用TNonblockingServer工作模式:

  1. package com.test.service;
  2. import org.apache.thrift.TProcessor;
  3. import org.apache.thrift.protocol.TBinaryProtocol;
  4. import org.apache.thrift.server.TNonblockingServer;
  5. import org.apache.thrift.server.TServer;
  6. import org.apache.thrift.transport.TFramedTransport;
  7. import org.apache.thrift.transport.TNonblockingServerSocket;
  8. import org.apache.thrift.transport.TTransportException;
  9. public class testMain {
  10. private static int m_thriftPort = 12356;
  11. private static TestThriftServiceImpl m_myService = new TestThriftServiceImpl();
  12. private static TServer m_server = null;
  13. private static void createNonblockingServer() throws TTransportException
  14. {
  15. TProcessor tProcessor = new TestThriftService.Processor<TestThriftService.Iface>(m_myService);
  16. TNonblockingServerSocket nioSocket = new TNonblockingServerSocket(m_thriftPort);
  17. TNonblockingServer.Args tnbArgs = new TNonblockingServer.Args(nioSocket);
  18. tnbArgs.processor(tProcessor);
  19. tnbArgs.transportFactory(new TFramedTransport.Factory());
  20. tnbArgs.protocolFactory(new TBinaryProtocol.Factory());
  21. // 使用非阻塞式IO,服务端和客户端需要指定TFramedTransport数据传输的方式
  22. m_server = new TNonblockingServer(tnbArgs);
  23. }
  24. public static boolean start()
  25. {
  26. try {
  27. createNonblockingServer();
  28. } catch (TTransportException e) {
  29. System.out.println("start server error!" + e);
  30. return false;
  31. }
  32. System.out.println("service at port: " + m_thriftPort);
  33. m_server.serve();
  34. return true;
  35. }
  36. public static void main(String[] args)
  37. {
  38. if(!start())
  39. {
  40. System.exit(0);
  41. }
  42. }
  43. }

代码2.3

在服务器端启动thrift框架的部分代码比较简单,不过在写这些启动代码之前需要先确定服务器采用哪种工作模式对外提供服务,Thrift对外提供几种工作模式,例如:TSimpleServer、TNonblockingServer、TThreadPoolServer、TThreadedSelectorServer等模式,每种服务模式的通信方式不一样,因此在服务启动时使用了那种服务模式,客户端程序也需要采用对应的通信方式。

另外,Thrift支持多种通信协议格式:TCompactProtocol、TBinaryProtocol、TJSONProtocol等,因此,在使用Thrift框架时,客户端程序与服务器端程序所使用的通信协议一定要一致,否则便无法正常通信。

以上述代码2.3采用的TNonblockingServer为例,说明服务器端如何使用Thrift框架,在服务器端创建并启动Thrift服务框架的过程为:

[1]为自己的服务实现类定义一个对象,如代码2.3中的:

TestThriftServiceImpl m_myService =newTestThriftServiceImpl();

这里的TestThriftServiceImpl类就是代码2.2中我们自己定义的服务器端对各服务接口的实现类。

[2]定义一个TProcess对象,在根据Thrift文件生成java源码接口文件TestThriftService.java中,Thrift已经自动为我们定义了一个Processor;后续节中将对这个TProcess类的功能进行详细描述;如代码2.3中的:

TProcessor tProcessor = NewTestThriftService.Processor(m_myService);

[3]定义一个TNonblockingServerSocket对象,用于tcp的socket通信,如代码2.3中的:

TNonblockingServerSocketnioSocket = newTNonblockingServerSocket(m_thriftPort);

在创建server端socket时需要指明监听端口号,即上面的变量:m_thriftPort。

[4]定义TNonblockingServer所需的参数对象TNonblockingServer.Args;并设置所需的参数,如代码2.3中的:

  1. TNonblockingServer.Args tnbArgs = new TNonblockingServer.Args(nioSocket);
  2. tnbArgs.processor(tProcessor);
  3. tnbArgs.transportFactory(new TFramedTransport.Factory());
  4. tnbArgs.protocolFactory(new TBinaryProtocol.Factory());

在TNonblockingServer模式下我们使用二进制协议:TBinaryProtocol,通信方式采用TFramedTransport,即以帧的方式对数据进行传输。

[5]定义TNonblockingServer对象,并启动该服务,如代码2.3中的:

m_server = new TNonblockingServer(tnbArgs);

m_server.serve();

2.6、 编写客户端代码

Thrift的客户端代码同样需要服务器开头的那两步:添加三个jar包和生成的java接口文件TestThriftService.java。

  1. m_transport = new TSocket(THRIFT_HOST, THRIFT_PORT,2000);
  2. TProtocol protocol = new TBinaryProtocol(m_transport);
  3. TestThriftService.Client testClient = new TestThriftService.Client(protocol);
  4. try {
  5. m_transport.open();
  6. String res = testClient.getStr("test1", "test2");
  7. System.out.println("res = " + res);
  8. m_transport.close();
  9. } catch (TException e){
  10. // TODO Auto-generated catch block
  11. e.printStackTrace();
  12. }

代码2.4

由代码2.4可以看到编写客户端代码非常简单,只需下面几步即可:

[1]创建一个传输层对象(TTransport),具体采用的传输方式是TFramedTransport,要与服务器端保持一致,即:

mtransport =new TFramedTransport(newTSocket(_THRIFT_HOST,THRIFT_PORT, 2000));

这里的THRIFT_HOST, THRIFT_PORT分别是Thrift服务器程序的主机地址和监听端口号,这里的2000是socket的通信超时时间;

[2]创建一个通信协议对象(TProtocol),具体采用的通信协议是二进制协议,这里要与服务器端保持一致,即:

TProtocolprotocol =new TBinaryProtocol(m_transport);

[3]创建一个Thrift客户端对象(TestThriftService.Client),Thrift的客户端类TestThriftService.Client已经在文件TestThriftService.java中,由Thrift编译器自动为我们生成,即:

TestThriftService.ClienttestClient =new TestThriftService.Client(protocol);

[4]打开socket,建立与服务器直接的socket连接,即:

m_transport.open();

[5]通过客户端对象调用服务器服务函数getStr,即:

String res = testClient.getStr(“test1”,”test2”);

  1. System._out_.println("res = " +res);

[6]使用完成关闭socket,即:

m_transport.close();

  1. 这里有以下几点需要说明:

[1]在同步方式使用客户端和服务器的时候,socket是被一个函数调用独占的,不能多个调用同时使用一个socket,例如通过m_transport.open()打开一个socket,此时创建多个线程同时进行函数调用,这时就会报错,因为socket在被一个调用占着的时候不能再使用;

[2]可以分时多次使用同一个socket进行多次函数调用,即通过m_transport.open()打开一个socket之后,你可以发起一个调用,在这个次调用完成之后,再继续调用其他函数而不需要再次通过m_transport.open()打开socket;

2.7、 需要注意的问题

(1)Thrift的服务器端和客户端使用的通信方式要一样,否则便无法进行正常通信;

Thrift的服务器端的种模式所使用的通信方式并不一样,因此,服务器端使用哪种通信方式,客户端程序也要使用这种方式,否则就无法进行正常通信了。例如,上面的代码2.3中,服务器端使用的工作模式为TNonblockingServer,在该工作模式下需要采用的传输方式为TFramedTransport,也就是在通信过程中会将tcp的字节流封装成一个个的帧,此时就需要客户端程序也这么做,否则便会通信失败。出现如下问题:

服务器端会爆出如下出错log:

  1. 2015-01-06 17:14:52.365 ERROR [Thread-11] [AbstractNonblockingServer.java:348] - Read an invalid frame size of -2147418111. Are you using TFramedTransport on the client side?

客户端则会报出如下出错log:

  1. org.apache.thrift.transport.TTransportException: java.net.SocketException: Connection reset
  2. at org.apache.thrift.transport.TIOStreamTransport.read(TIOStreamTransport.java:129)
  3. at org.apache.thrift.transport.TTransport.readAll(TTransport.java:84)
  4. at org.apache.thrift.protocol.TBinaryProtocol.readAll(TBinaryProtocol.java:362)
  5. at org.apache.thrift.protocol.TBinaryProtocol.readI32(TBinaryProtocol.java:284)
  6. at org.apache.thrift.protocol.TBinaryProtocol.readMessageBegin(TBinaryProtocol.java:191)
  7. at org.apache.thrift.TServiceClient.receiveBase(TServiceClient.java:69)
  8. at com.browan.freepp.dataproxy.service.DataProxyService$Client.recv_addLiker(DataProxyService.java:877)
  9. at com.browan.freepp.dataproxy.service.DataProxyService$Client.addLiker(DataProxyService.java:862)
  10. at com.browan.freepp.dataproxy.service.DataProxyServiceTest.test_Likers(DataProxyServiceTest.java:59)
  11. at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  12. at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
  13. at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
  14. at java.lang.reflect.Method.invoke(Method.java:606)
  15. at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
  16. at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
  17. at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
  18. at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
  19. at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)
  20. at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70)
  21. at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
  22. at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
  23. at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
  24. at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
  25. at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
  26. at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
  27. at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
  28. at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
  29. at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
  30. at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
  31. at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
  32. at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
  33. at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)
  34. Caused by: java.net.SocketException: Connection reset
  35. at java.net.SocketInputStream.read(SocketInputStream.java:196)
  36. at java.net.SocketInputStream.read(SocketInputStream.java:122)
  37. at java.io.BufferedInputStream.fill(BufferedInputStream.java:235)
  38. at java.io.BufferedInputStream.read1(BufferedInputStream.java:275)
  39. at java.io.BufferedInputStream.read(BufferedInputStream.java:334)
  40. at org.apache.thrift.transport.TIOStreamTransport.read(TIOStreamTransport.java:127)
  41. ... 31 more

(2)在服务器端或者客户端直接使用IDL生成的接口文件时,可能会遇到下面两个问题:

[1]、Cannotreduce the visibility of the inherited method fromProcessFunction

[2]、The typeTestThriftService.Processor.getStr must implement theinherited abstract methodProcessFunction.isOneway()

问题产生的原因:

问题[1] 是继承类的访问权限缩小所造成的;

问题[2] 是因为存在抽象函数isOneWay所致;

解决办法:

问题[1]的访问权限由protected修改为public;

问题[2]的解决办法是为抽象函数添加一个空的函数体即可。

2.8、 应用技巧

(1) 为调用加上一个事务ID

在分布式服务开发过程中,一次事件(事务)的执行可能跨越位于不同机子上多个服务程序,在后续维护过程中跟踪log将变得非常麻烦,因此在系统设计的时候,系统的一个事务产生之处应该产生一个系统唯一的事务ID,该ID在各服务程序之间进行传递,让一次事务在所有服务程序输出的log都以此ID作为标识。

在使用Thrift开发服务器程序的时候,也应该为每个接口函数提供一个事务ID的参数,并且在服务器程序开发过程中,该ID应该在内部函数调用过程中也进行传递,并且在日志输出的时候都加上它,以便问题跟踪。

(2) 封装返回结果

Thrift提供的RPC方式的服务,使得调用方可以像调用自己的函数一样调用Thrift服务提供的函数;在使用Thrift开发过程中,尽量不要直接返回需要的数据,而是将返回结果进行封装,例如上面的例子中的getStr函数就是直接返回了结果string,见Thrift文件test_service.thrift中对该函数的描述:

string getStr(1:string srcStr1, 2:string srcStr2)

在实际开发过程中,这是一种很不好的行为,在返回结果为null的时候还可能造成调用方产生异常,需要对返回结果进行封装,例如:

  1. /*String类型返回结果*/
  2. struct ResultStr
  3. {
  4. 1: ThriftResult result,
  5. 2: string value
  6. }

其中ThriftResult是自己定义的枚举类型的返回结果,在这里可以根据自己的需要添加任何自己需要的返回结果类型:

  1. enum ThriftResult
  2. {
  3. SUCCESS, /*成功*/
  4. SERVER_UNWORKING, /*服务器处于非Working状态*/
  5. NO_CONTENT, /*请求结果不存在*/
  6. PARAMETER_ERROR, /*参数错误*/
  7. EXCEPTION, /*内部出现异常*/
  8. INDEX_ERROR, /*错误的索引或者下标值*/
  9. UNKNOWN_ERROR, /*未知错误*/
  10. DATA_NOT_COMPLETE, /*数据不完全*/
  11. INNER_ERROR, /*内部错误*/
  12. }

此时可以将上述定义的getStr函数修改为:

ResultStr getStr(1:string srcStr1, 2:string srcStr2)

在此函数中,任何时候都会返回一个ResultStr对象,无论异常还是正常情况,在出错时还可以通过ThriftResult返回出错的类型。

(3) 将服务与数据类型分开定义

在使用Thrift开发一些中大型项目的时候,很多情况下都需要自己封装数据结构,例如前面将返回结果进行封装的时候就定义了自己的数据类型ResultStr,此时,将数据结构和服务分开定义到不通的文件中,可以增加thrift文件的易读性。例如:

在thrift文件:thrift_datatype.thrift中定义数据类型,如:

  1. namespace java com.browan.freepp.thriftdatatype
  2. const string VERSION = "1.0.1"
  3. /**为ThriftResult添加数据不完全和内部错误两种类型
  4. */
  5. /
  6. * 定义返回值,
  7. * 枚举类型ThriftResult,表示返回结果,成功或失败,如果失败,还可以表示失败原因
  8. * 每种返回类型都对应一个封装的结构体,该结构体其命名遵循规则:"Result" + "具体操作结果类型",结构体都包含两部分内容:
  9. * 第一部分为枚举类型ThriftResult变量result,表示操作结果,可以 表示成功,或失败,失败时可以给出失败原因
  10. * 第二部分的变量名为value,表示返回结果的内容;
  11. ****/
  12. enum ThriftResult
  13. {
  14. SUCCESS, /成功/
  15. SERVER_UNWORKING, /服务器处于非Working状态/
  16. NO_CONTENT, /请求结果不存在/
  17. PARAMETER_ERROR, /参数错误/
  18. EXCEPTION, /内部出现异常/
  19. INDEX_ERROR, /错误的索引或者下标值/
  20. UNKNOWN_ERROR /未知错误/
  21. DATA_NOT_COMPLETE /数据不完全/
  22. INNER_ERROR /内部错误*/
  23. }
  24. /*bool类型返回结果*/
  25. struct ResultBool
  26. {
  27. 1: ThriftResult result,
  28. 2: bool value
  29. }
  30. /*int类型返回结果*/
  31. struct ResultInt
  32. {
  33. 1: ThriftResult result,
  34. 2: i32 value
  35. }
  36. /*String类型返回结果*/
  37. struct ResultStr
  38. {
  39. 1: ThriftResult result,
  40. 2: string value
  41. }
  42. /*long类型返回结果*/
  43. struct ResultLong
  44. {
  45. 1: ThriftResult result,
  46. 2: i64 value
  47. }
  48. /*double类型返回结果*/
  49. struct ResultDouble
  50. {
  51. 1: ThriftResult result,
  52. 2: double value
  53. }
  54. /*list<string>类型返回结果*/
  55. struct ResultListStr
  56. {
  57. 1: ThriftResult result,
  58. 2: list<string> value
  59. }
  60. /*Set<string>类型返回结果*/
  61. struct ResultSetStr
  62. {
  63. 1: ThriftResult result,
  64. 2: set<string> value
  65. }
  66. /*map<string,string>类型返回结果*/
  67. struct ResultMapStrStr
  68. {
  69. 1: ThriftResult result,
  70. 2: map<string,string> value
  71. }

代码2.5

在另外一个文件test_service.thrift中定义服务接口函数,如下所示:

  1. namespace java com.test.service
  2. include "thrift_datatype.thrift"
  3. service TestThriftService
  4. {
  5. /**
  6. *value 中存放两个字符串拼接之后的字符串
  7. */
  8. thrift_datatype.ResultStr getStr(1:string srcStr1, 2:string srcStr2),
  9. thrift_datatype.ResultInt getInt(1:i32 val)
  10. }

代码 2.6

由于在接口服务定义的thrift文件test_service.thrift中要用到对数据类型定义的thrift文件:thrift_datatype.thrift,因此需要在其文件前通过include把自己所使用的thrift文件包含进来,另外在使用其他thrift文件中定义的数据类型时要加上它的文件名,如:thrift_datatype.ResultStr

(4) 为Thrift文件添加版本号

在实际开发过程中,还可以为Thrift文件加上版本号,以方便对thrift的版本进行控制,如代码2.5所示。

Thrift的工作原理

三、 Thrift的工作原理

1.普通的本地函数调用过程

例如,有如下关于本地函数的调用的java代码,在函数caller中调用函数getStr获取两个字符串的拼接结果:

代码3.1

  1. **本地函数调用调用方和被调用方都在一个程序内部**,只是cpu在执行调用的时候切换去执行被调用的函数,执行完被调用函数之后,再切换回来执行调用之后的代码,其调用过程如下图3.1所示:

图3.1

  1. 站在调用方的角度,在本地函数调用过程中,执行被调用函数期间,调用方会被卡在那里一直等到被调用函数执行完,然后再继续执行剩下的代码。

2.Thrift的RPC调用过程

  1. **远程过程调用(RPC)的调用方和被调用方不在同一个进程内,甚至都不在同一台机子上**,因此远程过程调用中,必然涉及网络传输;假设有如下代码3.2所示的客户端代码:

代码3.2

上述代码含义在“二”中已经有详细解释,其调用过程如下图3.2所示:

图3.2 thrift的RPC调用过程

Thrift框架的远程过程调用的工作过程如下:

  1. **(1)**通过IDL定义一个接口的thrift文件,然后通过thrift的多语言编译功能,将接口定义的thrift文件翻译成对应的语言版本的接口文件;
  2. **(2)** Thrift生成的特定语言的接口文件中包括客户端部分和服务器部分;
  3. **(3)**客户端通过接口文件中的客户端部分生成一个Client对象,这个客户端对象中包含所有接口函数的存根实现,然后用户代码就可以通过这个Client对象来调用thrift文件中的那些接口函数了,但是,客户端调用接口函数时实际上调用的是接口函数的本地存根实现,如图3.2中的箭头1所示;
  4. **(4)**接口函数的存根实现将调用请求发送给thrift服务器端,然后thrift服务器根据调用的函数名和函数参数,调用实际的实现函数来完成具体的操作,如图3.2中的箭头2所示;
  5. **(5)**Thrift服务器在完成处理之后,将函数的返回值发送给调用的Client对象;如图3.2中的箭头3所示;

(6) Thrift的Client对象将函数的返回值再交付给用户的调用函数,如图3.2中的箭头4所示;

说明:

  1. [1]本地函数的调用方和被调方在同一进程的地址空间内部,因此在调用时cpu还是由当前的进行所持有,只是在调用期间,cpu去执行被调用函数,从而导致调用方被卡在那里,直到cpu执行完被调用函数之后,才能切换回来继续执行调用之后的代码;
  2. [2]RPC在调用方和被调用方一般不在一台机子上,它们之间通过网络传输进行通信,一般的RPC都是采用tcp连接,如果同一条tcp连接同一时间段只能被一个调用所独占,这种情况与[1]中的本地过程更为相似,这种情况是**同步调用**,很显然,这种方式通信的效率比较低,因为服务函数执行期间,tcp连接上没有数据传输还依然被本次调用所霸占;另外,这种方式也有优点:实现简单。
  3. [3]在一些的RPC服务框架中,为了提升网络通信的效率,客户端发起调用之后不被阻塞,这种情况是**异步调用**,它的通信效率比同步调用高,但是实现起来比较复杂。

四、 Thrift源码分析

  1. 源码分析主要分析thrift生成的java接口文件,并以TestThriftService.java为例,以该文件为线索,逐渐分析文件中遇到的其他类和文件;在thrift生成的服务接口文件中,共包含以下几部分:
  2. 1)异步客户端类AsyncClient和异步接口AsyncIface,本节暂不涉及这些异步操作相关内容;
  3. 2)同步客户端类Client和同步接口IfaceClient类继承自TServiceClient,并实现了同步接口IfaceIface就是根据thrift文件中所定义的接口函数所生成;Client类是在开发Thrift的客户端程序时使用,Client类是Iface的客户端存根实现, Iface在开发Thrift服务器的时候要使用,Thrift的服务器端程序要实现接口Iface
  4. 3Processor类,该类主要是开发Thrift服务器程序的时候使用,该类内部定义了一个map,它保存了所有函数名到函数对象的映射,一旦Thrift接到一个函数调用请求,就从该map中根据函数名字找到该函数的函数对象,然后执行它;
  5. 4)参数类,为每个接口函数定义一个参数类,例如:为接口getInt产生一个参数类:getInt_args,一般情况下,接口函数参数类的命名方式为:接口函数名_args;
  6. 5)返回值类,每个接口函数定义了一个返回值类,例如:为接口getInt产生一个返回值类:getInt_result,一般情况下,接口函数返回值类的命名方式为:接口函数名_result;
  7. 参数类和返回值类中有对数据的读写操作,在参数类中,将按照协议类将调用的函数名和参数进行封装,在返回值类中,将按照协议规定读取数据。
  8. Thrift调用过程中,Thrift客户端和服务器之间主要用到传输层类、协议层类和处理类三个主要的核心类,这三个类的相互协作共同完成rpc的整个调用过程。在调用过程中将按照以下顺序进行协同工作:
  9. 1 将客户端程序调用的函数名和参数传递给协议层(TProtocol),协议层将函数名和参数按照协议格式进行封装,然后封装的结果交给下层的传输层。此处需要注意:要与Thrift服务器程序所使用的协议类型一样,否则Thrift服务器程序便无法在其协议层进行数据解析;
  10. 2 传输层(TTransport)将协议层传递过来的数据进行处理,例如传输层的实现类TFramedTransport就是将数据封装成帧的形式,即“数据长度+数据内容”,然后将处理之后的数据通过网络发送给Thrift服务器;此处也需要注意:要与Thrift服务器程序所采用的传输层的实现类一致,否则Thrift的传输层也无法将数据进行逆向的处理;
  11. 3 Thrift服务器通过传输层(TTransport)接收网络上传输过来的调用请求数据,然后将接收到的数据进行逆向的处理,例如传输层的实现类TFramedTransport就是将“数据长度+数据内容”形式的网络数据,转成只有数据内容的形式,然后再交付给Thrift服务器的协议类(TProtocol);
  12. 4 Thrift服务端的协议类(TProtocol)将传输层处理之后的数据按照协议进行解封装,并将解封装之后的数据交个Processor类进行处理;
  13. 5 Thrift服务端的Processor类根据协议层(TProtocol)解析的结果,按照函数名找到函数名所对应的函数对象;
  14. 6 Thrift服务端使用传过来的参数调用这个找到的函数对象;
  15. 7 Thrift服务端将函数对象执行的结果交给协议层;
  16. 8 Thrift服务器端的协议层将函数的执行结果进行协议封装;
  17. 9 Thrift服务器端的传输层将协议层封装的结果进行处理,例如封装成帧,然后发送给Thrift客户端程序;
  18. 10 Thrift客户端程序的传输层将收到的网络结果进行逆向处理,得到实际的协议数据;
  19. 11 Thrift客户端的协议层将数据按照协议格式进行解封装,然后得到具体的函数执行结果,并将其交付给调用函数;

上述过程如图4.1所示:

图4.1、调用过程中Thrift的各类协同工作过程

  1. 上图4.1的客户端协议类和服务端协议类都是指具体实现了TProtocol接口的协议类,在实际开发过程中二者必须一样,否则便不能进行通信;同样,客户端传输类和服务端传输类是指TTransport的子类,二者也需保持一致;
  2. 在上述开发thrift客户端和服务器端程序时需要用到三个类:传输类(TTransport)、协议接口(TProtocol)和处理类(Processor),其中TTransport是抽象类,在实际开发过程中可根据具体清空选择不同的实现类;TProtocol是个协议接口,每种不同的协议都必须实现此接口才能被thrift所调用。例如TProtocol类的实现类就有TBinaryProtocol等;在Thrift生成代码的内部,还需要将待传输的内容封装成消息类TMessage。处理类(Processor)主要在开发Thrift服务器端程序的时候使用。

1. TMessage

  1. Thrift在客户端和服务器端传递数据的时候(包括发送调用请求和返回执行结果),都是将数据按照TMessage进行组装,然后发送;TMessage包括三部分:消息的名称、消息的序列号和消息的类型,消息名称为字符串类型,消息的序列号为32位的整形,消息的类型为byte类型,消息的类型共有如下17种:

publicfinal classTType {

public staticfinal byteSTOP =0;

public staticfinal byteVOID =1;

public staticfinal byteBOOL =2;

public staticfinal byteBYTE =3;

public staticfinal byteDOUBLE = 4;

public staticfinal byteI16 =6;

public staticfinal byteI32 =8;

public staticfinal byteI64 =10;

public staticfinal byteSTRING = 11;

public staticfinal byteSTRUCT = 12;

public staticfinal byteMAP =13;

public staticfinal byteSET =14;

public staticfinal byteLIST =15;

public staticfinal byteENUM =16;

}

Byte共可表示0~255个数字,这里只是用了前17个

2. 传输类(TTransport)

  1. 传输类或其各种实现类,都是对I/O层的一个封装,可更直观的理解为它封装了一个socket,不同的实现类有不同的封装方式,例如TFramedTransport类,它里面还封装了一个读写buf,在写入的时候,数据都先写到这个buf里面,等到写完调用该类的flush函数的时候,它会将写buf的内容,封装成帧再发送出去;
  2. TFramedTransport是对TTransport的继承,由于tcp是基于字节流的方式进行传输,因此这种基于帧的方式传输就要求在无头无尾的字节流中每次写入和读出一个帧,TFramedTransport是按照下面的方式来组织帧的:每个帧都是按照4字节的帧长加上帧的内容来组织,帧内容就是我们要收发的数据,如下:

+———————-+———————-+

| 4字节的帧长 | 帧的内容 |

+———————-+———————-+

3. 协议接口(TProtocol)

  1. 提供了一组操作协议接口,主要用于规定采用哪种协议进行数据的读写,它内部包含一个传输类(TTransport)成员对象,通过TTransport对象从输入输出流中读写数据;它规定了很多读写方式,例如:

readByte()

readDouble()

readString()

  1. 每种实现类都根据自己所实现的协议来完成TProtocol接口函数的功能,例如实现了TProtocol接口的TBinaryProtocol类,对于readDouble()函数就是按照二进制的方式读取出一个Double类型的数据。
  2. TBinaryProtocolTProtocol的一个实现类,TBinaryProtocol协议规定采用这种协议格式的进行消息传输时,需要为消息内容封装一个首部,TBinaryProtocol协议的首部有两种操作方式:一种是严格读写模式,一种值普通的读写模式;这两种模式下消息首部的组织方式不一样,在创建时也可以自己指定使用那种模式,但是要注意,如果要指定模式,Thrift的服务器端和客户端都需要指定。
  3. 严格读写模型下的消息首部的前16字节固定为版本号:0x8001,如图4.2所示;

图4.2二进制协议中严格读写模式下的消息组织方式

  1. 在严格读写模式下,首部中前32字节包括固定的16字节协议版本号0x80018字节的0x008字节的消息类型;然后是若干字节字符串格式的消息名称,消息名称的组织方式也是“长度+内容”的方式;再下来是32位的消息序列号;在序列号之后的才是消息内容。
  2. 普通读写模式下,没有版本信息,首部的前32字节就是消息的名称,然后是消息的名字,接下来是32为的消息序列号,最后是消息的内容。

图4.3 二进制协议中普通读写模式下的消息组织方式

  1. 通信过程中消息的首部在TBinaryProtocol类中进行通过readMessageBegin读取,通过writeMessageBegin写入;但是消息的内容读取在返回值封装类(例如:getStr_result)中进行;

(1) TBinaryProtocol的读取数据过程:

在Client中调用TBinaryProtocol读取数据的过程如下:

readMessageBegin()

读取数据

readMessageEnd()

readMessageBegin详细过程如下:

[1]首先从传输过来的网络数据中读取32位数据,然后根据首位是否为1来判断当前读到的消息是严格读写模式还是普通读写模式;如果是严格读写模式则这32位数据包括版本号和消息类型,否则这32位保存的是后面的消息名称

[2]读取消息的名称,如果是严格读写模式,则消息名称为字符串格式,保存方式为“长度+内容”的方式,如果是普通读写模式,则消息名称的长度直接根据[1]中读取的长度来读取消息名称;

[3]读取消息类型,如果是严格读写模式,则消息类型已经由[1]中读取出来了,在其32位数据中的低8位中保存着;如果是普通读写模式,则要继续读取一字节的消息类型;

[4]读取32为的消息序列号;

读取数据的过程是在函数返回值的封装类中来完成,根据读取的数值的类型来具体读取数据的内容;在TBinaryProtocol协议中readMessageEnd函数为空,什么都没有干。

(2) TBinaryProtocol的写入数据过程:

在sendBase函数调用TBinaryProtocol将调用函数和参数发送到Thrift服务器的过程如下:

writeMessageBegin(TMessage m)

写入数据到TTransport实现类的buf中

writeMessageEnd();

getTransport().flush();

writeMessageBegin函数需要一个参数TMessage作为消息的首部,写入过程与读取过程类似,首先判断需要执行严格读写模式还是普通读写模式,然后分别按照读写模式的具体消息将消息首部写入TBinaryProtocol的TTransport成员的buf中;

4. Thrift客户端存根代码追踪调试

下面通过跟踪附件中thrift客户端代码的test()函数,在该函数中调用了Thrift存根函数getStr,通过追踪该函数的执行过程来查看整个Thrift的调用流程:

(1)客户端代码先打开socket,然后调用存根对象的

m_transport.open();

String res = testClient.getStr(“test1”,”test2”);

(2)在getStr的存根实现中,首先发送调用请求,然后等待Thrift服务器端返回的结果:

send_getStr(srcStr1, srcStr2);

return recv_getStr();

(3)发送调用请求函数send_getStr中主要将参数存储到参数对象中,然后把参数和函数名发送出去:

getStr_args args = new getStr_args();//创建一个该函数的参数对象

args.setSrcStr1(srcStr1);//将参数值设置带参数对象中

args.setSrcStr2(srcStr2);

sendBase(“getStr”, args);//将函数名和参数对象发送出去

(4)sendBase函数,存根类Client继承自基类TServiceClient,sendBase函数即是在TServiceClient类中实现的,它的主要功能是调用协议类将调用的函数名和参数发送给Thrift服务器:

oprot.writeMessageBegin(new TMessage(methodName,TMessageType.CALL, ++seqid));//将函数名,消息类型,序号等信息存放到oprot_的TFramedTransport成员的buf中

args.write(oprot);//将参数存放到oprot的TFramedTransport成员的buf中

oprot_.writeMessageEnd();

oprot.getTransport().flush();//将oprot的TFramedTransport成员的buf中的存放的消息发送出去;

这里的oprot_就是在TProtocol的子类,本例中使用的是TBinaryProtocol,在调用TBinaryProtocol的函数时需要传入一个TMessage对象(在本节第2小节中有对TMessage的描述),这个TMessage对象的名字就是调用函数名,消息的类型为TMessageType.CALL,调用序号使用在客户端存根类中(实际上是在基类TServiceClient)中保存的一个序列号,每次调用时均使用此序列号,使用完再将序号加1。

在TBinaryProtocol中包含有一个TFramedTransport对象,而TFramedTransport对象中维护了一个缓存,上述代码中,写入函数名、参数的时候都是写入到TFramedTransport中所维护的那个缓存中,在调用TFramedTransport的flush函数的时候,flush函数首先计算缓存中数据的长度,将长度和数据内容组装成帧,然后发送出去,帧的格式按照“长度+字段”的方式组织,如:

+———————-+———————-+

| 4字节的帧长 | 帧的内容 |

+———————-+———————-+

(5)recv_getStr,在调用send_getStr将调用请求发送出去之后,存根函数getStr中将调用recv_getStr等待Thrift服务器端返回调用结果,recv_getStr的代码为:

getStr_resultresult = new getStr_result();//为接收返回结果创建一个返回值对象

receiveBase(result, “getStr”);//等待Thrift服务器将结果返回

(6)receiveBase,在该函数中,首先通过协议层读取消息的首部,然后由针对getStr生成的返回值类getStr_result读取返回结果的内容;最后由协议层对象结束本次消息读取操作;如下所示:

iprot_.readMessageBegin();//通过协议层对象读取消息的首部

……

result.read(iprot_);//通过返回值类对象读取具体的返回值;

……

iprot_.readMessageEnd();//调用协议层对象结束本次消息读取

在本节第4小节中有对readMessageBegin函数的描述;

5. 处理类(Processor)

该类主要由Thrift服务器端程序使用,它是由thrift编译器根据IDL编写的thrift文件生成的具体语言的接口文件中所包含的类,例如2.5节中提到的TestThriftService.java文件,处理类(Processor)主要由thrift服务器端使用,它继承自基类TBaseProcessor。

例如,2.5节中提到服务器端程序的如下代码:

TProcessor tProcessor =

NewTestThriftService.Processor(m_myService);

这里的TestThriftService.Processor就是这里提到的Processor类,包括尖括号里面的接口TestThriftService.Iface也是由thrift编译器自动生成。Processor类主要完成函数名到对应的函数对象的映射,它内部维护一个map,map的key就是接口函数的名字,而value就是接口函数所对应的函数对象,这样服务器端从网络中读取到客户端发来的函数名字的时候,就通过这个map找到该函数名对应的函数对象,然后再用客户端发来的参数调用该函数对象;在Thrift框架中,每个接口函数都有一个函数对象与之对应,这里的函数对象继承自虚基类ProcessFunction。

ProcessFunction类,它采用类似策略模式的实现方法,该类有一个字符串的成员变量,用于存放该函数对象对应的函数名字,在ProcessFunction类中主要实现了process方法,此方法的功能是通过协议层从传输层中读取并解析出调用的参数,然后再由具体的函数对象提供的getResult函数计算出结果;每个继承自虚基类ProcessFunction的函数对象都必须实现这个getResult函数,此函数内部根据函数调用参数,调用服务器端的函数,并获得执行结果;process在通过getResult函数获取到执行结果之后,通过协议类对象将结果发送给Thrift客户端程序。

Thrift服务器端程序在使用Thrrift服务框架时,需要提供以下几个条件:

(1)定义一个接口函数实现类的对象,在开发Thrift服务程序时,最主要的功能就是开发接口的实现函数,这个接口函数的实现类implements接口Iface,并实现了接口中所有函数;

(2)创建一个监听socket,Thrift服务框架将从此端口监听新的调用请求到来;

(3)创建一个实现了TProtocol接口的协议类对象,在与Thrift客户端程序通信时将使用此协议进行网络数据的封装和解封装;

(4)创建一个传输类的子类,用于和Thrift服务器之间进行数据传输;

Thrift server端的几种工作模式分析

Thrift服务器端几种工作模式分析与总结

Thrift为服务器端提供了多种工作模式,本文中将涉及以下5中工作模式:TSimpleServer、TNonblockingServer、THsHaServer、TThreadPoolServer、TThreadedSelectorServer,这5中工作模式的详细工作原理如下:

1. TSimpleServer模式

TSimpleServer的工作模式只有一个工作线程,循环监听新请求的到来并完成对请求的处理,它只是在简单的演示时候使用,它的工作方式如图5.1所示:

图5.1 TSimpleServer的工作模式

TSimpleServer的工作模式采用最简单的阻塞IO,实现方法简洁明了,便于理解,但是一次只能接收和处理一个socket连接,效率比较低,主要用于演示Thrift的工作过程,在实际开发过程中很少用到它。

2. TNonblockingServer模式

TNonblockingServer工作模式,该模式也是单线程工作,但是该模式采用NIO的方式,所有的socket都被注册到selector中,在一个线程中通过seletor循环监控所有的socket,每次selector结束时,处理所有的处于就绪状态的socket,对于有数据到来的socket进行数据读取操作,对于有数据发送的socket则进行数据发送,对于监听socket则产生一个新业务socket并将其注册到selector中,如下图5.2所示:

图5.2、TNonblockingServer工作模式

上图5.2中读取数据之后的业务处理就是根据读取到的调用请求,调用具体函数完成处理,只有完成函数处理才能进行后续的操作;

TNonblockingServer模式优点:

相比于TSimpleServer效率提升主要体现在IO多路复用上,TNonblockingServer采用非阻塞IO,同时监控多个socket的状态变化;

TNonblockingServer模式缺点:

TNonblockingServer模式在业务处理上还是采用单线程顺序来完成,在业务处理比较复杂、耗时的时候,例如某些接口函数需要读取数据库执行时间较长,此时该模式效率也不高,因为多个调用请求任务依然是顺序一个接一个执行。

3. THsHaServer模式(半同步半异步)

THsHaServer类是TNonblockingServer类的子类,在5.2节中的TNonblockingServer模式中,采用一个线程来完成对所有socket的监听和业务处理,造成了效率的低下,THsHaServer模式的引入则是部分解决了这些问题。THsHaServer模式中,引入一个线程池来专门进行业务处理,如下图5.3所示;

图5.3 THsHaServer模式

THsHaServer的优点:

与TNonblockingServer模式相比,THsHaServer在完成数据读取之后,将业务处理过程交由一个线程池来完成,主线程直接返回进行下一次循环操作,效率大大提升;

THsHaServer的缺点:

由图5.3可以看出,主线程需要完成对所有socket的监听以及数据读写的工作,当并发请求数较大时,且发送数据量较多时,监听socket上新连接请求不能被及时接受。

4. TThreadPoolServer模式

TThreadPoolServer模式采用阻塞socket方式工作,,主线程负责阻塞式监听“监听socket”中是否有新socket到来,业务处理交由一个线程池来处理,如下图5.4所示:

图5.4 线程池模式工作过程

TThreadPoolServer模式优点:

线程池模式中,数据读取和业务处理都交由线程池完成,主线程只负责监听新连接,因此在并发量较大时新连接也能够被及时接受。线程池模式比较适合服务器端能预知最多有多少个客户端并发的情况,这时每个请求都能被业务线程池及时处理,性能也非常高。

TThreadPoolServer模式缺点:

线程池模式的处理能力受限于线程池的工作能力,当并发请求数大于线程池中的线程数时,新请求也只能排队等待。

5. TThreadedSelectorServer

TThreadedSelectorServer模式是目前Thrift提供的最高级的模式,它内部有如果几个部分构成:

(1) 一个AcceptThread线程对象,专门用于处理监听socket上的新连接;

(2) 若干个SelectorThread对象专门用于处理业务socket的网络I/O操作,所有网络数据的读写均是有这些线程来完成;

(3) 一个负载均衡器SelectorThreadLoadBalancer对象,主要用于AcceptThread线程接收到一个新socket连接请求时,决定将这个新连接请求分配给哪个SelectorThread线程。

(4) 一个ExecutorService类型的工作线程池,在SelectorThread线程中,监听到有业务socket中有调用请求过来,则将请求读取之后,交个ExecutorService线程池中的线程完成此次调用的具体执行;

图5.5 TThreadedSelectorServer模式的工作过程

如上图5.5所示,TThreadedSelectorServer模式中有一个专门的线程AcceptThread用于处理新连接请求,因此能够及时响应大量并发连接请求;另外它将网络I/O操作分散到多个SelectorThread线程中来完成,因此能够快速对网络I/O进行读写操作,能够很好地应对网络I/O较多的情况;TThreadedSelectorServer对于大部分应用场景性能都不会差,因此,如果实在不知道选择哪种工作模式,使用TThreadedSelectorServer就可以。

Thrift传输格式

  1. TBinaryProtocol-二进制格式
  2. TCompactProtocol-压格式
  3. TJSONProtocol-JSON格式
  4. TSimpleJSONProtocol-提供JSON只写格式,生成的文件很容易通过脚本语言解析
  5. TDebugProtocol-试用易懂的可读文本格式,以便于debug(调式)

Thrift数据传输格式

  • TSocket -阻寒式socket
  • TFramedTransport-以frame为单位进行传输,排阻寒式服务中使用。
  • TFileTransport-以文件形式进行传输。
  • TMemoryTransport-将内存用于I/O.Java实现时内部实际使用了简单的ByteArrayOutputStream。
  • TZlibTransport-使用zlib进行压缩,与其他传输方式联合使用。当前无Java实现。