天天看点

Thrift RPC详解

RPC

RPC, 远程过程调用(Remote Procedure Call,RPC)是一个计算机通信协议,该协议允许运行于一台计算机的程序程调用另一台计算机的上的程序。通俗讲,RPC通过把网络通讯抽象为远程的过程调用,调用远程的过程就像调用本地的子程序一样方便,从而屏蔽了通讯复杂性,使开发人员可以无需关注网络编程的细节,将更多的时间和精力放在业务逻辑本身的实现上,提高工作效率。

RPC本质上是一种 Inter-process communication(IPC)——进程间通信的形式。常见的进程间通信方式如管道、共享内存是同一台物理机上的两个进程间的通信,而RPC就是两个在不同物理机上的进程之间的通信。概括的说,RPC就是在一台机器上调用另一台机器上的方法,这种调用在远程机器上对代码的执行就像在本机上对代码的执行一样,只是迁移了一个执行环境而已。

RPC是一种C/S架构的服务模型,server端提供接口供client调用,client端向server端发送数据,server端接收client端的数据进行相关计算并将结果返回给client端。

执行一次RPC通常需要经历以下步骤(摘自 Wikipedia):

1.The client calls the client stub. The call is a local procedure call, with parameters pushed on to the stack in the normal way.
2.The client stub packs the parameters into a message and makes a system call to send the message. Packing the parameters is called marshalling.
3.The client's local operating system sends the message from the client machine to the server machine.
4.The local operating system on the server machine passes the incoming packets to the server stub.
5.The server stub unpacks the parameters from the message. Unpacking the parameters is called unmarshalling.
6.Finally, the server stub calls the server procedure. The reply traces the same steps in the reverse direction
           

为了实现上述RPC步骤,许多RPC工具被研发出来。这些RPC工具大多使用“接口描述语言” —— interface description language (IDL) 来提供跨平台跨语言的服务调用。现在生产中用的最多的IDL是Google开源的protobuf。

在日常开发中通常有两种形式来使用RPC,一种是团队内部完全实现上述RPC的6个步骤,自己序列化数据,然后自己利用socket或者http传输数据,最常见的就是游戏开发了。另一种就是利用现成的RPC工具,这些RPC工具实现了底层的数据通信,开发人员只需要利用IDL定义实现自己的服务即可而不用关心数据是如何通信的,最常见的RPC工具是Facebook开源的Thrift RPC框架。本文将重点讲解Thrift RPC。

Thrift实际上是实现了C/S模式,通过代码生成工具将接口定义文件生成服务器端和客户端代码(可以为不同语言),从而实现服务端和客户端跨语言的支持。用户在Thirft描述文件中声明自己的服务,这些服务经过编译后会生成相应语言的代码文件,然后用户实现服务(客户端调用服务,服务器端提服务)便可以了。其中protocol(协议层, 定义数据传输格式,可以为二进制或者XML等)和transport(传输层,定义数据传输方式,可以为TCP/IP传输,内存共享或者文件共享等)被用作运行时库。

Thrift的协议栈如下图所示:

在Client和Server的最顶层都是用户自定义的处理逻辑,也就是说用户只需要编写用户逻辑,就可以完成整套的RPC调用流程。用户逻辑的下一层是Thrift自动生成的代码,这些代码主要用于结构化数据的解析,发送和接收,同时服务器端的自动生成代码中还包含了RPC请求的转发(Client的A调用转发到Server A函数进行处理)。

协议栈的其他模块都是Thrift的运行时模块:

  • 底层IO模块,负责实际的数据传输,包括Socket,文件,或者压缩数据流等。
  • TTransport负责以字节流方式发送和接收Message,是底层IO模块在Thrift框架中的实现,每一个底层IO模块都会有一个对应TTransport来负责Thrift的字节流(Byte Stream)数据在该IO模块上的传输。例如TSocket对应Socket传输,TFileTransport对应文件传输。
  • TProtocol主要负责结构化数据组装成Message,或者从Message结构中读出结构化数据。TProtocol将一个有类型的数据转化为字节流以交给TTransport进行传输,或者从TTransport中读取一定长度的字节数据转化为特定类型的数据。如int32会被TBinaryProtocol Encode为一个四字节的字节数据,或者TBinaryProtocol从TTransport中取出四个字节的数据Decode为int32。
  • TServer负责接收Client的请求,并将请求转发到Processor进行处理。TServer主要任务就是高效的接受Client的请求,特别是在高并发请求的情况下快速完成请求。
  • Processor(或者TProcessor)负责对Client的请求做出相应,包括RPC请求转发,调用参数解析和用户逻辑调用,返回值写回等处理步骤。Processor是服务器端从Thrift框架转入用户逻辑的关键流程。Processor同时也负责向Message结构中写入数据或者读出数据。

Thrift的模块设计非常好,在每一个层次都可以根据自己的需要选择合适的实现方式。同时也应该注意到Thrift目前的特性并不是在所有的程序语言中都支持。例如C++实现中有TDenseProtocol没有TTupleProtocol,而Java实现中有TTupleProtocol没有TDenseProtocol。

利用Thrift用户只需要做三件事:

(1). 利用IDL定义数据结构及服务
(2). 利用代码生成工具将(1)中的IDL编译成对应语言(如C++、JAVA),编译后得到基本的框架代码(实现接口定义)
(3). 在(2)中框架代码基础上完成完整代码(纯C++代码、JAVA代码等)(实现业务逻辑)
           
Thrift RPC详解

Linux下安装

        Thrift的安装包括上面提到生成代码生成器和应用框架库,网页(http://thrift.apache.org/docs/install/)描述了安装依赖项,除了gcc及其编译工具本身外,编译Thrift最大的依赖就是boost。安装过程并不复杂,请参阅相关网上文章。

Windows下Thrift的使用

  1. 下载thrift服务的安装包https://download.csdn.net/download/qq_31854907/16348757 下载下来是一个.exe的运行文件
  2. 将下载下来的thrift-0.14.1.exe 文件重命名为thrift.exe
    Thrift RPC详解
  3. 配置thrift的环境变量,即为了能够执行thrift指令
    Thrift RPC详解
  4. 测试window环境thrift是否安装完成
    Thrift RPC详解

Thrift基本概念与应用

Thrift有以下几个概念:

类型系统(typesystem)

Thrift定义了一套数据传输描述语言(有点类似IDL),它是“语言中性”的,这个就是它的类型系统。它分为五种类型(数据类型表达3种,预定义类/结构1种,接口表达1种):

基本类型(basictype),也就是bool、byte、i16、i32、i64、double、string,任何语言都有这些基本类型,比较有意思的是string,它即表达text,也表达binary bytes。另一个特点是整型没有unsigned,原因比较简单,因为有些语言不支持。

  1.     结构类型(struct):就是C语言中的struct,将基本类型组合起来。
  2.     容器类型(container):就是集合类型(list/set/map),其中的元素是任何Thrift可识别的基本、结构、容器类型。【不知道是否有不支持list/set/map的语言,那么Thrift如何处理呢?】
  3.     异常类型(exception):从数据结构讲就是结构类型,可以认为是便于异常的处理而单独拿出来的、预定义的、有特殊意义的结构类型。
  4.     服务定义类型(service):这个类型实际是用来定义接口的,Thrift代码生成器会根据这个定义,生成代码框架。

传输(transport)

也就是信息的传输渠道以及读写方式,例如,介质可以是socket、shared memory或file,Thrift规定了一些基本的操作(open/close/isOpen/read/write/flush,对server,再加上listen/accept)。特别的,针对Socket方式,有TScoket类,对file方式,有TFileTransport类,上面类比较底层,还有几个实用的类:TBufferedTransport,TFramedTransport,TMemoryBuffer等。

协议(protocol)

是对传输协议的封装,也就是传输采用二进制、XML或者text来表示信息,它的功能有两个:1.双向的消息队列;2.信息的编码和解码(也就是对上面类型的读/写)。关于流式格式,thrift数据类型是自我分割的,意思是说,thrift会自己在数据域的分割处插入标志,在解码的时候,即使没有数据域定义,thrift也能成功分割出各数据域。在若干篇文章中,都提到thrift的二进制流式编码有相当的效率(可以配合压缩),因此首选的协议应该是binary协议。

版本(versioning)

如果一个程序分开来开发,那版本问题就是绕不过去的问题。Thrift的版本是通过“field identifiers”来实现的,每个结构由其标识,结构中的每个域有其标识,这两个标识唯一决定了一个数据域。在解码的时候,数据域的标识被检查,如果不能识别,则该数据域被抛弃。Thrift也可以通过”Isset”机制来明确某些域的设置与否(发送端用来指明是否设置,接收端用来检测是否设置)。

四种情况:

    添加了数据域, 旧客户端,新服务器端:客户端发送的数据中没有该域,服务器端能检测出来,可按缺省值处理。

    删除了数据域, 旧客户端,新服务器端:客户端发送的数据中有该域,服务器端忽略该域。

    添加了数据域, 新客户端,旧服务器端:客户端发送的数据中有该域,服务器端忽略该域。

    删除了数据域, 新客户端,旧服务器端:客户端发送的数据中没有该域,服务器端可能不知道如何处理这种情况。

 处理器(processor)

就是如何将各部分协调起来,形成代码(或用户代码的框架)。它有两个重要的类:TProcessor和TServer。TProcessor用来实现RPC调用,TServer是所有Server类的基类,TServer类主要处理连接和线程,而不管诸如传输、编码等。用户代码主要关注的一是.thrift文件,二就是这个接口。Thrift为此实现了TSimpleServer(单线程), TThreadedServer(每连接一个线程)和 TThreadPoolServer(线程池)等类。

下图是thrift生成代码的基本结构(C++)。

Thrift RPC详解

图中,ServiceIf是根据接口文件(.thrift)生成的虚接口类,用户的具体实现在ServiceHandler中。各种调用方式在TServer中实现。【详细的描述见实例】

2.Thrift实现上的几个考虑

目标语言

虽然有多种选择,但最常用的(可能也是支持最好的)是C++, Java, and Python。

生成的结构体

数据域成员都是公有的,没有set,get之类的东西,虽然建议采用isset,但也可以不用,系统足够强健来处理类似“FieldNotSetException”之类的问题,因而也没有涉及该异常。Read和write方面也是公有的,这样用户可以在固有的RPC之外来使用它们。

RPC方法标识:实现RPC时,建立函数名与函数指针之间的映射,大致如下(不同的语言表达方式不同,C++,map):

std::map<std::string,函数指针> processMap_;

这样加快函数调用。

多线程

对C++实现,在开发过程中,thrift开发人员研究过boost,ACE中与thread,timer相关的东西,开发人员不想引入过多的第三方依赖,因此thrift中只有对boost::shared_ptr的引用是必须的,但为了跨平台或获得更多的功能,一般情况下,boost中thread,timer及其依赖库也是需要的。

ThreadManager和TimerManager

线程管理类用来管理线程池,定时器管理类可以定时触发Runnable的对象,开启一件事情(可以放到或不放到一个单独线程)。

NonblockingOperation

这个东西需要libevent的支持。

Compiler(代码生成器)

这个东西是用C++写的,依赖于lex/yacc。代码生成分两步:第一,检查包含的文件和类型定义文件,生成“解析树”(the parse tree);第二,将各类型放到解析树中,根据解析树生成代码。

TFileTransport

这个类(及其继承类)可以将request消息记入文件,为提高性能,它先缓存记录,并存入磁盘。记录文件是分块的(文件固定大小),采用padding,记录不能跨块。

thrift示例

1、thrift的协议

namespace java com.moredian.onpremise.tripartite.wechat
service Hello {
string helloString(1:string word)
}
           

经过thrift服务进行编译会得到相应语言的接口

例如java的

thrift --gen java helloService.thrift

在当前目录下生成gen-java目录,里面生成了HelloService.java文件。

Thrift --gen py helloService.thrift 可以生成python相关代码。

使用java实现server方式

import java.net.InetSocketAddress;

import org.apache.thrift.protocol.TBinaryProtocol.Factory;
import org.apache.thrift.server.TServer;
import org.apache.thrift.server.TThreadPoolServer;
import org.apache.thrift.server.TThreadPoolServer.Args;
import org.apache.thrift.transport.TServerSocket;
import org.apache.thrift.transport.TTransportException;

import com.moredian.onpremise.tripartite.demo.thrift.Hello;
import com.moredian.onpremise.tripartite.demo.thrift.Hello.Processor;


/**
 * 启动服务
 *
 */
public class ThriftServer {
    /**
     * 启动Thrift服务器
     */
    public void startServer() {
        try {
            // 定义传输的socket,设置服务端口为6789
            TServerSocket serverTransport = new TServerSocket(new InetSocketAddress("localhost",9000));
//            serverTransport.getServerSocket().bind(new InetSocketAddress("localhost",9000));

            // 设置协议工厂为 TBinaryProtocol.Factory
            Factory proFactory = new Factory(true, true);

            // 关联处理器与 Hello服务的实现
            Hello.Processor processor = new Processor(new HelloServiceImpl());

            // 定义服务端的参数值
            Args args = new Args(serverTransport);
            args.processor(processor);
            args.protocolFactory(proFactory);
            TServer server = new TThreadPoolServer(args);

            // 服务端开启服务s
            server.serve();
        } catch (TTransportException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        System.out.println("ServerStart!");
        ThriftServer server = new ThriftServer();
        server.startServer();
    }

}
           
import org.apache.thrift.TException;

import com.moredian.onpremise.tripartite.demo.thrift.Hello.Iface;

/**
 * 服务端实现类
 *
 * @author pallas_cat
 */
public class HelloServiceImpl implements Iface {

    public String helloString(String word) throws TException {
        //输出word是在服务端输出
        System.out.println(word);
        //return是给客户端返回信息
        return "1";
    }
}
           

使用java实现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 java.io.IOException;

import com.moredian.onpremise.tripartite.demo.thrift.Hello;

public class SimilarityThriftClient {

    public static void main(String[] args) {
        try {
            TTransport transport;
            transport = new TSocket("192.168.1.134", 9000);
            transport.open();
            TProtocol    protocol = new TBinaryProtocol(transport);
            Hello.Client client   = new Hello.Client(protocol);
            perform(client);
            transport.close();
        } catch (TException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void perform(Hello.Client client) throws TException, IOException {
        String str = "001.jpg,002.jpg,003.jpg,004.jpg,005.jpg,demo.jpg";
        String[] paths = str.split(",");
        for(String path:paths){
            //ratio是客户端吧path传过去后服务端的返回信息,即服务端代码里的1
            String ratio = client.helloString(path);
            System.out.println(ratio);
        }
    }
}