gRPC 的一些实践

gRPC 是啥

A high performance, open-source universal RPC framework
一款高性能的开源 RPC 框架。

gRPC 的特点:

  • 简单的服务定义:使用 Protocol Buffers(做序列化的工具)来定义服务
  • 跨语言跨平台:可以自动生成不同语言对应的 Client Stubs 和 Server Stubs。如下图所示,服务端可以用 C++ 来实现,但是客户端来说,可以提供 Ruby 的版本和 Java 的版本。
    3415798-9cdb18f9ff8ae780.png
    跨语言跨平台
  • 入手简单,并且可扩展
  • 双向数据流

基本思想

参考:https://grpc.io/docs/guides/

  • 通过一种方式来定义服务 Service 及这个服务下面包含的方法,同时定义这些方法需要的参数类型和返回值类型,这些方法就是远程调用的对象
  • 在服务提供者这里,需要实现上面定义好的接口,运行 gRPC Server 来处理调用请求
  • 在服务调用者这里,通过使用 Client Stub 来调用上面定义好的方法

关于 Protocol Buffers

什么是 Protocol Buffers,请参考:https://developers.google.com/protocol-buffers/docs/overview

Protocol buffers are a flexible, efficient, automated mechanism for serializing structured data – think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages.
Protocol buffers 是一个序列化结构化数据的方法,类似于 XML,但是比 XML 更节省空间,更快,更简单。你首先通过一种语法定义好你想要的数据结构,然后可以编译成不同语言对应的源代码(例如 Java 中的实体类),随后你就可以通过这些源代码来进行数据的读写。

Protocol Buffers 与 XML 的比较:

  • are simpler 更简单
  • are 3 to 10 times smaller 更节省空间
  • are 20 to 100 times faster 更快
  • are less ambiguous
  • generate data access classes that are easier to use programmatically

gRPC 与 Protocol Buffers

gRPC 默认使用 Protocol Buffers 来:

  • 作为接口定义语言(Interface Definition Language)来定义服务 Service
  • 定义传输过程中的数据结构

我们可以创建一个 .proto 后缀名的文件来表示一个结构化数据,例如:

message Person {
  string name = 1;
  int32 id = 2;
  bool has_ponycopter = 3;
}

随后可以将这个 .proto 文件编译成不同语言对应的源代码,例如编译成 Java 中的 Person.class 类,这个类里提供了 get set 方法。

gRPC 的基本概念

在 gRPC 中可以定义四种类型的服务方法:

  • 一元 RPC(Unary RPCs),客户端发送简单请求,得到简单响应,类似于一个函数调用:
rpc SayHello(HelloRequest) returns (HelloResponse){
}
  • 服务端流式 RPC(Server streaming RPCs),客户端发送请求,得到一个流 Stream,然后从 Stream 读取内容:
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){
}
  • 客户端流式 RPC(Client streaming RPCs),客户端通过流 Stream 来写入内容,然后发送给服务端,最后得到响应:
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {
}
  • 双向流式 RPC(Bidirectional streaming RPCs),上面两种方式的结合体:
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){
}

关于同步调用 Vs 异步调用

  • 同步 RPC 会阻塞客户端的当前线程,直到获得了响应
  • 异步 RPC 不会阻塞客户端的当前线程

关于 gRPC 的超时 Timeout

  • 客户端可以定超时 Timeout。
  • 服务端可以知道某一个 RPC Call 是否已超时。

关于 gRPC 调用的中断 termination
服务端和客户端都可以随时中断调用。

关于 gRPC 调用的取消 cancel
服务端和客户端都可以随时取消调用。

Java gRPC 快速入门

参考:https://grpc.io/docs/quickstart/java.html
gRPC Java API 文档:https://grpc.io/grpc-java/javadoc/

// 从 Github 上获取示例代码
git clone -b v1.18.0 https://github.com/grpc/grpc-java

// 进入示例代码目录
cd grpc-java/examples

// 编译服务端和客户端
./gradlew installDist

// 启动服务端
./build/install/examples/bin/hello-world-server

// 启动客户端
./build/install/examples/bin/hello-world-client
3415798-6c49497ffd480717.png
启动服务端
3415798-d1279f1bc105fea0.png
启动客户端

我们看看示例中的几个重要文件。
首先是 /examples/src/main/proto/helloworld.proto,在这里我们通过 protocol buffers 来定义了 gRPC 所提供的服务,也就是 Services。可以看出,服务名称为 Greeter,它提供了一个 RPC,名称为 SayHello,其中请求输入为 HelloRequest,包含一个字符串 name,请求响应为 HelloReply,包含一个字符串 message

syntax = "proto3";

option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW";

package helloworld;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

随后我们通过 protoc 编译器来进行编译,这里使用的是 proto3 版本。protoc 编译器可以作为插件集成到主流的 Java 构建工具中,例如 Gradle 和 Maven。

这个文件在编译过后,会产生对应的字节码文件,也就是产生了对应的类,位置在 /examples/build/classes/java/main/io/grpc/examples/helloworld

  • java_outer_classname = "HelloWorldProto" 编译后会产生 HelloWorldProto.class

  • message HelloRequest 编译后会产生 HelloRequest.class 及对应的几个内部类,代表了 gRPC 请求的实体。可以通过它们来构造请求并获取请求中的内容。

    3415798-45b63fcdb0430ba8.png
    HelloRequest.class 及对应的几个内部类

  • message HelloReply 编译后会产生 HelloReply.class 及对应的几个内部类,代表了 gRPC 响应的实体。可以通过它们来构造响应并获取响应中的内容。

    3415798-27ace71db2742f55.png
    `HelloReply.class 及对应的几个内部类

  • service Greeter 编译后会产生 GreeterRrpc.class 及对应的几个内部类,代表了服务的实体

    • 注意:这个 GreeterRrpc 类及对应的几个内部类在下面都会用到,作为 Client Stubs 和 Server Stubs
    • GreeterGrpc.GreeterImplBase 里面有 sayHello() 方法,可以作为 Server Stubs
    • GreeterGrpc.GreeterBlockingStub 里面有 sayHello() 方法,可以作为 Client Stubs
      3415798-71151cb98a52986e.png
      GreeterRrpc.class 及对应的几个内部类

下面来看 RPC 服务是如何被提供的,文件位置 /examples/src/main/java/io/grpc/examples/helloworld/HelloWorldServer.java。可以看出:

  • 服务端通过重载 GreeterGrpc.GreeterImplBase 里的 sayHello() 方法来提供服务的具体实现
  • 通过 ServerBuilder 来启动 gRPC server 来处理请求
  • gRPC 框架负责帮我们解码请求(将请求内容转换为 HelloRequest 类),执行服务方法,编码响应(将 HelloReply 类转换成 Protocol Buffers 对应的格式)
package io.grpc.examples.helloworld;

import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.stub.StreamObserver;
import java.io.IOException;
import java.util.logging.Logger;

/**
 * Server that manages startup/shutdown of a {@code Greeter} server.
 */
public class HelloWorldServer {
  private static final Logger logger = Logger.getLogger(HelloWorldServer.class.getName());

  private Server server;

  private void start() throws IOException {
    /* The port on which the server should run */
    int port = 50051;
    server = ServerBuilder.forPort(port)
        .addService(new GreeterImpl())
        .build()
        .start();
    logger.info("Server started, listening on " + port);
    Runtime.getRuntime().addShutdownHook(new Thread() {
      @Override
      public void run() {
        // Use stderr here since the logger may have been reset by its JVM shutdown hook.
        System.err.println("*** shutting down gRPC server since JVM is shutting down");
        HelloWorldServer.this.stop();
        System.err.println("*** server shut down");
      }
    });
  }

  private void stop() {
    if (server != null) {
      server.shutdown();
    }
  }

  /**
   * Await termination on the main thread since the grpc library uses daemon threads.
   */
  private void blockUntilShutdown() throws InterruptedException {
    if (server != null) {
      server.awaitTermination();
    }
  }

  /**
   * Main launches the server from the command line.
   */
  public static void main(String[] args) throws IOException, InterruptedException {
    final HelloWorldServer server = new HelloWorldServer();
    server.start();
    server.blockUntilShutdown();
  }

  static class GreeterImpl extends GreeterGrpc.GreeterImplBase {

    @Override
    public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
      HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + req.getName()).build();
      responseObserver.onNext(reply);
      responseObserver.onCompleted();
    }
  }
}

下面来看 RPC 服务是如何被调用的,文件位置 /examples/src/main/java/io/grpc/examples/helloworld/HelloWorldClient.java。可以看出:

  • 通过指定主机和端口来创建 gRPC channel 管道,代表对 gRPC server 的连接
  • 客户端通过 GreeterGrpc.newBlockingStub(channel) 来得到客户端调用的 Stub,然后再调用具体的方法
  • gRPC 框架负责帮我们编码请求,发送请求,得到响应,解码相应
package io.grpc.examples.helloworld;

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.StatusRuntimeException;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * A simple client that requests a greeting from the {@link HelloWorldServer}.
 */
public class HelloWorldClient {
  private static final Logger logger = Logger.getLogger(HelloWorldClient.class.getName());

  private final ManagedChannel channel;
  private final GreeterGrpc.GreeterBlockingStub blockingStub;

  /** Construct client connecting to HelloWorld server at {@code host:port}. */
  public HelloWorldClient(String host, int port) {
    this(ManagedChannelBuilder.forAddress(host, port)
        // Channels are secure by default (via SSL/TLS). For the example we disable TLS to avoid
        // needing certificates.
        .usePlaintext()
        .build());
  }

  /** Construct client for accessing HelloWorld server using the existing channel. */
  HelloWorldClient(ManagedChannel channel) {
    this.channel = channel;
    blockingStub = GreeterGrpc.newBlockingStub(channel);
  }

  public void shutdown() throws InterruptedException {
    channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
  }

  /** Say hello to server. */
  public void greet(String name) {
    logger.info("Will try to greet " + name + " ...");
    HelloRequest request = HelloRequest.newBuilder().setName(name).build();
    HelloReply response;
    try {
      response = blockingStub.sayHello(request);
    } catch (StatusRuntimeException e) {
      logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
      return;
    }
    logger.info("Greeting: " + response.getMessage());
  }

  /**
   * Greet server. If provided, the first element of {@code args} is the name to use in the
   * greeting.
   */
  public static void main(String[] args) throws Exception {
    HelloWorldClient client = new HelloWorldClient("localhost", 50051);
    try {
      /* Access a service running on the local machine on port 50051 */
      String user = "world";
      if (args.length > 0) {
        user = args[0]; /* Use the arg as the name to greet if provided */
      }
      client.greet(user);
    } finally {
      client.shutdown();
    }
  }
}

猜你喜欢

转载自blog.csdn.net/weixin_33885253/article/details/86986708