gRPC实战包含一系列文章,包括原创和翻译。最终会形成一个完整的系列,后续会不断完善,增加新的内容:
- gRPC简介:why,what,how?
- gRPC服务健康检查最佳实践
- Kubernetes中使用envoy负载均衡gRPC流量
- 用Golang编写通过gRPC进行通信的服务
- 如何在NodeJS中有效使用gRPC流
=============================================================
What is gRPC: gRPC,顾名思义, Google远程过程调用。这是Google创建的一种远程通信协议,可让不同的服务轻松高效地相互通信。它提供与服务之间的同步和异步通信。要了解有关gRPC的更多信息,请访问 gRPC.io
gRPC最适合内部通信。它使客户调用变得更加简洁,我们无需担心序列化,类型安全以及所有这些事情,因为gRPC为我们做到了这一点。
gPRC使用protobuf,一种类型安全的二进制传输格式,旨在实现有效的网络通信。要了解有关protobuf的更多信息,请访问此链接。
性能基准测试结果表明,如果开发人员需要性能和本地调用体验,则gRPC比http/http2更好。具体测评细节查看该文章。
使用Golang构建微服务
我们选择Golang(也称为Go)作为此服务的编程语言,选择gRPC作为其他服务的通信协议,以与我们的服务进行对话,并使用经过验证的OAuth 2.0协议上的OpenId身份层来保护我们的服务。
创建Message
为此,首先我们需要在gRPC中创建一个简单的实体表示形式,称为message。用gRPC术语表示的消息可以用作从另一个服务到一个服务的消息(使用protobuf语法定义的消息)。您可以想象一只鸽子从一个承载“冬天来了”消息的服务到另一个服务,而该服务正在消费该消息来执行上下文操作。
现在,在上面的示例中,发送鸽子的服务是gRPC客户端,“冬天来了”是我们的消息,而使用该消息的服务是gRPC服务器在侦听该消息。关于消息的好处是它可以来回传送。
message Repository {
int64 id = 1;
string name = 2;
int64 userId = 3;
bool isPrivate = 4;
}
定义 gRPC 服务
现在我们已经创建了一个名为存储库的message以用于通信,下一步是定义gRPC服务。
service RepositoryService {
//For now we'll try to implement "insert" operation.
rpc add (Repository) returns (AddRepositoryResponse);
}
message AddRepositoryResponse {
Repository addedRepository = 1;
Error error = 2;
}
message Error {
string code = 1;
string message = 2;
}
在这里,我们告诉gRPC编译器,以“service”关键字开头的代码段应被视为gRPC服务。带有“rpc”关键字的方法表示它是一个远程过程调用,并且编译器应为客户端和服务器运行时生成适当的存根。
我们还定义了2条消息,告诉鸽子在执行操作后返回成功响应或错误响应。
为Golang服务创建文件夹结构
我假设您已经安装了Go运行时。如果不这样做,请按照其官方文档中的步骤进行操作,网址为 https://golang.org/doc/instal...
我们还将使用dep作为我们项目的依赖管理工具。 Dep是用于管理golang项目中外部依赖关系的成熟解决方案。我们使用dep是因为尚未正式发布Go模块支持。
如果您是Windows用户,则将dep安装的路径放在环境的PATH变量中。这使您更容易使用,而无需指定可执行文件的完整路径即可使用它。
安装Go运行时后,请执行以下步骤。
在 $GOPATH/src 中创建名为 bitbucket-repository-management-service”的目录。
然后在目录中设置标准子包,整个项目架构具体如下:
-
导航到项目的根目录并执行以下命令
- 如果是windows系统, "dep.exe init"
- 如果是linux系统, "dep init"
- 上面的命令将创建一个名为“vendor”的文件夹以及“Gopkg.lock”和“ Gopkg.toml”文件。这两个文件对于管理我们项目的不同依赖关系很重要。
- 我们的下一步是将原型文件放入“internal”文件夹,因为这些文件严格绑定到我们的应用程序。以后,如果我们想使用不同的编程语言将相同的文件用于不同的服务,则将为此创建一个单独的存储库。但是为了简单起见,我们现在将它们放在同一目录中。
- 如下图所示,在“内部”包中创建名为“proto-files”的文件夹。
-
在“proto-files”文件夹中,创建两个子文件夹:
- domain
- service
因此最终项目的程序架构布局将如下所示。
接下来,我们将以下代码粘贴到名为“repository.proto”的文件中。此代码定义了用protobuf语法编写的框架消息,该消息将在grpc客户端和服务器之间交换。
syntax = "proto3";
package domain;
option go_package = "bitbucket-repository-management-service/internal/gRPC/domain";
message Repository {
int64 id = 1;
string name = 2;
int64 userId = 3;
bool isPrivate = 4;
}
之后,我们将下面的代码粘贴到名为“repository-service.proto”的文件中。该代码定义了grpc服务定义。它定义了grpc服务器将支持的操作以及可能的输入和返回类型。
syntax = "proto3";
package service;
option go_package = "bitbucket-repository-management-service/internal/gRPC/service";
import "bitbucket-repository-management-service/internal/proto-files/domain/repository.proto";
//RepositoryService Definition
service RepositoryService {
rpc add (domain.Repository) returns (AddRepositoryResponse);
}
message AddRepositoryResponse {
domain.Repository addedRepository = 1;
Error error = 2;
}
message Error {
string code = 1;
string message = 2;
}
安装gRPC编译器
如果在我们的系统中未安装gRPC编译器,我们将无法生成存根。
要安装协议编译器,
- 导航到此链接
- 选择最新版本的标签,请确保选择一个稳定的版本。
- 下载适合您的操作系统的二进制文件。
- 下载后,将其解压缩到操作系统的path变量正在扫描的位置。
安装Go绑定并生成存根
没有Go绑定,我们的存根就没有用了。 Go绑定提供了辅助结构,接口和函数,可用于注册gRPC服务,封送和解封二进制消息等。
为此,我们首先需要将非常简单的Go代码添加到我们的server.go文件中,因为默认情况下,如果项目中没有go代码,则dep(我们的依赖性管理工具)不会下载任何库。
为了满足dep的要求,我们将一些非常基本的go代码放入cmd/gRPC/main.go文件。
package main
import "fmt"
func main() {
fmt.Println("gRPC In Action!")
}
现在,我们都可以为原型缓冲区安装go绑定了。我们将执行以下命令进行安装。
Linux
dep ensure --add google.golang.org/gRPC/github.com/golang/protobuf/protoc-gen-go
Windows
dep.exe ensure -add google.golang.org/gRPC github.com/golang/protobuf/protoc-gen-go
上面的命令会将go绑定下载到“vendor”文件夹中。
现在该生成存根了。
如果您在Windows上,请执行此命令。
protoc.exe -I $env:GOPATH\src --go_out=$env:GOPATH\src $env:GOPATH\src\bitbucket-repository-management-service\internal\proto-files\domain\repository.proto
protoc.exe -I $env:GOPATH\src --go_out=plugins=gRPC:$env:GOPATH\src $env:GOPATH\src\bitbucket-repository-management-service\internal\proto-files\service\repository-service.proto
如果您在Linux上,请执行此命令。
protoc -I $GOPATH/src --go_out=$GOPATH/src $GOPATH/src/bitbucket-repository-management-service/internal/proto-files/domain/repository.proto
protoc -I $GOPATH/src --go_out=plugins=gRPC:$GOPATH/src $GOPATH/src/bitbucket-repository-management-service/internal/proto-files/service/repository-service.proto
上面的命令将在以下标记的子目录中生成存根。
实现 gRPC Service Stub
接下来,编写我们自己的服务实现,
- 我们将在“internal”目录中创建一个名为“impl”的软件包。
- 我们将创建一个名为RepositoryServiceGrpcImpl的结构,
- 确保我们的结构实现了所有gRPC存根方法。
因此,我们知道我们的gRPC服务有一个称为add的方法。在此过程的早期,我们将其定义写入了原始文件中。
rpc add (domain.Repository) returns (AddRepositoryResponse);
为了实现它的服务契约,我们将首先声明一个负责RepositoryService实现的结构。
package impl
import (
"bitbucket-repository-management-service/internal/gRPC/domain"
"bitbucket-repository-management-service/internal/gRPC/service"
"context"
"log"
)
//RepositoryServiceGrpcImpl is a implementation of RepositoryService Grpc Service.
type RepositoryServiceGrpcImpl struct {
}
//NewRepositoryServiceGrpcImpl returns the pointer to the implementation.
func NewRepositoryServiceGrpcImpl() *RepositoryServiceGrpcImpl {
return &RepositoryServiceGrpcImpl{}
}
//Add function implementation of gRPC Service.
func (serviceImpl *RepositoryServiceGrpcImpl) Add(ctx context.Context, in *domain.Repository) (*service.AddRepositoryResponse, error) {
log.Println("Received request for adding repository with id " + strconv.FormatInt(in.Id, 10))
//Logic to persist to database or storage.
log.Println("Repository persisted to the storage")
return &service.AddRepositoryResponse{
AddedRepository: in,
Error: nil,
}, nil
}
现在是时候编写服务器配置,端口配置和最小的测试客户端了,我们可以执行这些操作来验证整个流程。
让我们先从gRPC服务器开始。
配置 gRPC Server
我们将创建一个RepositoryServiceGrpcImpl的实例。repositoryServiceImpl:= impl.NewRepositoryServiceGrpcImpl()
我们将创建net.Listener:
func getNetListener(port uint) net.Listener {
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
panic(fmt.Sprintf("failed to listen: %v", err))
}
return lis
}
创建gRPC server:
gRPCServer := gRPC.NewServer()
我们将服务实现注册到gRPC服务器。
service.RegisterRepositoryServiceServer(gRPCServer, repositoryServiceImpl)
我们将绑定net.Listener和gRPC服务器,以使其从指定端口进行通信。
// start the server
if err := gRPCServer.Serve(netListener); err != nil {
log.Fatalf("failed to serve: %s", err)
}
如果我们把所有东西都连接起来,我们将得到以下内容:
package main
import (
"bitbucket-repository-management-service/internal/gRPC/impl"
"bitbucket-repository-management-service/internal/gRPC/service"
"fmt"
"log"
"net"
"google.golang.org/gRPC"
)
func main() {
netListener := getNetListener(7000)
gRPCServer := gRPC.NewServer()
repositoryServiceImpl := impl.NewRepositoryServiceGrpcImpl()
service.RegisterRepositoryServiceServer(gRPCServer, repositoryServiceImpl)
// start the server
if err := gRPCServer.Serve(netListener); err != nil {
log.Fatalf("failed to serve: %s", err)
}
}
func getNetListener(port uint) net.Listener {
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
panic(fmt.Sprintf("failed to listen: %v", err))
}
return lis
}
配置gRPC Client
要配置客户端:
我们将创建与gRPC服务器的连接。
serverAddress := "localhost:7000"
conn, e := gRPC.Dial(serverAddress, gRPC.WithInsecure())
我们将把该连接传递给gRPC客户端。
client := service.NewRepositoryServiceClient(conn)
调用gRPC方法:
client.Add(context.Background(), &repositoryModel);
如果我们在这里也连接起来,它将像:
package main
import (
"bitbucket-repository-management-service/internal/gRPC/domain"
"bitbucket-repository-management-service/internal/gRPC/service"
"context"
"fmt"
"google.golang.org/gRPC"
)
func main() {
serverAddress := "localhost:7000"
conn, e := gRPC.Dial(serverAddress, gRPC.WithInsecure())
if e != nil {
panic(e)
}
defer conn.Close()
client := service.NewRepositoryServiceClient(conn)
for i := range [10]int{} {
repositoryModel := domain.Repository{
Id: int64(i),
IsPrivate: true,
Name: string("Grpc-Demo"),
UserId: 1245,
}
if responseMessage, e := client.Add(context.Background(), &repositoryModel); e != nil {
panic(fmt.Sprintf("Was not able to insert Record %v", e))
} else {
fmt.Println("Record Inserted..")
fmt.Println(responseMessage)
fmt.Println("=============================")
}
}
}
测试
要运行gRPC服务器,请从项目的根目录执行以下命令。
go run .\cmd\gRPC\server\main.go
运行客户端:
go run .\cmd\gRPC\client\main.go
您应该在客户端的标准输出流上看到类似的内容。
在服务端应该可以看到如下的内容:
总结
我们创建了一个最小程序,并考虑了gRPC请求响应的最佳实践。一方面,我们的gRPC服务器正在侦听和处理请求,另一方面,客户端正在向服务器发送请求。我们正在使用自定义消息来往/从gRPC服务器/客户端传递消息。
我们上面的实现是同步的。我们尚未解决服务器的异步响应和流式处理。