gRPC初探

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=========

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值