gRPC实战–用Golang编写通过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服务器/客户端传递消息。

我们上面的实现是同步的。我们尚未解决服务器的异步响应和流式处理。