java grpc实例分析
一、Protocol Buffer
我们还是先给出一个在实际开发中经常会遇到的系统场景。比如:我们的客户端程序是使用Java开发的,可能运行自不同的平台,如:Linux、Windows或者是Android,而我们的服务器程序通常是基于Linux平台并使用C++开发完成的。在这两种程序之间进行数据通讯时存在多种方式用于设计消息格式,如:
1. 直接传递C/C++语言中一字节对齐的结构体数据,只要结构体的声明为定长格式,那么该方式对于C/C++程序而言就非常方便了,仅需将接收到的数据按照结构体类型强行转换即可。事实上对于变长结构体也不会非常麻烦。在发送数据时,也只需定义一个结构体变量并设置各个成员变量的值之后,再以char*的方式将该二进制数据发送到远端。反之,该方式对于Java开发者而言就会非常繁琐,首先需要将接收到的数据存于ByteBuffer之中,再根据约定的字节序逐个读取每个字段,并将读取后的值再赋值给另外一个值对象中的域变量,以便于程序中其他代码逻辑的编写。对于该类型程序而言,联调的基准是必须客户端和服务器双方均完成了消息报文构建程序的编写后才能展开,而该设计方式将会直接导致Java程序开发的进度过慢。即便是Debug阶段,也会经常遇到Java程序中出现各种域字段拼接的小错误。
2. 使用SOAP协议(WebService)作为消息报文的格式载体,由该方式生成的报文是基于文本格式的,同时还存在大量的XML描述信息,因此将会大大增加网络IO的负担。又由于XML解析的复杂性,这也会大幅降低报文解析的性能。总之,使用该设计方式将会使系统的整体运行性能明显下降。
对于以上两种方式所产生的问题,Protocol Buffer均可以很好的解决,不仅如此,Protocol Buffer还有一个非常重要的优点就是可以保证同一消息报文新旧版本之间的兼容性。
Protocol Buffers是一个跨语言、跨平台的具有可扩展机制的序列化数据工具。也就是说,我在ubuntu下用python语言序列化一个对象,并使用http协议传输到使用java语言的android客户端,java使用对用的代码工具进行反序列化,也可以得到对应的对象。听起来好像跟json没有多大区别。。。其实区别挺多的。
Google说protobuf是smaller,faster,simpler,我们使用google规定的proto协议定义语言,之后使用proto的工具对代码进行“编译”,生成对应的各个平台的源代码,我们可以使用这些源代码进行工作。
二 、GRPC基本框架
gRPC就是其中的一种RPC框架。
如上图所示,在gRPC中,客户端应用程序可以就像调用本地对象方法一样直接调用不同服务器上的应用程序方法,使您更容易创建分布式应用程序和服务。与许多RPC系统一样,gRPC基于定义服务的思想,定义可以远程调用的方法,包括方法的参数和返回类型。在服务器端,服务器实现此接口并运行一个gRPC服务器来处理客户端调用。在客户端,客户端有一个“存根stub”(简称为某些语言的客户端),提供与服务器相同的方法。所有的数据传输都使用protobuf。
关于protobuf的格式,可以参考:http://www.cnblogs.com/stephen-liu74/archive/2013/01/02/2841485.html
三、grpc-java示例
该例子主要实现了,客户端向服务端添加手机号码的功能。
注意:经过实验中央maven仓库有些jar下载不下来,最好用阿里的仓库,添加方法见:
http://www.cnblogs.com/boshen-hzb/p/6590277.html
1、新建maven工程grpc-demo
添加grpc1.0 maven依赖
<repositories> <repository> <!-- Maven 自带的中央仓库使用的Id为central 如果其他的仓库声明也是用该Id 就会覆盖中央仓库的配置 --> <id>nexus-aliyun</id> <name>Nexus aliyun</name> <url>http://maven.aliyun.com/nexus/content/groups/public/</url> <layout>default</layout> <releases> <enabled>true</enabled> </releases> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories> <modelVersion>4.0.0</modelVersion> <groupId>com.grpc.addphone</groupId> <artifactId>grpc-addphone</artifactId> <version>0.0.1-SNAPSHOT</version> <name>grpcAddPhone</name> <properties> <grpc.version>1.0.3</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> </dependencies>
配置protobuf 插件,可以自动将.proto文件生成对应的java代码,一旦生成代码以后最好将protobuf插件注释掉,这样是为了,在以后mvn install等命令时,重复生成代码。
如下面加粗部分:
<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.0.2:exe:${os.detected.classifier}</protocArtifact> <pluginId>grpc-java</pluginId> <pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact> <protoSourceRoot>${basedir}/src/main/proto</protoSourceRoot> <!-- <outputDirectory>${basedir}/src/main/java</outputDirectory> --> <!-- <attachProtoSources>true</attachProtoSources> --> </configuration> <executions> <execution> <goals> <goal>compile</goal> <goal>compile-custom</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
2、定义proto,将文件放到/src/main/proto目录下
addphone.proto
syntax = "proto3"; option java_multiple_files = true; option java_package = "com.grpc.addphone"; option java_outer_classname = "addphoneProto"; package addphone; enum PhoneType { HOME = 0; WORK = 1; OTHER = 2; } message ProtobufUser { int32 id = 1; string name = 2; message Phone{ PhoneType phoneType = 1; string phoneNumber = 2; } repeated Phone phones = 3; } message AddPhoneToUserRequest{ int32 uid = 1; PhoneType phoneType = 2; string phoneNumber = 3; } message AddPhoneToUserResponse{ bool result = 1; } service PhoneService { rpc addPhoneToUser(AddPhoneToUserRequest) returns (AddPhoneToUserResponse); }
3、执行mvn install则会根据上面的proto文件定义在target下生成java文件,生成目录如下所示:
4、将生成的/target/generated-sources/protobuf/里面的grpc-java和java里面的com复制到src/main/java里面。
5、新建包com.service、com.server、com.client
6、在com.service里面新建PhoneServiceImp类,用来处理客户端发过来的请求。
package com.service; import com.grpc.addphone.PhoneServiceGrpc; import com.grpc.addphone.AddPhoneToUserRequest; import com.grpc.addphone.AddPhoneToUserResponse; import io.grpc.stub.StreamObserver; public class PhoneServiceImp extends PhoneServiceGrpc.PhoneServiceImplBase{ @Override public void addPhoneToUser(AddPhoneToUserRequest request, StreamObserver<AddPhoneToUserResponse> responseObserver) { // TODO Auto-generated method stub AddPhoneToUserResponse response = null; if(request.getPhoneNumber().length() == 11 ){ System.out.printf("uid = %s , phone type is %s, nubmer is %s\n", request.getUid(), request.getPhoneType(), request.getPhoneNumber()); response = AddPhoneToUserResponse.newBuilder().setResult(true).build(); }else{ System.out.printf("The phone nubmer %s is wrong!\n",request.getPhoneNumber()); response = AddPhoneToUserResponse.newBuilder().setResult(false).build(); } responseObserver.onNext(response); responseObserver.onCompleted(); } }
代码很简单,我们只是检查手机号是不是11位,如果是把客户端的请求参数打印出来,给客户端返回true,如果不是11位,提示手机号错误,给客户端返回false。
7、在com.server包里面创建GRpcServer类,该类主要是负责启动服务端监听线程,而真正的处理类是PhoneServiceImp
package com.server; import io.grpc.Server; import io.grpc.ServerBuilder; import java.io.IOException; import java.util.logging.Logger; import com.service.PhoneServiceImp; public class GRpcServer { private static final Logger logger = Logger.getLogger(GRpcServer.class.getName()); private Server server; private void start() throws IOException { /* The port on which the server should run */ int port = 50051; server = ServerBuilder .forPort(port) .addService(new PhoneServiceImp()) .build() .start(); logger.info("Server started, listening on " + port); //关闭钩子本质是一个线程(也称为Hook线程),用来监听jvm的关闭。通过Runtime的addShutdownHook可以向JVM注册一个关闭钩子。Hook线程在JVM正常关闭才会执行,强制关闭时不会执行。 Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { System.err.println("*** shutting down gRPC server since JVM is shutting down"); GRpcServer.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 IOException, InterruptedException { final GRpcServer server = new GRpcServer(); server.start(); server.blockUntilShutdown(); } }
8、在com.client包里面新建GRpcClient
package com.client; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.grpc.StatusRuntimeException; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import com.grpc.addphone.PhoneServiceGrpc; import com.grpc.addphone.AddPhoneToUserRequest; import com.grpc.addphone.AddPhoneToUserResponse; import com.grpc.addphone.PhoneType; public class GRpcClient { private static final Logger logger = Logger.getLogger(GRpcClient.class.getName()); private final ManagedChannel channel; private final PhoneServiceGrpc.PhoneServiceBlockingStub blockingStub; /** Construct client connecting to gRPC server at {@code host:port}. */ public GRpcClient(String host, int port) { ManagedChannelBuilder<?> channelBuilder = ManagedChannelBuilder.forAddress(host, port).usePlaintext(true); channel = channelBuilder.build(); blockingStub = PhoneServiceGrpc.newBlockingStub(channel); } public void shutdown() throws InterruptedException { channel.shutdown().awaitTermination(5, TimeUnit.SECONDS); } /** add phone to user. */ public void addPhoneToUser(int uid, PhoneType phoneType, String phoneNubmer) { logger.info("Will try to add phone to user " + uid); AddPhoneToUserRequest request = AddPhoneToUserRequest.newBuilder().setUid(uid).setPhoneType(phoneType) .setPhoneNumber(phoneNubmer).build(); AddPhoneToUserResponse response; try { response = blockingStub.addPhoneToUser(request); } catch (StatusRuntimeException e) { logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus()); return; } logger.info("Result: " + response.getResult()); } public static void main(String[] args) throws Exception { GRpcClient client = new GRpcClient("localhost", 50051); try { client.addPhoneToUser(1, PhoneType.WORK, "13888888888"); } finally { client.shutdown(); } } }