在生成服务器端骨架时,将得到建立 gRPC 连接、相关消息类型和接口的基础代码。实现服务的任务就是实现代码生成阶段所得到的接口。下面首先从实现 Go 服务开始,然后再看一下如何使用 Java 语言实现相同的服务。

01. 使用 Go 语言实现 gRPC 服务

实现 Go 服务分为 3 步:首先,生成服务定义的存根文件;其次,实现该服务中远程方法的业务逻辑;最后,创建服务器,监听特定的端口并注册该服务,从而接受来自客户端的请求。我们从创建一个新的 Go 模块开始,这里会创建一个新的模块以及该模块中的子目录。productinfo/service 模块用来存放服务代码,子目录(ecommerce)用来保存自动生成的存根文件。然后,在productinfo 目录下创建名为 service 的子目录。进入 service 子目录并执行如下命令来创建 productinfo/service 模块:

go mod init productinfo/service

创建完模块并在模块内创建完子目录之后,将得到如下所示的模块结构:

└─ productinfo
    └─ service
        ├── go.mod
            ├ . . .
            └── ecommerce
                └── . . .

我们还需要更新 go.mod 文件的依赖项,具体的版本如下所示。

module productinfo/service
require (
    github.com/gofrs/uuid v3.2.0
    github.com/golang/protobuf v1.3.2
    github.com/google/uuid v1.1.1
    google.golang.org/grpc v1.24.0
)

Go 1.11 引入了名为模块(module)的新概念,可以让开发人员在 GOPATH 之外构建和运行 Go 项目。要创建 Go 模块,需要在 $GOPATH/src 之外的任意地方创建新目录,然后进入该目录,使用模块名来执行如下命令,从而初始化模块:

go mod int <module_name>

当模块完成初始化之后,模块的根目录会创建 go.mod 文件。接下来就可以在模块中创建 Go 源代码文件并进行构建。Go 语言将使用 go.mod 文件中所列的特定依赖模块的版本来解析导入项。

生成客户端存根或服务器端骨架。现在,使用 protocol buffers 编译器来手动生成客户端存根或服务器端骨架。为了实现这一点,需要满足下列先决条件。

  • 从 GitHub 的发布页面下载并安装最新的 protocol buffers 编译器(版本 3)。

    在下载编译器时,选择适合所用平台的编译器。假设正在使用 64 位 Linux 机器,同时需要获得版本为 x.x.x 的protocol buffers 编译器,就需要下载 protoc-x.x.x-linuxx86_64.zip 文件。

  • 使用如下命令安装 gRPC 库。

    go get -u google.golang.org/grpc
  • 使用如下命令安装 Go 语言的 protoc 插件。

    go get -u github.com/golang/protobuf/protoc-gen-go

当满足这些先决条件之后,就可以通过执行如下所示的 protoc 命令为服务定义生成代码了:

protoc -I ecommerce \ ➊
ecommerce/product_info.proto \ ➋
--go_out=plugins=grpc:<module_dir_path>/ecommerce ➌

❶ 指定源 proto 文件和依赖的 proto 文件的目录路径(通过 –proto_path 或 -I 命令行标记来指定)。如果不指定该值,则将使用当前目录作为源目录。在这个目录下,需要根据包名来存放依赖的 proto 文件。
❷ 指定希望编译的 proto 文件路径。编译器将阅读该文件并生成输出的 Go 文件。
❸ 指定生成的代码要存放的目标目录。

当执行该命令时,在模块的给定子目录下(ecommerce)会生成一个存根文件(product_info.pb.go)。获得这个存根文件后,需要使用生成的代码来实现业务逻辑。

实现业务逻辑。首先需要在 Go 模块(productinfo/service)中创建名为 productinfo_service.go 的 Go 文件,然后实现如代码清单 2-6 所示的远程方法。

代码清单 2-6 使用 Go 语言编写的 ProductInfo 服务的gRPC 服务实现

package main
    import (
    "context"
    "errors"
    "log"
    "github.com/gofrs/uuid"
    pb "productinfo/service/ecommerce")
// 用来实现ecommerce/product_info的服务器
type server struct{ ➋
    productMap map[string]*pb.Product
}
// 实现ecommerce.AddProduct的AddProduct方法
func (s *server) AddProduct(ctx context.Context,in *pb.Product) (*pb.ProductID, error) { ➌➎➏
    out, err := uuid.NewV4()
    if err != nil {
        return nil, status.Errorf(codes.Internal, "Error while generating Product ID", err)
    }
    in.Id = out.String()
    if s.productMap == nil {
        s.productMap = make(map[string]*pb.Product)
    }
    s.productMap[in.Id] = in
    return &pb.ProductID{Value: in.Id}, status.New(codes.OK, "").Err()
}
// 实现ecommerce.GetProduct的GetProduct方法
func (s *server) GetProduct(ctx context.Context, in *pb.ProductID) (*pb.Product, error) { ➍➎➏
    value, exists := s.productMap[in.Value]
    if exists {
        return value, status.New(codes.OK, "").Err()
    }
    return nil, status.Errorf(codes.NotFound, "Product does not exist.", in.Value)
}

❶ 导入刚刚通过 protobuf 编译器所生成的代码所在的包。
❷ server 结构体是对服务器的抽象。可以通过它将服务方法附加到服务器上。
❸ AddProduct 方法以 Product 作为参数并返回一个ProductID。Product 和 ProductID 结构体定义在product_info.pb.go 文件中,该文件是通过 product_info.proto 定义自动生成的。
❹ GetProduct 方法以 ProductID 作为参数并返回 Product。
❺ 这两个方法都有一个 Context 参数。Context 对象包含一些元数据,比如终端用户授权令牌的标识和请求的截止时间。这些元数据会在请求的生命周期内一直存在。
❻ 这两个方法都会返回一个错误以及远程方法的返回值(方法有多种返回类型)。这些错误会传播给消费者,用来进行消费者端的错误处理。

这样就实现了 ProductInfo 服务的业务逻辑。接下来可以创建简单的服务器,来托管该服务并接受来自客户端的请求。

创建 Go 服务器。要用 Go 语言创建服务器,需要在相同的 Go 包(productinfo/service)中创建名为 main.go 的新 Go 文件,并实现如代码清单 2-7 所示的 main 方法。

代码清单 2-7 使用 Go 语言编写的托管 ProductInfo 服务的gRPC 服务器实现

package main
import (
    "log"
    "net"
    pb "productinfo/service/ecommerce""google.golang.org/grpc"
)
const (
    port = ":50051"
)
func main() {
    lis, err := net.Listen("tcp", port) ➋
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer() ➌
    pb.RegisterProductInfoServer(s, &server{}) ➍
    log.Printf("Starting gRPC listener on port " + port)
    if err := s.Serve(lis); err != nil { ➎
        log.Fatalf("failed to serve: %v", err)
    }
}

❶ 导入通过 protobuf 编译器所生成的代码所在的包。
❷ 希望由 gRPC 服务器所绑定的 TCP 监听器在给定的端口(50051)上创建。
❸ 通过调用 gRPC Go API 创建新的 gRPC 服务器实例。
❹ 通过调用生成的 API,将之前生成的服务注册到新创建的 gRPC服务器上。
❺ 在指定端口(50051)上开始监听传入的消息。

现在,我们已通过 Go 语言为业务场景构建了 gRPC 服务。同时,我们创建了简单的服务器,该服务器将暴露服务方法,并接收来自gRPC 客户端的消息。

如果你更喜欢使用 Java 语言,那么也可以使用该语言来构建相同的服务。该实现过程与 Go 语言的非常类似,接下来将使用 Java 语言再次构建服务。如果你对如何使用 Go 语言构建客户端更感兴趣,那么可以直接阅读 2.2.2 节。

02. 使用 Java 语言实现 gRPC 服务

在创建 Java gRPC 项目时,最佳的实现方式是使用现有的构建工具,如 Gradle、Maven 或 Bazel,它们能够管理所有的依赖项和代码生成功能。在示例中,我们使用 Gradle 来管理项目,同时讨论如何使用 Gradle 来创建 Java 项目,以及如何实现服务中所有远程方法的业务逻辑。最后,创建服务器并注册服务,从而接受来自客户端的请求。

Gradle 是一个自动化构建工具,它支持多种语言,包括Java、Scala、Android、C、C++ 和 Groovy,并且与 Eclipse 和IntelliJ IDEA 等开发工具紧密集成。可以按照其官网页面给出的步骤在自己的机器上安装 Gradle。

搭建 Java 项目。首先来创建一个 Gradle Java 项目(productinfo-service)。在创建完项目之后,会得到下面这样的项目结构:

product-info-service
├── build.gradle
├ . . .
└── src
    ├── main
    │ ├── java
    │ └── resources
    └── test
        ├── java
        └── resources

在 src/main 目录下,创建 proto 目录,并将 ProductInfo 服务定义文件(.proto 文件)放到 proto 目录下。

然后,需要更新 build.gradle 文件,为 Gradle 添加依赖项和protobuf 插件。更新后的 build.gradle 文件内容如代码清单 2-8 所示。

代码清单 2-8 适用于 gRPC Java 项目的 Gradle 配置

apply plugin: 'java'
apply plugin: 'com.google.protobuf'
repositories {
    mavenCentral()
}
def grpcVersion = '1.24.1' ➊
dependencies { ➋
    compile "io.grpc:grpc-netty:${grpcVersion}"
    compile "io.grpc:grpc-protobuf:${grpcVersion}"
    compile "io.grpc:grpc-stub:${grpcVersion}"
    compile 'com.google.protobuf:protobuf-java:3.9.2'
}
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies { ➌
        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10'
    }
}
protobuf { ➍
    protoc {
        artifact = 'com.google.protobuf:protoc:3.9.2'
    }
    plugins {
        grpc {
            artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}"
        }
    }
    generateProtoTasks {
        all()*.plugins {
            grpc {}
        }
    }
}
sourceSets { ➎
    main {
        java {
            srcDirs 'build/generated/source/proto/main/grpc'
            srcDirs 'build/generated/source/proto/main/java'
        }
    }
}
jar { ➏
    manifest {
        attributes "Main-Class": "ecommerce.ProductInfoServer"
    }
    from {
        configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
    }
}
apply plugin: 'application'
startScripts.enabled = false

❶ Gradle 项目所使用的 gRPC Java 库的版本。
❷ 该项目所使用的外部依赖项。
❸ 该项目所使用的 Gradle protobuf 插件的版本。如果 Gradle 版本低于 2.12,那么需要使用 0.7.5 版本的插件。
❹ 在 protobuf 插件中,需要指定 protobuf 编译器的版本和 protobufJava 可执行包的版本。
❺ 这会告知 IntelliJ IDEA、Eclipse 或 NetBeans 等集成开发环境有关生成代码的信息。
❻ 配置运行应用程序所使用的主类。然后,运行下面的命令来构建库并根据 protobuf 构建插件来生成存根代码:

$ ./gradle build

现在 Java 项目已包含生成的代码。接下来实现服务接口并为远程方法添加业务逻辑。

实现业务逻辑。首先,在 src/main/java 源代码目录下创建 Java 包(ecommerce),并在包中创建 Java 类(ProductInfoImpl.java)。然后,实现如代码清单 2-9 所示的远程方法。

代码清单 2-9 使用 Java 语言编写的 ProductInfo 服务的gRPC 服务实现

package ecommerce;
import io.grpc.Status;
import io.grpc.StatusException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class ProductInfoImpl extends ProductInfoGrpc.ProductInfoImplBase { ➊
    private Map productMap = new HashMap<String, ProductInfoOuterClass.Product>();
    @Override
    public void addProduct(ProductInfoOuterClass.Product request,io.grpc.stub.StreamObserver <ProductInfoOuterClass.ProductID> responseObserver ) { ➋➍
        UUID uuid = UUID.randomUUID();
        String randomUUIDString = uuid.toString();
        productMap.put(randomUUIDString, request);
        ProductInfoOuterClass.ProductID id = ProductInfoOuterClass.ProductID.newBuilder()
            .setValue(randomUUIDString).build();
        responseObserver.onNext(id); ➎
        responseObserver.onCompleted(); ➏
    }
    @Override
    public void getProduct(ProductInfoOuterClass.ProductID request,io.grpc.stub.StreamObserver<ProductInfoOuterClass.Product> responseObserver ) { ➌➍
        String id = request.getValue();
        if (productMap.containsKey(id)) {
            responseObserver.onNext((ProductInfoOuterClass.Product) productMap.get(id)); ➎
            responseObserver.onCompleted(); ➏
        } else {
            responseObserver.onError(new StatusException(Status.NOT_FOUND)); ➐
        }
    }
}

❶ 扩展插件生成的抽象类
(ProductInfoGrpc.ProductInfoImplBase)。这样一来,就能够为服务定义文件中所定义的 addProduct 方法和 getProduct方法添加业务逻辑了。
❷ addProduct 方法接受 Product 类
(ProductInfoOuterClass.Product)作为参数。Product 类从服务定义中生成,并在 ProductInfoOuterClass 类中进行定义。
❸ getProduct 方法接受 ProductID 类
(ProductInfoOuterClass.ProductID)作为参数。ProductID类从服务定义中生成,并在 ProductInfoOuterClass 类中进行定义。
❹ responseObserver 对象用来发送响应给客户端并关闭流。
❺ 发送响应给客户端。
❻ 通过关闭流终结客户端调用。
❼ 发送错误给客户端。

至此,我们就使用 Java 实现了 ProductInfo 服务的业务逻辑。接下来创建一台简单的服务器,使用它来托管服务并接受来自客户端的请求。

创建 Java 服务器。为了将服务暴露出去,我们需要创建一个gRPC 服务器实例,并将 ProductInfo 服务注册到该服务器上。该服务器将监听指定端口,并将所有的请求分派给相关的服务。这里需要在包中创建一个主类(ProductInfoServer.java),如代码清单 2-10 所示。

代码清单 2-10 使用 Java 语言编写的托管 ProductInfo 服务的 gRPC 服务器实现

package ecommerce;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import java.io.IOException;
public class ProductInfoServer {
    public static void main(String[] args) throws IOException, InterruptedException {
        int port = 50051;
        Server server = ServerBuilder.forPort(port) ➊
        .addService(new ProductInfoImpl())
        .build()
        .start();
        System.out.println("Server started, listening on " + port);
        Runtime.getRuntime().addShutdownHook(new Thread(() -> { ➋
            System.err.println("Shutting down gRPC server since JVM is shutting down");
            if (server != null) {
                server.shutdown();
            }
            System.err.println("Server shut down");
        }));
        server.awaitTermination(); ➌
    }
}

❶ 服务器实例在端口 50051 创建。我们希望服务器绑定到该端口,并监听传入的消息。另外,在该服务器上添加了ProductInfo 服务实现。
❷ 添加运行时的关闭 hook,这会在 JVM 关闭时关闭 gRPC 服务器。
❸ 在方法的最后,服务器的线程会一直保持,直到服务器终止。

现在,我们已经用两种语言实现了 gRPC 服务,接下来可以开始实现 gRPC 客户端了。

文档更新时间: 2023-09-02 04:17   作者:Minho