基于Golang的gRPC框架使用与开发

一、gRPC 基础

包括 rpcgRPCProtobuf 等概念,网上已经有详细的介绍,此处不再过多说明。

关于 gRPC 的介绍,可以参考官方介绍文档
以及微软官方文档介绍

通俗的来说,gRPC 是一种 rpc 的具体实现,其利用 Protobuf 将数据进行序列化并用于传输。


二、开发前置准备

默认已经安装了 Golang 等相关内容,这里着重基于 GolanggRPC 相关工具的安装。

主要需要 2 个工具的安装:

  1. Protocol Buffer 编译器:protoc
  2. protocgolang 插件

2.1 protoc

该工具的作用,是将 .proto 文件编译成目标语言的源码。
Golang 就会根据 .proto 文件编译为 xxx.go文件。

官方安装说明文档参考。

共三种安装方法:通过包管理、预编译包安装和源码编译安装。
以下是详细说明。

2.1.1 包管理方式安装

# ubuntu
apt install -y protobuf-compiler

# mac
brew install protobuf

# 安装完毕记得检查版本号,保证版本 3 +
protoc --version  # Ensure compiler version is 3+

2.1.2 预编译二进制安装

下载地址

根据系统和处理器架构,选择合适的包,以 protoc-21.6-linux-x86_64.zip 为例进行操作。

show all

这里选择linux-x86_64
可以提取下载链接,直接下载到开发环境中,也可以下载并拷贝。

以直接下载为例:

# ---------
# 第一步下载
# wget 下载 zip 包
wget https://github.com/protocolbuffers/protobuf/releases/download/v21.6/protoc-21.6-linux-x86_64.zip
# 或者 curl命令,二选一即可
curl -LO https://github.com/protocolbuffers/protobuf/releases/download/v21.6/protoc-21.6-linux-x86_64.zip

# ---------
# 第二步解压
# 下载完毕后解压
# 需要安装 unzip 并创建 ~/.local 文件夹
# apt install unzip zip; mkdir ~/.local
# 想放到其他目录也可自行配置
unzip protoc-21.6-linux-x86_64.zip -d ~/.local

# -----------------------------
# 第三步,将 protoc 加入环境变量,可[临时添加]或[永久添加],二选一即可
# 3.1 临时
export PATH="$PATH:~/.local/bin"

# 3.2 永久,根据使用的shell和使用习惯变更对应的shell配置文件
# /etc/profile, ~/.bashrc, ~/.zshrc xxx 等
# 此处以 /etc/profile 为例
vi /etc/profile
# 在末尾插入:
PATH="$PATH:~/.local/bin"
# 刷新环境变量
source /etc/profile

# ---------------
# 第四步,检验安装
# 查看 protoc version
protoc --version
# 出现:libprotoc 3.xx.x 即为安装成功

2.1.3 编译安装

2.2 Golang 插件

因为 protoc 工具本身并不支持 Go 语言的编译,所以需要安装 Golang 的插件以支持编译。

$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

protoc-gen-go 生成将 protobuf 消息进行序列化/反序列化的 go 代码,也就是定义 go 语言版本的protobuf 协议。

protoc-gen-go-grpc 是根据这些 protobuf 协议,生成 gRPC 服务的 go 代码。

要想使用基于 Golang 的 gRPC,二者缺一不可。

两个工具的具体使用方法,见本文 4.1。

Tips:网上有些教程只安装了一个插件就可以使用了,因为使用的是旧版。新旧说明可以参考这里


三、proto 文件语法

在这里,proto文件主要是为了定义一种 rpc 服务。

详细语法可以参考官方英文文档

后文内容如出现语法不一致,以官方为准。

3.1 头部语法

syntax = "proto3"; // 版本声明

package hello; // 包名,proto 的包名,如果有多个 proto 文件,依此区分

option go_package="./;hello"; 
// option go_package = "[path];[name]";
// path: 输出存放地址
// name: 生成 go 文件所属 package name

// 还有这种写法:
option go_package = "github.com/protocolbuffers/protobuf/examples/go/tutorialpb";
// 没有 ";[name]",会根据最后一个名称,也就是 'tutorialpb' 来作为 go 的 package name
// 个人不是很建议用这种方法

3.2 定义服务

语法如下:

service service_name{
    rpc function_name (msg_type1) returns (msg_type2);
}

上述语法说明:

  • service_name 注册到 gRPC 中的具体服务
  • function_name 提供调用的方法,server 端提供,client 端用来调佣
  • msg_type1, msg_type2 具体的消息类型,其实就是结构体,具体定义见3.3

3.3 定义消息类型

消息类型,其实就是传输的结构体,对应上面的 msg_type

// message 是关键字,msg_name 是自定义的消息名称
message msg_name {
    [访问修饰符]  [数据类型] [数据名称] = [num],
    optional string test1= 1;
    string test2= 2;
    string test3= 3;
}

语法说明:

  • 访问修饰符:官方称作“Field Rules”,常见的有 require(表示该项必填,默认)optional(选填)repeated(重复项或数组)
  • 数据类型:参考官方文档
    常见的有 string int32 int64 uint32 uint64 bool bytes
  • 数据名称:自定义即可
  • num,这个数字是在传输的时候的排序,可以自定义排序,但不能重复。
    num 取值范围 [1, 229-1]
    其中,num 取值 [1, 15] 时占用 1 字节,[16, 2047] 占用 2 个字节,因此应将频繁传输的字段分配 1 - 15 ;
    此外,19000 -19999 被预占用,不可使用,否则会报错。
  • 同样的,message 还支持嵌套,类似于结构体嵌套。

综上,写一个 hello.proto 作为示例,以便后文说明使用。

// hello.proto
syntax = "proto3";

package hello; // 包名

option go_package="./;hello";

// The Hello service definition.
service Hello {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message
message HelloRequest {
  string name = 1;
}

// The response message
message HelloReply {
  string message = 1;
}

四、Golang gRPC 开发

可参考 grpc 官方文档中 Go Basics tutorial

4.1 将 proto 文件编译为 go 文件

借助第二部分下载的工具,以及第三部分的 proto 文件,可以生成 go 源码文件,具体操作如下:

建议创建一个单独的文件夹,来保存 proto 文件和生成的 go 文件(在 go 项目中)。

操作命令:

# Tips:将--plugin= 后边的 protoc-gen-go 和 protoc-gen-go-rpc 的前缀路径换成自己实际的地址
protoc -I=./ ./*.proto --plugin=/go/bin/protoc-gen-go --go_out=./ --plugin=/go/bin/protoc-gen-go-grpc --go-grpc_out=./

简单说明一下上述命令中的内容:

  • protoc 命令主体
  • -I=./ ./*.proto 前边 -I 是指定 proto 文件所在目录,后边 ./*.proto 是说明要具体编译哪些 proto 文件,这里是指定文件夹内的所有 proto 文件
  • --plugin=xxx --xxx_out=xxx 这两个是一个组合,分别是使用的插件和输出目标存放文件夹。
    在这里,
    protoc-gen-go 对应的是 --go_out
    protoc-gen-go-grpc 对应的是 --go-rpc_out,有所区别需要注意。
  • 两个插件最好直接写绝对路径,这样避免找不到这两个命令。

操作完成后,可以看到当前 proto 文件夹中,每一个 proto 文件生成了两个对应的 go 文件。
即由 xxx.proto 生成了 xxx.pb.goxxx_grpc.pb.go

4.2 gRPC server 端

官方文档给出的 server 端流程如下:

flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
if err != nil {
  log.Fatalf("failed to listen: %v", err)
}
var opts []grpc.ServerOption
...
grpcServer := grpc.NewServer(opts...)
pb.RegisterRouteGuideServer(grpcServer, newServer())
grpcServer.Serve(lis)

流程概况:

  1. 通过 net.Listen(...) 建立一个TCP监听,指定监听地址和端口;
  2. 通过 grpc.NewServer(...) 建立一个 gRPC server;
  3. 通过pb.RegisterxxxServer(...) 注册 gRPC 服务方法的具体实现;
    这里,pb 调用的具体方法,要看通过 protoc 生成的 xxx_grpc.pb.go 文件里的具体方法名。
    同时,第二个参数,是一个结构体,该结构体要包含 xxx_grpc.pb.go 中以 Unimplemented开头的一个空结构体,示例如下:
    // 见 xxx_grpc.pb.go
    // UnimplementedHelloServer must be embedded to have forward compatible implementations.
    type UnimplementedHelloServer struct {
    }
    
  4. 通过 Server(...) 绑定 net.Listen() 建立的listen,并阻塞监听。

所以这里的业务处理核心就是 pb.RegisterxxxServer(...) 的第二个参数,应该传入一个结构体 s 对象,该结构体应该包含 xxx_grpc.pb.go 中的 UnimplementedXXXServer 这个空结构体。
且该结构体应该实现具体的业务逻辑方法,即在 proto 文件中所定义的 service,会在 xxx_grpc.pb.go 中对应 server 中的 interface 中看到应该实现的方法和限定参数。如下代码所示:

// 节选自 hello_grpc.pb.go 部分代码,展示应该实现的具体方法

// HelloServer is the server API for Hello service.
// All implementations must embed UnimplementedHelloServer
// for forward compatibility
type HelloServer interface {
	SayHello(context.Context, *HelloRequest) (*HelloReply, error)
	mustEmbedUnimplementedHelloServer()
}

以第三部分的 proto 文件为例,定义一个server:

// myserver.go
package main

import (
	"net"

	pb "grpc_demo/proto"

	"golang.org/x/net/context"
	"google.golang.org/grpc"
)

// 该结构体需要包含 hello_grpc.pb.go 中的 Unimplementedxxxx
type helloService struct {
	pb.UnimplementedHelloServer
}

// 定义具体的方法,与业务相关的核心内容
// 限定传输参数见 hello_grpc.pb.go 中定义的 interface
func (s helloService) SayHello(c context.Context, r *pb.HelloRequest) (*pb.HelloReply, error) {
	ret := new(pb.HelloReply)
	ret.Message = "hello, " + r.Name

	return ret, nil
}

// 展示了一个 grpc server 的创建监听、绑定grpc服务的全过程
func myServer() {
	lis, err := net.Listen("tcp", ":3399")
	if err != nil {
		panic(err)
	}

	grpcServer := grpc.NewServer()
	pb.RegisterHelloServer(grpcServer, &helloService{})
	grpcServer.Serve(lis)
}

func main() {
	myServer()
}

4.3 gRPC client 端

client 端首先需要建立一个 grpc 连接,流程如下:

var opts []grpc.DialOption
...
conn, err := grpc.Dial(*serverAddr, opts...)
if err != nil {
  ...
}
defer conn.Close()

其中:

  • serverAddr 即连接地址(IP + port);
  • opts 是用来设置身份验证凭证,可以是TLS、GCE 或 JWT 等;本示例中通过 insecure.NewCredentials() 关闭了安全传输,在服务端无需进行安全验证。
  • 如需配置进阶安全传输,请参考官方文档

获得 grpc 的 conn 连接后,根据 xxx_grpc.pb.go 文件中的 NewxxxClient 方法来获得一个对应服务的客户端连接 client

client 获取后,再构建对应的 request 请求,具体的请求结构体,参照 xxx.pb.go 文件中根据 proto 文件生成的 Request 结构。

最后,通过 client 提供的 grpc 请求方法(参照 xxx_grpc.pb.go ),完成请求调用即可。

同样,以第三部分和 4.2 server 端代码为准,构造 client 端代码:

package main

import (
	"fmt"
	pb "grpc_learn/proto"

	"golang.org/x/net/context"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

func myClient() {
	conn, err := grpc.Dial(":3399", grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		// handle error
		panic(err)
	}
	defer conn.Close()

	client := pb.NewHelloClient(conn)

	req := pb.HelloRequest{
		Name: "world",
	}
	reply, err := client.SayHello(context.Background(), &req)
	if err != nil {
		fmt.Println("client.SayHello error:", err)
		return
	}

	fmt.Printf("get msg from server:[%v] \n", reply)

}

func main() {
	myClient()
}


五、demo

直接给一个从 proto 文件,到编译出 go 文件,到 server 端和 client 端的例子,请保证前述 golang、protoc、go pkg 的安装成功。

前文已经给出了各代码,这里做个总结汇总。

5.1 各文件及编译

文件目录:

./
├── myclient.go
├── myserver.go
└── proto
    ├── hello.pb.go
    ├── hello.proto
    └── hello_grpc.pb.go

hello.proto 文件

// proto/hello.proto

syntax = "proto3";

package hello; // 包名

option go_package="./;hello";

// The Hello service definition.
service Hello {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message
message HelloRequest {
  string name = 1;
}

// The response message
message HelloReply {
  string message = 1;
}

使用以下命令,将 proto 文件编译为 go 源码
(注意protoc-gen-go和protoc-gen-go-grpc的具体路径):

protoc -I=./ ./*.proto --plugin=/go/bin/protoc-gen-go --go_out=./ --plugin=/go/bin/protoc-gen-go-grpc --go-grpc_out=./

服务端:

// myserver.go
package main

import (
	"net"

	pb "grpc_demo/proto"

	"golang.org/x/net/context"
	"google.golang.org/grpc"
)

type helloService struct {
	pb.UnimplementedHelloServer
}

func (s helloService) SayHello(c context.Context, r *pb.HelloRequest) (*pb.HelloReply, error) {
	ret := new(pb.HelloReply)
	ret.Message = "hello, " + r.Name

	return ret, nil
}

func myServer() {
	lis, err := net.Listen("tcp", ":3399")
	if err != nil {
		panic(err)
	}

	grpcServer := grpc.NewServer()
	pb.RegisterHelloServer(grpcServer, &helloService{})
	grpcServer.Serve(lis)
}

func main() {
	myServer()
}

客户端:

// client.go
package main

import (
	"fmt"
	pb "grpc_learn/proto"

	"golang.org/x/net/context"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

func myClient() {
	conn, err := grpc.Dial(":3399", grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		// handle error
		panic(err)
	}
	defer conn.Close()

	client := pb.NewHelloClient(conn)

	req := pb.HelloRequest{
		Name: "world",
	}
	reply, err := client.SayHello(context.Background(), &req)
	if err != nil {
		fmt.Println("client.SayHello error:", err)
		return
	}

	fmt.Printf("get msg from server:[%v] \n", reply)

}

func main() {
	myClient()
}

5.2 结果

一个 shell go run myserver.go 开启服务后,再启动另一个 shell 执行 go run myclient.go

// myclient.go 响应
get msg from server:[message:"hello, world"]

写在最后:
由于时间限制和本人水平不足,本文可能存在错误和不足,欢迎指正。

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值