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
生成如下图两个包中的类。
二、编写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");
}
});
}
成功启动服务端:
三、编写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方法返回给客户端响应。如下图:
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);
}
以下是客户端调用四个方法的截图:
完整工程链接:
链接: https://pan.baidu.com/s/1s7uDWJZq3AK4jZmGJZHYfw 提取码: cntr
参考 https://grpc.io/docs/languages/java/basics/