Thrift与Protobuf | 青训营笔记

655 阅读8分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 8 天

这两个都是用来定义接口的。用的地方比较多。

ProtoBuf

首先还是安装protobuf。我们只需要protoc就可以了。从github的release中可以下载到对应的版本。解压即可用。

随后安装protoc-gen-go。注意路径是google.golang.org/protobuf/cmd/protoc-gen-go@latest

不是github.com/golang/protobuf/protoc-gen-go@latest

之后我们在使用protoc的时候加上--go_out参数即可导出go代码了。

下面是一个简单的protobuf的代码:

syntax = "proto3";
option go_package=".";

message Student {
    string name = 1;
    bool male = 2;
    repeated int32 scores = 3;
}

第一行我们标定使用的是proto3。如果你用的是proto2,这个字段就写proto2。

由于我们这个文件不从属于任何一个go package,所以我们需要指定其当前目录输出,否则我们生成代码的时候会报错。

下面就是我们的一个消息结构了。以message开头, Student为这个消息的名称。其中有三个参数,参数的定义和c语言模式很像。注意后面的等于号与标号,这个是必须的。

repeated和singular是一对关键字。通常singular可以不行,默认添加,表示单个。repeated其实就是表示数组,在go里面就是切片。

proto可以写注释,格式和c格式一致。一个proto文件可以写多个message(也就是多个结构体,如果需要表示复合类型的,那么多个message就是必须的,否则无法表示复合类型)。这里只要将这个message看成sturct或者class其实就行了。proto通过给定的字段来生成类,添加上proto的方法就是我们的输出了。

使用以下命令导出代码:

protoc --go_out=. student.proto 

可以看到导出了一个student.pb.go的文件。

type Student struct {
        state         protoimpl.MessageState
        sizeCache     protoimpl.SizeCache
        unknownFields protoimpl.UnknownFields

        Name   string  `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
        Male   bool    `protobuf:"varint,2,opt,name=male,proto3" json:"male,omitempty"`
        Scores []int32 `protobuf:"varint,3,rep,packed,name=scores,proto3" json:"scores,omitempty"`
}

导出的结构体和我们提供的一致,这里为了要导出所以首字母都变大写了。go这种按照名称是否大写开头来判断是否导出还是挺有意思的。

为了让结构更加清晰,我们将go_package设为了"./student"也就是新建了一个student包来放我们的代码。test是我的项目。

package main

import (
	"fmt"
	"google.golang.org/protobuf/proto"
	"test/student"
)

func main() {
	test := &student.Student{
		Name:   "test",
		Male:   true,
		Scores: []int32{98, 85, 88},
	}
	data, _ := proto.Marshal(test)

	newTest := &student.Student{}
	proto.Unmarshal(data, newTest)

	// Now test and newTest contain the same data.
	if test.GetName() != newTest.GetName() {
		fmt.Printf("data mismatch %q != %q", test.GetName(), newTest.GetName())
	}
}

就已经可以使用了。

下面是嵌套的情况:

syntax = "proto3";
option go_package = "./student";

message Address {
    string locate = 1;
    int32 room = 2;
}

message Student {
    string name = 1;
    bool male = 2;
    repeated int32 scores = 3;
    Address locate = 4;
}

编译出来为:

type Address struct {
	state         protoimpl.MessageState
	sizeCache     protoimpl.SizeCache
	unknownFields protoimpl.UnknownFields

	Locate string `protobuf:"bytes,1,opt,name=locate,proto3" json:"locate,omitempty"`
	Room   int32  `protobuf:"varint,2,opt,name=room,proto3" json:"room,omitempty"`
}

...

type Student struct {
	state         protoimpl.MessageState
	sizeCache     protoimpl.SizeCache
	unknownFields protoimpl.UnknownFields

	Name   string   `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
	Male   bool     `protobuf:"varint,2,opt,name=male,proto3" json:"male,omitempty"`
	Scores []int32  `protobuf:"varint,3,rep,packed,name=scores,proto3" json:"scores,omitempty"`
	Locate *Address `protobuf:"bytes,4,opt,name=locate,proto3" json:"locate,omitempty"`
}

protobuf是支持rpc的。

syntax = "proto3";
option go_package = "./student";

message Address {
    string locate = 1;
    int32 room = 2;
}

message Student {
    string name = 1;
    bool male = 2;
    repeated int32 scores = 3;
}

service GetAddressService {
    rpc GetAddress(Student) returns(Address);
}

使用service关键字创建service,这里我们创建一个GetAddress服务,入参Student返回值Address。但是protogo没有实现service所以无法导出interface,想要用就需要使用插件。这里我们略过。

Thrift

Thrift和protobuf类似。

又是艰难的安装过程。

[Apache Thrift - Download]下载源文件。

进入根目录执行:

./configure
make 
sudo make install

go要使用的话,还需要go get github.com/apache/thrift/lib/go/thrift

Thrift有以下类型:

基本类型:

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

结构体类型: struct: 定义的结构体对象

容器类型:

  • list: 有序元素列表
  • set: 无序无重复元素集合
  • map: 有序的key/value集合

异常类型: exception: 异常类型

服务类型: service: 具体对应服务的类

从官方给的样例来看吧。

官方给出了两个文件,一个是需要用到的部分共享定义:

/**
 * This Thrift file can be included by other Thrift files that want to share
 * these definitions.
 */

namespace cl shared
namespace cpp shared
namespace d share // "shared" would collide with the eponymous D keyword.
namespace dart shared
namespace java shared
namespace perl shared
namespace php shared
namespace haxe shared
namespace netstd shared


struct SharedStruct {
  1: i32 key
  2: string value
}

service SharedService {
  SharedStruct getStruct(1: i32 key)
}

另一个是:

/**
 * Thrift files can reference other Thrift files to include common struct
 * and service definitions. These are found using the current path, or by
 * searching relative to any paths specified with the -I compiler flag.
 *
 * Included objects are accessed using the name of the .thrift file as a
 * prefix. i.e. shared.SharedObject
 */
include "shared.thrift"

/**
 * Thrift files can namespace, package, or prefix their output in various
 * target languages.
 */

namespace cl tutorial
namespace cpp tutorial
namespace d tutorial
namespace dart tutorial
namespace java tutorial
namespace php tutorial
namespace perl tutorial
namespace haxe tutorial
namespace netstd tutorial

/**
 * Thrift lets you do typedefs to get pretty names for your types. Standard
 * C style here.
 */
typedef i32 MyInteger

/**
 * Thrift also lets you define constants for use across languages. Complex
 * types and structs are specified using JSON notation.
 */
const i32 INT32CONSTANT = 9853
const map<string,string> MAPCONSTANT = {'hello':'world', 'goodnight':'moon'}

/**
 * You can define enums, which are just 32 bit integers. Values are optional
 * and start at 1 if not supplied, C style again.
 */
enum Operation {
  ADD = 1,
  SUBTRACT = 2,
  MULTIPLY = 3,
  DIVIDE = 4
}

/**
 * Structs are the basic complex data structures. They are comprised of fields
 * which each have an integer identifier, a type, a symbolic name, and an
 * optional default value.
 *
 * Fields can be declared "optional", which ensures they will not be included
 * in the serialized output if they aren't set.  Note that this requires some
 * manual management in some languages.
 */
struct Work {
  1: i32 num1 = 0,
  2: i32 num2,
  3: Operation op,
  4: optional string comment,
}

/**
 * Structs can also be exceptions, if they are nasty.
 */
exception InvalidOperation {
  1: i32 whatOp,
  2: string why
}

/**
 * Ahh, now onto the cool part, defining a service. Services just need a name
 * and can optionally inherit from another service using the extends keyword.
 */
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()

}

原文档中的注释已经足够的详细。

第一句include,我们可以像c语言一样包含另一个thrift文件。两个文件中包含了名称一样的namespace,但他们所属于的包不同一个是属于shared一个属于tutorial,所以不会有碰撞的问题。

namespace标明了我们这个thrift在相应的语言中被翻译成什么,对go来说就是package,对cpp来说就是class。

namespace go mypackage  // 生成go代码的话生成代码就在mypackage这个包。

后面我们看到typedef这个关键字,我们可以像cpp一样来定义我们的别名。

thrift支持常量。可以看到给出的常量定义和cpp语法一致。你要是学过cpp的话,对thrift肯定能立马上手。

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

结构体中的结构需要有标号,也就是前面的1:这个玩意,可以给字段附上初始值。optional关键字可以让字段不进入序列化。exception的定义和结构体一致。

service可以继承。使用extends关键字即可。

service Calculator extends shared.SharedService {
   void ping(),
   i32 add(1:i32 num1, 2:i32 num2),
   i32 calculate(1:i32 logid, 2:Work w) throws (1:InvalidOperation ouch),
   oneway void zip()
}

我们的Calculator服务继承了shared中的SharedService服务。服务中包含四个方法(继承的没算)。

定义方法的格式和cpp的函数定义类似,不过我们的参数列表格式则是和上方的struct的字段定义一致。

方法可以有异常,使用throws告知这个方法会存在异常,并在throws的列表中添加这个参数。

oneway表示这个方法只能被请求,但是不会有返回,oneway方法必须为void。

我没有找到thrift的详细文档,我只能通过他人的实例我学习thrift在代码中的使用,导致有很多地方我也不明白。(自己用起来感觉没有protobuf好用)

我们编写一个简单的示例:

namespace go user

struct User {
    1: i32 ID,
    2: string Name,
    3: optional string Address
}

service GetUserService {
    User GetUserByName(1: string name)
    User GetUserByID(1: i32 ID)
}

生成代码。生成代码中,包依赖可能需要自己手动修改一下。

服务端:

package main

import (
	"context"
	"demo/gen-go/user"
	"fmt"
	thrift "github.com/apache/thrift/lib/go/thrift"
)

// define a service
type GetUser struct{}

// 下面来实现thrift提供的接口

func (service *GetUser) GetUserByName(ctx context.Context, name string) (_r *user.User, _err error) {
	return &user.User{ID: 12, Name: name}, nil
}

func (service *GetUser) GetUserByID(ctx context.Context, ID int32) (_r *user.User, _err error) {
	fmt.Println("get a call")
	return &user.User{ID: ID, Name: "lihua"}, nil
}


func main() {
        
        // 创建handle,为了使用其实现的函数。
	handler := &GetUser{}
        
        // 别人的代码里面很多的方法都弃用了。这边就是得到两个类: 
        // 一个是transport的工厂类,一个是protocol的工厂类。
	transportFactory := thrift.NewTFramedTransportFactoryConf(thrift.NewTTransportFactory(), &thrift.TConfiguration{})
	protocolFactory := thrift.NewTBinaryProtocolFactoryConf(&thrift.TConfiguration{})

	var transport thrift.TServerTransport

	const address = "127.0.0.1:8888"

	var err error
        // 创建套接字
	transport, err = thrift.NewTServerSocket(address)
	if err != nil {
		fmt.Println("Server runing error.")
		panic(err)
	}
	fmt.Printf("Service start on %s\n", address)
        
        // 定义工作
	processor := user.NewGetUserServiceProcessor(handler)
        
        // 创建服务, 这里使用的simpleserver一般只有在演示的时候才用,毕竟是单线程阻塞模式。
	server := thrift.NewTSimpleServer4(processor, transport, transportFactory, protocolFactory)
        
        // 开启服务
	err = server.Serve()

	if err != nil {
		fmt.Println("Server running error.")
		panic(err)
	}
}

随后我们实现client。部分设置保持一致:

package main

import (
	"client/gen-go/user"
	"context"
	"fmt"
	thrift "github.com/apache/thrift/lib/go/thrift"
)

const remote = "127.0.0.1:8888"

func main() {

	transportFactory := thrift.NewTFramedTransportFactoryConf(thrift.NewTTransportFactory(), &thrift.TConfiguration{})
	protocolFactory := thrift.NewTBinaryProtocolFactoryConf(&thrift.TConfiguration{})
        
        // 老代码里使用的是NewTSocket,但是被弃用了。
	transport := thrift.NewTSocketConf(remote, &thrift.TConfiguration{})

	clientTransport, _ := transportFactory.GetTransport(transport)
        
        // 创建client,通过transport和protocol保存的设置来生成client。
	client := user.NewGetUserServiceClientFactory(clientTransport, protocolFactory)
        
        // 注意这里是transport open不是client open。
	if err := transport.Open(); err != nil {
		fmt.Println("Start transport error.")
		panic(err)
	}
	defer transport.Close()
        
        // thrift0.17版本自动添加了context参数,使用时需要传入context。
	ctx := context.Background()
	fmt.Println(client.GetUserByID(ctx, 31))
}

运行:

server:
Service start on 127.0.0.1:8888
get a call

client:
User({ID:31 Name:lihua Address:<nil>}) <nil>