简介
gRPC 是一种现代化开源的高性能 RPC 框架,使用 HTTP2 作为传输协议,默认采用 Protocol Buffers 数据序列化协议,支持多种开发语言。
gRPC 是一项进程间通信技术,可以用来连接、调用、操作和调试分布式异构应用程序,它是基于定义一个服务,指定一个可以远程调用的带有参数和返回类型的的方法。
服务端负责实现定义好的接口并处理客户端的请求,客户端根据接口描述直接调用需要的服务。客户端和服务端可以分别使用 gRPC 支持的不同语言实现,如下图所示:
主要特性如下:
- 强大的 IDL
gRPC 使用ProtoBuf 来定义服务,ProtoBuf 是由Google开发的一种数据序列化协议(类似于 XML、JSON、hessian ),能够将数据进行序列化,并广泛应用在数据存储、通信协议等方面。
- 多语言支持
gRPC 支持多种语言,并能够基于语言自动生成客户端和服务端功能库。目前已提供了 C 版本 grpc、Java 版本 grpc-java 和 Go 版本 grpc-go ,其它语言的版本正在积极开发中,其中,grpc 支持 C、C++、Node.js、Python、Ruby、Objective-C、PHP 和 C# 等语言,grpc-java 已经支持 Android 开发。
- HTTP2
gRPC 基于 HTTP2 标准设计,所以相对于其它的 RPC 框架,gRPC 带来了更多强大功能,如双向流、头部压缩、多复用请求等。这些功能给移动设备带来重大益处(如节省带宽、降低 TCP 链接次数、节省 CPU 使用和延长电池寿命等)。同时,gRPC 还能够提高了云端服务和 Web 应用的性能,gRPC 既能够在客户端应用,也能够在服务器端应用,从而以透明的方式实现客户端和服务器端的通信和简化通信系统的构建。
更多介绍请查看 官方网站 。
安装
Protocol Buffers v3
(1)下载 Protocol Buffers v3 离线包,进入 官网 或输入如下命令:
wget https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protobuf-all-21.12.zip
(2)解压、编译并安装,依次执行如下的命令:
unzip protobuf-all-21.12.zip
cd protobuf-21.12/
./configure
make
make install
(3)检查是否安装成功,输入如下命令:
protoc --version
若出现以下错误(Protocol Buffers Libraries 的默认安装路径在 /usr/local/lib
目录下,而这里安装了一个新的动态链接库,ldconfig 一般在系统启动时运行,所以现在会找不到这个 lib )因此需要手动执行 ldconfig
命令,让动态链接库为系统所共享。
protoc: error while loading shared libraries: libprotobuf.so.15: cannot open shared object file: No such file or directory
Protocol Buffers 是 Google 推出的一种数据描述语言,支持多语言、多平台,它是一种二进制的格式,更小、更快、更简单、更灵活,目前分别有 v2、v3 的版本,推荐使用 v3 ,使用参考以下官方文档:
gRPC 及插件
(1)安装 gRPC ,输入如下命令:
go get google.golang.org/grpc@latest
或使用源码方式,依次执行以下命令:
cd $GOTPATH/src
mkdir google.golang.org
cd google.golang.org/
git clone https://github.com/grpc/grpc-go
mv grpc-go/ grpc/
(2)安装 protoc 的 Go 插件,输入如下命令:
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
该插件会根据 .proto
文件生成一个后缀为 .pb.go
的文件,包含所有 .proto
文件中定义的类型及其序列化方法。
(3)安装 gRPC 插件 ,输入如下命令:
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
该插件会生成一个后缀为 _grpc.pb.go
的文件,其中包含:一种接口类型(或存根) ,供客户端调用的服务方法以及服务器要实现的接口类型。
(4)更新环境变量以便 protoc 编译器能够找到插件,输入如下命令:
export PATH="$PATH:$(go env GOPATH)/bin"
(5)检查是否安装成功,依次输入如下命令:
protoc-gen-go --version
protoc-gen-go-grpc --version
正确输出版本信息表明安装成功,如果这里提示 protoc-gen-go / protoc-gen-go-grpc 不是可执行的程序,请确保 GOPATH 下的 bin 目录在电脑的环境变量中。
(6)安装 grpc-gateway 插件,依次输入如下命令:
cd $GOPATH/src/google.golang.org
git clone https://github.com/google/go-genproto.git
mv go-genproto/ genproto/
## 将 grpc-gateway 的可执行文件从 $GOPATH 中移动到 $GOBIN
mv $GOPATH/bin/protoc-gen-grpc-gateway $GOBIN/bin
使用方法及示例
gRPC 的使用
gRPC 开发分为以下步骤:
(1) 编写 proto 文件定义服务
gRPC 基于定义服务的思想,指定可以通过参数和返回类型远程调用的方法。默认情况下,gRPC 使用 Protocol Buffers 作为接口定义语言 (IDL) 来描述服务接口和有效负载消息的结构,可以根据需要使用其它的 IDL 代替。
例如,编写 proto
文件定义了一个 HelloService 服务,该文件的具体内容如下:
service HelloService {
rpc SayHello (HelloRequest) returns (HelloResponse);
}
message HelloRequest {
string greeting = 1;
}
message HelloResponse {
string reply = 1;
}
在 gRPC 中可以定义四种类型的服务方法,具体如下所示:
- 普通 RPC ,客户端向服务器发送一个请求,然后得到一个响应,就像普通的函数调用一样。
rpc SayHello(HelloRequest) returns (HelloResponse);
- 服务器流式 RPC ,其中客户端向服务器发送请求,并获得一个流来读取一系列消息;客户端从返回的流中读取,直到没有更多的消息,gRPC 保证在单个 RPC 调用中的消息是有序的。
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);
- 客户端流式 RPC,客户端写入一系列消息并将其发送到服务器,同样使用提供的流。一旦客户端完成了消息的写入,它就等待服务器读取消息并返回响应,gRPC 保证在单个 RPC 调用中对消息进行排序。
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);
- 双向流式 RPC ,双方使用读写流发送一系列消息,这两个流独立运行,因此客户端和服务器可以按照自己喜欢的顺序读写,服务器可以等待接收所有客户端消息后再写响应或可以交替读取消息然后写入消息或其他读写组合,每个流中的消息是有序的。
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse);
(2)生成指定语言的代码
在 proto
文件中的定义好服务之后,gRPC 提供了生成客户端和服务器端代码的 Protocol Buffers 编译器插件。使用这些插件可以根据需要生成 Java、Go、C++、Python 等语言的代码,通常会在客户端调用这些 API,并在服务器端实现相应的 API。
-
在服务器端,服务器实现服务声明的方法,并运行一个 gRPC 服务器来处理客户端发来的调用请求。gRPC 底层会对传入的请求进行解码,执行被调用的服务方法,并对服务响应进行编码。
-
在客户端,客户端有一个称为存根(stub)的本地对象,它实现了与服务相同的方法,客户端可以在本地对象上调用这些方法,将调用的参数包装在适当的 Protocol Buffers 消息类型中—— gRPC 在向服务器发送请求并返回服务器的 Protocol Buffers 响应之后进行处理。
(3)编写业务逻辑代码
gRPC 解决了 RPC 中的服务调用、数据传输以及消息编解码,剩下的工作就是要编写业务逻辑代码。在服务端编写业务代码实现具体的服务方法,在客户端按需调用这些方法。
gRPC 程序示例
(1)编写 proto 文件
Protocol Buffers 是一种与语言无关,平台无关的可扩展机制,用于序列化结构化数据,使用 Protocol Buffers 可以一次定义结构化的数据,然后可以使用特殊生成的源代码轻松地在各种数据流中使用各种语言编写和读取结构化数据,例如定义以下内容的 proto
文件:
syntax = "proto3"; // 版本声明,使用 Protocol Buffers v3 版本
option go_package = "../pb"; // 指定生成的 Go 代码在项目中的导入路径
package pb; // 包名用来生成代码,防止协议消息类型之间发生命名冲突的包名
// 定义 gRPC 服务的接口
service Greeter {
// SayHello 方法(远程方法只能有一个参数并返回一个值)
rpc SayHello (HelloRequest) returns (HelloResponse) {}
}
// 请求消息
message HelloRequest {
// 字段编号(唯一性)
string name = 1;
}
// 响应消息
message HelloResponse {
string reply = 1;
}
(2)编写 gRPC 服务器端程序
在任意目录下创建 server 项目文件并初始化( go mod init server
),再新建一个 proto 文件夹存放上面的 proto
文件( hello.proto
),添加 proto 文件的路径,将 go_package 按如下方式进行修改:
// ...
option go_package = "server/proto";
// ...
此时,项目的目录结构如以下形式:
server
├── go.mod
├── go.sum
└── proto
└── hello.proto
根据 hello.proto
文件在 server 项目下生成 gRPC 源码存根,在 proto 目录下执行以下的命令:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto
生成后的源码文件会保存在 proto 文件夹下,参考以下的目录结构:
server
├── go.mod
├── go.sum
└── proto
├── hello.pb.go
├── hello.proto
└── hello_grpc.pb.go
在服务端需要实现该服务定义并运行 gRPC 服务器来处理客户端的调用,编写服务端程序重载服务基类(实现所生成的服务器骨架的逻辑),创建 gRPC 服务器,注册服务并指定端口监听传入的消息,该程序的具体代码如下:
package main
import (
"context"
"fmt"
"hello_server/pb"
"net"
"google.golang.org/grpc"
)
// hello server
type server struct {
pb.UnimplementedGreeterServer
}
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
return &pb.HelloResponse{Reply: "Hello " + in.Name}, nil
}
func main() {
// 由 gRPC 服务端所绑定的 TCP 监听器监听指定的 8972 端口上创建
lis, err := net.Listen("tcp", ":8972")
if err != nil {
fmt.Printf("failed to listen: %v", err)
return
}
// 调用 gRPC GO API 创建 gRPC 服务器实例
s := grpc.NewServer()
// 在 gRPC 服务端上注册服务
pb.RegisterGreeterServer(s, &server{})
// 启动服务,在指定的端口上开始监听传入的消息
err = s.Serve(lis)
if err != nil {
fmt.Printf("failed to serve: %v", err)
return
}
}
运行 gRPC 服务器,监听来自客户端的请求并返回响应,在 server 项目下编译并执行程序,输入如下命令:
go build
./server
(3)编写 gRPC 客户端程序
在任意目录下创建 client 项目并初始化( go mod init client
),再新建一个 proto
文件夹存放重新创建一个 hello.proto
文件,该文件的内容和上面的一样,添加 proto 文件的路径,将 go_package 按如下方式修改:
// ...
option go_package = "client/proto";
// ...
根据 hello.proto
文件在 client 项目下生成 gRPC 源码存根,在 proto 目录下执行如下命令:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto
此时,项目的目录结构如以下形式:
client
├── go.mod
├── go.sum
└── proto
├── hello.pb.go
├── hello.proto
└── hello_grpc.pb.go
在 client 项目下编写一个调用 server 提供的 SayHello RPC 服务的程序,该程序的具体代码如下:
package main
import (
"context"
"flag"
"log"
"time"
"hello_client/pb"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
// hello_client
const (
defaultName = "cqupthao"
)
var (
addr = flag.String("addr", "127.0.0.1:8972", "the address to connect to")
name = flag.String("name", defaultName, "Name to greet")
)
func main() {
flag.Parse()
// 根据提供的地址创建到服务端的连接,连接到服务端,此处禁用安全传输,没有加密和验证
conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("Did not connect: %v", err)
}
defer conn.Close()
// 传递连接并创建存根文件(包含可调用服务器的所以远程方法)
c := pb.NewGreeterClient(conn)
// 创建 context 以传递给远程调用(context 对象包含一些元数据,该对象会在请求的生命周期一直存在),执行 RPC 调用并打印收到的响应数据
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel() // 关闭连接
// 调用远程方法
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.GetReply())
}
在 client 项目下编译并执行程序,输入如下命令:
go build
./hello_client -name=cqupthao
若 RPC 调用成功,客户端会输出以下结果:
2023/02/11 17:31:52 Greeting: Hello cquptaho
(4)自定义一个 service 服务
- 在 proto 文件里面添加一个 RPC 方法,添加后的文件内容如下:
syntax = "proto3"; // 版本声明,使用 Protocol Buffers v3 版本
option go_package = "../pb"; // 指定生成的 Go 代码在项目中的导入路径
package pb; // 包名用来生成代码,防止协议消息类型之间发生命名冲突的包名
// 定义 gRPC 服务的接口
service Greeter {
// SayHello 方法(远程方法)
rpc SayHello (HelloRequest) returns (HelloResponse) {}
// SayHelloAgain 方法(远程方法)
rpc SayHelloAgain (HelloRequest) returns (HelloResponse) {}
}
// 请求消息
message HelloRequest {
// 字段编号(唯一性)
string name = 1;
}
// 响应消息
message HelloResponse {
string reply = 1;
}
- 根据
hello.proto
文件在 server 和 client 项目下生成 gRPC 源码文件,在 proto 目录下重新执行以下的命令:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto
- 编辑
server/main.go
程序添加一个方法,入参和返回值和 SayHello() 方法完全相同,使用已经定义过的结构体 HelloRequest 和 HelloResponse ,具体的程序代码如下:
func (s *server) SayHelloAgain(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
return &pb.HelloResponse{Reply: "Hello " + in.Name + " again!"}, nil
}
- 编辑
client/main.go
程序添加一个调用 SayHelloAgain() 方法的代码,具体的程序代码如下:
r, err = c.SayHelloAgain(ctx, &pb.HelloRequest{Name: *name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.GetReply())
打开两个终端,分别在 server 和 client 项目下编译并执行程序,输入如下命令:
go run main.go
./client -name=cqupthao
若 RPC 调用成功,客户端会输出以下结果:
2023/02/11 17:41:52 Greeting: Hello cquptaho
2023/02/11 17:41:52 Greeting: Hello cquptaho again!
-
参考链接:gRPC 官网
-
参考书籍:《gRPC与云原生应用开发:以Go和Java为例》([斯里兰卡] 卡山 • 因德拉西里 丹尼什 • 库鲁普 著)