B班课代表的Go学习笔记-【4】-微服务框架gRPC与protobuf

131 阅读14分钟

一、认识gRPC

1.1RPC概念

远程过程调用(英语:Remote Procedure Call,RPC)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一个地址空间(通常为一个开放网络的一台计算机)的子程序,而程序员就像调用本地程序一样,无需额外地为这个交互作用编程(无需关注细节)。RPC是一种服务器-客户端(Client/Server)模式,经典实现是一个通过发送请求-接受回应进行信息交互的系统。

1.2 RPC通俗解释

假设你要在一个村庄里建造一座房子。你需要购买建材、雇佣工人和安排施工等事情。但是,你只是一个普通的居民,无法亲自去完成这些任务。所以你需要委托其他人来帮你完成。

在这个例子中,你就是客户端,而购买建材的人、雇佣工人的人以及施工队伍就是服务器。你需要与这些人进行通信,并告诉他们你的需求,然后等待他们完成任务并将结果告诉你。

远程过程调用(RPC)就是你与这些人进行通信的方式。你可以给购买建材的人打电话或发短信告诉他们你要购买的建材种类和数量,然后他们会帮你购买并将结果告诉你。同样,你可以通过电话或短信与雇佣工人的人联系,告诉他们你需要多少工人和工作时间,他们会帮你找到合适的工人并安排工作。施工队伍也可以通过电话或短信来与你沟通施工进度和完成情况。

在这个例子中,RPC就是你与购买建材的人、雇佣工人的人和施工队伍之间进行通信的方式。它允许你像打电话或发短信一样与这些人交流,并获取他们的处理结果。RPC让你不需要亲自去购买建材、雇佣工人或监督施工,而是通过委托其他人来完成,并通过通信方式获取结果。

RPC实际上就是一种让不同的程序之间能够交流和合作的技术,它简化了分布式系统的开发和通信过程。

1.3 gRPC

你可以认为是google RPC,就是由谷歌开发并开源的RPC工具库。

二、protobuf

1.1 protobuf的作用

Protobuf(Protocol Buffers)是一种用于结构化数据序列化的开源数据交换格式。它由Google开发,并广泛用于各种应用中,特别是在分布式系统、通信协议和数据存储等领域。

Protobuf通过定义数据结构的IDL(Interface Definition Language)文件来描述数据的结构和格式。IDL文件使用简洁的语法定义了消息的字段、类型和规则,类似于定义类和属性的方式。然后,基于IDL文件,使用Protobuf编译器可以生成各种编程语言的代码,用于在应用程序中进行数据的序列化和反序列化。

下面是一些Protobuf的特点和作用:

  1. 高效的序列化和反序列化:Protobuf使用二进制编码,相比于文本格式,它更紧凑,占用更少的存储空间和网络带宽。同时,Protobuf的序列化和反序列化速度也更快。
  2. 跨语言支持:Protobuf可以生成多种编程语言的代码,包括但不限于C++, Java, Python, Go等,使得不同语言的应用程序可以方便地进行数据交换和通信。
  3. 版本兼容性:Protobuf支持向后和向前兼容的数据结构演化。当数据结构发生变化时,可以通过定义可选字段、默认值和扩展字段等方式,实现不同版本的应用程序之间的兼容性。
  4. 可读性和可维护性:虽然Protobuf的二进制格式不可读,但IDL文件本身是可读的,有助于开发人员理解和维护数据结构。
  5. 灵活的消息定义:Protobuf支持嵌套消息、枚举类型、重复字段等丰富的数据结构定义,可以更灵活地描述复杂的数据模型。
  6. 自动化工具支持:Protobuf提供了丰富的工具,如编译器、插件和库,用于生成代码、验证数据、序列化/反序列化等操作。

1.2 protobuf与RPC的关系

Protobuf(Protocol Buffers)和RPC(Remote Procedure Call)是两个独立但常常一起使用的技术。

Protobuf用于定义数据的结构和格式,并提供了序列化和反序列化的机制用于在不同系统或语言之间传输和存储结构化数据。 它可以帮助开发人员定义数据的模型,并生成相应的代码来处理数据的序列化和反序列化操作。

RPC是一种通信机制,用于实现分布式系统中不同节点之间的远程调用。它允许一个节点(客户端)调用另一个节点(服务器)上的方法或函数,并获取返回结果,就像调用本地函数一样。RPC隐藏了底层通信的复杂性,使得远程调用过程对开发人员来说更加透明和简单。

Protobuf和RPC的关系在于,Protobuf可以作为RPC的数据交换格式。 在RPC中,当客户端需要将参数传递给服务器或服务器需要返回结果给客户端时,这些参数和结果可以使用Protobuf进行序列化和反序列化。通过使用Protobuf作为数据交换格式,可以确保在不同节点之间传输的数据的一致性和兼容性。

1.3 protobuf与RPC协作通俗解释

我们将Protobuf应用于之前的村庄造房子的案例中。

假设你使用Protobuf来定义房子的数据结构。首先,你需要创建一个IDL文件,例如 house.proto,其中定义了房子的属性,如房屋类型、面积和房主姓名等。

syntax = "proto3";

message House {
  enum HouseType {
    VILLA = 0;
    APARTMENT = 1;
    COTTAGE = 2;
	}

  string owner_name = 1;
  HouseType type = 2;
  float area = 3;
}

接下来,你可以使用Protobuf编译器将这个IDL文件编译成你所需的编程语言,例如Go。编译器会生成与你定义的数据结构相对应的类或结构体

然后,你可以在购买建材的人、雇佣工人的人和施工队伍之间使用这个数据结构来进行数据交换。例如,你可以通过RPC调用告诉购买建材的人你需要购买的建材种类和数量,并将这些信息使用Protobuf进行序列化,以便在网络上进行传输。

购买建材的人收到请求后,可以使用相同的Protobuf定义来反序列化该请求,并根据其中的信息购买建材。购买完成后,购买建材的人可以将购买的结果序列化为Protobuf格式,并通过RPC调用将结果返回给你。

类似地,你可以与雇佣工人的人和施工队伍之间进行类似的通信,使用Protobuf序列化和反序列化工人的数量、工作时间和施工进度等信息。

通过使用Protobuf,你可以确保不同节点之间传输的数据格式是统一的,并且可以轻松地在不同的编程语言中使用生成的代码进行数据的序列化和反序列化操作。

或者,大家可以想象一下医生的处方单子,你可以把protobuf语法看做医生的处方单一样【上面的案例中,也可以理解为约定格式的材料清单】。可以让处方信息在医疗系统内部人员之间统一流通,确保不同人员之间的数据格式一致性和可靠性。

1.4 windows安装protobuf

github.com/protocolbuf…

  1. 下载适合自己系统的版本

  1. 将其解压在自定义目录中

  1. 配置环境变量

  1. 终端查看protoc的版本

1.5 安装grpc-gateway

github.com/grpc-ecosys…

  1. 安装 protoc-gen-go: 在命令行中执行以下命令,使用 go install 命令安装 protoc-gen-go 工具,该工具用于将 Protocol Buffers 文件生成 Go 代码:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
  1. 安装 protoc-gen-grpc-gateway:,该工具用于将 gRPC 服务定义生成 gRPC-Gateway 代码:
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway
```
  1. 安装 protoc-gen-openapiv2: gRPC-Gateway 还需要安装 protoc-gen-openapiv2 工具,用于生成 OpenAPI 规范文件。在命令行中执行以下命令进行安装:
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2
```
  1. 添加可执行文件路径: 将 $GOPATH/bin 路径添加到系统的 PATH 环境变量中,以便可以在任何位置运行 protoc-gen-go、protoc-gen-grpc-gateway 和 protoc-gen-openapiv2。

4-1 使用go env命令查看GOPATH路径

go env GOPATH

4.2 配置环境变量

三、使用protoBuf

3.1 新建.proto文件

在项目目录下新建.proto文件,目录如下

3.2 house.proto文件语法

syntax = 'proto3';
package cargo;
option go_package="GoCar/server/proto/gen/go;house_pb";
message House {
  string owner_name = 1;
  int64 type = 2;
  float area = 3;
}

3.3 生成go对应代码

  1. 在proto目录下打开终端

  1. 输入protoc命令,在gen/go目录中生成go代码
protoc -I=D:\GoCar\server\proto --go_out=paths=source_relative:gen/go house.proto
  1. 生成的house.pb.go代码一览
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// 	protoc-gen-go v1.31.0
// 	protoc        v4.24.4
// source: house.proto

package house_pb

import (
	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
	reflect "reflect"
	sync "sync"
)

const (
	// Verify that this generated code is sufficiently up-to-date.
	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
	// Verify that runtime/protoimpl is sufficiently up-to-date.
	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)

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

	OwnerName string  `protobuf:"bytes,1,opt,name=owner_name,json=ownerName,proto3" json:"owner_name,omitempty"`
	Type      int64   `protobuf:"varint,2,opt,name=type,proto3" json:"type,omitempty"`
	Area      float32 `protobuf:"fixed32,3,opt,name=area,proto3" json:"area,omitempty"`
}

func (x *House) Reset() {
	*x = House{}
	if protoimpl.UnsafeEnabled {
		mi := &file_house_proto_msgTypes[0]
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
		ms.StoreMessageInfo(mi)
	}
}

func (x *House) String() string {
	return protoimpl.X.MessageStringOf(x)
}

func (*House) ProtoMessage() {}

func (x *House) ProtoReflect() protoreflect.Message {
	mi := &file_house_proto_msgTypes[0]
	if protoimpl.UnsafeEnabled && x != nil {
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
		if ms.LoadMessageInfo() == nil {
			ms.StoreMessageInfo(mi)
		}
		return ms
	}
	return mi.MessageOf(x)
}

// Deprecated: Use House.ProtoReflect.Descriptor instead.
func (*House) Descriptor() ([]byte, []int) {
	return file_house_proto_rawDescGZIP(), []int{0}
}

func (x *House) GetOwnerName() string {
	if x != nil {
		return x.OwnerName
	}
	return ""
}

func (x *House) GetType() int64 {
	if x != nil {
		return x.Type
	}
	return 0
}

func (x *House) GetArea() float32 {
	if x != nil {
		return x.Area
	}
	return 0
}

var File_house_proto protoreflect.FileDescriptor

var file_house_proto_rawDesc = []byte{
	0x0a, 0x0b, 0x68, 0x6f, 0x75, 0x73, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x63,
	0x61, 0x72, 0x67, 0x6f, 0x22, 0x4e, 0x0a, 0x05, 0x48, 0x6f, 0x75, 0x73, 0x65, 0x12, 0x1d, 0x0a,
	0x0a, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
	0x09, 0x52, 0x09, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04,
	0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65,
	0x12, 0x12, 0x0a, 0x04, 0x61, 0x72, 0x65, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x02, 0x52, 0x04,
	0x61, 0x72, 0x65, 0x61, 0x42, 0x24, 0x5a, 0x22, 0x47, 0x6f, 0x43, 0x61, 0x72, 0x2f, 0x73, 0x65,
	0x72, 0x76, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67,
	0x6f, 0x3b, 0x68, 0x6f, 0x75, 0x73, 0x65, 0x5f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
	0x6f, 0x33,
}

var (
	file_house_proto_rawDescOnce sync.Once
	file_house_proto_rawDescData = file_house_proto_rawDesc
)

func file_house_proto_rawDescGZIP() []byte {
	file_house_proto_rawDescOnce.Do(func() {
		file_house_proto_rawDescData = protoimpl.X.CompressGZIP(file_house_proto_rawDescData)
	})
	return file_house_proto_rawDescData
}

var file_house_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_house_proto_goTypes = []interface{}{
	(*House)(nil), // 0: cargo.House
}
var file_house_proto_depIdxs = []int32{
	0, // [0:0] is the sub-list for method output_type
	0, // [0:0] is the sub-list for method input_type
	0, // [0:0] is the sub-list for extension type_name
	0, // [0:0] is the sub-list for extension extendee
	0, // [0:0] is the sub-list for field type_name
}

func init() { file_house_proto_init() }
func file_house_proto_init() {
	if File_house_proto != nil {
		return
	}
	if !protoimpl.UnsafeEnabled {
		file_house_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
			switch v := v.(*House); i {
			case 0:
				return &v.state
			case 1:
				return &v.sizeCache
			case 2:
				return &v.unknownFields
			default:
				return nil
			}
		}
	}
	type x struct{}
	out := protoimpl.TypeBuilder{
		File: protoimpl.DescBuilder{
			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
			RawDescriptor: file_house_proto_rawDesc,
			NumEnums:      0,
			NumMessages:   1,
			NumExtensions: 0,
			NumServices:   0,
		},
		GoTypes:           file_house_proto_goTypes,
		DependencyIndexes: file_house_proto_depIdxs,
		MessageInfos:      file_house_proto_msgTypes,
	}.Build()
	File_house_proto = out.File
	file_house_proto_rawDesc = nil
	file_house_proto_goTypes = nil
	file_house_proto_depIdxs = nil
}

3.4 运用此go文件提供的结构体

  1. 在server项目目录下新建hello.go文件

  1. 使用house.pb.go提供的struct
package main

import (
    "encoding/json"
    "fmt"
    "google.golang.org/protobuf/proto"
    house_pb "server/proto/gen/go"
)

func main() {
    //1. 使用protobuf生成的go结构体,按需创建结构体数据实例
    house := house_pb.House{
        OwnerName: "三丰",
        Area:      99.8,
        Type:      1,
    }
    fmt.Println(&house)             //2. 通过地址,打印观察数据实例
    b, err := proto.Marshal(&house) //3.将数据包转为二进制数据流,可以让微服务之间高效传输数据
    if err != nil {
        panic(err)
    }
    fmt.Printf("%X\n", b) // 0A06E4B889E4B8B010011D9A99C742

    var house1 house_pb.House
    err = proto.Unmarshal(b, &house1) //4. 将二进制数据流,重新转回原始结构体格式
    if err != nil {
        panic(err)
    }
    fmt.Println(&house1)

    s, err := json.Marshal(&house) //5. 将结构体数据,转为json数据,可以提供给前端使用
    if err != nil {
        panic(err)
    }
    fmt.Printf("%s\n", s) //{"owner_name":"三丰","type":1,"area":99.8}

}
  1. 运行hello.go,查看结果

3.5 其他proto语法

  1. 改造house.proto代码
syntax = 'proto3';
package cargo;
option go_package="GoCar/server/proto/gen/go;house_pb";
message HouseRoom { 
  string room_name = 1;
  float room_area = 2;
}
enum HouseState { 
  NORMAL = 0;
  GOOD = 1;
  BAD = 2;
}
message House {
  string owner_name = 1;
  int64 type = 2;
  float area = 3;
  HouseRoom master_room = 4;  //组合
  repeated HouseRoom other_rooms = 5;   //切片
  HouseState state = 6; //枚举
}
  1. hello.go中对应书写格式
package main

import (
	"encoding/json"
	"fmt"
	"google.golang.org/protobuf/proto"
	house_pb "server/proto/gen/go"
)

func main() {
	//1. 使用protobuf生成的go结构体,按需创建结构体数据实例
	house := house_pb.House{
		OwnerName: "三丰",
		Area:      99.8,
		Type:      1,
		MasterRoom: &house_pb.HouseRoom{ //组合
			RoomName: "主卧",
			RoomArea: 20.5,
		},
		OtherRooms: []*house_pb.HouseRoom{ //切片
			{
				RoomName: "次卧",
				RoomArea: 10,
			},
			{
				RoomName: "书房",
				RoomArea: 10,
			},
		},
		State: 1, //枚举
	}

}

四、使用protoBuf创建gRPC服务

4.1 使用proto创建gRPC服务端

  1. 在house.proto中新增服务代码

参考文档:grpc.io/docs/langua…

syntax = 'proto3';
package cargo;
option go_package="./gen/go;house_pb";  //这里的路径做了调整
message HouseRoom {
  string room_name = 1;
  float room_area = 2;
}
enum HouseState {
  NORMAL = 0;
  GOOD = 1;
  BAD = 2;
}
message House {
  string owner_name = 1;
  int64 type = 2;
  float area = 3;
  HouseRoom master_room = 4;
  repeated HouseRoom other_rooms = 5;
  HouseState state = 6;
}

service HouseService {
  rpc GetHouse (GetHouseRequest) returns (GetHouseResponse){}
}
message GetHouseRequest {
  string id = 1;
}
message GetHouseResponse {
  string id = 1;
  House data = 2;
}
  1. 安装插件
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

如果不安装这个插件,可能会报错:'protoc-gen-go-grpc' 不是内部或外部命令,也不是可运行的程序

  1. 使用protoc生成服务代码

注意:此处因为有服务端代码,命令有所变化

 protoc -I . house.proto --go-grpc_out=:. --go_out=.
  1. 生成后的目录结构

4.2 测试grpc服务流程

1. 目录改造

2. 编写服务端逻辑

package main

import (
    "context"
    "net"
    house_pb "server/proto/gen/go"

    "google.golang.org/grpc"
)

type Service struct {
    house_pb.HouseServiceServer
}

func (*Service) GetHouse(ctx context.Context, req *house_pb.GetHouseRequest) (*house_pb.GetHouseResponse, error) {
    return &house_pb.GetHouseResponse{
        Id: req.Id,
        Data: &house_pb.House{
            OwnerName: "三丰",
            Area:      99.8,
            Type:      1,
            MasterRoom: &house_pb.HouseRoom{
                RoomName: "主卧",
                RoomArea: 20.5,
            },
            State: 1,
        },
    }, nil
}

func main() {
    g := grpc.NewServer()
    house_pb.RegisterHouseServiceServer(g, &Service{})
    lis, err := net.Listen("tcp", "0.0.0.0:8088")
    if err != nil {
        panic("端口监听失败")
    }
    err = g.Serve(lis)
    if err != nil {
        panic("rgpc服务启动失败")
    }
}

3. 编写客户端逻辑

package main

import (
	"context"
	"fmt"
	"google.golang.org/grpc"
	house_pb "server/proto/gen/go"
)

func main() {
	connect, err := grpc.Dial("127.0.0.1:8088", grpc.WithInsecure())
	if err != nil {
		panic(err)
	}
	defer connect.Close()
	c := house_pb.NewHouseServiceClient(connect)
	r, err := c.GetHouse(context.Background(), &house_pb.GetHouseRequest{Id: "111"})
	if err != nil {
		panic(err)
	}
	fmt.Println(r)
}

4. 启动服务端

启动server/house.go中的main

可能会报如下错误:

C:\Users\Yooye\go\pkg\mod\google.golang.org\grpc@v1.58.0\internal\pretty\pretty.go:27:2: missing go.sum entry for module providing package github.com/golang/protobuf/jsonpb (imported by google.golang.org/grpc/internal/pretty); to add:
go get google.golang.org/grpc/internal/pretty@v1.58.0

解决方案:

go get google.golang.org/grpc/internal/pretty@v1.58.0

5. 启动客户端

运行client/house.go中的main,可以在终端查看到如下结果: