gRPC(golang版) 入门教程

1,798 阅读7分钟

gRPC

介绍

gRPC是google开发的一个RPC框架,跨语言,跨平台,基于Protobuf序列化协议。简单的说就是:是一个RPC框架,使用Protobuf序列化数据。本文以实战为主,一步步走完gRPC入门教程。

首先gRPC是以Protobuf序列化的,因此先来介绍Protobuf。

Protobuf

介绍

protobuf的全名是Google Protocol Buffers,是google开发的,与语言无关,平台无关,可扩展的系列化结构数据的方法,可用于数据通信,数据存储等。简单点说,就是和json或者xml类似的结构数据序列化方法。其特点:

  • 跨平台,跨语言
  • 序列化后体积更小,二进制形式,速度更快
  • 兼容好,protobuf的设计有很好的向下或者向上兼容

编写

.proto文件

Protobuf文件以 .proto作为文件后缀,在.proto文件里定义好数据结构之后,就可以用工具将这个文件翻译成具体的代码。来看看.proto文件怎么写的

message

syntax = "proto3"; // 使用proto3版本语法,2和3版本的语法有些差异,这里使用3

message Person {
    string name = 1;
    int32 age = 2;
    repeated string address = 3;
}

来解释一下这个文件的含义

  • messaage: 结构定义的关键词,与之相同的还有service,enum,后面接一个名字,例子里是Person,即定义里一个Person的消息结构
  • repeated 限定修饰词,即表示该字段可以出现多次(包括0),在2的版本里,还有required,optional两个限定修饰词,不过这里使用的是3,因此不在这样演示
  • string, int32都是数据类型
  • name,age, address 自定义的字段名字
  • 1,2,3是字段编码值,有了这个值,通信双方才能互相识别对方的字段,范围为1~2^23,同一个message里不能有相同的字段编码值,其中1~15的编码时间和空间效率是最高的,编码值越大,效率就越低,通常建议设置在1~15之间

具体字段定义时的格式为:[限定修饰词] 数据类型 字段名称 = 字段编码值

service

message是用来定义消息结构体的,而service是定义服务的。比如想要定义一个RPC服务并具有一个方法,该方法有Person参数,返回Person,则可以如下定义

service PersonService {
    rpc getPersonInfo (Person) returns (Person) {
    }
}

安装

编写完的.proto文件并不能直接使用,需要用protoc工具,将文件翻译成对应的代码。

安装的方式有多种,我采用的是最简单的方法:

  1. 在该项目的仓库上,下载 protoc-3.11.4-osx-x86_64.zip 这个版本(写这篇文章时,最新版本是3.11.4,mac系统),根据自己的环境下载对应的版本
  2. 下载解压之后,目录里bin文件夹里有个protoc执行文件,把该文件放到 /usr/local/bin/目录下就可以了
  3. 在命令行里,执行protoc --version可以查看当前版本,我的是libprotoc 3.11.4。当然也可以下源码编译安装 ╮(╯▽╰)╭

编译

安装完之后,就可以进行编译,自动的将写好的.proto文件转为相对应的代码,由于原生的protoc只能生成c++,python等代码,要生成go语言,需要安装protoc-gen-go插件,如果你能科学上网,直接go get -u github.com/golang/protobuf/protoc-gen-go 安装。不行的话,可以使用下面这个方法:

  1. 到protoc-gen-go的github仓库,选择一个版本,下载Source code(zip)
  2. 解压zip包,进入目录,进入protoc-gen-go文件夹里,执行go build,就会生成一个protoc-gen-go的可执行文件
  3. 把生成的protoc-gen-go可执行文件,放到 /usr/local/go/bin/ 目录下即可。

插件安装好了之后,就可以执行命令了,在编写好的.proto文件目录里,执行protoc --go_out=plugins=grpc:. ./person.proto 命令。就会在目录下生成一个person.pb.go文件,这个文件就是根据.proto文件,生成的go代码。

命令的通用格式

protoc --go_out=plugins=grpc:生成的目录 定义好的.proto文件路径

其中plugins=grpc:是使用grpc的形式生成,会把.proto里定义的service生成对应的接口。如果去掉,即命令变成protoc --go_out=. ./person.proto,只会生成message定义的,service定义的就不会生成。

protobuf的入门教程就介绍到这里,如果有兴趣深入研究的话,可以去看官方文档╮(╯▽╰)╭

编写gRPC服务端和客户端

分析

先来分析一下person.pb.go文件,这个文件大致可以分为三个部分,

一是结构体的定义

type Person struct {
	Name                 string   `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
	Age                  int32    `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"`
	Address              []string `protobuf:"bytes,3,rep,name=address,proto3" json:"address,omitempty"`
	XXX_NoUnkeyedLiteral struct{} `json:"-"`
	XXX_unrecognized     []byte   `json:"-"`
	XXX_sizecache        int32    `json:"-"`
}

这里就是将.proto文件的message转化成了结构体。

二是服务端定义

// PersonServiceServer is the server API for PersonService service.
type PersonServiceServer interface {
	GetPersonInfo(context.Context, *Person) (*Person, error)
}

这里将.proto文件里的service转化为interface,而我们在服务端的文件里,就是要实现这个interface。

下面这个函数,则是将PersonServiceServe注册到gRPC中,需要我们在服务端调用。

func RegisterPersonServiceServer(s *grpc.Server, srv PersonServiceServer) {
	s.RegisterService(&_PersonService_serviceDesc, srv)
}

三是客户端定义

type PersonServiceClient interface {
	GetPersonInfo(ctx context.Context, in *Person, opts ...grpc.CallOption) (*Person, error)
}

type personServiceClient struct {
	cc grpc.ClientConnInterface
}

func NewPersonServiceClient(cc grpc.ClientConnInterface) PersonServiceClient {
	return &personServiceClient{cc}
}

func (c *personServiceClient) GetPersonInfo(ctx context.Context, in *Person, opts ...grpc.CallOption) (*Person, error) {
	out := new(Person)
	err := c.cc.Invoke(ctx, "/person.PersonService/getPersonInfo", in, out, opts...)
	if err != nil {
		return nil, err
	}
	return out, nil
}

这一部分是在编写客户端时使用的。

服务端

开始编写的时候,需要用到google.golang.org/grpc这个包,我使用的go module方式管理包,因此十分的简单,如果不清楚go module可以看看我之前写的总结

先看看目录结构:

├── client.go
├── go.mod
├── go.sum
├── proto
│   ├── person.pb.go
│   └── person.proto
└── server.go

服务端分为四个步骤

  1. 监听端口
  2. 实例化gRPC
  3. 注册服务
  4. 启动服务
// server.go
package main

import (
	"context"
	pb "gRPC/proto" // 导入刚刚生成的person.pb.go包
	"google.golang.org/grpc"
	"log"
	"net"
)

// 定义空结构体,用于实现PersonServiceServer接口
type PersonService struct {
}

// GetPersonInfo 按照person.pb.go里的方法签名,实现这个方法,使得PersonService实现PersonServiceServer接口
func (p *PersonService) GetPersonInfo(ctx context.Context, req *pb.Person) (*pb.Person, error) {
	// 具体逻辑实现
	if req.Name == "张三" {
		return &pb.Person{
			Name:    "张三",
			Age:     18,
			Address: []string{"广东", "上海"},
		}, nil
	}
	return nil, nil
}

func main() {
	//1、监听
	listener, err := net.Listen("tcp", ":6000")
	if err != nil {
		log.Fatal(err)
	}

	//2、实例化gRPC
	s := grpc.NewServer()

	//3、gRPC上注册服务
	p := PersonService{} 
	pb.RegisterPersonServiceServer(s, &p) // 空结构体 PersonService 就是在这里使用的

	//4、启动服务端
	s.Serve(listener)
}

服务端的实现,最主要的就是用一个结构体(一般是空结构体),来实现生成的xxxServer 接口,也就是为.proto里定义的service里的方法实现具体逻辑。然后再把这个结构体注册到gRPC上,绑定这些方法。

客户端

客服端也可以分为四个步骤

  1. 连接服务端
  2. 实例化一个客户端
  3. 组装请求参数
  4. 调用服务
// client.go
package main

import (
	"context"
	"fmt"
	pb "gRPC/proto"
	"google.golang.org/grpc"
	"log"
)

func main() {
	//1、连接服务端
	conn, err := grpc.Dial(":6000", grpc.WithInsecure())
	if err != nil {
		log.Fatal(err)
	}

	defer conn.Close()
	//2、实例化一个客户端
	client := pb.NewPersonServiceClient(conn)

	//3、组装请求参数
	body := pb.Person{Name: "张三"}

	//4、调用接口
	resp, err := client.GetPersonInfo(context.Background(), &body)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(resp) // 这里会打印 name:"\345\274\240\344\270\211" age:18 address:"\345\271\277\344\270\234" address:"\344\270\212\346\265\267" 因为是中文,所以序列化之后变成了数字的形式
	fmt.Println(resp.Name) // 单独打印一个属性则是正常的 显示 "张三"
}

客户端的逻辑就比较简单一些,基本就是调用person.pb.go里的函数。

总结

基本上整个gRPC的入门过程就是这样,当然这只是最简单的应用,不过流程基本是这样啦~

Thanks!