Thrift入门:用IDL定义你的第一个RPC服务

345 阅读6分钟

Thrift

Thrift是一种接口描述语言和二进制通讯协议,它被用来定义和创建跨语言的服务。它被当作一个远程过程调用(RPC)框架来使用,是由Facebook为“大规模跨语言服务开发”而开发的。它通过一个代码生成引擎联合了一个软件栈,来创建不同程度的、无缝的跨平台高效服务,可以使用C#、C++(基于POSIX兼容系统)、Cappuccino、Cocoa、Delphi、Erlang、Go、Haskell、Java、Node.js、OCaml、Perl、PHP、Python、Ruby和Smalltalk。虽然它以前是由Facebook开发的,但它现在是Apache软件基金会的开源项目了。该实现被描述在2007年4月的一篇由Facebook发表的技术论文中,该论文现由Apache掌管。


IDL接口描述语言

1.基本类型

  • bool:布尔值(true 或 false)
  • byte:一个 8 位有符号整数
  • i16:一个 16 位有符号整数
  • i32:一个 32 位有符号整数
  • i64:一个 64 位有符号整数
  • double:一个 64 位浮点数
  • string:使用 UTF-8 编码编码的文本字符串
  • binary:未编码字节的序列

请注意,缺少无符号整数类型。这是因为许多编程语言中没有原生的无符号整数类型。

  1. 容器类型

Thrift 容器是强类型容器,可映射到大多数编程语言中常用和常用的容器类型。

有三种容器类型:

  • list<type>:元素为type类型的有序列表。与 java 的 List 对应。
  • set<type>:一组无序的唯一元素。与 java 的 Set 对应
  • map<type1,type2>:严格唯一的键到值的映射。与 java 的 Map 对应

容器元素可以是任何有效的 Thrift 类型。

  1. 常量类型

const 常量类型 常量名称 = 常量值 ,如

const i32 INT32CONSTANT = 9853
const map<string,string> MAPCONSTANT = {'hello':'world','goodnight':'moon'}
  1. 枚举类型

enum ,一组 32 位整数常量,不支持嵌套,如

enum Operation {
	ADD = 1,
	SUBTRACT = 2,
	MULTIPY = 3,
}

也可以省略常量值, 如

enum Operation {
	ADD,
	SUBTRACT,
	MULTIPY,
}

默认从1开始自动递增

  1. 结构体类型

struct ,封装一组不同类型的数据,与 Java 中的类对应,如

struct Work {
	1: i32 num1 = 0,
	2: i32 num2, // 默认为 optional
	3: Operation op,
	4: optional string comment,
}

字段修饰符:required(必须赋值)、optional(可选,推荐使用)。

optional 关键字表示该字段值可选,如果构建的结构体类型数据中可选字段没有设置值,则在编码生成的消息数据中不会包含可选字段

  • struct 不能继承,可以嵌套,不能嵌套自己
  • 编号不能重复成员分隔符可以是逗号(,)也可以是分号(;)
  • optional不填充就不序列化,required是必须填充,一定会序列化
  • 字段可以设置默认值
  1. 异常类型

exception,可以自定义异常中包含的数据内容,如

exception FileException {
	1: i32 code,
	2: string message
}
  1. 服务接口

service , 定义服务接口的方法和参数

使用 namespace 关键字,按语言指定包路径:

namespace <语言> <包路径>

service 服务名称 {
  返回值类型 方法名(参数列表) [throws (异常列表)],
  // 其他方法...
}

namespace java com.huang.thrift

service Calculator {
  i32 add(1:i32 a, 2:i32 b),                // 简单方法
  double divide(1:double x, 2:double y) throws (1:FileException e), // 抛出异常
  oneway void log(1:string message)         // 异步方法
}

说明:

  • 方法可以不带参数,如带参数,须指明参数的序号和参数类型
  • 方法名前须指明返回值,void 表示没有返回值
  • oneway 表示客户端发起请求后不再等待响应返回,oneway 方法必须是 void 返回类型
  • throws 表示可能抛出的异常
  1. 服务继承

使用 extends 可以继承扩展另一个服务

通过 include 引入其他文件的服务,通过 文件名.xxx 可以进行使用

include "base.thrift"
service UserService extends base.Service {
	i32 getUsername(i:i32 id) throw (1:Excepion e)
}
  1. 其他

Thrift 支持多种注释方式

// 单行注释
/* 多行注释*/

使用 typedef 可以为类型取别名,如

typedef i32 int

Thrift编译器安装

我这里只介绍在 windows 安装

下载地址:[[Apache Download Mirrors](Apache Archive Distribution Directory)](thrift.apache.org/download)

下载18版本,新版本我没解决报错问题,我不知道有没有影响,但是看着不舒服

下载完毕后,找到一个文件夹放入,并将其名称更改为thrift.exe

image.png

打开环境变量,将thrift.exe的目录写入

image.png

cmd打开黑窗口,输入thrift --version,出现版本号即安装成功

image.png

IDL文件编译

IDL 文件可以直接用来生成各种语言的代码

thrift --gen <语言> [选项] <文件名.thrift>

示例:生成Java代码

thrift --gen java user_service.thrift
// 指定输出目录
thrift --gen java -o target user_service.thrift

常用编译选项

选项作用
-out <目录>指定代码生成目录
-I <路径>添加include文件搜索路径
-strict启用严格模式(警告视为错误)
-v显示详细编译日志
-r递归编译include的依赖文件

Thrift 协议(Protocol)定义了数据在网络传输中的编码方式,直接影响通信效率和兼容性。以下是 Thrift 支持的主要协议类型及其核心特性

协议名称编码方式特点适用场景
TBinaryProtocol二进制编码高性能,跨语言支持完善默认选择,生产环境
TCompactProtocol紧凑二进制编码体积比二进制更小,效率更高高吞吐、带宽敏感场景
TJSONProtocolJSON文本编码可读性强,但体积大、效率低调试、与前端交互
TSimpleJSONProtocol简化JSON仅生成JSON,无元信息兼容非Thrift系统
TDebugProtocol调试文本格式人类可读,用于日志记录开发阶段调试

编译生成代码

我在这里提前准备了一个idl的文件(.thrift 结尾)

image.png

在 thrift 的文件目录执行上面讲的编译命令,便会生成对应的java文件,从图中可以发现,有一些报错,这是因为我们没有导入thrift的相关依赖

image.png

解决报错:

导入相关依赖

<dependency>
    <groupId>org.apache.thrift</groupId>
    <artifactId>libthrift</artifactId>
    <version>0.18.0</version>
</dependency>
<dependency>
    <groupId>javax.annotation</groupId>
    <artifactId>javax.annotation-api</artifactId>
    <version>1.3.2</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.5.11</version>
</dependency>

入门Demo实现

服务端实现

  1. 实现定义的服务接口

把生成的代码复制到我们自己的service目录中

编写服务端逻辑,实现IDL定义的接口

然后创建一个类实现生成的这个 UserService类中的Iface接口,并实现它的抽象方法

public class UserServiceImpl implements UserService.Iface {
    @Override
    public String getUserName(int userId) throws TException {
        System.out.println("getUserName, userid :" + userId);
        return "成功获取用户名!";
    }
}
  1. 启动服务端的服务器
public class Service {
    public static void main(String[] args) {
        // 创建处理器和传输层
        UserService.Processor<UserServiceImpl> processor =
                new UserService.Processor<>(new UserServiceImpl());
        TServerSocket serverSocket = null;
        try {
            serverSocket = new TServerSocket(8081);
            // 配置协议和传输方式
            TServer.Args serverArgs = new TServer.Args(serverSocket)
                    .processor(processor)
                    .protocolFactory(new TBinaryProtocol.Factory());
            // 启动服务
            TServer server = new TSimpleServer(serverArgs);
            System.out.println("Server started on port 8081...");
            server.serve();
        } catch (TTransportException e) {
            throw new RuntimeException(e);
        }finally {
            if (serverSocket != null)
                serverSocket.close();
        }
    }
}

客户端实现

public class Client {
    public static void main(String[] args) {
        try {
            // 配置传输和协议
            TSocket tSocket = new TSocket("localhost", 8081);
            tSocket.open();
            TProtocol protocol = new TBinaryProtocol(tSocket);
            UserService.Client client = new UserService.Client(protocol);

            // 调用远程方法
            String result = client.getUserName(156);
            System.out.println(result);
            tSocket.close();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

能分别从服务端和客户端看到信息的打印,就算成功了。