gRPC提供了四种提供服务的模式,它们分别是:
① 简单模式(Unary RPCs);
② 客户端流模式(Client streaming RPCs);
③ 服务端流模式(Server streaming RPCs);
④ 双向流模式(Bidirectional streaming RPCs )

  • 简单模式:客户端发出单个请求,服务端返回单个响应。
  • 客户端流模式:客户端将连续的数据流发送到服务端,服务端返回一个响应;用在客户端发送多次请求到服务端情况,如分段上传图片场景等。
  • 服务端流模式:客户端发起一个请求到服务端,服务端返回连续的数据流;一般用在服务端分批返回数据的情况,客户端能持续接收服务端的数据。
  • 双向流模式:双向流就是服务端流和客户端流的整合,请求和返回都可以通过流的方式交互。

接下来,我们将通过官网的一个例子来学习一下这四种模式。

还是用到gRPC-Java(一):构建一个使用Java语言的gRPC工程中已经创建好的项目,完整项目链接附在文末。

##一、编写.proto文件并生成代码

这里面涉及到一些protocol-buffers的语法,可以暂时不用深究,不影响理解大局。

下面的router_guide.proto文件中,使用service关键字定义了一个名为RouteGuide的服务,这个RouteGuide服务中又提供了四个使用rpc关键字定义的RPC方法,分别是:

简单模式:GetFeature;
服务端流模式:ListFeatures;
客户端流模式:RecordRoute;
双向流模式:RouteChat。

区别这四种模式的方式就是,在流模式的rpc方法参数前面加stream。举个例子:
服务端流模式ListFeatures,是这么定义的:
rpc ListFeatures(Rectangle) returns (stream Feature) {} 响应体Feature前面加了stream。

客户端流模式RecordRoute,是这么定义的:
rpc RecordRoute(stream Point) returns (RouteSummary) {} 请求体Point前面加了stream。

双向流模式RouteChat,则是在请求和响应体之前都加stream:
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}

OK,了解了proto文件里面的基本内容之后,我们继续。本文中实例程序所需要的完整的route_guide.proto文件如下:

syntax = "proto3";

option java_multiple_files = true;
option java_package = "com.zhb.grpc.examples.routeguide";
option java_outer_classname = "RouteGuideProto";
option objc_class_prefix = "RTG";

package routeguide;

// service关键字描述一个RPC Server,里面的rpc关键字描述一个RPC方法
service RouteGuide {
  // A simple RPC.
  //
  // Obtains the feature at a given position.
  //
  // A feature with an empty name is returned if there's no feature at the given
  // position.
  rpc GetFeature(Point) returns (Feature) {}

  // A server-to-client streaming RPC.
  //
  // Obtains the Features available within the given Rectangle.  Results are
  // streamed rather than returned at once (e.g. in a response message with a
  // repeated field), as the rectangle may cover a large area and contain a
  // huge number of features.
  rpc ListFeatures(Rectangle) returns (stream Feature) {}

  // A client-to-server streaming RPC.
  //
  // Accepts a stream of Points on a route being traversed, returning a
  // RouteSummary when traversal is completed.
  rpc RecordRoute(stream Point) returns (RouteSummary) {}

  // A Bidirectional streaming RPC.
  //
  // Accepts a stream of RouteNotes sent while a route is being traversed,
  // while receiving other RouteNotes (e.g. from other users).
  rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
}

// Points are represented as latitude-longitude pairs in the E7 representation
// (degrees multiplied by 10**7 and rounded to the nearest integer).
// Latitudes should be in the range +/- 90 degrees and longitude should be in
// the range +/- 180 degrees (inclusive).
message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

// A latitude-longitude rectangle, represented as two diagonally opposite
// points "lo" and "hi".
message Rectangle {
  // One corner of the rectangle.
  Point lo = 1;

  // The other corner of the rectangle.
  Point hi = 2;
}

// A feature names something at a given point.
//
// If a feature could not be named, the name is empty.
message Feature {
  // The name of the feature.
  string name = 1;

  // The point where the feature is detected.
  Point location = 2;
}

// Not used in the RPC.  Instead, this is here for the form serialized to disk.
message FeatureDatabase {
  repeated Feature feature = 1;
}

// A RouteNote is a message sent while at a given point.
message RouteNote {
  // The location from which the message is sent.
  Point location = 1;

  // The message to be sent.
  string message = 2;
}

// A RouteSummary is received in response to a RecordRoute rpc.
//
// It contains the number of individual points received, the number of
// detected features, and the total distance covered as the cumulative sum of
// the distance between each point.
message RouteSummary {
  // The number of points received.
  int32 point_count = 1;

  // The number of known features passed while traversing the route.
  int32 feature_count = 2;

  // The distance covered in metres.
  int32 distance = 3;

  // The duration of the traversal in seconds.
  int32 elapsed_time = 4;
}

然后按照上一篇文章中的那样,使用protobuf-maven-plugin根据proto文件生成Java代码。执行如下两条命令:

mvn protobuf:compile
mvn protobuf:compile-custom

生成如下图两个包中的类。

grpc双向流式 grpc双向流原理_grpc双向流式

二、编写RPC Server端

在第一步生成代码的基础上编写一个RPC Server端,并让Server端运行起来,大致可分为如下步骤:

① 重写生成的xxxGrpc.xxxImplBase类的rpc方法,做真正的业务逻辑。
② 创建一个RPC Server,并监听指定的端口。稍微具体点来说就是,使用ServerBuilder.forPort(port)得到一个ServerBuilder对象,然后在此ServerBuilder对象上调用addService方法,方法的参数为①中的类。再调用build方法,建造出一个真正的包含了监听端口信息、提供的服务信息的RPC Server端。
③ 调用②中server的start方法,启动RPC Server。

那我们接下来就一步一步按照这个流程编写一个RPC Server吧:

第一步,重写自动生成的xxxImplBase类里面的rpc方法。如下,我们重写了在proto文件中定义的4个rpc方法。关于这4种rpc的参数类型和返回值类型我们后面再讲。

private static class RouteGuideService extends RouteGuideGrpc.RouteGuideImplBase {
    private final Collection<Feature> features;
    private final ConcurrentMap<Point, List<RouteNote>> routeNotes =
        new ConcurrentHashMap<Point, List<RouteNote>>();

    RouteGuideService(Collection<Feature> features) {
      this.features = features;
    }

    /**
     * Gets the {@link Feature} at the requested {@link Point}. If no feature at that location
     * exists, an unnamed feature is returned at the provided location.
     *
     * @param request          the requested location for the feature.
     * @param responseObserver the observer that will receive the feature at the requested point.
     */
    @Override
    public void getFeature(Point request, StreamObserver<Feature> responseObserver) {
      responseObserver.onNext(checkFeature(request));
      responseObserver.onCompleted();
    }

    /**
     * Gets all features contained within the given bounding {@link Rectangle}.
     *
     * @param request          the bounding rectangle for the requested features.
     * @param responseObserver the observer that will receive the features.
     */
    @Override
    public void listFeatures(Rectangle request, StreamObserver<Feature> responseObserver) {
      int left = min(request.getLo().getLongitude(), request.getHi().getLongitude());
      int right = max(request.getLo().getLongitude(), request.getHi().getLongitude());
      int top = max(request.getLo().getLatitude(), request.getHi().getLatitude());
      int bottom = min(request.getLo().getLatitude(), request.getHi().getLatitude());

      for (Feature feature : features) {
        if (!RouteGuideUtil.exists(feature)) {
          continue;
        }

        int lat = feature.getLocation().getLatitude();
        int lon = feature.getLocation().getLongitude();
        if (lon >= left && lon <= right && lat >= bottom && lat <= top) {
          responseObserver.onNext(feature);
        }
      }
      responseObserver.onCompleted();
    }

    /**
     * Gets a stream of points, and responds with statistics about the "trip": number of points,
     * number of known features visited, total distance traveled, and total time spent.
     *
     * @param responseObserver an observer to receive the response summary.
     * @return an observer to receive the requested route points.
     */
    @Override
    public StreamObserver<Point> recordRoute(final StreamObserver<RouteSummary> responseObserver) {
      return new StreamObserver<Point>() {
        int pointCount;
        int featureCount;
        int distance;
        Point previous;
        final long startTime = System.nanoTime();

        @Override
        public void onNext(Point point) {
          pointCount++;
          if (RouteGuideUtil.exists(checkFeature(point))) {
            featureCount++;
          }
          // For each point after the first, add the incremental distance from the previous point to
          // the total distance value.
          if (previous != null) {
            distance += calcDistance(previous, point);
          }
          previous = point;
        }

        @Override
        public void onError(Throwable t) {
          logger.log(Level.WARNING, "recordRoute cancelled");
        }

        @Override
        public void onCompleted() {
          long seconds = NANOSECONDS.toSeconds(System.nanoTime() - startTime);
          responseObserver.onNext(RouteSummary.newBuilder().setPointCount(pointCount)
                                      .setFeatureCount(featureCount).setDistance(distance)
                                      .setElapsedTime((int) seconds).build());
          responseObserver.onCompleted();
        }
      };
    }

    /**
     * Receives a stream of message/location pairs, and responds with a stream of all previous
     * messages at each of those locations.
     *
     * @param responseObserver an observer to receive the stream of previous messages.
     * @return an observer to handle requested message/location pairs.
     */
    @Override
    public StreamObserver<RouteNote> routeChat(final StreamObserver<RouteNote> responseObserver) {
      return new StreamObserver<RouteNote>() {
        @Override
        public void onNext(RouteNote note) {
          List<RouteNote> notes = getOrCreateNotes(note.getLocation());

          // Respond with all previous notes at this location.
          for (RouteNote prevNote : notes.toArray(new RouteNote[0])) {
            responseObserver.onNext(prevNote);
          }

          // Now add the new note to the list
          notes.add(note);
        }

        @Override
        public void onError(Throwable t) {
          logger.log(Level.WARNING, "routeChat cancelled");
        }

        @Override
        public void onCompleted() {
          responseObserver.onCompleted();
        }
      };
    }

    /**
     * Get the notes list for the given location. If missing, create it.
     */
    private List<RouteNote> getOrCreateNotes(Point location) {
      List<RouteNote> notes = Collections.synchronizedList(new ArrayList<RouteNote>());
      List<RouteNote> prevNotes = routeNotes.putIfAbsent(location, notes);
      return prevNotes != null ? prevNotes : notes;
    }

    /**
     * Gets the feature at the given point.
     *
     * @param location the location to check.
     * @return The feature object at the point. Note that an empty name indicates no feature.
     */
    private Feature checkFeature(Point location) {
      for (Feature feature : features) {
        if (feature.getLocation().getLatitude() == location.getLatitude()
            && feature.getLocation().getLongitude() == location.getLongitude()) {
          return feature;
        }
      }

      // No feature was found, return an unnamed feature.
      return Feature.newBuilder().setName("").setLocation(location).build();
    }

    /**
     * Calculate the distance between two points using the "haversine" formula.
     * The formula is based on http://mathforum.org/library/drmath/view/51879.html.
     *
     * @param start The starting point
     * @param end   The end point
     * @return The distance between the points in meters
     */
    private static int calcDistance(Point start, Point end) {
      int r = 6371000; // earth radius in meters
      double lat1 = toRadians(RouteGuideUtil.getLatitude(start));
      double lat2 = toRadians(RouteGuideUtil.getLatitude(end));
      double lon1 = toRadians(RouteGuideUtil.getLongitude(start));
      double lon2 = toRadians(RouteGuideUtil.getLongitude(end));
      double deltaLat = lat2 - lat1;
      double deltaLon = lon2 - lon1;

      double a = sin(deltaLat / 2) * sin(deltaLat / 2)
          + cos(lat1) * cos(lat2) * sin(deltaLon / 2) * sin(deltaLon / 2);
      double c = 2 * atan2(sqrt(a), sqrt(1 - a));

      return (int) (r * c);
    }
  }

第②步,创建一个RPC Server对象。
看第二个构造方法,使用ServerBuilder.forPort得到一个SeverBuilder对象,然后传给第三个构造方法去使用。先忽略第三个构造方法中features参数,这个参数是我们自己程序从文件中读出来的的信息,与gRPC框架关系不大。

private final int port;
private final Server server;

public RouteGuideServer(int port) throws IOException {
    this(port, RouteGuideUtil.getDefaultFeaturesFile());
  }

  /**
   * Create a RouteGuide server listening on {@code port} using {@code featureFile} database.
   */
  public RouteGuideServer(int port, URL featureFile) throws IOException {
    this(ServerBuilder.forPort(port), port, RouteGuideUtil.parseFeatures(featureFile));
  }

  /**
   * Create a RouteGuide server using serverBuilder as a base and features as data.
   */
  public RouteGuideServer(ServerBuilder<?> serverBuilder, int port, Collection<Feature> features) {
    this.port = port;
    server = serverBuilder.addService(new RouteGuideService(features))
        .build();
  }

第③步:调用第②步中创建的Server对象的start方法,启动服务端。

public void start() throws IOException {
    // 启动服务端
    server.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");
        try {
          RouteGuideServer.this.stop();
        } catch (InterruptedException e) {
          e.printStackTrace(System.err);
        }
        System.err.println("*** server shut down");
      }
    });
  }

成功启动服务端:

grpc双向流式 grpc双向流原理_java_02

三、编写RPC Client端

编写完并启动RPC Server后,我们继续编写客户端。
编写客户端也是有范式的,主要有如下几步:

① 创建一个stub(存根),用来像调用本地方法一样调用RPC方法。
② 调用远程Rpc方法,处理响应。

好,我们先进行第一步创建stub,stub的话有两种,一种是blocking/synchronous stub;另一种是non-blocking/asynchronous stub。顾名思义,第一种是同步阻塞的stub,第二种是非阻塞异步的stub。在使用客户端流模式和双向流模式时,必须用asynchronous stub。也很好理解,因为如果不是异步的话,发送一个请求就必须同步地等待服务器响应,那就违反了客户端流模式和双向流模式的初衷了。

public RouteGuideClient(String host, int port) {
  // 创建stub需要用到此ChannelBuilder构造出来的Channel
  this(ManagedChannelBuilder.forAddress(host, port).usePlaintext());
}

/** Construct client for accessing RouteGuide server using the existing channel. */
public RouteGuideClient(ManagedChannelBuilder<?> channelBuilder) {
  channel = channelBuilder.build();
  // 同步stub
  blockingStub = RouteGuideGrpc.newBlockingStub(channel);
  // 异步stub
  asyncStub = RouteGuideGrpc.newStub(channel);
}

第②步,调用RPC方法,并处理响应。
下面的代码演示了对四种模式的rpc方法该如何调用。其中的参数类型和返回值类型,我们后面再讲

简单模式:

// 构造请求体
Point request = Point.newBuilder().setLatitude(lat).setLongitude(lon).build();
Feature feature;
try {
   // 调用RPC方法获得返回值对象
  feature = blockingStub.getFeature(request);
} catch (StatusRuntimeException e) {
  logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
  return;
}

服务端流模式:

// 构造请求体
Rectangle request =
    Rectangle.newBuilder()
        .setLo(Point.newBuilder().setLatitude(lowLat).setLongitude(lowLon).build())
        .setHi(Point.newBuilder().setLatitude(hiLat).setLongitude(hiLon).build()).build();
// 使用Iterator对象接收响应。
Iterator<Feature> features;
try {
  features = blockingStub.listFeatures(request);
} catch (StatusRuntimeException e) {
  logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
  return;
}

客户端流模式:

这个代码需要对照服务端的recordRoute方法一起看。
客户端一侧的responseObserver对象是用来处理服务端响应的,asyncStub调用RPC方法recordRoute用responseObserver对象作为参数,方法返回一个StreamObserver对象requestObserver,返回的这个requestObserver对象的onNext和onCompleted等方法的具体逻辑都在服务器端被重写过。使用requestObserver对象的onNext方法来向服务端发送多次请求,使用requestObserver对象的onCompleted标识发送数据结束。然后服务端根据重写的逻辑执行

public void recordRoute(List<Feature> features, int numPoints) throws InterruptedException {
  info("*** RecordRoute");
  final CountDownLatch finishLatch = new CountDownLatch(1);
  StreamObserver<RouteSummary> responseObserver = new StreamObserver<RouteSummary>() {
    @Override
    public void onNext(RouteSummary summary) {
      info("Finished trip with {0} points. Passed {1} features. "
          + "Travelled {2} meters. It took {3} seconds.", summary.getPointCount(),
          summary.getFeatureCount(), summary.getDistance(), summary.getElapsedTime());
    }

    @Override
    public void onError(Throwable t) {
      Status status = Status.fromThrowable(t);
      logger.log(Level.WARNING, "RecordRoute Failed: {0}", status);
      finishLatch.countDown();
    }

    @Override
    public void onCompleted() {
      info("Finished RecordRoute");
      finishLatch.countDown();
    }
  };

  StreamObserver<Point> requestObserver = asyncStub.recordRoute(responseObserver);
  try {
    // Send numPoints points randomly selected from the features list.
    Random rand = new Random();
    for (int i = 0; i < numPoints; ++i) {
      int index = rand.nextInt(features.size());
      Point point = features.get(index).getLocation();
      info("Visiting point {0}, {1}", RouteGuideUtil.getLatitude(point),
          RouteGuideUtil.getLongitude(point));
      requestObserver.onNext(point);
      // Sleep for a bit before sending the next one.
      Thread.sleep(rand.nextInt(1000) + 500);
      if (finishLatch.getCount() == 0) {
        // RPC completed or errored before we finished sending.
        // Sending further requests won't error, but they will just be thrown away.
        return;
      }
    }
  } catch (RuntimeException e) {
    // Cancel RPC
    requestObserver.onError(e);
    throw e;
  }
  // Mark the end of requests
  requestObserver.onCompleted();

  // Receiving happens asynchronously
  finishLatch.await(1, TimeUnit.MINUTES);
}

双向流模式:

双向流模式的客户端侧代码与客户端流模式一样,区别在于服务端的代码。在客户端流模式下,服务端的onNext()会处理请求参数,然后会在客户端调用onCompleted方法后才返回响应。但是双向流模式下,服务端不必等到客户端调用onCompleted方法后再返回响应,可以直接在onNext方法里就调用传进来的responseObserver的onNext方法返回给客户端响应。如下图:

grpc双向流式 grpc双向流原理_RPC_03

public void routeChat() throws Exception {
  info("*** RoutChat");
  final CountDownLatch finishLatch = new CountDownLatch(1);
  StreamObserver<RouteNote> requestObserver =
      asyncStub.routeChat(new StreamObserver<RouteNote>() {
        @Override
        public void onNext(RouteNote note) {
          info("Got message \"{0}\" at {1}, {2}", note.getMessage(), note.getLocation()
              .getLatitude(), note.getLocation().getLongitude());
        }

        @Override
        public void onError(Throwable t) {
          Status status = Status.fromThrowable(t);
          logger.log(Level.WARNING, "RouteChat Failed: {0}", status);
          finishLatch.countDown();
        }

        @Override
        public void onCompleted() {
          info("Finished RouteChat");
          finishLatch.countDown();
        }
      });

  try {
    RouteNote[] requests =
        {newNote("First message", 0, 0), newNote("Second message", 0, 1),
            newNote("Third message", 1, 0), newNote("Fourth message", 1, 1)};

    for (RouteNote request : requests) {
      info("Sending message \"{0}\" at {1}, {2}", request.getMessage(), request.getLocation()
          .getLatitude(), request.getLocation().getLongitude());
      requestObserver.onNext(request);
    }
  } catch (RuntimeException e) {
    // Cancel RPC
    requestObserver.onError(e);
    throw e;
  }
  // Mark the end of requests
  requestObserver.onCompleted();

  // Receiving happens asynchronously
  finishLatch.await(1, TimeUnit.MINUTES);
}

以下是客户端调用四个方法的截图:

grpc双向流式 grpc双向流原理_服务端_04

完整工程链接:
链接: https://pan.baidu.com/s/1s7uDWJZq3AK4jZmGJZHYfw 提取码: cntr

参考 https://grpc.io/docs/languages/java/basics/