实习前的准备之一——Thrift入门

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

RPC是Remote Procedure Call,意为远程过程调用,具体自行百度。

Thrift是一种c/s的架构体系.在最上层是用户自行实现的业务逻辑代码.第二层是由thrift编译器自动生成的代码,主要用于结构化数据的解析,发送和接收。TServer主要任务是高效的接受客户端请求,并将请求转发给Processor处理。Processor负责对客户端的请求做出响应,包括RPC请求转发,调用参数解析和用户逻辑调用,返回值写回等处理。从TProtocol以下部分是thirft的传输协议和底层I/O通信。TProtocol是用于数据类型解析的,将结构化数据转化为字节流给TTransport进行传输。TTransport是与底层数据传输密切相关的传输层,负责以字节流方式接收和发送消息体,不关注是什么数据类型。底层IO负责实际的数据传输,包括socket、文件和压缩数据流等。

具体见下图

 

实习前的准备之一——Thrift入门_第1张图片

 

下面请看Thrift的一个小例子,每个步骤在看完小例子后都会有具体的讲解

 1,创建一个服务Hello,创建文件Hello.thrift,代码如下:

namespace java service.demo
service Hello{
    string helloString(1:string para)
}

  这里定义了一个名为helloString的方法,入参和返回值都是一个string类型的参数.

 2,终端进入Hello.thrift所在目录,执行命令:

thrift -r -gen java Hello.thrift

  发现在当前目录下多了一个gen-java的目录,里面的有一个Hello.java的文件.这个java文件包含Hello服务的接口定义Hello.Iface,以及服务调用的底层通信细节,包括客户端的调用逻辑Hello.Client以及服务端的处理逻辑Hello.Processor,

  3,创建一个Maven管理的Java项目,pom.xml中添加相关的依赖,并将Hello.java文件复制到项目中:

 

    
      org.apache.thrift
      libthrift
      0.10.0
    
    
      org.slf4j
      slf4j-log4j12
      1.7.5
    

 

      4,创建HelloServiceImpl实现Hello.Iface接口:

 

package service.demo;
import org.apache.thrift.TException;
/**
 * @author yogo.wang
 * @date 2017/02/21-下午2:13.
 */
public class HelloServiceImpl implements Hello.Iface {
    public String helloString(String para) throws TException {
        return "result:"+para;
    }
}

 

 5,创建服务端实现代码HelloServiceServer,把HelloServiceImpl作为一个具体的处理器传递给Thrift服务器:

 

package service.demo;
import org.apache.thrift.TProcessor;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.server.TServer;
import org.apache.thrift.server.TSimpleServer;
import org.apache.thrift.transport.TServerSocket;
import org.apache.thrift.transport.TTransportException;

/**
 * @author yogo.wang
 * @date 2017/02/21-下午2:15.
 */
public class HelloServiceServer {
    /**
     * 启动thrift服务器
     * @param args
     */
    public static void main(String[] args) {
        try {
            System.out.println("服务端开启....");
            TProcessor tprocessor = new Hello.Processor(new HelloServiceImpl());
            // 简单的单线程服务模型
            TServerSocket serverTransport = new TServerSocket(9898);
            TServer.Args tArgs = new TServer.Args(serverTransport);
            tArgs.processor(tprocessor);
            tArgs.protocolFactory(new TBinaryProtocol.Factory());
            TServer server = new TSimpleServer(tArgs);
            server.serve();
            }catch (TTransportException e) {
            e.printStackTrace();
        }
    }
}

 

 6,创建客户端实现代码HelloServiceClient,调用Hello.client访问服务端的逻辑实现:

 

package service.demo;

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;

/**
 * @author yogo.wang
 * @date 2017/02/21-下午2:35.
 */
public class HelloServiceClient {

    public static void main(String[] args) {
        System.out.println("客户端启动....");
        TTransport transport = null;
        try {
            transport = new TSocket("localhost", 9898, 30000);
            // 协议要和服务端一致
            TProtocol protocol = new TBinaryProtocol(transport);
            Hello.Client client = new Hello.Client(protocol);
            transport.open();
            String result = client.helloString("哈哈");
            System.out.println(result);
        } catch (TTransportException e) {
            e.printStackTrace();
        } catch (TException e) {
            e.printStackTrace();
        } finally {
            if (null != transport) {
                transport.close();
            }
        }
    }
}

 

 全部工作完成后,下面来测试一下,先执行服务端main方法,在执行客户端main方法,会在客户端控制台打印出:result:哈哈.

 

下面是刚刚每个步骤的详解

首先是IDL描述的Thrift文件

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

namespace javacom.test.service

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

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

namespace java com.test.service

include "thrift_datatype.thrift"

service TestThriftService
{

    /**
    *value 中存放两个字符串拼接之后的字符串
    */
    thrift_datatype.ResultStr getStr(1:string srcStr1, 2:string srcStr2),
    
    thrift_datatype.ResultInt getInt(1:i32 val)
    
}

代码2.1

 

这里的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, 枚举类型,例如,定义一个枚举类型:

enum Numberz
{
  ONE = 1,
  TWO,
  THREE,
  FIVE = 5,
  SIX,
  EIGHT = 8
}


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

 

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

struct TestV1 {
       1: i32 begin_in_both,
       3: string old_string,
       12: i32 end_in_both
}


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

(4)  定义类型别名

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

typedefi32  Integer 

就可以为i32类型重新起个名字Integer。
 

下面讲解将IDL文件编译成对应语言的接口文件

搭建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)。

接下来讲解服务端代码

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

package com.test.service;
 
import org.apache.thrift.TException;
 
public class TestThriftServiceImpl implements TestThriftService.Iface
{
 
    @Override
    public String getStr(String srcStr1, String srcStr2) throws TException {
        
        long startTime = System.currentTimeMillis();
        String res = srcStr1 + srcStr2; 
        long stopTime = System.currentTimeMillis();
        
        System.out.println("[getStr]time interval: " + (stopTime-startTime));
        return res;
    }
 
    @Override
    public int getInt(int val) throws TException {
        long startTime = System.currentTimeMillis();
        int res = val * 10; 
        long stopTime = System.currentTimeMillis();
        
        System.out.println("[getInt]time interval: " + (stopTime-startTime));
        return res;
    }
 
}
代码2.2

 

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

 

package com.test.service;
import org.apache.thrift.TProcessor;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.server.TNonblockingServer;
import org.apache.thrift.server.TServer;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TNonblockingServerSocket;
import org.apache.thrift.transport.TTransportException;
public class testMain {
    private static int m_thriftPort = 12356;
    private static TestThriftServiceImpl m_myService = new TestThriftServiceImpl();
    private static TServer m_server = ;
    private static void createNonblockingServer() throws TTransportException
    {
        TProcessor tProcessor = new TestThriftService.Processor(m_myService);
        TNonblockingServerSocket nioSocket = new TNonblockingServerSocket(m_thriftPort);
        TNonblockingServer.Args tnbArgs = new TNonblockingServer.Args(nioSocket);
        tnbArgs.processor(tProcessor);
        tnbArgs.transportFactory(new TFramedTransport.Factory());
        tnbArgs.protocolFactory(new TBinaryProtocol.Factory());
        // 使用非阻塞式IO,服务端和客户端需要指定TFramedTransport数据传输的方式
        m_server = new TNonblockingServer(tnbArgs);
    }
    public static boolean start()
    {
        try {
            createNonblockingServer();
        } catch (TTransportException e) {
            System.out.println("start server error!" + e);
            return false;
        }
        System.out.println("service at port: " + m_thriftPort);
        m_server.serve();
        return true;
    }
    public static void main(String[] args)
    {
        if(!start())
        {
            System.exit(0);
        }
    }
    
}
代码2.3


 

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

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

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

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

TestThriftServiceImpl m_myService =new TestThriftServiceImpl();

这里的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中的:

TNonblockingServerSocket nioSocket = new TNonblockingServerSocket(m_thriftPort);

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

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

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


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

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

m_server = new TNonblockingServer(tnbArgs);

…

m_server.serve();


 

接下来讲解客户端代码

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

      

  m_transport = new TSocket(THRIFT_HOST, THRIFT_PORT,2000);
         TProtocol protocol = new TBinaryProtocol(m_transport);
         TestThriftService.Client testClient = new TestThriftService.Client(protocol);
        
         try {
             m_transport.open();
             
             String res = testClient.getStr("test1", "test2");
             System.out.println("res = " + res);
             m_transport.close();
        } catch (TException e){
                // TODO Auto-generated catch block
                e.printStackTrace();
        }
代码2.4

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

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

m_transport =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");

System.out.println("res = " +res);

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

m_transport.close();

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

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

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

 

————————————————————————————————————————————

转载整理自


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

https://www.cnblogs.com/fingerboy/p/6424248.html

你可能感兴趣的:(实习前的准备之一——Thrift入门)