thrift入门学习

本文参考自,侵删,感谢原创作者!

Thrift简介

什么是thrift?

简单来说,是Facebook公布的一款开源跨语言的RPC框架.

什么是RPC框架?

RPC (Remote Procedure Call Protocal),远程过程调用协议。

在这里插入图片描述
注意数字代表时序。

是不是还是不太懂什么是RPC
RPC, 远程过程调用直观说法就是A通过网络调用B的过程方法。
简单的说,RPC就是从一台机器(客户端)上通过参数传递的方式调用另一台机器(服务器)上的一个函数或方法(可以统称为服务)并得到返回的结果
RPC 会隐藏底层的通讯细节(不需要直接处理Socket通讯或Http通讯) RPC 是一个请求响应模型。
客户端发起请求,服务器返回响应(类似于Http的工作方式) RPC 在使用形式上像调用本地函数(或方法)一样去调用远程的函数(或方法)。

早期单机时代,一台电脑上运行多个进程,大家各干各的,老死不相往来。假如A进程需要一个画图的功能,B进程也需要一个画图的功能,程序员就必须为两个进程都写一个画图的功能。这不是整人么?于是就出现了IPC(Inter-process communication,单机中运行的进程之间的相互通信)。OK,现在A既然有了画图的功能,B就调用A进程上的画图功能好了,程序员终于可以偷下懒了。
到了网络时代,大家的电脑都连起来了。以前程序只能调用自己电脑上的进程,那么现在能不能调用其他机器上的进程呢?于是就程序员就把IPC扩展到网络上,这就是RPC(远程过程调用)了。现在不仅单机上的进程可以相互通信,多机器中的进程也可以相互通信了。要知道实现RPC很麻烦呀,什么多线程、什么Socket、什么I/O,都是让咱们普通程序员很头疼的事情。于是就有牛人开发出RPC框架(比如,CORBA、RMI、Web Services、RESTful Web Services等等)。OK,现在可以定义RPC框架的概念了。简单点讲,RPC框架就是可以让程序员来调用远程进程上的代码一套工具。有了RPC框架,咱程序员就轻松很多了,终于可以逃离多线程、Socket、I/O的苦海了。
RPC框架就是一套工具,这套工具可以让程序员来调用远程进程上的代码。
thrift就是一款RPC框架。好了,现在大概弄懂thrift是干嘛的了。

thrift有一个特性特别好,就是跨语言特性。
Thrift最初由Facebook研发,主要用于各个服务之间的RPC通信,支持跨语言,常用的语言比如C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml都支持。Thrift是一个典型的CS(客户端/服务端)结构,客户端和服务端可以使用不同的语言开发。既然客户端和服务端能使用不同的语言开发,那么一定就要有一种中间语言来关联客户端和服务端的语言,没错,这种语言就是IDL(Interface Description Language)。
thrift通过一个中间语言IDL(接口定义语言)来定义RPC的数据类型和接口,这些内容写在以.thrift结尾的文件中,然后通过特殊的编译器来生成不同语言的代码,以满足不同需要的开发者,比如java开发者,就可以生成java代码,c++开发者可以生成c++代码,生成的代码中不但包含目标语言的接口定义,方法,数据类型,还包含有RPC协议层和传输层的实现代码.

thrift的协议栈结构

在这里插入图片描述

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

图中,TProtocol(协议层),定义数据传输格式,例如:

  • TBinaryProtocol:二进制格式;
  • TCompactProtocol:压缩格式;
  • TJSONProtocol:JSON格式;
  • TSimpleJSONProtocol:提供JSON只写协议, 生成的文件很容易通过脚本语言解析;
  • TDebugProtocol:使用易懂的可读的文本格式,以便于debug

TTransport(传输层),定义数据传输方式,可以为TCP/IP传输,内存共享或者文件共享等)被用作运行时库。

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

Thrift支持的服务模型

  • TSimpleServer:简单的单线程服务模型,常用于测试;
  • TThreadPoolServer:多线程服务模型,使用标准的阻塞式IO;
  • TNonblockingServer:多线程服务模型,使用非阻塞式IO(需使用TFramedTransport数据传输方式);

Thrift实际上是实现了C/S模式,通过代码生成工具将thrift文生成服务器端和客户端代码(可以为不同语言),从而实现服务端和客户端跨语言的支持。用户在Thirft文件中声明自己的服务,这些服务经过编译后会生成相应语言的代码文件,然后客户端调用服务,服务器端提供服务便可以了。
一般将服务放到一个.thrift文件中,服务的编写语法与C语言语法基本一致,在.thrift文件中有主要有以下几个内容:变量声明(variable)、数据声明(struct)和服务接口声明(service, 可以继承其他接口)。

client端和sever端代码要调用编译.thrift生成的中间文件。
下面分析cpp文件下面的CppClient.cpp和CppServer.cpp代码。

先给出thrift文件中的内容:

// 包含头文件
include “shared.thrift”        

// 指定目标语言
namespace cpp tutorial            

// 定义变量
const i32 INT32CONSTANT = 9853        

// 定义结构体
struct Work {
  1: i32 num1 = 0,
  2: i32 num2,
  3: Operation op,
  4: optional string comment,
}

// 定义服务
service Calculator extends shared.SharedService {
 /**
   * A method definition looks like C code. It has a return type, arguments,
   * and optionally a list of exceptions that it may throw. Note that argument
   * lists and exception lists are specified using the exact same syntax as
   * field lists in struct or exception definitions.
   */

   void ping(),

   i32 add(1:i32 num1, 2:i32 num2),

   i32 calculate(1:i32 logid, 2:Work w) throws (1:InvalidOperation ouch),

   /**
    * This method has a oneway modifier. That means the client only makes
    * a request and does not listen for any response at all. Oneway methods
    * must be void.
    */
   oneway void zip()

}

在这里插入图片描述
在client端,用户自定义CalculatorClient类型的对象(用户在.thrift文件中声明的服务名称是Calculator, 则生成的中间代码中的主类为CalculatorClient), 该对象中封装了各种服务,可以直接调用(如client.ping()), 然后thrift会通过封装的rpc调用server端同名的函数。
在server端,需要实现在.thrift文件中声明的服务中的所有功能,以便处理client发过来的请求。

自己理解的thrift:

就是一个黑盒子,用python写的客户端可以调用java写的服务端。
为什么要存在.thrift? 因为要实现这个黑盒子。

然后来介绍一下Thrift IDL

基本类型

thrift不支持无符号类型,因为很多编程语言不存在无符号类型,比如python
byte: 有符号字节
i16: 16位有符号整数
i32: 32位有符号整数
i64: 64位有符号整数
double: 64位浮点数
string: 字符串

容器类型

集合中的元素可以是除了service之外的任何类型,包括exception。

list: 一系列由T类型的数据组成的有序列表,元素可以重复
set: 一系列由T类型的数据组成的无序集合,元素不可重复
map<K, V>: 一个字典结构,key为K类型,value为V类型,相当于python中的{k:v}

结构体(struct)

就像C语言一样,thrift也支持struct类型,目的就是将一些数据聚合在一起,方便传输管理。struct的定义形式如下:

struct People {
     1: string name;
     2: i32 age;
     3: string sex;
}

枚举(enum)

枚举的定义形式和c语言的enum定义差不多,例如:

enum Sex {
    MALE,
    FEMALE
}

异常(exception)

thrift支持自定义exception,规则和struct一样,如下:

exception RequestException {
    1: i32 code;
    2: string reason;
}

服务(service)

thrift定义服务相当于Java中创建Interface一样,创建的service经过代码生成命令之后就会生成客户端和服务端的框架代码。定义形式如下:

service HelloWordService {
     // service中定义的函数,相当于Java interface中定义的函数
     string doAction(1: string name, 2: i32 age);
 }

类型定义

thrift支持类似C++一样的typedef定义,比如:

typedef i32 Integer
typedef i64 Long

注意,末尾没有逗号或者分号

常量(const)

thrift也支持常量定义,使用const关键字,例如:

const i32 MAX_RETRIES_TIME = 10
const string MY_WEBSITE = "http://qifuguang.me";

末尾的分号是可选的,可有可无,并且支持16进制赋值

命名空间

thrift的命名空间相当于python中的package的意思,主要目的是组织代码。thrift使用关键字namespace定义命名空间,这点跟C很像。例如:

namespace java com.winwill.thrift

格式是:namespace 语言名 路径, 注意末尾不能有分号。

文件包含

thrift也支持文件包含,相当于C/C++中的include,python中的import。使用关键字include定义,例 如:

include "global.thrift"

注释

thrift注释方式支持shell风格的注释,支持C/C++风格的注释,即#//开头的语句都单当做注释,/**/包裹的语句也是注释。

可选与必选

thrift提供两个关键字required,optional,分别用于表示对应的字段时必填的还是可选的。例如:

struct People {
    1: required string name;
    2: optional i32 age;
}

表示name是必填的,age是可选的。

生成代码

知道了怎么定义thirtf文件之后,我们需要用定义好的thrift文件生成我们需要的目标语言的源码,本文以生成java源码为例。假设现在定义了如下一个thrift文件:

namespace java com.winwill.thrift

enum RequestType {
   SAY_HELLO,   //问好
   QUERY_TIME,  //询问时间
}

struct Request {
   1: required RequestType type;  // 请求的类型,必选
   2: required string name;       // 发起请求的人的名字,必选
   3: optional i32 age;           // 发起请求的人的年龄,可选
}

exception RequestException {
   1: required i32 code;
   2: optional string reason;
}

// 服务名
service HelloWordService {
   string doAction(1: Request request) throws (1:RequestException qe); // 可能抛出异常。
}

在终端运行如下命令(前提是已经安装thrift):

thrift --gen java Test.thrift

则在当前目录会生成一个gen-java目录,该目录下会按照namespace定义的路径名一层层生成文件夹,到gen-java/com/winwill/thrift/目录下可以看到生成的4个Java类。

可以看到,thrift文件中定义的enum,struct,exception,service都相应地生成了一个Java类,这就是能支持Java语言的基本的框架代码。

服务端实现

上面代码生成这一步已经将接口代码生成了,现在需要做的是实现HelloWordService具体逻辑,实现的方式就是创建一个Java类implements com.winwill.thrift.HelloWordService,例如:

package com.winwill.thrift;


import org.apache.commons.lang3.StringUtils;
import org.apache.thrift.TException;

import java.util.Date;

/**
 * @author qifuguang
 * @date 15/9/11 15:53
 */
public class HelloWordServiceImpl implements com.winwill.thrift.HelloWordService.Iface {
    // 实现这个方法完成具体的逻辑。
    public String doAction(com.winwill.thrift.Request request) throws com.winwill.thrift.RequestException, TException {
        System.out.println("Get request: " + request);
        if (StringUtils.isBlank(request.getName()) || request.getType() == null) {
            throw new com.winwill.thrift.RequestException();
        }
        String result = "Hello, " + request.getName();
        if (request.getType() == com.winwill.thrift.RequestType.SAY_HELLO) {
            result += ", Welcome!";
        } else {
            result += ", Now is " + new Date().toLocaleString();
        }
        return result;
    }
}

运行之后看到控制台的输出为:

Running server...

客户端请求

现在服务已经启动,可以通过客户端向服务端发送请求了,客户端的代码如下:

package com.winwill.thrift;

import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;

/**
 * @author qifuguang
 * @date 15/9/11 16:13
 */
public class HelloWordClient {
    public static void main(String[] args) throws Exception {
        TTransport transport = new TSocket("localhost", 8888);
        TProtocol protocol = new TBinaryProtocol(transport);

        // 创建client
        com.winwill.thrift.HelloWordService.Client client = new com.winwill.thrift.HelloWordService.Client(protocol);

        transport.open();  // 建立连接

        // 第一种请求类型
        com.winwill.thrift.Request request = new com.winwill.thrift.Request()
                .setType(com.winwill.thrift.RequestType.SAY_HELLO).setName("winwill2012").setAge(24);
        System.out.println(client.doAction(request));

        // 第二种请求类型
        request.setType(com.winwill.thrift.RequestType.QUERY_TIME).setName("winwill2012");
        System.out.println(client.doAction(request));

        transport.close();  // 请求结束,断开连接
    }
}

运行客户端代码,得到结果:

Hello, winwill2012, Welcome!
Hello, winwill2012, Now is 2015-9-11 16:37:22

并且此时,服务端会有请求日志:

Running server...
Get request: Request(type:SAY_HELLO, name:winwill2012, age:24)
Get request: Request(type:QUERY_TIME, name:winwill2012, age:24)

可以看到,客户端成功将请求发到了服务端,服务端成功地将请求结果返回给客户端,整个通信过程完成。

怎么用thrift生成目标代码?
thrift -r --gen py mdd_cityqueue.thrift

yum install thrift # 安装thrift到linux
# py代表这是生成python代码
# 执行后,会在当前目录生成gen-py目录

猜你喜欢

转载自blog.csdn.net/weixin_41687289/article/details/88846548