本文参考文章:
https://blog.csdn.net/sunsun314/article/details/73780169
https://www.cnblogs.com/mzsg/p/5643367.html
https://grpc.io/docs/tutorials/basic/java.html
http://doc.oschina.net/grpc?t=60134
如果你不了解gRPC的话,可以先看一下这篇文章。
这些文章都有相应的代码实现,这里我就不重复劳动了。
本文主要以gRPC官方例子中的RouteGuide示例为准,总结一下我粗浅的理解。这篇例子的文章也就是gRPC Basics - Java。
代码地址:https://github.com/grpc/grpc-java/tree/master/examples/src/main/java/io/grpc/examples/routeguide
那么首先,我们来看如何实现一个能够运行的gRPC服务。
构建gRPC的步骤
- 在pom文件中添加相关依赖及插件
<properties>
<grpc.version>1.4.0</grpc.version><!-- CURRENT_GRPC_VERSION -->
</properties>
<dependencies>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-testing</artifactId>
<version>${grpc.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>1.9.5</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.4.1.Final</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.5.0</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:3.3.0:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
-
接着需要在src/main/proto文件夹下创建
.proto
文件,之后使用命令mvn install
,可以在target包中找到两个生成文件,一个是Proto文件,是生成的数据结构javaBean,其中包含input和output;另一个是接口方法类ServiceGrpc这类的,这个类提供接口方法的调用。 -
我们将这两个文件复制到对应java代码目录中,接下来需要新建3个类(也可以是两个):接口方法类的实现类Impl;server类;client类。
到这里,一个gRPC服务的构建就完成了。
由于第二步中的文件是通过插件自动生成的,所以我们可操作的部分就是在第三步了,接下来就看一下我们如何实现这3个类。
相关代码的实现
- Impl实现类中主要就是继承ServiceGrpc接口方法类中的一个基础实现类ServiceImplBase,并实现我们在.proto文件中定义的那些方法。也就是说,该类就是我们的业务逻辑类。
如:
package com.mzsg.demo.grpc.qryaccount;
import io.grpc.stub.StreamObserver;
/**
* @Auther: yubt
* @Description:
* @Date: Created in 10:24 2018/12/24
* @Modified By:
*/
public class QryAccountServiceImpl extends QryAccountServiceGrpc.QryAccountServiceImplBase {
@Override
public void qry(com.mzsg.demo.grpc.qryaccount.QryAccountProto.AccountQryRequest request, StreamObserver<QryAccountProto.AccountQryResponse> responseObserver) {
System.out.println("qry " + request.getUserId());
QryAccountProto.AccountQryResponse response = QryAccountProto.AccountQryResponse.newBuilder().setRc(1).setAmount(666).build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}
- server类主要实现一个start()方法,能够将server启动,需要通过ServerBuilder来新建出对应的server,并且,在build server的时候,需要加上对应的端口及相应的服务,而这个服务就是我们刚刚实现的impl实现类。
如:
package com.mzsg.demo.grpc.qryaccount;
import java.io.IOException;
/**
* @Auther: yubt
* @Description:
* @Date: Created in 10:30 2018/12/24
* @Modified By:
*/
public class Server {
private static int port = 8848;
private static io.grpc.Server server;
// 就是我在上面说的start()方法,只是命名不同
public void run(){
QryAccountServiceGrpc.QryAccountServiceImplBase modifyAccountServiceImpl = new QryAccountServiceImpl();
server = io.grpc.ServerBuilder.forPort(port).addService(modifyAccountServiceImpl).build();
try {
server.start();
System.out.println("Server start success on port:" + port);
server.awaitTermination();
}catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server();
server.run();
}
}
- client方法主要就是调用相关方法了。它需要根据.proto文件中定义的request去新建对应的request,之后build channel,再根据channel新建stub,之后通过stub调用相应的方法(接口中的)并等待回应response,这个response也是.proto文件中定义的。
如:
package com.mzsg.demo.grpc.qryaccount;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import java.io.FileNotFoundException;
import java.io.IOException;
/**
* @Auther: yubt
* @Description:
* @Date: Created in 10:35 2018/12/24
* @Modified By:
*/
public class Client {
public static void main(String[] args) throws FileNotFoundException, IOException {
QryAccountProto.AccountQryRequest request = QryAccountProto.AccountQryRequest.newBuilder().setUserId("20012").setRequestId("123").build();
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 8848).usePlaintext(true).build();
QryAccountServiceGrpc.QryAccountServiceBlockingStub stub = QryAccountServiceGrpc.newBlockingStub(channel);
for (int j = 0; j < 20; j++) {
long start = System.currentTimeMillis();
for(int i=0; i<1000; i++){
QryAccountProto.AccountQryResponse rsp = stub.qry(request);
// System.out.println(rsp);
}
System.out.println(System.currentTimeMillis() - start + " MS");
}
}
}
从上述代码中,我们可以比较直观的了解到各个类的功能以及应该实现什么内容。
不过,上述例子过于简单,当然非常直观。下面我们就看一下复杂的实现。也就是官网的例子RouteGuide。
我们先看.proto
文件中的定义
syntax = "proto3";
option java_package = "io.grpc.examples.routeguide";
option java_outer_classname = "RouteGuideProto";
package routeguide;
// Interface exported by the server.
service RouteGuide {
rpc GetFeature(Point) returns (Feature) {}
rpc ListFeatures(Rectangle) returns (stream Feature) {}
rpc RecordRoute(stream Point) returns (RouteSummary) {}
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
}
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
message Rectangle {
Point lo = 1;
Point hi = 2;
}
message Feature {
string name = 1;
Point location = 2;
}
message FeatureDatabase {
repeated Feature feature = 1;
}
message RouteNote {
Point location = 1;
string message = 2;
}
message RouteSummary {
int32 point_count = 1;
int32 feature_count = 2;
int32 distance = 3;
int32 elapsed_time = 4;
}
首先我们看Server中的实现,因为例子中将server和impl写到一起了,我们拆开来看。
server的代码
package io.grpc.examples.routeguide;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.stub.StreamObserver;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* @Auther: yubt
* @Description:
* @Date: Created in 14:27 2018/12/24
* @Modified By:
*/
public class RouteGuideServer {
private static final Logger logger = Logger.getLogger(RouteGuideServer.class.getName());
private final int port;
private final Server server;
public RouteGuideServer(int port) throws IOException {
this(port, RouteGuideUtil.getDefaultFeaturesFile());
}
public RouteGuideServer(int port, URL featureFile) throws IOException {
this(ServerBuilder.forPort(port), port, RouteGuideUtil.parseFeatures(featureFile));
}
public RouteGuideServer(ServerBuilder<?> serverBuilder, int port, Collection<RouteGuideProto.Feature> features) {
this.port = port;
server = serverBuilder.addService(new RouteGuideService(features))
.build();
}
public void start() throws IOException {
server.start();
logger.info("Server started, listening on " + port);
Runtime.getRuntime().addShutdownHook(new Thread(){
@Override
public void run() {
System.err.println("*** shutting down gRPC server since JVM is shutting down");
RouteGuideServer.this.stop();
System.err.println("*** server shut down");
}
});
}
public void stop(){
if (server != null) {
server.shutdown();
}
}
private void blockUntilShutdown() throws InterruptedException {
if (server != null) {
server.awaitTermination();
}
}
public static void main(String[] args) throws Exception{
RouteGuideServer server = new RouteGuideServer(8980);
server.start();
server.blockUntilShutdown();
}
}
可以看到核心内容还是构建server,实现start()方法,以及阻塞方法。和我们上面的说法基本一致。
impl实现类,我们将内容精简。
private static class RouteGuideService extends RouteGuideGrpc.RouteGuideImplBase {
private final Collection<RouteGuideProto.Feature> features;
private final ConcurrentMap<RouteGuideProto.Point, List<RouteGuideProto.RouteNote>> routeNotes =
new ConcurrentHashMap<RouteGuideProto.Point, List<RouteGuideProto.RouteNote>>();
RouteGuideService(Collection<RouteGuideProto.Feature> features) {
this.features = features;
}
@Override
public void getFeature(RouteGuideProto.Point request, StreamObserver<RouteGuideProto.Feature> responseObserver) {
responseObserver.onNext(checkFeature(request));
responseObserver.onCompleted();
}
@Override
public void listFeatures(RouteGuideProto.Rectangle request, StreamObserver<RouteGuideProto.Feature> responseObserver) {
...
}
@Override
public StreamObserver<RouteGuideProto.Point> recordRoute(final StreamObserver<RouteGuideProto.RouteSummary> responseObserver) {
...
}
@Override
public StreamObserver<RouteGuideProto.RouteNote> routeChat(final StreamObserver<RouteGuideProto.RouteNote> responseObserver){
...
}
// 其他辅助方法
...
}
我们可以看到impl类的核心功能还是实现我们在.proto
文件中定义的接口。
// Interface exported by the server.
service RouteGuide {
rpc GetFeature(Point) returns (Feature) {}
rpc ListFeatures(Rectangle) returns (stream Feature) {}
rpc RecordRoute(stream Point) returns (RouteSummary) {}
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
}
我们再看client类的实现,代码也做相应的精简
package io.grpc.examples.routeguide;
import com.google.common.annotations.VisibleForTesting;
import com.google.protobuf.Message;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.stub.StreamObserver;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* @Auther: yubt
* @Description:
* @Date: Created in 15:10 2018/12/24
* @Modified By:
*/
public class RouteGuideClient {
private static final Logger logger = Logger.getLogger(RouteGuideClient.class.getName());
private final ManagedChannel channel;
private final RouteGuideGrpc.RouteGuideBlockingStub blockingStub;
private final RouteGuideGrpc.RouteGuideStub asyncStub;
private Random random = new Random();
private TestHelper testHelper;
public RouteGuideClient(String host, int port){
this(ManagedChannelBuilder.forAddress(host, port).usePlaintext(true));
}
public RouteGuideClient(ManagedChannelBuilder<?> channelBuilder){
channel = channelBuilder.build();
blockingStub = RouteGuideGrpc.newBlockingStub(channel);
asyncStub = RouteGuideGrpc.newStub(channel);
}
public void shutdown() throws InterruptedException {
channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
}
// 该方法主要用于构造request及处理response,调用了server中的相关方法,而server中又使用的impl中的实现方法
public void getFeature(int lat, int lon){
...
}
// 该方法主要用于构造request及处理response,同上
public void listFeatures(int lowLat, int lowLon, int hiLat, int hiLon){
...
}
// 该方法主要用于构造request及处理response,同上
public void recordRoute(List<RouteGuideProto.Feature> features, int numPoints) throws InterruptedException {
...
}
// 该方法主要用于构造request及处理response,同上
public CountDownLatch routeChat(){
...
}
// 进行了相关方法的调用
public static void main(String[] args) throws InterruptedException{
List<RouteGuideProto.Feature> features;
try {
features = RouteGuideUtil.parseFeatures(RouteGuideUtil.getDefaultFeaturesFile());
} catch (IOException ex) {
ex.printStackTrace();
return;
}
RouteGuideClient client = new RouteGuideClient("localhost", 8980);
try {
// Looking for a valid feature
client.getFeature(409146138, -746188906);
// Feature missing.
client.getFeature(0, 0);
// Looking for features between 40, -75 and 42, -73.
client.listFeatures(400000000, -750000000, 420000000, -730000000);
// Record a few randomly selected points from the features file.
client.recordRoute(features, 10);
// Send and receive some notes.
CountDownLatch finishLatch = client.routeChat();
if (!finishLatch.await(1, TimeUnit.MINUTES)) {
client.warning("routeChat can not finish within 1 minutes");
}
} finally {
client.shutdown();
}
}
// 相关辅助方法等
...
}
将代码精简后,我们可以看到这个类的功能一目了然。
不过,当我们重新审视省略的代码时,就会有新的发现。
当我们使用到流式请求/响应时,我们需要实现对应的流式处理方法,包括处理request及response。
当我们在实现impl类时,会实现相应的流式请求/响应,此时会用到一个接口StreamObserver,该接口有三个方法onNext()、onError()、onCompleted().
当我们在返回流式响应数据的时候,我们需要新建对应的StreamObserver,并实现其对应的相关方法逻辑。以供客户端调用,并处理对应的流式响应。
举一例如下:
client端构建的routeChat()方法中的request
StreamObserver<RouteGuideProto.RouteNote> requestObserver = asyncStub.routeChat(new StreamObserver<RouteGuideProto.RouteNote>() {
@Override
public void onNext(RouteGuideProto.RouteNote routeNote) {
info("Got message \"{0}\" at {1}, {2}", routeNote.getMessage(), routeNote.getLocation()
.getLatitude(), routeNote.getLocation().getLongitude());
if (testHelper != null) {
testHelper.onMessage(routeNote);
}
}
@Override
public void onError(Throwable throwable) {
warning("RouteChat Failed: {0}", Status.fromThrowable(throwable));
if (testHelper != null) {
testHelper.onRpcError(throwable);
}
finishLatch.countDown();
}
@Override
public void onCompleted() {
info("Finished RouteChat");
finishLatch.countDown();
}
});
而我们在server端是这么处理这个request的:
@Override
public StreamObserver<RouteGuideProto.RouteNote> routeChat(final StreamObserver<RouteGuideProto.RouteNote> responseObserver){
return new StreamObserver<RouteGuideProto.RouteNote>() {
@Override
public void onNext(RouteGuideProto.RouteNote routeNote) {
List<RouteGuideProto.RouteNote> notes = getOrCreateNotes(routeNote.getLocation());
for (RouteGuideProto.RouteNote prevNote : notes.toArray(new RouteGuideProto.RouteNote[0])){
responseObserver.onNext(prevNote);
}
notes.add(routeNote);
}
@Override
public void onError(Throwable throwable) {
logger.log(Level.WARNING, "routeChat cancelled");
}
@Override
public void onCompleted() {
responseObserver.onCompleted();
}
};
}
代码中的responseObserver
就是我们client中的requestObserver
。可以看到当我们需要返回对应的流式响应时,我们也需要实现StreamObserver
接口,并在对应方法中使用另一端实现的对应方法。
grpc与http的性能比较
之后我使用了这篇文章中的代码来测试。
http使用httpclient包进行测试,代码如下:
package com.yubotao.http;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
/**
* @Auther: yubt
* @Description:
* @Date: Created in 15:05 2018/12/25
* @Modified By:
*/
public class TestWithHttpClient {
public static void main(String[] args) {
try {
for (int j = 0; j < 20; j++) {
long start = System.currentTimeMillis();
for (int i=0; i<1000; i++) {
HttpClient client = HttpClientBuilder.create().build();
HttpGet get = new HttpGet("http://localhost:8081/qry/20010");
HttpResponse res = client.execute(get);
}
System.out.println(System.currentTimeMillis() - start + " MS");
}
}catch (Exception e){
e.printStackTrace();
}
}
}
@RequestMapping(value = "/qry/{userId}", method = RequestMethod.GET)
public Response qry(@PathVariable String userId){
System.out.println("qry " + userId);
Response response = new Response();
response.setRc("1");
response.setAmount("666");
return response;
}
private class Response {
private String rc;
private String amount;
public String getRc() {
return rc;
}
public void setRc(String rc) {
this.rc = rc;
}
public String getAmount() {
return amount;
}
public void setAmount(String amount) {
this.amount = amount;
}
@Override
public String toString() {
return "Response{" +
"rc='" + rc + '\'' +
", amount='" + amount + '\'' +
'}';
}
}
请求单次时:
意外的发现,http性能要略优于grpc。
grpc
http
在只测试方法执行时间上,http优于grpc,测试完整请求(包括构建client,执行方法,打印结果等等),http优于grpc。
多次重复请求时:
20组测试数据,每组请求1000次方法。
grpc如图:
由于http每次请求都需要重新建立连接,所以十分耗时。如下图
还是能够非常明显的看出,http请求和grpc请求的差距的,主要还是因为http需要每次都重新建立连接,但是grpc由于是基于http2的,所以可以保持连接。
经过上面的测试,可以得到相应的结论了,就是grpc关于大量的重复请求,确实是比http节省时间,因为省去了建立连接的时间;但是如果针对单点请求来说,httpclient的性能比grpc略强。