gRPC是google新开源的一个基于protobuf的rpc框架,使用通信协议为HTTP2,网络通信层基于netty实现;
它首先提供移动客户端的rpc功能,同时也是一个通用的rpc框架。
下面是我做的一个简单的gRPC的demo。
通过IDL定义服务接口和消息格式
如下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 显得更干净和整洁。
由IDL文件生成服务器端和客户端代码
这里使用的构建工具时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 SearchWithSimpleRpc (SearchRequest) returns (SearchResponse) {};
这种方式是最简单的一种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 SearchWithServerSideStreamRpc (SearchRequest) returns (stream SearchResponse) {};
服务器端的 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 SearchWithClientSideStreamRpc (stream SearchRequest) returns (SearchResponse) {};
这种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 SearchWithBidirectionalStreamRpc(stream SearchRequest) returns (stream SearchResponse) {};
这种方式实现的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=========