首页
gRPC 与 Protobuf 介绍

微服务架构中,由于每个服务对应的代码库是独立运行的,无法直接调用,彼此间的通信就是个大问题。

Q:什么是 gRPC

gRPC可以实现微服务,将大的项目拆分为多个小且独立的业务模块,也就是服务,各服务间使用高效的protobuf协议进行 RPC 调用。

gRPC 是由 google 开发的一款语言中立、平台中立、开源的远程过程调用系统。gRPC 客户端和服务端可以在多种环境中运行和交互,例如用 java 写一个服务端,可以用 go 语言写客户端调用。gRPC 默认使用 protocol buffers,这是 google 开源的一套成熟的结构数据序列化机制(当然也可以使用其他数据格式如 JSON)

image.png

Q:什么是 Protobuf ?

Protocol Buffers 是一种与语言、平台无关,可扩展的序列化结构化数据的方法。

常用于通信协议,数据存储等等。相较于 JSON、XML,它更小、更快、更简单,因此也更受开发人员的青睐。

Q:使用 Protocol Buffer 有什么好处?

Protocol buffers 非常适合任何需要以语言中立、平台中立、可扩展的方式序列化结构化、类记录、类型化数据的情况。

它们最常用于定义通信协议(与 gRPC 一起)和数据存储。

使用协议缓冲区的一些优点包括:

  • 紧凑的数据存储

  • 快速解析

  • 可用于多种编程语言

  • 通过自动生成的类优化功能

Q:Protobuf 如何使用?

安装

https://github.com/protocolbuffers/protobuf/releases/tag/v25.1

mac 使用 homebrew 安装

brew update

# 使用 brew 安装 protobuf
brew install protobuf

# 安装 go 的支持
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2

语法

使用.proto 文件来定义一个消息类型。

syntax = "proto3"; // 指定版本信息,不指定会报错,默认是 proto2

// 指定包名,分号前面的表示生成的文件路径;分号后面表示生成的go文件的包名
option go_package = "./pb;pb";

// 定义结构体
message UserRequest {
    string name = 1;
}

// 响应结构体
message UserResponse {
    int32 id = 1;
    string name = 2;
    int32 age = 3;
    // repeated修饰符是可变数组,golang中会生成string类型的切片
    repeated string hobby = 4;
}

// service 定义方法
service UserInfoService {
    // 定义了一个 GetUserInfo 的 rpc 服务
    rpc GetUserInfo (UserRequest) returns (UserResponse);
}
  • message 每一个字段包含三个属性:类型、字段名称、字段编号

使用

使用 Protocol buffer 编译器生成对应语言的文件

# 将当前文件夹所有的 proto 文件转换为 go 文件
protoc --go_out=./ *.proto

# 生成 grpc 的 go 文件
protoc --go_out=. --go-grpc_out=. *.proto

序列化和反序列化

生成的 go 文件,我们可以引入来使用。

  • 使用proto.Marshal()可以将消息序列化为二进制文件。

  • 使用 proto.Unmarshal()可以反序列化

package main

import (
	"fmt"

	"github.com/KINGMJ/go-learning/tutorial6/demo3/proto/pb"
	"google.golang.org/protobuf/proto"
)

func main() {
	u := &pb.UserResponse{
		Name:  "张三",
		Age:   20,
		Hobby: []string{"吃饭", "睡觉", "打豆豆"},
	}
	fmt.Println(u)
	fmt.Println(u.GetName())

	// Protobuf序列化
	data, _ := proto.Marshal(u)
	fmt.Println(data)

	// Protobuf 反序列化
	user := pb.UserResponse{}
	proto.Unmarshal(data, &user)
	fmt.Println(user.GetHobby())
}

Q:gPRC 如何使用

  1. 编写 proto 文件,上面已经实现。我们定义了一个 UserInfoService微服务,里面有一个 GetUserInfo函数。

  2. 编译 proto 为 go 文件:protoc --go_out=. --go-grpc_out=. *.proto

  3. 服务端实现

package main

import (
	"context"
	"flag"
	"fmt"
	"log"
	"net"

	"github.com/KINGMJ/go-learning/tutorial6/demo3/proto/pb"
	"google.golang.org/grpc"
)

var (
	port = flag.Int("port", 50051, "The server port")
)

// 定义空接口
type UserInfoService struct {
	pb.UnimplementedUserInfoServiceServer
}

func (s *UserInfoService) GetUserInfo(ctx context.Context, req *pb.UserRequest) (res *pb.UserResponse, err error) {
	// 通过用户名查询用户信息
	name := req.Name
	// 模拟数据库里查询用户信息
	if name == "zs" {
		res = &pb.UserResponse{
			Id:    1,
			Name:  name,
			Age:   22,
			Hobby: []string{"吃饭", "睡觉", "打豆豆"},
		}
	}
	return
}

func main() {
	flag.Parse()
	// 1. 监听 tcp 端口
	lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
	// 2. 实例化 grpc
	s := grpc.NewServer()
	// 3. 在 grpc 上注册微服务
	pb.RegisterUserInfoServiceServer(s, &UserInfoService{})

	// 4. 启动服务端
	log.Printf("server listening at %v", lis.Addr())
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

  1. 客户端实现
package main

import (
	"context"
	"flag"
	"log"
	"time"

	"github.com/KINGMJ/go-learning/tutorial6/demo3/proto/pb"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

var (
	addr = flag.String("addr", "localhost:50051", "the address to connect to")
)

func main() {
	flag.Parse()
	// 1. 连接服务端
	conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	// 2. 实例化 grpc 客户端
	client := pb.NewUserInfoServiceClient(conn)

	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()

	// 3. 组装请求参数
	req := new(pb.UserRequest)
	req.Name = "zs"
	// 4. 调用接口
	res, err := client.GetUserInfo(ctx, req)
	if err != nil {
		log.Fatalf("could not greet: %v", err)
	}
	log.Printf("响应结果: %v\n", res)
}