U2647's blog 一个热爱学习的 Java 程序员,喜欢 Vue,喜欢深度学习 Dubbo Flutter SpringBoot Debug Notes Java LeetCode Python Redis Android DesignPattern mdi-home-outline 首页 mdi-cloud-outline 标签云 mdi-timeline-text-outline 时间轴 mdi-draw-pen 文章总数 62
Thrift 学习笔记 Thrift 学习笔记 Java Thrift RPC mdi-cursor-default-click-outline 点击量 62

1. Thrift 简介

Thrift 是有 Facebook 开源的一套 RPC 框架, 支持多种语言,它是通过自身的 中间语言(IDL),并借助代码生成引擎来生成各种主流语言的代码模板

2. IDL 语言

Thrift 采用IDL(Interface Definition Language)来定义通用的服务接口,然后通过Thrift提供的编译器,可以将服务接口编译成不同语言编写的代码,通过这个方式来实现跨语言的功能。

IDL 中有基本类型、结构体类型、容器类型、枚举类型、异常类型、服务类型等六种类型。熟悉了这些常用类型后基本上可以应付日常开发。

2.1 基本类型:

  • bool: 布尔值
  • byte: 8位有符号整数
  • i16: 16位有符号整数
  • i32: 32位有符号整数
  • i64: 64位有符号整数
  • double: 64位浮点数
  • string: UTF-8编码的字符串
  • binary: 二进制串

2.2 struct(结构体):

类似于 C 语言中的结构体。在 Java 中就是 POJO,struct 类型有以下几个要求:

  1. struct不能继承,但是可以嵌套,不能嵌套自己。
  2. 其成员都是有明确类型
  3. 成员是被正整数编号过的,其中的编号使不能重复的,这个是为了在传输过程中编码使用。
  4. 成员分割符可以是逗号(,)或是分号(;),而且可以混用
  5. 字段会有optional和required之分和protobuf一样,但是如果不指定则为无类型–可以不填充该值,但是在序列化传输的时候也会序列化进去,optional是不填充则部序列化,required是必须填充也必须序列化。
  6. 每个字段可以设置默认值
  7. 同一文件可以定义多个struct,也可以定义在不同的文件,进行include引入。

例如:

struct User{
  1: required string name, //改字段必须填写
  2: optional i32 age = 0; //默认值
  3: bool gender //默认字段类型为optional
}

2.3 Container (容器)

  • list: 元素类型为t的有序表,容许元素重复。对应 Java 的 ArrayList
  • set: 元素类型为t的无序表,不容许元素重复。对应 Java 的 HashSet
  • map<t, t>: 键类型为t,值类型为t的kv对,键不容许重复。对应 Java 的 HashMap

例如:

struct Test {
  1: map<string, User> usermap,
  2: set<i32> intset,
  3: list<double> doublelist
}

2.4 enum (枚举):

Thrift不支持枚举类嵌套,枚举常量必须是32位的正整数。

例如:

enum HttpStatus {
  OK = 200,
  NOTFOUND=404
}

2.5 Exception (异常):

异常在语法和功能上类似于结构体,不过使用的关键字是 exception。对应 Java 的 Exception

例如:

exception MyException {
    1: i32 errorCode,
    2: string message
}

2.6 Service (服务定义类型):

即我们需要提供的服务接口。

例如:

service HelloService {
    i32 sayInt(1:i32 param)
    string sayString(1:string param)
    bool sayBoolean(1:bool param)
    void sayVoid()
}

2.7 常量类型

在 Java 中我们可能还会经常用到常量类型,在 IDL 中 常亮类型可以使用 const 关键字。

例如:

const i32 const_int = 1;

2.8 Namespace (名字空间)

IDL 中的命名空间即是 Java 中的包名

例如:

namespace java com.example.test

会被转换成:

package com.example.test

2.9 注释

IDL 支持 单行注释和多行注释

/** 
 * 这里是多行注释
 * 
 */

// 这里是单行注释

2.10 Include(导入)

类似于 Java 中的 import 在本文件中引入其他文件中定义的内容。

注意:thrift文件名要用双引号包含,末尾没有逗号或者分号。

例如:

include "test.thrift"   
...
struct HelloTest {
    1: in32 uid;
     ...
}

3. Java 版的 Hello World

3.1 安装

由于 Thrift 需要使用代码生成引擎来将 Thrift 代码转换成其他语言(Java)的代码,所以需要在本地安装一下 Thrift

注意:本地安装的 Thrift 版本最好和生产环境保持一致。不然很可能会出现一些问题

使用以下命令安装:

brew install thrift

本人使用的是 Mac OS ,你和可以去 Thrift 官网,下载对应的安装包进行安装。

最终,如果你在命令行下执行下面的命令,出现 Thrift 版本号,就说明安装成功了。

> thrift -version
Thrift version 0.12.0

IDEA 默认是不支持 Thrift 所以需要安装 Thrift Support 插件。

3.2 配置

安装好插件,重启后就能在 File -> new -> Project... 菜单下 看到 Thrift 工程了。

thrift-001

创建完成 Thrift 项目后,还需要配置一下 Thrift 代码生成引擎相关的东西。

打开菜单 File - > Project Structure... -> Project Settings -> Facets ,选择 Thrift

thrift-002

然后会让你代码输出的位置。

thrift-003

3.3 创建 Thrift 文件

新建 HelloService.thrift,在菜单中可能找不到创建 thrift 文件的菜单,直接新建 File 然后以 thrift 作为文件后缀就可以。

内容如下:

namespace java com.zdran.test.thrift
service ThriftHelloService{
    string sayHello(1:string userName)
}

在这个接口文件中我们定义了一个 sayHello 的接口,它接收一个 String 类型的参数,并返回一个 String 类型的值。

现在我需要编译该 thrift 文件,然后就会自动生成我们需要的代码。

我们可以直接在控制台执行以下命令。

thrift -gen java HelloService.thrift

然后就能在gen-java目录中看到生成后的 java 文件了。

还有一种办法,就是将 thrift 编译器添加到右键菜单里,直接在 IDEA 里编译。

打开 Preferences... 菜单,设置好你的 Thrift 的安装目录,然后勾选上下面的这个选项,就可以在 IDEA 里直接编译 thrift 文件了。

thrift-005

注意:thrift 文件 必须要在 src 目录下,或者手动设置的 source 目录(IDEA显示蓝色的文件夹)下才会有 Recompile 菜单

PS: 搞了半天,最后可以编译了,结果报错:

Error:Internal error: (java.util.concurrent.ExecutionException) com.intellij.util.xmlb.XmlSerializationException: Cannot deserialize class com.intellij.plugins.thrift.config.ThriftCompilerOptions
java.util.concurrent.ExecutionException: com.intellij.util.xmlb.XmlSerializationException: Cannot deserialize class com.intellij.plugins.thrift.config.ThriftCompilerOptions
    
    ... ... 

也不知道是 IDEA 的问题,还是插件的问题,网上搜了一下啊,貌似没有遇到这个问题的,有时间再研究吧,直接在控制台编译好,然后我们再新建一个 Maven 项目。并添加 thrift 依赖

    <dependencies>
        <dependency>
            <groupId>org.apache.thrift</groupId>
            <artifactId>libthrift</artifactId>
            <version>0.12.0</version>
        </dependency>
    </dependencies>

然后把上面生成的 java 文件复制到 对应的包下面。

然后我再创建一个实现类ThriftHelloServiceImpl.java,来实现我们定义好的接口。内容如下:

package com.test.thrift;
import org.apache.thrift.TException;

public class ThriftHelloServiceImpl implements ThriftHelloService.Iface {
    @Override
    public String sayHello(String userName) throws TException {

        return "hello "+ userName;
    }
}

3.4 创建服务端和客户端类

我们这里采用 Main 方法的形式来进行客户端与服务端的通信,即启动两个 Main 方法,分别表示服务端与客户端。

首先创建服务端的 Main 方法,代码如下:

public class ServiceMain {
    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket(9090);
        TServerSocket serverTransport = new TServerSocket(serverSocket);
        ThriftHelloService.Processor processor =
                new ThriftHelloService.Processor<ThriftHelloService.Iface>(new ThriftHelloServiceImpl());

        TBinaryProtocol.Factory protocolFactory = new TBinaryProtocol.Factory();
        TSimpleServer.Args tArgs = new TSimpleServer.Args(serverTransport);
        tArgs.processor(processor);
        tArgs.protocolFactory(protocolFactory);

        TServer tServer = new TSimpleServer(tArgs);
        System.out.println("启动 Thrift 服务端");
        tServer.serve();
    }
}

然后再创建客户端的 Main 方法,代码如下:

public class ClientMain {
    public static void main(String[] args) {
        TTransport transport = null;
        try {
            transport = new TSocket("127.0.0.1", 9090, 6000);
            TProtocol protocol = new TBinaryProtocol(transport);
            ThriftHelloService.Client client = new ThriftHelloService.Client(protocol);
            transport.open();

            String result = client.sayHello("thrift-1");
            System.out.println(result);
        } catch (TException e) {
            e.printStackTrace();
        } finally {
            if (null != transport) {
                transport.close();
            }
        }
    }
}

然后启动服务端的 Main 方法,再启动客户端的 Main 方法,会看到 hello thrift-1,说明 RPC 通信成功了。

4. 关于 Thrift 生成好的 Java 代码

可能你会很奇怪,我们仅仅在 .thrift 文件中定义了一个接口,但是在生成后的 .java 文件中,却生成了大量的代码。

其实我们没必要太过关注生成后的代码,主要关注下面几个东西就可以了。

服务端关注以下两个接口类:

  • ThriftHelloService.Iface

这个是服务端提供同步调用的接口。就是说,你实现的这个接口下面的方法,都会采用同步的方式调用。

  • ThriftHelloService.AsyncIface

同样,如果客户端和服务端需要采用异步的方式通信,服务端就需要实现 AsyncIface 下的接口。

同样,消费端也有两个 Client 一个是同步调用的,一个是异步调用的。

  • ThriftHelloService.Client
    执行同步调用。

  • ThriftHelloService.AsyncClient
    执行异步调用。

版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
我的GitHub 我的LeetCode 我的掘金
Powered by Hexo Powered by three-cards
Copyright © 2017 - {{ new Date().getFullYear() }} 某ICP备xxxxxxxx号