gRPC初探
http://www.infoq.com/cn/news/2015/03/grpc-google-http2-protobuf
官网:http://www.grpc.io
gRPC是google新开源的一个基于protobuf的rpc框架,使用通信协议为HTTP2,网络通信层基于netty实现;
它首先提供移动客户端的rpc功能,同时也是一个通用的rpc框架。
下面是我做的一个简单的gRPC的demo。
如下IDL文件,定义了服务接口和消息格式,
SearchService.proto文件
syntax = "proto3"; package search; option java_multiple_files = true; option java_package = "com.usoft.grpc.example.search"; option java_outer_classname = "SearchProto"; service SearchService { // 四种rpc method rpc SearchWithSimpleRpc (SearchRequest) returns (SearchResponse) {}; rpc SearchWithServerSideStreamRpc (SearchRequest) returns (stream SearchResponse) {}; rpc SearchWithClientSideStreamRpc (stream SearchRequest) returns (SearchResponse) {}; rpc SearchWithBidirectionalStreamRpc(stream SearchRequest) returns (stream SearchResponse) {}; } message SearchRequest { string query = 1; int32 page_number = 2; int32 result_per_page = 3; } message SearchResponse { message Result { string url = 1; string title = 2; repeated string snippets = 3; } repeated Result result = 1; }
使用service 和 message 关键字分别定义了服务接口和基于该服务接口的消息格式。
message可以嵌套定义。
这里是基于protobuf 3 定义的服务接口和消息格式,在protobuf 3 中不再使用 required 和 optional 关键字,只保留了repeated 关键字。
使用protobuf 3 定义的消息格式比protobuf 2 显得更干净和整洁。
这里使用的构建工具时gradle,使用gRPC的gradle插件,
apply plugin: 'com.google.protobuf'
运行插件的generate任务
服务器端只需要实现SearchServiceGrpc.SearchService这个接口就可以,其实这个接口就是我们在proto文件中用service关键字定义的接口。
在上边的proto文件中,我们定义了四种rpc method,分别是
rpc SearchWithSimpleRpc (SearchRequest) returns (SearchResponse) {}; rpc SearchWithServerSideStreamRpc (SearchRequest) returns (stream SearchResponse) {}; rpc SearchWithClientSideStreamRpc (stream SearchRequest) returns (SearchResponse) {}; rpc SearchWithBidirectionalStreamRpc(stream SearchRequest) returns (stream SearchResponse) {};
这四种rpc method 对应着客户端和服务器端的实现是不同的,即表现在发送消息的方式,有stream关键词意味着是否是以stream(流式)的方式发送消息。
这种方式是最简单的一种rpc 方式,客户端通过一个stub 阻塞式的调用远程服务器方法,阻塞式表现在客户端调用后等待服务器端的返回消息。
j2ee里面的stub是这样说的..为屏蔽客户调用远程主机上的对象,必须提供某种方式来模拟本地对象,这种本地对象称为存根(stub),存根负责接收本地方法调用,并将它们委派给各自的具体实现对象
stub 在gRPC中也是这个意思。通过stub调用远程服务接口。
在客户端定义两种阻塞式的stub 和 异步方式的stub,如下,
blockingStub 和 asyncStub
private final ManagedChannel channel; private final SearchServiceGrpc.SearchServiceBlockingStub blockingStub; private final SearchServiceGrpc.SearchServiceStub asyncStub; /** * Construct client connecting to HelloWorld server at {@code host:port}. */ public SearchClient(String host, int port) { channel = ManagedChannelBuilder.forAddress(host, port) .usePlaintext(true).build(); blockingStub = SearchServiceGrpc.newBlockingStub(channel); asyncStub = SearchServiceGrpc.newStub(channel); }
客户端的实现
/** * simple rpc * * @param pageNo * @param pageSize */ public void searchWithSimpleRpc(int pageNo, int pageSize) { try { logger.info( "search param pageNo=" + pageNo + ",pageSize=" + pageSize); SearchRequest request = SearchRequest.newBuilder() .setPageNumber(pageNo).setResultPerPage(pageSize).build(); SearchResponse response = blockingStub.searchWithSimpleRpc(request); logger.info("search result: " + response.toString()); } catch (RuntimeException e) { logger.log(Level.WARNING, "RPC failed", e); return; } }
在客户端通过这行代码调用 远程服务器方法
SearchResponse response = blockingStub.searchWithSimpleRpc(request);
直到一个消息返回
服务器端方法实现
/** * Simple RPC * A simple RPC where the client sends a request to the server using the * stub and waits for a response to come back, just like a normal function * call. * * @param request * @param responseObserver */ @Override public void searchWithSimpleRpc(SearchRequest request, StreamObserver<SearchResponse> responseObserver) { System.out.println("pageNo=" + request.getPageNumber()); System.out.println("query string=" + request.getQuery()); System.out.println("pageSize=" + request.getResultPerPage()); List<SearchResponse.Result> results = new ArrayList<SearchResponse.Result>( 10); for (int i = 0; i < request.getResultPerPage(); i++) { SearchResponse.Result result = SearchResponse.Result.newBuilder() .setTitle("title" + i).setUrl("dev.usoft.com") .addSnippets("snippets" + i).build(); results.add(result); } SearchResponse response = SearchResponse.newBuilder() .addAllResult(results).build(); responseObserver.onNext(response); //We use the response observer's onCompleted() method to specify that we've finished dealing with the RPC. responseObserver.onCompleted(); }
服务器端的 rpc 方法的stream 方式实现。这种方式下客户端和服务器端主要交互方式表现为 当客户端发送一个消息后,服务器可以连续多次返回消息,而客户端回连续读取消息,直到服务器发送完毕。
客户端实现
/** * server side stream rpc * * @param pageNo * @param pageSize */ public void searchWithSeverSideStreamRpc(int pageNo, int pageSize) { try { logger.info( "search param pageNo=" + pageNo + ",pageSize=" + pageSize); SearchRequest request = SearchRequest.newBuilder() .setPageNumber(pageNo).setResultPerPage(pageSize).build(); Iterator<SearchResponse> responseIterator = blockingStub .searchWithServerSideStreamRpc(request); while (responseIterator.hasNext()) { SearchResponse r = responseIterator.next(); if (r.getResult(0).getSnippets(0).equals("the last")) { logger.info("the end: \n" + r.toString()); break; } logger.info("search result:\n " + r.toString()); } } catch (RuntimeException e) { logger.log(Level.WARNING, "RPC failed", e); return; } }
关键代码
Iterator<SearchResponse> responseIterator = blockingStub .searchWithServerSideStreamRpc(request);
客户端通过一个阻塞式的stub调用一个远程服务器的方法后,会返回一个消息的 iterator ,通过iterator 遍历返回的所有消息,读取一个消息的序列。
服务器端实现
/** * Server-side streaming RPC * A server-side streaming RPC where the client sends a request to the * server and gets a stream to read a sequence of messages back. The client * reads from the returned stream until there are no more messages. As you * can see in our example, you specify a server-side streaming method by * placing the stream keyword before the response type. * * @param request * @param responseObserver */ @Override public void searchWithServerSideStreamRpc(SearchRequest request, StreamObserver<SearchResponse> responseObserver) { System.out.println("pageNo=" + request.getPageNumber()); System.out.println("query string=" + request.getQuery()); System.out.println("pageSize=" + request.getResultPerPage()); List<SearchResponse.Result> results = new ArrayList<SearchResponse.Result>( 10); for (int i = 0; i < request.getResultPerPage(); i++) { SearchResponse.Result result = SearchResponse.Result.newBuilder() .setTitle("title" + i).setUrl("dev.usoft.com") .addSnippets("snippets" + i).build(); results.add(result); } SearchResponse response = SearchResponse.newBuilder() .addAllResult(results).build(); responseObserver.onNext(response); SearchResponse.Result result = SearchResponse.Result.newBuilder() .setTitle("title").setUrl("dev.usoft.com").addSnippets("the last") .build(); SearchResponse theNext = SearchResponse.newBuilder().addResult(result) .build(); responseObserver.onNext(theNext); responseObserver.onCompleted(); }
服务器端 连续发送了两次消息,
responseObserver.onNext(response);
responseObserver.onNext(theNext);
这就表现为 服务器端rpc 方法的流式实现,返回消息的序列。
这种rpc 方法和第二种正好相反,这种是客户端的rpc方法的流式实现,也就是说客户端可以发送连续的消息给服务器端。
客户端实现
/** * client side stream rpc * * @param pageNo * @param pageSize * @throws Exception */ public void searchWithClientSideStreamRpc(int pageNo, int pageSize) throws Exception { final SettableFuture<Void> finishFuture = SettableFuture.create(); StreamObserver<SearchResponse> responseObserver = new StreamObserver<SearchResponse>() { @Override public void onNext(SearchResponse searchResponse) { logger.info( "response with result=\n" + searchResponse.toString()); } @Override public void onError(Throwable throwable) { finishFuture.setException(throwable); } @Override public void onCompleted() { finishFuture.set(null); } }; StreamObserver<SearchRequest> requestObserver = asyncStub .searchWithClientSideStreamRpc(responseObserver); try { // 发送三次search request for (int i = 1; i <= 3; i++) { SearchRequest request = SearchRequest.newBuilder() .setPageNumber(pageNo).setResultPerPage(pageSize + i) .build(); requestObserver.onNext(request); if (finishFuture.isDone()) { logger.log(Level.WARNING, "finish future is done"); break; } } requestObserver.onCompleted(); finishFuture.get(); logger.log(Level.INFO, "finished"); } catch (Exception e) { requestObserver.onError(e); logger.log(Level.WARNING, "Client Side Stream Rpc Failed", e); throw e; } }
这种方式客户端实现比较复杂了,简单来说就是通过StreamObserver 的匿名类来处理消息的返回,关键代码,
StreamObserver<SearchResponse> responseObserver = new StreamObserver<SearchResponse>() { @Override public void onNext(SearchResponse searchResponse) { logger.info( "response with result=\n" + searchResponse.toString()); } @Override public void onError(Throwable throwable) { finishFuture.setException(throwable); } @Override public void onCompleted() { finishFuture.set(null); } };
服务器端实现
/** * Client-side streaming RPC * A client-side streaming RPC where the client writes a sequence of * messages and sends them to the server, again using a provided stream. * Once the client has finished writing the messages, it waits for the * server to read them all and return its response. You specify a * server-side streaming method by placing the stream keyword before the * request type. * * @param responseObserver * @return */ @Override public StreamObserver<SearchRequest> searchWithClientSideStreamRpc( final StreamObserver<SearchResponse> responseObserver) { return new StreamObserver<SearchRequest>() { int searchCount; SearchRequest previous; long startTime = System.nanoTime(); @Override public void onNext(SearchRequest searchRequest) { searchCount++; if (previous != null && previous.getResultPerPage() == searchRequest .getResultPerPage() && previous.getPageNumber() == searchRequest .getPageNumber()) { logger.info("do nothing"); return; } previous = searchRequest; } @Override public void onError(Throwable throwable) { System.out.println("error"); } @Override public void onCompleted() { logger.info("search count = " + searchCount); List<SearchResponse.Result> results = new ArrayList<SearchResponse.Result>( 10); for (int i = 0; i < previous.getResultPerPage(); i++) { SearchResponse.Result result = SearchResponse.Result .newBuilder().setTitle("title" + i) .setUrl("dev.usoft.com").addSnippets("snippets" + i) .build(); results.add(result); } SearchResponse response = SearchResponse.newBuilder() .addAllResult(results).build(); responseObserver.onNext(response); responseObserver.onCompleted(); logger.info("spend time = " + String.valueOf(System.nanoTime() - startTime)); } }; }
服务器端的实现也是比较复杂的,但是思路还是很清晰的。
onNext 方法一个一个的处理客户端连续发送的消息,对应着客户端的一次onNext 调用。
onCompleted方法表示 客户端发送消息结束,对应着客户端的一次onCompleted 调用。
这种方式实现的rpc 是双向流式实现。主要表现是客户端和服务器端都可以连续的发送消息。
先看客户端的实现
/** * bidirectional stream rpc * * @param pageNo * @param pageSize */ public void searchWithBidirectionalStreamRpc(int pageNo, int pageSize) throws Exception { final SettableFuture<Void> finishFuture = SettableFuture.create(); StreamObserver<SearchRequest> requestObserver = asyncStub .searchWithBidirectionalStreamRpc( new StreamObserver<SearchResponse>() { @Override public void onNext(SearchResponse searchResponse) { logger.info("response with result = \n" + searchResponse.toString()); } @Override public void onError(Throwable throwable) { finishFuture.setException(throwable); } @Override public void onCompleted() { finishFuture.set(null); } }); try { // 发送三次search request for (int i = 1; i <= 3; i++) { SearchRequest request = SearchRequest.newBuilder() .setPageNumber(pageNo).setResultPerPage(pageSize + i) .build(); requestObserver.onNext(request); } requestObserver.onCompleted(); finishFuture.get(); logger.log(Level.INFO, "finished"); } catch (Exception e) { requestObserver.onError(e); logger.log(Level.WARNING, "Bidirectional Stream Rpc Failed", e); throw e; } }
代码看起来很多,但还是清晰的。就是表现在 onNext 方法 和 onCompleted方法分别处理不同的消息发送。onNext 表示一次消息的发送,onCompleted表示消息发送完毕。
服务器端的实现
/** * Bidirectional streaming RPC * A bidirectional(双向的) streaming RPC where both sides send a sequence of * messages using a read-write stream. The two streams operate * independently, so clients and servers can read and write in whatever * order they like: for example, the server could wait to receive all the * client messages before writing its responses, or it could alternately * read a message then write a message, or some other combination of reads * and writes. The order of messages in each stream is preserved. You * specify this type of method by placing the stream keyword before both the * request and the response. * * @param responseObserver * @return */ @Override public StreamObserver<SearchRequest> searchWithBidirectionalStreamRpc( final StreamObserver<SearchResponse> responseObserver) { return new StreamObserver<SearchRequest>() { int searchCount; SearchRequest previous; long startTime = System.nanoTime(); @Override public void onNext(SearchRequest searchRequest) { searchCount++; if (previous != null && previous.getResultPerPage() == searchRequest .getResultPerPage() && previous.getPageNumber() == searchRequest .getPageNumber()) { logger.info("do nothing"); return; } previous = searchRequest; logger.info("search count = " + searchCount); List<SearchResponse.Result> results = new ArrayList<SearchResponse.Result>( 10); for (int i = 0; i < searchRequest.getResultPerPage(); i++) { SearchResponse.Result result = SearchResponse.Result .newBuilder().setTitle("title" + i) .setUrl("dev.usoft.com").addSnippets("snippets" + i) .build(); results.add(result); } SearchResponse response = SearchResponse.newBuilder() .addAllResult(results).build(); responseObserver.onNext(response); logger.info("spend time = " + String.valueOf(System.nanoTime() - startTime)); } @Override public void onError(Throwable throwable) { System.out.println("error"); } @Override public void onCompleted() { responseObserver.onCompleted(); } }; }
好的,代码也很清晰,onNext 处理 rpc客户端的每次消息发送,同时服务器端处理客户端发送消息然后返回消息结果。这是一个客户端和服务器端多次交互的过程。
完整的客户端代码,省略代码实现,
package com.usoft.example.search; import com.google.common.util.concurrent.SettableFuture; import com.usoft.grpc.example.search.SearchRequest; import com.usoft.grpc.example.search.SearchResponse; import com.usoft.grpc.example.search.SearchServiceGrpc; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.grpc.stub.StreamObserver; import java.util.Iterator; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; /** * Created by xinxingegeya on 15/9/25. */ public class SearchClient { private static final Logger logger = Logger .getLogger(SearchClient.class.getName()); private final ManagedChannel channel; private final SearchServiceGrpc.SearchServiceBlockingStub blockingStub; private final SearchServiceGrpc.SearchServiceStub asyncStub; /** * Construct client connecting to HelloWorld server at {@code host:port}. */ public SearchClient(String host, int port) { channel = ManagedChannelBuilder.forAddress(host, port) .usePlaintext(true).build(); blockingStub = SearchServiceGrpc.newBlockingStub(channel); asyncStub = SearchServiceGrpc.newStub(channel); } public void shutdown() throws InterruptedException { channel.shutdown().awaitTermination(5, TimeUnit.SECONDS); } /** * simple rpc * * @param pageNo * @param pageSize */ public void searchWithSimpleRpc(int pageNo, int pageSize) { } /** * server side stream rpc * * @param pageNo * @param pageSize */ public void searchWithSeverSideStreamRpc(int pageNo, int pageSize) { } /** * client side stream rpc * * @param pageNo * @param pageSize * @throws Exception */ public void searchWithClientSideStreamRpc(int pageNo, int pageSize) throws Exception { } /** * bidirectional stream rpc * * @param pageNo * @param pageSize */ public void searchWithBidirectionalStreamRpc(int pageNo, int pageSize) throws Exception { } /** * client */ public static void main(String[] args) throws Exception { SearchClient client = new SearchClient("localhost", 50051); try { // client.searchWithSimpleRpc(1, 13); // client.searchWithSeverSideStreamRpc(1, 2); // client.searchWithClientSideStreamRpc(1, 3); client.searchWithBidirectionalStreamRpc(1, 3); } finally { client.shutdown(); } } }
完整的服务器端实现
package com.usoft.example.search; import com.usoft.grpc.example.search.SearchServiceGrpc; import io.grpc.Server; import io.grpc.ServerBuilder; import java.util.logging.Logger; /** * Created by xinxingegeya on 15/9/25. */ public class SearchServer { private static final Logger logger = Logger .getLogger(SearchServer.class.getName()); /* The port on which the server should run */ private int port = 50051; private Server server; private void start() throws Exception { server = ServerBuilder.forPort(port) .addService(SearchServiceGrpc.bindService(new SearchServiceImpl())) .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"); SearchServer.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 Exception { final SearchServer searchServer = new SearchServer(); searchServer.start(); searchServer.blockUntilShutdown(); } }
总结:
1.gRPC使用protobuf定义消息格式,使消息的序列化和反序列高效,并且序列化后数据小,占用带宽小。
2.服务间通信的接口和消息格式通过IDL文件明确定义。
3.在上面服务器端实现中,当启动服务器时,可以实现服务注册,或者说加入服务注册的逻辑,比如在zk上注册服务,从而做到客户端的服务发现。
4.在客户端中,可以加入服务发现的逻辑,从而实现服务的高可用。
5.gRPC基于netty实现的HTTP2 通信协议,对于后端的分布式微服务化,可以脱离具体的Servlet容器或Java EE服务器,更加轻便,同时可以嵌入jetty等嵌入式的Servlet容器。
6.通过zk实现服务注册和服务发现,实现服务的治理中心。
7.相对于spring mvc实现的后端的服务化接口,省略了controller层实现,客户端直接通过stub调用服务器端的逻辑。
=========END=========