从0开始在Go当中使用Apache Thrift框架(万字讲解+图文教程+详细代码)

一、前言

Thrift 简介

Thrift 协议是一种用于跨编程语言和平台进行数据通信的二进制通信协议。它最初由 Facebook 开发,用于解决不同编程语言之间的数据传输和序列化问题。

Thrift 协议定义了一种接口描述语言(IDL),通过该语言可以定义数据结构和服务接口。基于这个 IDL,Thrift 提供了一套代码生成工具,可以根据 IDL 文件生成相应语言的代码,包括客户端和服务器端的代码。

Thrift 协议使用二进制编码来传输数据,因此相比于文本协议,如 XML 或 JSON,它具有更高的效率和更小的数据包大小。Thrift 支持多种数据类型,包括整型、浮点型、字符串、枚举、结构体、列表和映射等。这些数据类型可以在 IDL 中定义,并且可以跨不同编程语言进行互操作。

Thrift 协议的优势在于它的跨语言支持和高性能特性。通过生成不同语言的代码,开发者可以轻松地在不同的编程语言中使用 Thrift 协议进行数据通信。而且,Thrift 生成的代码通常是高度优化的,能够提供快速且高效的数据传输。

Thrift 广泛应用于分布式系统中,特别是在大规模的服务架构中。它被用于构建高性能的通信层,用于跨语言的服务调用、数据传输和序列化。同时,Thrift 还被许多大型的互联网公司广泛使用,如 Facebook、Apache Cassandra 等。

总结来说,Thrift 协议是一种跨语言的二进制通信协议,用于高效地传输数据和进行序列化。它提供了接口描述语言和代码生成工具,能够在不同编程语言之间实现快速、可靠的数据通信。

Thrift核心概念

Thrift 的核心概念主要包括以下几个方面:

  1. 接口描述语言(IDL):Thrift 使用接口描述语言来定义数据结构和服务接口。IDL 提供了一种跨语言、平台无关的方式来描述数据类型和服务接口。开发者使用 IDL 来定义消息格式、结构体、枚举、异常以及服务的方法和参数等。

  2. 数据类型:Thrift 支持多种数据类型,包括基本数据类型(如整型、浮点型、布尔型、字符串等)、容器类型(如列表、映射)和结构化数据类型(如结构体)。这些数据类型可以在 IDL 中定义,并在不同编程语言之间进行映射。

  3. 服务接口:Thrift 允许定义服务接口,服务接口包含一组方法,用于远程调用。开发者可以在 IDL 中定义服务接口及其方法,以及方法的参数和返回值。Thrift 生成的代码中包含了服务接口的抽象和具体实现,开发者可以通过实现服务接口来提供服务的具体逻辑。

  4. 代码生成:Thrift 提供了代码生成工具,可以根据 IDL 文件自动生成各种编程语言的代码。通过代码生成工具,开发者可以根据需要生成客户端和服务器端的代码,包括数据结构的序列化和反序列化方法、服务接口的代理类和实现类等。

  5. 传输层:Thrift 支持多种传输层协议,包括传统的 Socket 传输、HTTP 传输和非阻塞传输等。开发者可以根据需求选择合适的传输层协议,以实现数据在客户端和服务器之间的传输。

  6. 序列化和反序列化:Thrift 使用二进制协议进行数据的序列化和反序列化。在客户端和服务器之间进行数据传输时,Thrift 将数据对象序列化为二进制格式进行传输,接收方再将接收到的二进制数据反序列化为原始数据对象。Thrift 的二进制序列化提供了高效、紧凑的数据传输方式。

  7. 多语言支持:Thrift 具有跨语言的特性,允许不同编程语言之间进行数据通信和服务调用。Thrift 生成的代码可以在多种编程语言中使用,例如 Java、Python、C++、Golang 等,这使得不同团队或不同组件之间可以使用不同编程语言来实现相互通信。

综上所述,这些核心概念是理解和使用 Thrift 的基础。通过定义 IDL、生成代码、定义服务接口以及选择传输层协议,开发者可以使用 Thrift 构建跨语言的分布式系统,实现高效、可靠的数据通信和服务调用。

二、安装Thrift

目前因为我的环境是windows,因此我这里只记录windows的环境配置过程

1.官方地址:http://thrift.apache.org/

2.点击download

3.下载可执行的二进制文件包(.exe文件)

4.下载好的exe文件,存放到自己设置的文件夹,然后改名为thrift.exe(原本下载好的文件会带版本号,需要改名)

5.配置环境变量

系统变量的Path变量当中,新增一条:thrift.exe的路径(D:\Go\thrift)

6.验证是否生效:cmd下输入 thrift -version

三、Thrift IDL

Thrift 的 IDL(接口描述语言)是用于定义数据结构和服务接口的语言。它提供了一种跨语言、平台无关的方式来描述数据类型和服务接口。

Thrift 的 IDL 具有以下特点和语法:

1、命名空间(Namespace):可以通过namespace关键字指定 IDL 的命名空间,用于避免命名冲突。例如:namespace java com.example

2、数据类型(Data Types):Thrift 支持多种数据类型,包括基本数据类型(如整型、浮点型、布尔型、字符串等)、容器类型(如列表、集合、映射)和结构化数据类型(如结构体、异常)。每个数据类型都可以指定标识符和可选的修饰符(如requiredoptional等)。例如:1: required string name

3、结构体(Structs):可以使用struct关键字定义一个结构体,用于组织多个字段。结构体可以包含多个字段,每个字段都有一个唯一的标识符和数据类型。例如:

复制代码
 struct Person {
   1: required string name,
   2: optional i32 age
 }

4、枚举(Enums):可以使用enum关键字定义一个枚举类型,枚举类型由一组命名的常量值组成。例如:

复制代码
 enum Gender {
   MALE,
   FEMALE
 }

5、异常(Exceptions):可以使用exception关键字定义一个异常类型,用于表示在服务调用过程中可能出现的异常情况。异常类型和结构体类似,但在异常类型中的字段都是必需的。例如:

复制代码
 exception InvalidRequest {
   1: required i32 errorCode,
   2: required string errorMessage
 }

6、服务接口(Services):可以使用service关键字定义一个服务接口,服务接口包含一组方法的声明。每个方法都有一个唯一的标识符、返回类型和参数列表。例如:

复制代码
 service ExampleService {
   void sayHello(1: string name)
 }

以上是 Thrift IDL 的一些基本语法和特性。通过使用这些语法,开发者可以定义数据结构、枚举类型、异常类型以及服务接口,然后使用 Thrift 的代码生成工具根据 IDL 文件生成相应语言的代码,以实现跨语言的数据通信和服务调用。

单纯这么讲,肯定很难理解,因此我们可以通过一个实际例子来感受一下

第一个Go项目的Thrift应用

先下载go的Thrift第三方库

复制代码
 go get github.com/apache/thrift/lib/go/thrift

我们定义一个IDL文件

复制代码
 namespace go com.example   // 定义所使用的命名空间
 ​
 struct Person {             // 定义一个结构体
   1: required string name,  // 姓名字段
   2: optional i32 age       // 年龄字段
 }
 ​
 service ExampleService {    // 定义一个服务接口
   void sayHello(1: string name) // sayHello方法,接收一个姓名参数
 }

然后使用Thrift代码生成工具生成所需语言的代码

复制代码
 thrift --gen go example.thrift // 默认创建一个gen-go的目录,下面有example子目录,里面包含了生成的代码
 ​
 thrift --gen go:out=自定义目录名 example.thrift // 可以自定义主目录名

运行之后生成如下文件

这里发现一个问题:可以看到example_service-remote.go这个文件当中的import存在问题

解决方式为:修改为全路径即可

修改前

修改后

服务端

创建一个server包,结构如下

复制代码
 ├── server/
 │   ├── main.go          # 服务端启动逻辑
 │   └── service/

service包下模拟一个service

Go 复制代码
package service
 ​
 import (
     "context"
     "fmt"
 )
 ​
 // ExampleServiceImpl 实现 Thrift 生成的 ExampleService 接口
 type ExampleServiceImpl struct{}
 ​
 // SayHello 实现接口中的 SayHello 方法
 func (e *ExampleServiceImpl) SayHello(ctx context.Context, name string) error {
     fmt.Printf("Hello, %s!\n", name) // 业务逻辑:打印问候语
     return nil
 }

main.go当中编写服务端启动逻辑

Go 复制代码
package main
 ​
 import (
     "fmt"
     "github.com/apache/thrift/lib/go/thrift"
     // 引入 Thrift 生成的接口和本地的服务实现
     "thrift-rpc-demo/gen-go/com/example"
     "thrift-rpc-demo/server/service" // 导入 service 包
 )
 ​
 func main() {
     // 1. 创建 TCP 传输层(监听 9090 端口)
     transport, err := thrift.NewTServerSocket(":9090")
     if err != nil {
         panic(err)
     }
 ​
     // 2. 定义协议(二进制协议,需与客户端一致)
     protocolFactory := thrift.NewTBinaryProtocolFactoryDefault()
 ​
     // 3. 实例化服务实现(从 service 包中获取)
     handler := &service.ExampleServiceImpl{}
 ​
     // 4. 注册服务处理器(将实现与 Thrift 框架绑定)
     processor := example.NewExampleServiceProcessor(handler)
 ​
     // 5. 创建并启动服务器
     server := thrift.NewTSimpleServer4(
         processor,
         transport,
         thrift.NewTTransportFactory(),
         protocolFactory,
     )
 ​
     fmt.Println("Thrift server starting on :9090...")
     if err := server.Serve(); err != nil {
         panic(err)
     }
 }
复制代码

客户端

创建一个client包,结构如下

Go 复制代码
└── client/               # 客户端启动逻辑
     └── main.go

main.go当中编写客户端启动逻辑

Go 复制代码
 package main
 ​
 import (
     "context"
     "fmt"
 ​
     "github.com/apache/thrift/lib/go/thrift"
     // 引入生成的代码包(路径根据实际模块名调整)
     "thrift-rpc-demo/gen-go/com/example"
 )
 ​
 func main() {
     // 1. 创建传输层(连接服务端)
     transport, err := thrift.NewTSocket("localhost:9090")
     if err != nil {
        panic(err)
     }
 ​
     // 2. 包装传输层(带缓冲的传输)
     transportFactory := thrift.NewTTransportFactory()
     trans, err := transportFactory.GetTransport(transport)
     if err != nil {
        panic(err)
     }
     defer trans.Close() // 确保关闭连接
 ​
     // 3. 打开连接
     if err := trans.Open(); err != nil {
        panic(err)
     }
 ​
     // 4. 定义协议(需与服务端一致,此处为二进制协议)
     protocolFactory := thrift.NewTBinaryProtocolFactoryDefault()
     client := example.NewExampleServiceClientFactory(trans, protocolFactory)
 ​
     // 5. 调用服务方法
     err = client.SayHello(context.Background(), "Thrift")
     if err != nil {
        panic(err)
     }
     fmt.Println("Client: 调用 sayHello 成功")
 }

启动服务器

启动客户端

Go 复制代码
 thrift-rpc-demo/
 ├── go.mod
 ├── example.thrift
 ├── gen-go/
 │   └── com/example/
 ├── server/
 │   ├── main.go          # 服务端启动逻辑
 │   └── service/
 │       └── example_service.go  # ExampleService 接口的实现
 └── client/
     └── main.go

至此,我们便实现了一个简单的Thrift RPC Demo

完整项目目录如下

bash 复制代码
 thrift-rpc-demo/
 ├── go.mod
 ├── example.thrift
 ├── gen-go/
 │   └── com/example/
 ├── server/
 │   ├── main.go          # 服务端启动逻辑
 │   └── service/
 │       └── example_service.go  # ExampleService 接口的实现
 └── client/
     └── main.go

为了方便管理,我们放在了同一个项目下,实际项目开发时,肯定是分团队开发,客户端和服务端分开,具体实现在后面会有所展示

四、IDL 详细语法

1.数据类型

Thrift 提供了多种数据类型,用于定义数据结构的字段类型、方法的参数类型和返回值类型等。以下是一些常用的 Thrift 数据类型:

  1. 基本数据类型(Primitive Types):

    • bool:布尔类型,表示真或假。

    • byte:字节类型,表示 8 位有符号整数。

    • i16:16 位有符号整数类型。

    • i32:32 位有符号整数类型。

    • i64:64 位有符号整数类型。

    • double:双精度浮点数类型。

    • string:字符串类型。

  2. 容器类型(Container Types):

    • list:列表类型,表示有序的元素集合。

    • set:集合类型,表示无序的唯一元素集合。

    • map:映射类型,表示键值对的集合。

  3. 结构化数据类型(Structured Types):

    • struct:结构体类型,由一组具有字段名称和字段类型的数据组成。

    • enum:枚举类型,表示一组预定义的常量值。

  4. 异常类型(Exception Types):

    • exception:异常类型,类似于结构体,用于表示在服务调用过程中可能出现的异常情况。
  5. 服务类型(Service Types):

    • service:服务类型,用于定义服务接口和方法。

Thrift 的数据类型可以通过 IDL 文件进行定义,并且在不同的编程语言中进行映射。代码生成工具可以根据 IDL 文件生成对应编程语言的数据类型和相关的序列化 / 反序列化方法,以便在跨语言的通信和服务调用中使用。

除了上述提到的常用数据类型外,Thrift 还支持自定义数据类型,允许开发者根据需要定义更复杂的数据结构和类型。

这么看可能会有点乱,我们可以从分层的角度来理解

Thrift作为RPC协议,本质上核心目的是,让远程调用和内部调用一样简单,原理可以在前言里看到

所有数据类型当中,我们可以这样分层

1.service层

作为最外层,也是IDL当中必然出现的,这个类型主要对应的就是服务端提供的接口/客户端远程调用的接口

2.method层

方法层,这是我自己模拟的名字,它包含在service层当中,因为我们远程调用,肯定是调用一个接口当中的方法

3.type层

基础层,包含在method当中,方法需要定义返回值、参数、异常,而这些都需要数据类型来定义(基本数据类型,容器,结构体,枚举,异常)

举个例子

bash 复制代码
namespace go com.example
 ​
 struct Person {
   1: required string name,  // 必选字段:必须传值,否则会报错
   2: optional i32 age       // 可选字段:可传可不传
 }
 ​
 service ExampleService {
   void sayHello(1: string name)
   // 新增方法:接收 Person 结构体,返回一个字符串
   string introduce(1: Person person)
   // 新增方法:返回一个 Person 结构体
   Person getDefaultPerson()
 }

服务端上有一个ExampleService接口,里面有三个方法

**sayHello:**无返回值,参数为基本类型

**introduce:**返回值为基本类型,参数为Person结构体,需要定义

**getDefaultPerson:**返回值为Person结构体,需要定义,无参数

因此我们在编写Thrift文件的时候,可以按照这三层的逻辑,先创建接口,然后补充方法,再创建需要定义的结构体

当然,这是我个人思考后得出的理解,大家也可以有自己的思考和理解方式,运用自己习惯的方法

下面我会按照我的三层理解,从上到下,针对每个不同类型,给出具体的实例和细节演示

2.服务类型

定义

bash 复制代码
 service MyService {
   void Method1(1: string param1),
   i32 Method2(1: i32 param2, 2: string param3) throws (1: MyException ex)
 }

在上述示例中,定义了一个名为 MyService 的服务类型,其中包含两个方法:Method1Method2

Method1 接受一个字符串参数,并且没有返回值

Method2 接受一个整数参数和一个字符串参数,并返回一个整数值,同时可能抛出 MyException 异常。

实现

Go 复制代码
type MyServiceImpl struct{}

func (s *MyServiceImpl) Method1(param1 string) error {
  // 实现Method1的逻辑
  return nil
}

func (s *MyServiceImpl) Method2(param2 int32, param3 string) (int32, error) {
  // 实现Method2的逻辑
  return 0, nil
}

在服务端,我们需要去定义一个实现接口的类,当然在go当中,只要实现了这个接口的方法就算做实现类了

在上述示例中,定义了一个名为 MyServiceImpl 的结构体,实现了 Thrift 生成的服务接口类型中定义的方法。根据业务需求,编写具体的逻辑代码,并根据方法签名返回相应的结果或错误

启动服务(注册服务端的接口实现)

在生成的 Go 代码中,还会生成一个服务处理器类型和一个服务器类型。可以使用这些类型来启动 Thrift 服务器,监听指定的端口,并提供对定义的服务方法的调用。例如:

Go 复制代码
func main() {
  handler := &MyServiceImpl{}
  processor := myservice.NewMyServiceProcessor(handler)
  transport, err := thrift.NewTServerSocket(":9090")
  if err != nil {
    log.Fatal(err)
  }
  server := thrift.NewTSimpleServer2(processor, transport)
  log.Println("Starting the server...")
  server.Serve()
}

在上述示例中,创建了一个 handler实例化服务实现

使用 myservice.NewMyServiceProcessor 来创建服务处理器类型

然后,创建一个 TServerSocket 来监听指定的端口,并创建一个 TSimpleServer 来启动 Thrift 服务器

最后,调用 server.Serve() 开始监听并处理客户端的请求。

通过定义和实现 Thrift 服务类型,可以构建基于 RPC 的服务,使客户端能够通过网络调用这些服务方法,并实现相应的业务逻辑。

这里需要注意,myservice是由我们在使用Thrift代码生成工具创建代码时,定义的包名

创建服务处理器类型的方法为:New + 包名 + Processor,因此这个方法名会根据包名进行变化

至于其他的创建和定义,我在后续服务端/客户端语法的篇幅会讲到

3.结构体

在thrift当中,结构体的作用就是用来传递我们在服务端/客户端定义好的,需要在方法当中使用的类/结构体

定义

在 Thrift 的 IDL 文件中,可以使用 struct 关键字来定义一个结构体,并为每个字段指定唯一的标识符、字段名称和字段类型。例如:

Go 复制代码
 struct Person {
   1: required string name,
   2: optional i32 age,
   3: optional double height
 }

在上述示例中,定义了一个名为 Person 的结构体,它包含三个字段:nameageheightname 字段是必需的,而 ageheight 字段是可选的。

字段修饰符

Thrift 结构体字段可以具有不同的修饰符,用于指定字段的属性和行为。常见的修饰符包括:

  • required:表示字段是必需的,必须提供字段值。

  • optional:表示字段是可选的,可以选择性地提供字段值。

  • default:为字段指定默认值,在字段未被设置时将使用该默认值。

Go 复制代码
 struct Person {
   1: required string name,
   2: optional i32 age,
   3: optional double height = 0.0
 }

在上述示例中,name 字段是必需的,age 字段是可选的,height 字段具有默认值为 0.0。

访问结构体字段

其实,我们在thrift文件里面利用IDL编写,本质上类似于脚手架,然后让thrift帮我们生成对应的语言代码

在生成的代码中,Thrift 为每个结构体字段生成了相应的访问方法,以便在代码中访问和设置结构体的字段值。对于上述示例中的 Person 结构体,可以使用类似下面的代码访问其字段:

java 复制代码
 Person person = new Person();
 person.setName("John");
 person.setAge(25);
 person.setHeight(1.75);
 String name = person.getName();
 int age = person.getAge();
 double height = person.getHeight();

以上这个是Java里面的方式

Go里面略有不同,以下是生成的Go语言代码

可以看到,Go里面没有set方法,但是结构体字段的首字母是大写,也就是说结构体对象的属性是公开的,可以直接设置

4.枚举类型

和结构体几乎一样,区别在于没有修饰符,并且有默认初始值

定义

在 Thrift 的 IDL 文件中,可以使用 enum 关键字来定义一个枚举类型,并为每个常量指定唯一的标识符和常量名称。例如:

java 复制代码
 enum Gender {
   MALE,
   FEMALE
 }

在上述示例中,定义了一个名为 Gender 的枚举类型,它包含两个常量值:MALEFEMALE

使用

在生成的代码中,Thrift 为每个枚举类型生成了相应的常量值。可以使用这些常量值来表示特定的选项。例如,对于上述示例中的 Gender 枚举类型,可以使用类似下面的代码:

java 复制代码
 Person person = new Person();
 person.setGender(Gender.MALE);

在上述Java代码的示例中,将 Gender.MALE 作为性别的选项进行设置。

枚举类型值的关联数值

默认情况下,Thrift 的枚举类型的常量值与其关联的数值是从 0 开始的递增序列。例如,对于上述示例中的 Gender 枚举类型,MALE 的值为 0,FEMALE 的值为 1。可以通过在枚举常量后面添加关联数值来自定义这些值。例如:

java 复制代码
 enum Gender {
   MALE = 1,
   FEMALE = 2
 }

在上述示例中,将 MALE 的值定义为 1,FEMALE 的值定义为 2。

5.容器类型

没有太多好说的,语法上和在Java、Go当中没有太大区别,和基本类型一样,都是运用在结构体当中,或者直接应用于方法的返回值、入参

列表类型示例:

java 复制代码
 struct Person {
   1: required string name,
   2: required list<string> hobbies
 }

在上述示例中,定义了一个名为 Person 的结构体,其中包含一个名为 hobbies 的列表字段,用于存储个人的爱好。

集合类型示例:

java 复制代码
 struct ShoppingCart {
   1: required set<string> items
 }

在上述示例中,定义了一个名为 ShoppingCart 的结构体,其中包含一个名为 items 的集合字段,用于存储购物车中的商品。

映射类型示例:

java 复制代码
 struct CityWeather {
   1: required string city,
   2: required map<string, double> temperatures
 }

在上述示例中,定义了一个名为 CityWeather 的结构体,其中包含一个名为 temperatures 的映射字段,用于存储不同城市的温度信息。

6.异常类型

Thrift 异常类型在 Go 中的使用频率不高,核心原因是 Go 原生的 error 机制已经能满足大部分错误处理需求,且开发者更习惯这种显式的错误返回风格。但在跨语言协作需要强结构化异常契约的场景下,Thrift 异常类型仍有其用武之地,只是使用场景相对局限。

因此,下面的示例主要针对Java

定义

在 Thrift 的 IDL 文件中,可以使用 exception 关键字来定义一个异常类型,并指定异常类型的名称、字段和字段类型。例如:

java 复制代码
 exception MyException {
   1: i32 code,
   2: string message
 }

在上述示例中,定义了一个名为 MyException 的异常类型,它包含两个字段:codemessage,它们分别表示异常代码和异常信息。

在服务定义中使用

在 Thrift 的服务定义中,可以将异常类型作为方法返回类型的一部分来使用。例如:

java 复制代码
 service MyService {
   void myMethod(1: i32 param) throws (1: MyException ex)
 }

在上述示例中,定义了一个名为 myMethod 的方法,它带有一个整型参数 param,并声明了一个可能抛出 MyException 异常的情况。

在代码中使用

在生成的代码中,Thrift 为每个异常类型生成了相应的异常类。可以使用这些异常类来表示方法调用过程中可能抛出的异常情况。例如,在 Java 中,可以使用类似下面的代码:

java 复制代码
 try {
   client.myMethod(param);
 } catch (MyException ex) {
   // 处理异常情况
 }

在上述示例中,使用 try-catch 语句来捕获可能抛出的 MyException 异常,并在 catch 块中进行异常处理。

五、服务端/客户端语法

服务端

1.创建服务器的方法

Go 复制代码
// 1. 创建 TCP 传输层(监听 9090 端口)
 transport, err := thrift.NewTServerSocket(":9090")
 if err != nil {
     panic(err)
 }
 ​
 // 2. 定义协议(二进制协议,需与客户端一致)
 protocolFactory := thrift.NewTBinaryProtocolFactoryDefault()
 ​
 // 3. 实例化服务实现(从 service 包中获取)
 handler := &service.ExampleServiceImpl{}
 ​
 // 4. 注册服务处理器(将实现与 Thrift 框架绑定)
 processor := example.NewExampleServiceProcessor(handler)
 ​
 // 5. 创建并启动服务器
 server := thrift.NewTSimpleServer4(
     processor,
     transport,
     thrift.NewTTransportFactory(),
     protocolFactory,
 )
 ​
 fmt.Println("Thrift server starting on :9090...")
 if err := server.Serve(); err != nil {
     panic(err)
 }

这是一个基本的服务端代码,其中,最关键的一个方法是创建并启动服务器 这一步的 thrift.NewTSimpleServer4 方法创建server

Go 复制代码
 // 5. 创建并启动服务器
 server := thrift.NewTSimpleServer4(
     processor,
     transport,
     thrift.NewTTransportFactory(),
     protocolFactory,
 )

我们从这个地方入手,去查看NewTSimpleServer4方法的源码,上下文总共有如下方法

Go 复制代码
func NewTSimpleServer2(processor TProcessor, serverTransport TServerTransport) *TSimpleServer {
         return NewTSimpleServerFactory2(NewTProcessorFactory(processor), serverTransport)
 }
 ​
 func NewTSimpleServer4(processor TProcessor, serverTransport TServerTransport, transportFactory TTransportFactory, protocolFactory TProtocolFactory) *TSimpleServer {
         return NewTSimpleServerFactory4(NewTProcessorFactory(processor),
                serverTransport,
                transportFactory,
                protocolFactory,
         )
 }
 ​
 func NewTSimpleServer6(processor TProcessor, serverTransport TServerTransport, inputTransportFactory TTransportFactory, outputTransportFactory TTransportFactory, inputProtocolFactory TProtocolFactory, outputProtocolFactory TProtocolFactory) *TSimpleServer {
         return NewTSimpleServerFactory6(NewTProcessorFactory(processor),
                serverTransport,
                inputTransportFactory,
                outputTransportFactory,
                inputProtocolFactory,
                outputProtocolFactory,
         )
 }
 ​
 func NewTSimpleServerFactory2(processorFactory TProcessorFactory, serverTransport TServerTransport) *TSimpleServer {
         return NewTSimpleServerFactory6(processorFactory,
                serverTransport,
                NewTTransportFactory(),
                NewTTransportFactory(),
                NewTBinaryProtocolFactoryDefault(),
                NewTBinaryProtocolFactoryDefault(),
         )
 }
 ​
 func NewTSimpleServerFactory4(processorFactory TProcessorFactory, serverTransport TServerTransport, transportFactory TTransportFactory, protocolFactory TProtocolFactory) *TSimpleServer {
         return NewTSimpleServerFactory6(processorFactory,
                serverTransport,
                transportFactory,
                transportFactory,
                protocolFactory,
                protocolFactory,
         )
 }
 ​
 func NewTSimpleServerFactory6(processorFactory TProcessorFactory, serverTransport TServerTransport, inputTransportFactory TTransportFactory, outputTransportFactory TTransportFactory, inputProtocolFactory TProtocolFactory, outputProtocolFactory TProtocolFactory) *TSimpleServer {
         return &TSimpleServer{
                processorFactory:       processorFactory,
                serverTransport:        serverTransport,
                inputTransportFactory:  inputTransportFactory,
                outputTransportFactory: outputTransportFactory,
                inputProtocolFactory:   inputProtocolFactory,
                outputProtocolFactory:  outputProtocolFactory,
                stopChan:               make(chan struct{}),
         }
 }

可以看到,两种方法都分为了2/4/6三个档位,对应2/4/6个参数

一般最常用的是4个参数的方法,NewTSimpleServer4 是最常用的平衡方案(兼顾定制性和简洁性),而 NewTSimpleServer2 适合快速原型开发

我们先看NewTSimpleServer2的2个参数

  • processor TProcessor:处理器本身,即thirft文件当中的接口在服务端的具体实现

  • serverTransport TServerTransport:服务端传输层协议(最常用的是TCP)

在2这个档次下,传输层工厂默认使用带缓冲传输,协议工厂默认使用二进制协议

我们再看NewTSimpleServer4多出来的2个参数

  • transportFactory:传输层工厂,控制传输层的行为(如是否启用缓冲、压缩等)

  • protocolFactory:协议工厂,控制序列化协议(如二进制、JSON、压缩协议等)

需要修改传输策略(如换成非缓冲传输)或协议(如用 JSON 协议)时使用。

最后是NewTSimpleServer6

本质上是把transportFactory和protocolFactory拆分成了input和output两个参数

  • 输入 / 输出传输层:可对请求和响应的传输层分别设置(如请求用缓冲传输,响应不用)

  • 输入 / 输出协议:可对请求和响应的序列化协议分别设置(如请求用二进制,响应用 JSON)

这种情况用的极少,我们把重心放到2/4就好

讲完了NewTSimpleServer,那NewTSimpleServerFactory的唯一区别只在第一个参数上

  • processor TProcessor:处理器本身

  • processorFactory TProcessorFactory :处理器工厂,支持动态创建处理器 (例如每个连接创建独立的处理器实例),而 TProcessor 是固定实例。需要为每个连接隔离处理器状态时使用(如处理器有线程不安全的成员变量

2.设置基本参数

TProcessor(单个/多个)

创建处理器,分为两种

1.创建单个处理器(thrift当中只有一个service)

第一步:获取到我们对thrift文件当中接口的实现类

Go 复制代码
 handler := &service.ExampleServiceImpl{}

第二步:注册服务处理器

Go 复制代码
 processor := example.NewExampleServiceProcessor(handler)

这里的example是包名,后面的方法名为New + 包名 + ServiceProcessor

单个处理器就创建好了

这种就适用于,thrift文件当中只配置了一个接口的情况

2.创建多个处理器(thrift当中有多个service)

存在两种方式:

一种是多端口各自监听,本质上来说是把每个处理器分开,设置不同的端口号,每个处理器创建的方式和创建单个一致,示例代码如下:

Go 复制代码
package main
 ​
 func startService1() {
     handler := &service1.Service1Impl{}
     processor := service1.NewService1Processor(handler)
     transport, _ := thrift.NewTServerSocket(":9091")
     server := thrift.NewTSimpleServer4(processor, transport, thrift.NewTTransportFactory(), thrift.NewTBinaryProtocolFactoryDefault())
     log.Println("Service1 starting on :9091...")
     server.Serve()
 }
 ​
 func startService2() {
     handler := &service2.Service2Impl{}
     processor := service2.NewService2Processor(handler)
     transport, _ := thrift.NewTServerSocket(":9092")
     server := thrift.NewTSimpleServer4(processor, transport, thrift.NewTTransportFactory(), thrift.NewTBinaryProtocolFactoryDefault())
     log.Println("Service2 starting on :9092...")
     server.Serve()
 }
 ​
 func main() {
     var wg sync.WaitGroup
     wg.Add(2)
 ​
     go func() {
         defer wg.Done()
         startService1()
     }()
 ​
     go func() {
         defer wg.Done()
         startService2()
     }()
 ​
     wg.Wait()
 }

另一种更符合我们的常用需要:单个端口需要处理多个service服务

TMultiplexedProcessor 是 Thrift 提供的多服务处理器,可以在同一个端口 上同时处理多个 service 的请求,通过服务名称区分不同的接口

完整实现步骤:

步骤 1:定义多个 service 的 IDL

Go 复制代码
// service1.thrift
 namespace go com.example.service1
 service Service1 {
   void method1(1: string param)
 }
 ​
 // service2.thrift
 namespace go com.example.service2
 service Service2 {
   void method2(1: i32 param)
 }

步骤 2:生成各 service 的 Go 代码

Go 复制代码
 thrift --gen go service1.thrift
 thrift --gen go service2.thrift

步骤 3:实现各 service 的逻辑

Go 复制代码
// service1/impl.go
 package service1
 ​
 import "github.com/yourname/thrift-demo/gen-go/com/example/service1"
 ​
 type Service1Impl struct{}
 ​
 func (s *Service1Impl) Method1(ctx context.Context, param string) error {
     // 业务逻辑
     return nil
 }
 ​
 // service2/impl.go
 package service2
 ​
 import "github.com/yourname/thrift-demo/gen-go/com/example/service2"
 ​
 type Service2Impl struct{}
 ​
 func (s *Service2Impl) Method2(ctx context.Context, param int32) error {
     // 业务逻辑
     return nil
 }

步骤 4:使用 TMultiplexedProcessor 注册多服务

Go 复制代码
package main
 ​
 import (
     "github.com/apache/thrift/lib/go/thrift"
     "github.com/yourname/thrift-demo/gen-go/com/example/service1"
     "github.com/yourname/thrift-demo/gen-go/com/example/service2"
     "github.com/yourname/thrift-demo/service1"
     "github.com/yourname/thrift-demo/service2"
     "log"
 )
 ​
 func main() {
     // 创建多服务处理器
     multiplexedProcessor := thrift.NewTMultiplexedProcessor()
 ​
     // 注册 Service1
     service1Impl := &service1.Service1Impl{}
     service1Processor := service1.NewService1Processor(service1Impl)
     multiplexedProcessor.RegisterProcessor("Service1", service1Processor)
 ​
     // 注册 Service2
     service2Impl := &service2.Service2Impl{}
     service2Processor := service2.NewService2Processor(service2Impl)
     multiplexedProcessor.RegisterProcessor("Service2", service2Processor)
 ​
     // 启动服务器(单端口监听)
     transport, err := thrift.NewTServerSocket(":9090")
     if err != nil {
         log.Fatal(err)
     }
 ​
     server := thrift.NewTSimpleServer4(
         multiplexedProcessor,
         transport,
         thrift.NewTTransportFactory(),
         thrift.NewTBinaryProtocolFactoryDefault(),
     )
 ​
     log.Println("Server starting on :9090...")
     if err := server.Serve(); err != nil {
         log.Fatal(err)
     }
 }

简单来说,就是先注册多个单个的处理器,然后利用multiplexedProcessor,把每个处理器注册到其中

需要注意的是,注册到multiplexedProcessor当中的时候,需要配置当前处理器的name,这个name,会在客户端,当做key获取到我们的service,具体见后文的客户端语法

TServerTransport

创建传输层,常用的有以下几种方式

方式一:TCP传输(最常用)

使用 NewTServerSocket 方法

Go 复制代码
 // 1. 创建 TCP 传输层(监听 9090 端口)
 transport, err := thrift.NewTServerSocket(":9090")
 if err != nil {
     panic(err)
 }

方式二:加密TCP传输

需先配置 TLS 证书

再使用 NewTSSLServerSocket 方法创建

Go 复制代码
import (
   "crypto/tls"
   "github.com/apache/thrift/lib/go/thrift"
 )
 ​
 // 加载 TLS 证书和密钥
 cert, _ := tls.LoadX509KeyPair("server.crt", "server.key")
 config := &tls.Config{Certificates: []tls.Certificate{cert}}
 ​
 // 创建加密的 TCP 传输
 transport, _ := thrift.NewTSSLServerSocket(":9090", config)

方式三:自定义TServerTransport

Go 复制代码
type MyServerTransport struct {
   // 自定义传输层的内部状态(如连接、监听等)
 }
 ​
 // 实现 TServerTransport 接口的所有方法
 func (t *MyServerTransport) Listen() error { /* 监听逻辑 */ }
 func (t *MyServerTransport) Accept() (thrift.TTransport, error) { /* 接受连接 */ }
 func (t *MyServerTransport) Close() error { /* 关闭资源 */ }

另外几种:

TUNIXServerSocket(UNIX 域套接字):只支持本地通信,比TCP更快

TNullServerTransport(空传输,仅用于测试)

TTransportFactory

Thrift Go 库中封装了多种常用的 TTransportFactory 实现,用于快速配置传输层的基础功能(如缓冲、压缩等),无需手动自定义。以下是主要的内置方法及用途:

1. thrift.NewTTransportFactory()

  • 功能:创建一个基础的传输工厂,返回原始传输对象(不做额外处理)。

  • 特点 :最简单的工厂,直接返回传入的 TTransport 实例,适用于不需要缓冲、压缩等额外功能的场景。

2. thrift.NewTBufferedTransportFactory(bufferSize int)

  • 功能 :创建带缓冲的传输工厂,为每个连接添加内存缓冲。

  • 参数bufferSize 为缓冲区大小(字节),常用值如 8192(8KB)

  • 特点:通过缓冲减少底层 I/O 次数,提升小数据传输效率(默认推荐使用)。

3. thrift.NewTFramedTransportFactory(maxFrameSize int)

  • 功能:创建帧传输工厂,将数据按 "帧"(固定格式的数据包)传输。

  • 参数maxFrameSize 为最大帧大小(字节),超过会报错。

  • 特点

    • 每个帧包含数据长度前缀,解决 TCP 粘包问题;

    • 适合长连接、高频小数据传输场景(如 Thrift 二进制协议常用)。

4. thrift.NewTZlibTransportFactory(compressionLevel int)

  • 功能:创建带 Zlib 压缩的传输工厂,自动压缩 / 解压传输数据。

  • 参数compressionLevel 为压缩级别(0 无压缩,9 最高压缩,-1 默认级别)。

  • 特点:减少网络传输数据量,适合大数据传输(但会增加 CPU 开销)。

5. thrift.NewTFileTransportFactory()

  • 功能:创建文件传输工厂,用于基于文件的传输(较少用于 RPC,多为测试或本地数据交换)。

  • 特点 :将文件 I/O 封装为 TTransport,适用于需要通过文件传递 Thrift 数据的场景。

6. 组合使用(多层包装)

内置工厂支持嵌套组合,实现多功能叠加(如 "缓冲 + 压缩"):

Go 复制代码
 // 先缓冲,再压缩
 bufferedFactory := thrift.NewTBufferedTransportFactory(8192)
 zlibFactory := thrift.NewTZlibTransportFactory(-1)
 ​
 // 组合方式:压缩工厂包装缓冲工厂
 combinedFactory := thrift.NewTTransportFactoryStack().
     Push(bufferedFactory).
     Push(zlibFactory)

总结

常用内置 TTransportFactory 及适用场景:

  • NewTTransportFactory:极简场景,无额外处理。

  • NewTBufferedTransportFactory:通用场景,通过缓冲提升效率(默认首选)。

  • NewTFramedTransportFactory:解决粘包问题,适合长连接高频传输。

  • NewTZlibTransportFactory:大数据传输,需权衡压缩率与 CPU 开销。

根据数据大小、传输频率、性能需求选择,多数情况下 TBufferedTransportFactoryTFramedTransportFactory 能满足需求。

TProtocolFactory

1.thrift.NewTBinaryProtocolFactoryDefault()

  • 特点

    • 最常用的协议,数据以二进制格式 紧凑编码,性能最优(序列化 / 反序列化速度快,体积小)。

    • strict 参数控制是否严格校验字段顺序和类型(默认 true,建议保持严格模式避免兼容性问题)。

    严格模式:需要设置读写是否使用二进制

Go 复制代码
// 源码
 func NewTBinaryProtocolFactory(strictRead, strictWrite bool) *TBinaryProtocolFactory {
         return NewTBinaryProtocolFactoryConf(&TConfiguration{
                TBinaryStrictRead:  &strictRead,
                TBinaryStrictWrite: &strictWrite,
 ​
                noPropagation: true,
         })
 }
 ​
 // 严格模式实际使用(读写都不严格校验)
 protocolFactory := thrift.NewTBinaryProtocolFactory(false, false)
  • 适用场景:绝大多数 RPC 场景(跨语言通信、高性能需求)。

2. thrift.NewTCompactProtocolFactory()

  • 特点:

    • 在二进制协议基础上进一步压缩(通过可变长度编码、字段 ID 压缩等),数据体积比普通二进制协议更小。

    • 性能略低于二进制协议,但网络传输效率更高

  • 适用场景:网络带宽有限的场景(如跨公网 RPC)。

3. thrift.NewTJSONProtocolFactory()

  • 特点

    • 数据以 JSON 格式编码,可读性强(便于调试),但体积大、性能较差(序列化 / 反序列化耗时)。

    • 支持跨语言(几乎所有语言都能解析 JSON),但对 Thrift 特有类型(如 binarymap 嵌套)的兼容性略差。

  • 适用场景:调试环境、对可读性要求高但性能不敏感的场景。

4. thrift.NewTJSONProtocolFactoryWithOptions(thrift.TJSONProtocolOptions{UseLengthPrefix: true})

  • 特点:

    • 在 JSON 协议基础上,为每个消息添加长度前缀,解决 JSON 无边界导致的粘包问题

    • 其他特性同普通 JSON 协议,但更适合流式传输。

  • 注意:这个方法我在Go当中没有看见,Thrift0.22.0版本好像这个方法没有了

5. thrift.NewTNullProtocolFactory()

  • 特点:不做实际序列化 / 反序列化,仅用于测试(如模拟协议层,避免真实编码开销)。

总结

常用 TProtocolFactory 的选择建议:

  • 生产环境优先TBinaryProtocolFactory(平衡性能和兼容性)或 TCompactProtocolFactory(带宽敏感场景)。

  • 调试 / 可读性优先TJSONProtocolFactory(适合开发阶段排查问题)。

3.重头再看代码

Go 复制代码
// 1. 创建 TCP 传输层(监听 9090 端口)
 transport, err := thrift.NewTServerSocket(":9090")
 if err != nil {
     panic(err)
 }
 ​
 // 2. 定义协议(二进制协议,需与客户端一致)
 protocolFactory := thrift.NewTBinaryProtocolFactoryDefault()
 ​
 // 3. 实例化服务实现(从 service 包中获取)
 handler := &service.ExampleServiceImpl{}
 ​
 // 4. 注册服务处理器(将实现与 Thrift 框架绑定)
 processor := example.NewExampleServiceProcessor(handler)
 multiplexedProcessor.RegisterProcessor("ExampleService", processor)
 ​
 // 5. 创建并启动服务器
 server := thrift.NewTSimpleServer4(
     processor,
     transport,
     thrift.NewTTransportFactory(),
     protocolFactory,
 )
 ​
 fmt.Println("Thrift server starting on :9090...")
 if err := server.Serve(); err != nil {
     panic(err)
 }

再看这个代码,我们就可以再一次深入理解

从server入手,创建服务器,采用的是最常见的server4方法,需要四个参数

1.处理器本身:因为thrift文件当中只有单个service,因此直接获取即可

2.传输层:使用最简单常用的TCP连接,端口号为9090

3.传输工厂:使用的最基础的传输工厂,没有缓存和压缩

4.协议工厂:使用的是默认的二进制传输,对字段顺序和类型有严格校验

至此,我们就把服务端的语法学习的差不多了

客户端

客户端相较于服务端,配置会少一些,我们依旧按照学习服务端的顺序,由浅入深的学习客户端

1.创建客户端的方法

先上示例代码

复制代码
 
Go 复制代码
// 1. 创建传输层(连接服务端)
 transport, err := thrift.NewTSocket("localhost:9090")
 if err != nil {
     panic(err)
 }
 ​
 // 2. 包装传输层(带缓冲的传输)
 transportFactory := thrift.NewTTransportFactory()
 trans, err := transportFactory.GetTransport(transport)
 if err != nil {
     panic(err)
 }
 defer trans.Close() // 确保关闭连接
 ​
 // 3. 打开连接
 if err := trans.Open(); err != nil {
     panic(err)
 }
 ​
 // 4. 定义协议(需与服务端一致,此处为二进制协议)
 protocolFactory := thrift.NewTBinaryProtocolFactoryDefault()
 client := example.NewExampleServiceClientFactory(trans, protocolFactory)
 ​
 // 5. 调用服务方法
 err = client.SayHello(context.Background(), "Thrift")
 if err != nil {
     panic(err)
 }
 fmt.Println("Client: 调用 sayHello 成功")

这是一个基本的客户端代码,其中,最关键的一个方法是获取client客户端 ,也就是**example.NewExampleServiceClientFactory()**这个方法

我们点进去查看源码,上下文当中总共有这三个方法可以获取到client

Go 复制代码
 func NewExampleServiceClientFactory(t thrift.TTransport, f thrift.TProtocolFactory) *ExampleServiceClient {
     return &ExampleServiceClient{
        c: thrift.NewTStandardClient(f.GetProtocol(t), f.GetProtocol(t)),
     }
 }
 ​
 func NewExampleServiceClientProtocol(t thrift.TTransport, iprot thrift.TProtocol, oprot thrift.TProtocol) *ExampleServiceClient {
     return &ExampleServiceClient{
        c: thrift.NewTStandardClient(iprot, oprot),
     }
 }
 ​
 func NewExampleServiceClient(c thrift.TClient) *ExampleServiceClient {
     return &ExampleServiceClient{
        c: c,
     }
 }

相较于服务端,这边的三个方法就好区分多了

1. NewExampleServiceClientFactory(t thrift.TTransport, f thrift.TProtocolFactory)

  • 作用 :通过传输层(TTransport协议工厂(TProtocolFactory 创建客户端,这俩参数在服务端已经学习过了,就不过多赘述

  • 特点

    • 内部会调用协议工厂的 f.GetProtocol(t) 方法,为输入(iprot)和输出(oprot)创建相同的协议实例(共用一个协议工厂)。

    • 无需手动创建协议对象,由工厂统一生成,简化配置。

  • 适用场景 :输入和输出协议完全一致的常规场景(大多数情况下首选)。

  • 示例

Go 复制代码
 transport, _ := thrift.NewTSocket("localhost:9090")
 protocolFactory := thrift.NewTBinaryProtocolFactoryDefault()
 client := example.NewExampleServiceClientFactory(transport, protocolFactory)

2. NewExampleServiceClientProtocol(t thrift.TTransport, iprot thrift.TProtocol, oprot thrift.TProtocol)

  • 作用 :直接传入传输层(TTransport输入协议(iprot输出协议(oprot 创建客户端。

  • 特点

    • 输入协议(iprot,用于解析服务端响应)和输出协议(oprot,用于序列化客户端请求)可以单独指定,支持差异化配置。

    • 需要手动创建并传入协议实例,灵活性更高,但配置更繁琐。

  • 适用场景 :输入和输出协议不同的特殊场景(极少用到,例如请求用二进制协议,响应用 JSON 协议)。

  • 示例

    Go 复制代码
    transport, _ := thrift.NewTSocket("localhost:9090")
     iprot := thrift.NewTBinaryProtocol(transport, true)  // 输入用二进制协议
     oprot := thrift.NewTJSONProtocol(transport)          // 输出用 JSON 协议
     client := example.NewExampleServiceClientProtocol(transport, iprot, oprot)

3. NewExampleServiceClient(c thrift.TClient)

  • 作用 :直接传入一个通用的 thrift.TClient 接口实例,包装为 ExampleServiceClient

  • 特点

    • thrift.TClient 是 Thrift 客户端的抽象接口,包含核心的 RPC 调用逻辑(如 Call 方法)。

    • 允许复用已有的 TClient 实例,或传入自定义的 TClient 实现(如带拦截器、日志的客户端)。

  • 适用场景 :需要自定义客户端底层逻辑的场景(如添加 RPC 调用拦截器、超时控制等)。

  • 示例

    Go 复制代码
     // 先创建一个标准 TClient
     transport, _ := thrift.NewTSocket("localhost:9090")
     protocol := thrift.NewTBinaryProtocol(transport, true)
     standardClient := thrift.NewTStandardClient(protocol, protocol)
     ​
     // 包装为 ExampleServiceClient
     client := example.NewExampleServiceClient(standardClient)

总结

  • 99% 的常规场景用 NewExampleServiceClientFactory 即可,兼顾简洁性和通用性。

  • 若需对输入 / 输出协议做特殊区分(极少见),用 NewExampleServiceClientProtocol

  • 若需扩展客户端功能(如添加调用日志、重试机制),可自定义 TClient 后用 NewExampleServiceClient 包装。

2.设置基本参数

客户端的基本参数其实只有两个:传输层、协议工厂/协议本身

TTransport

这个其实是对应了服务端的 TServerTransport 参数 + TTransportFactory参数

既可以只设置传输层本身,还可以设置传输工厂(定义是否缓冲、压缩)

创建传输层,通常需要3步

步骤1:创建传输层(连接服务端)

Go 复制代码
 transport, err := thrift.NewTSocket("localhost:9090")
 if err != nil {
     panic(err)
 }

步骤2:工厂包装传输层(可以省略)

我们可以看到,这个步骤2利用工厂包装传输层,在服务端,是作为一个参数TTransportFactory,需要设置的

在Server2当中默认为二进制+缓冲,Server4当中需要自己设置

但是在客户端当中,需要我们自己包装,同时也可以省略这一步

Go 复制代码
 transportFactory := thrift.NewTTransportFactory()
 trans, err := transportFactory.GetTransport(transport)
 if err != nil {
     panic(err)
 }

可以看到,这里关键的一步是利用 thrift.NewTTransportFactory() 先获取工厂对象,然后通过 GetTransport()方法传入传输层对象,完成工厂包装,对传输层进行增强处理

而获取工厂对象的方法,我们在服务端已经讲过了,可以看 TTransportFactory 这个目录下的详情,这里我把总结再复制过来一遍

常用内置 TTransportFactory 及适用场景:

  • NewTTransportFactory:极简场景,无额外处理。

  • NewTBufferedTransportFactory:通用场景,通过缓冲提升效率(默认首选)。

  • NewTFramedTransportFactory:解决粘包问题,适合长连接高频传输。

  • NewTZlibTransportFactory:大数据传输,需权衡压缩率与 CPU 开销。

根据数据大小、传输频率、性能需求选择,多数情况下 TBufferedTransportFactoryTFramedTransportFactory 能满足需求。

步骤3:打开/关闭连接

Go 复制代码
 defer trans.Close() // 确保关闭连接
 // 打开连接(必须在调用前执行)
 if err := trans.Open(); err != nil {
     panic(err)
 }

示例:多重包装

对应服务端的组合使用,客户端的传输层也可以被多层包装

Go 复制代码
 // 1. 原始传输
 transport, _ := thrift.NewTSocket("localhost:9090")
 ​
 // 2. 先缓冲,再压缩(工厂嵌套)
 bufferedFactory := thrift.NewTBufferedTransportFactory(8192)
 zlibFactory := thrift.NewTZlibTransportFactory(-1)
 ​
 // 先通过缓冲工厂包装,再用压缩工厂包装
 trans1 := bufferedFactory.GetTransport(transport)
 trans := zlibFactory.GetTransport(trans1)
 ​
 // 3. 打开连接
 defer trans.Close() 
 trans.Open()
TProtocolFactory

这个协议工厂的设置方式就和服务端的一模一样的

并且,也必须和服务端保持相同协议,否则会序列化、反序列化失败

我直接把总结复制过来,详情也可以去看服务端篇的内容

总结

常用 TProtocolFactory 的选择建议:

  • 生产环境优先TBinaryProtocolFactory(平衡性能和兼容性)或 TCompactProtocolFactory(带宽敏感场景)。

  • 调试 / 可读性优先TJSONProtocolFactory(适合开发阶段排查问题)。

3.重头再看代码

Go 复制代码
// 1. 创建传输层(连接服务端)
 transport, err := thrift.NewTSocket("localhost:9090")
 if err != nil {
     panic(err)
 }
 ​
 // 2. 包装传输层(带缓冲的传输)
 transportFactory := thrift.NewTTransportFactory()
 trans, err := transportFactory.GetTransport(transport)
 if err != nil {
     panic(err)
 }
 defer trans.Close() // 确保关闭连接
 ​
 // 3. 打开连接
 if err := trans.Open(); err != nil {
     panic(err)
 }
 ​
 // 4. 定义协议(需与服务端一致,此处为二进制协议)
 protocolFactory := thrift.NewTBinaryProtocolFactoryDefault()
 client := example.NewExampleServiceClientFactory(trans, protocolFactory)
 ​
 // 5. 调用服务方法
 err = client.SayHello(context.Background(), "Thrift")
 if err != nil {
     panic(err)
 }
 fmt.Println("Client: 调用 sayHello 成功")

从client入手,采用的是最常用的NewExampleServiceClientFactory方法

这里再次注意,就根据我们thrift文件生成的代码的方法,命名都是 New + 包名 + 方法名

需要两个参数:

1.传输层:创建后通过带缓冲功能的传输工厂包装

2.协议工厂:与服务端一致,采用二进制协议

然后就可以调用服务方法了

六、Thrift跨项目实战调用

经过了上面五大章的学习,对Thrift的基本使用,已经掌握了

实际开发当中呢,我们更多的场景是:

1.编写服务端代码,然后编写开发文档,让客户端的团队调用

2.编写客户端代码,通过服务端团队提供的文档,调用其编写好的接口服务

为了对Thrift这个框架进一步进行实战运用,我会分两个项目,模拟服务端和客户端

1.问题分析

功能需求

服务端:一个登录校验接口,主要作用是传入用户名和密码,响应用户详细信息

客户端:对外接口,调用服务端登录校验接口

实现过程

Thrift框架的典型开发流程:接口先行、契约驱动

为了符合Thrift的开发泛式,我把流程分为这几部分:本地接口实现 → 编写 IDL → 完善服务端 → 编写客户端

2.服务器接口实现

在此之前,我在编写初始DDD架构这篇文章的时候,已经完成了一个基于Gin+GORM框架实现的单体服务

我基于此作为我们的服务端,我这里简单展示我的部分服务器对外接口实现

Go 复制代码
 // route.go
 func (r *Route) Register(engine *gin.Engine) {
     userController, err := InitializeUserController()
     if err != nil {
        panic(err)
     }
     group := engine.Group("/user")
     group.POST("/login", userController.Login)
 }
 // user_controller
 func (c *UserController) Login(ctx *gin.Context) {
     // 接收请求参数
     request := &dto.UserLoginRequestDTO{}
     if err := ctx.ShouldBindJSON(request); err != nil {
        ctx.JSON(400, gin.H{"error": err.Error()})
        return
     }
     // 调用manager层方法
     response, err := c.UserManager.UserLogin(request)
     if err != nil {
        ctx.JSON(500, gin.H{"error": err.Error()})
     }
     // 返回响应
     ctx.JSON(200, response)
 }

在 Thrift 作为RPC协议,设计 IDL 时,核心原则是 "以对外暴露的接口契约为中心" ,即 IDL 应描述客户端需要调用的最终接口 (入参、出参、异常),而非服务端内部的分层实现,因此只需要描述客户端实际调用的接口 (即 Login 方法)即可

3.编写IDL

我这里建议的方式是:交给AI,然后自己读一遍,进行微调

Go 复制代码
// 命名空间:生成 Go 代码时的包路径(根据实际项目调整)
 namespace go user
 ​
 // 登录请求参数(对应 UserLoginRequestDTO)
 struct UserLoginRequest {
     1: optional i64 userLoginId,  // 可选:用户登录ID(对应 DTO 的 UserLoginId)
     2: required string userName,  // 必选:用户名(对应 DTO 的 UserName,非空)
     3: required string password   // 必选:密码(对应 DTO 的 Password,非空)
 }
 ​
 // 用户响应信息(对应 UserResponseDTO)
 struct UserResponse {
     1: optional i64 userLoginId,  // 用户登录ID
     2: optional string userName,  // 用户名
     3: optional string password,  // 密码(注意:实际场景可能不返回密码,此处仅按DTO映射)
     4: optional i64 userInfoId,   // 用户信息ID
     5: optional string realName,  // 真实姓名
     6: optional string phone,     // 手机号
     7: optional string email,     // 邮箱
     8: optional i8 gender,        // 性别(0:未知,1:男,2:女)
     9: optional string birthday,  // 出生日期(日期字符串,如 "2000-01-01")
     10: optional string createTime, // 创建时间(时间字符串,如 "2023-01-01 12:00:00")
     11: optional string updateTime  // 更新时间(同上)
 }
 ​
 // 登录业务异常(覆盖服务端可能返回的错误,如参数为空、密码错误等)
 exception LoginException {
     1: required string message,  // 错误详情(如 "用户名不能为空")
     2: optional i32 code         // 错误码(可选,用于客户端快速判断错误类型)
 }
 ​
 // 登录服务接口(对应 Login 接口)
 service UserService {
     // 登录方法:传入请求参数,返回用户信息;失败时抛出 LoginException
     UserResponse login(1: UserLoginRequest request) throws (1: LoginException ex)
 }

然后终端执行:thrift --gen go user.thrift

生成以下文件

看过我上面教程的同学,应该知道,user_service-remote.go这个文件里面还需要修改一个import路径,具体的可以看第三章当中的"第一个Go项目的Thrift应用"

4.完善服务端代码

这一步,我们可以类比为,需要编写一个新的Controller,只不过这个Controller不是本地调用,而是启动Thrift服务端让远程的客户端调用

实现IDL当中定义的 login 接口

Go 复制代码
 type UserHandler struct {
     UserManager manager.UserManager
 }
 ​
 ​
 func (h *UserHandler) Login(ctx context.Context, request *user.UserLoginRequest) (*user.UserResponse, error) {
     // 1.将 Thrift 请求转换为服务端的 DTO
     loginReq := &dto.UserLoginRequestDTO{
        UserLoginId: request.UserLoginId,
        UserName:    &request.UserName,
        Password:    &request.Password,
     }
     // 2.调用服务端的业务逻辑
     respDTO, err := h.userManager.UserLogin(loginReq)
     if err != nil {
        // 3. 转换业务错误为 Thrift 异常
        code := int32(500) // 可自定义错误码(如 400 表示参数错误,500 表示业务错误)
        return nil, &user.LoginException{
           Message: err.Error(),
           Code:    &code, // 可自定义错误码(如 400 表示参数错误,500 表示业务错误)
        }
     }
     // 4.将 DTO 响应转换为 Thrift 响应
     return &user.UserResponse{
        UserLoginId: respDTO.UserLoginId,
        UserName:    respDTO.UserName,
        Password:    respDTO.Password, // 实际建议移除
        UserInfoId:  respDTO.UserInfoId,
        RealName:    respDTO.RealName,
        Phone:       respDTO.Phone,
        Email:       respDTO.Email,
        Gender:      respDTO.Gender,
        Birthday:    respDTO.Birthday,
        CreateTime:  respDTO.CreateTime,
        UpdateTime:  respDTO.UpdateTime,
     }, nil
 }

在之前的文章中,我的文件命名是service

这里因为IDL实现的接口是Controller层的,因此这里感觉就是编写了一个新的Controller代码,然后调用Manager接口的方法,因此我的命名是Handler,具体情况需要看项目内的业务场景

然后既然写了Handler,我们利用wire框架进行依赖注入

Go 复制代码
 // wire.go
 func InitializeUserHandler() (*handler.UserHandler, error) {
     wire.Build(
        // 新增:PO 的提供者(必须放在 Repository 前面,因为 Repository 依赖 PO)
        po.NewUserInfoPO,
        po.NewUserLoginPO,
 ​
        // Repository
        repository.NewUserRepository,
        // Service
        service.NewUserService,
        // Manager
        manager.NewUserManager,
        // Controller
        wire.Struct(new(handler.UserHandler), "*"),
     )
     return nil, nil
 }
 ​
 // wire_gen.go
 func InitializeUserHandler() (*handler.UserHandler, error) {
     userLoginPO := po.NewUserLoginPO()
     userInfoPO := po.NewUserInfoPO()
     userRepository := repository.NewUserRepository(userLoginPO, userInfoPO)
     userService := service.NewUserService(userRepository)
     userManager := manager.NewUserManager(userService)
     userHandler := &handler.UserHandler{
         UserManager: userManager,
     }
     return userHandler, nil
 }

编写新的main方法,启动Thrift服务

Go 复制代码
 func main() {
     // 初始化数据库
     database.Init()
 ​
     // 1. 获取IDL当中服务的实现类
     userHandler, err := day01.InitializeUserHandler()
     if err != nil {
         panic(err)
     }
     // 2. 创建 Thrift 处理器(绑定实现类)
     processor := user.NewUserServiceProcessor(userHandler)
 ​
     // 3. 配置传输层和协议层
     transport, err := thrift.NewTServerSocket(":9090") // 监听 9090 端口
     if err != nil {
         panic(err)
     }
     transportFactory := thrift.NewTBufferedTransportFactory(8192) // 带缓冲,缓冲区大小为8kb
     protocolFactory := thrift.NewTBinaryProtocolFactoryDefault()  // 二进制协议
 ​
     // 4. 创建并启动服务器
     server := thrift.NewTSimpleServer4(processor, transport, transportFactory, protocolFactory)
     println("Thrift 服务端启动,监听 :9090...")
     if err := server.Serve(); err != nil {
         panic(err)
     }
 }

如果对go的web开发比较熟悉的同学会发现,这其实和web的main方法启动流程是几乎一致的

我们先初始化数据库,当然实际项目中,还需要初始化redis、消息队列等其他中间件

然后就是我们服务端的参数获取流程

1.通过wire框架完成依赖注入,可以直接通过InitializeUserHandler方法获取处理器,然后创建Thrift 处理器

2.配置transport和transportFactory和protocolFactory

3.利用Server4创建并启动服务器

测试运行

5.编写客户端

首先,客户端项目也需要引入 gen-go/user 代码

步骤和服务端一致,先导入 Thrift IDL 文件,再通过命令行生成代码

关键注意

  • IDL 一致性 :客户端和服务端的 user.thrift 必须完全一致(包括字段 ID、类型、服务定义),否则会出现 "字段不匹配""协议解析失败" 等错误。

  • 版本管理:建议将 IDL 文件纳入版本控制(如 Git),确保客户端和服务端始终使用同一版本的 IDL。

  • 依赖同步:客户端需与服务端使用相同版本的 Thrift 编译器生成代码,避免因编译器版本差异导致的兼容性问题。

然后编写客户端的main方法

Go 复制代码
 func main() {
     // 1. 创建传输层(连接服务端 Thrift 端口,如 9090)
     transport, err := thrift.NewTSocket("localhost:9090")
     if err != nil {
        panic(err)
     }
     defer transport.Close()
     transport.Open()
 ​
     // 2. 配置协议(与服务端一致,如二进制协议)
     protocolFactory := thrift.NewTBinaryProtocolFactoryDefault()
 ​
     // 3. 创建客户端实例
     client := user.NewUserServiceClientFactory(transport, protocolFactory)
 ​
     // 4. 调用服务端登录接口
     req := &user.UserLoginRequest{
        UserName: "admin",
        Password: "123456",
     }
     resp, err := client.Login(context.Background(), req)
     if err != nil {
        // 处理异常
        if ex, ok := err.(*user.LoginException); ok {
           fmt.Printf("登录失败: %s\n", ex.Message)
        } else {
           fmt.Printf("调用错误: %v\n", err)
        }
        return
     }
 ​
     // 5. 处理响应
     jsonData, _ := json.MarshalIndent(resp, "", "  ")
     fmt.Printf("登录成功,用户信息( JSON 格式):\n%s\n", string(jsonData))
 }

6.测试

数据库数据

测试案例

情况一:用户不存在

Go 复制代码
 req := &user.UserLoginRequest{
     UserName: "admin",
     Password: "123456",
 }

响应结果:

情况二:密码错误

Go 复制代码
 req := &user.UserLoginRequest{
     UserName: "lqf",
     Password: "123456",
 }

响应结果:

情况三:正常响应

Go 复制代码
 req := &user.UserLoginRequest{
     UserName: "lqf",
     Password: "768170",
 }

响应结果:

服务端日志情况:

也是正常访问的

至此,我们对Thrift这个RPC的框架,也完成了基本掌握了

七、总结

至此,本篇文章也接近尾声了,为了方便实际开发中快速上手,或者是有时候忘了整体流程,我在此做一个使用Thrift框架双端服务的流程总结

  • 服务端正常编写接口,我个人建议还是配合http框架(gin等)正常编写,方便本地测试

  • 根据服务端接口的Controller层确定入参出参,然后利用AI(或者根据项目文档、公司规范)编写对应的IDL,然后生成代码

  • 完善服务器代码:

    • 类似编写一个新的Controller/Handler,同样可以利用wire框架进行依赖注入,然后入参由前端传入变成从IDL生成的代码中获取,出参要转换成IDL生成代码的形式

    • 编写当前server的main方法(当然也可以编写这个main方法作为子方法,然后在主main方法当中异步调用),同样需要初始化各种服务(数据库、redis等),然后设置服务器创建需要的参数,然后创建并启动服务器

  • 编写客户端:这个就简单很多了,一般来说都是放在客户端项目的service层当中,封装成一个外部调用方法进行调用

最后简单对比(吐槽)一下,讲讲Thrift的优势和过去使用Spring Cloud的Open Feign的区别

首先,Open Feign 是通过注解 + 动态代理 的方式,将 HTTP 接口调用伪装成 "本地方法调用",但底层仍是基于 HTTP/JSON 通信(属于 "RESTful 风格的远程调用")

而Thrift可以或者说默认是二进制传输,性能上更好,但是确实学习成本较高

其他的RPC协议,比如gRPC、我没有实战应用过,但是从网上了解到的情况是,都会比Thrift简单

未来应该会进一步学习Kitex这个RPC框架,更符合Go生态和微服务的环境,性能也会更好

相关推荐
千禧皓月6 小时前
【C++】基于C++的RPC分布式网络通信框架(二)
c++·分布式·rpc
Kratos开源社区8 小时前
别卷 LangChain 了!Blades AI 框架让 Go 开发者轻松打造智能体
go·agent·ai编程
Kratos开源社区8 小时前
跟 Blades 学 Agent 设计 - 01 用“提示词链”让你的 AI 助手变身超级特工
llm·go·agent
观望过往11 小时前
Apache IoTDB 技术深度解析:存储引擎、查询优化与分布式架构在工业物联网、智慧能源和车联网场景的应用指南
apache·iotdb
ApachePulsar12 小时前
Apache Pulsar 在小红书线上场景的探索与实践
apache
百锦再14 小时前
第10章 错误处理
java·git·ai·rust·go·错误·pathon
草莓熊Lotso17 小时前
Linux 基础开发工具入门:软件包管理器的全方位实操指南
linux·运维·服务器·c++·人工智能·网络协议·rpc
迦蓝叶1 天前
Apache Jena SPARQL 查询完全指南:入门与实战案例
apache·知识图谱·图搜索算法·三元组·jena·sparql·图查询
Mgx2 天前
从 4.8 秒到 0.25 秒:我是如何把 Go 正则匹配提速 19 倍的?
go