Grpc在Android的使用

grpc简介

gRPC是由Google主导开发的RPC(Remote Procedure Call:远程过程调用协议)框架,使用HTTP/2协议并用ProtoBuf作为序列化工具。其客户端提供Objective-C、Java接口,服务器侧则有Java、Golang、C++等接口,从而为移动端(iOS/Androi)到服务器端通讯提供了一种解决方案。

最好提前了解RPC原理, 可参考如下文章:RPC原理及RPC实例分析

grpc的集成

  • app的gradle文件中

最顶部添加

apply plugin: 'com.google.protobuf'

添加protobuf编译器

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.4.0"
    }
    plugins {
        javalite {
            artifact = "com.google.protobuf:protoc-gen-javalite:3.0.0"
        }
        grpc {
            artifact = 'io.grpc:protoc-gen-grpc-java:1.7.0'
        }
    }
    generateProtoTasks {
        all().each { task ->
            task.plugins {
                javalite {}
                grpc {
                    // Options added to --grpc_out
                    option 'lite'
                }
            }
        }
    }
    configurations.all {
        resolutionStrategy.force 'com.google.code.findbugs:jsr305:1.3.9'
    }
}

添加依赖库文件

compile 'io.grpc:grpc-okhttp:1.7.0'
compile 'io.grpc:grpc-protobuf-lite:1.7.0'
compile 'io.grpc:grpc-stub:1.7.0'
compile 'javax.annotation:javax.annotation-api:1.2'
  • project的gradle文件
    project配置文件如下:
apply plugin: 'java'
buildscript {

    repositories {
        maven { url 'https://maven.google.com' }
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.3'
        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.1'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        maven { url 'https://maven.google.com' }
        jcenter()
    }
}
  • 添加权限
<uses-permission android:name="android.permission.INTERNET"/>
  • proto生成Java文件
    (1) 把自己的proto文件复制粘贴到main/proto目录下,点击Android Studio中的Build菜单下的Rebuild Project即可
    (2) Java文件生成位置:app/build/generated/source/proto/……
    (3) 将Java文件复制出来即可使用

protobuf语法

  • 指定proto语法版本
    在proto文件第一行添加:
syntax = "proto3"
  • 定义message
    (1)定义: 一个message相当于java中的实体类,里面定义了不同数据类型的数据,并且在结尾处标上标签序号
    (2)嵌套:一个message内部也可以定义message类型的数据
    (3)字段格式:修饰符 参数类型 参数名称=字段编码值 [字段默认值]
    (4)proto3字段格式特性:不允许加修饰符,不允许加字段默认值
    例子:
message Reply{
 // 修饰符 参数类型 参数名称=标识符 [字段默认值]
    bool Result = 1;
    string UUID = 2;
    string Token = 3;
    string extra = 4;

    //这里定义了一个Info message格式的数据
    Info info = 5;
}

message Info{
    string id = 1;
    string name = 2;
    int32 age = 3;
}
  • service 定义
    (1)定义访问服务端的函数名称,传递的参数,返回的参数
    (2)一个service可以定义多个待调用的函数
    例子:
service TestService {

    //格式: rpc 调用函数名 (传递请求的参数) returns (返回的参数) {}
    rpc sayHello (Request) returns (Reply) {}

    //这是另一个sayHello2函数
    rpc sayHello2 (Request) returns (Reply) {}
}
  • 保留标识符(reserved)
    message中每一个字段都对应有一个标识符(1,2,3,4,5,6…), 当版本更新的时候,删除或者注释掉某一个字段的时候,保留标识符可以让这个标识号不会被新的字段名称使用,这样避免了bug等等

例如:在上面的Info这个message中,age这个字段的标识符是3,如果在下一个版本V2中,我们将age删除了,不要了,添加了一个叫sex的字段
(1)如果我们对3这个标识符做保留操作, sex这个字段标识符不能是3,否则编译不通过,这样是正确的,合理的;
(2)如果我们对3这个标识符不做保留操作,sex这个字段标识符或者其它新的字段使用3这个标识符,会导致前后V1和V2两个版本3这个标识符代表的字段不一致,出现其它Bug

例子:

message Persion{
    string id = 1;
    string name = 2;

    //保留3,15,9,10,11这几个标识符不能被使用
    reserved  3, 15, 9 to 11;
}
  • 数据格式和Java语言格式对应表
protojava备注
doubledouble
floatfloat
int32int使用变长编码,对于负值的效率很低,如果你的域有可能有负值,请使用sint64替代
uint32int使用变长编码
uint64long使用变长编码
sint32int使用变长编码,这些编码在负值时比int32高效的多
sint64long使用变长编码,有符号的整型值。编码时比通常的int64高效。
sfixed32int总是4个字节
sfixed64long总是8个字节
fixed32int总是4个字节,如果数值总是比总是比228大的话,这个类型会比uint32高效。
fixed64long总是8个字节,如果数值总是比总是比256大的话,这个类型会比uint64高效。
boolboolean
stringString一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。
bytesByteString可能包含任意顺序的字节数据。

还支持了枚举类型

  • 数据格式默认值
    (1)对于strings,默认是一个空string
    (2)对于bytes,默认是一个空的bytes
    (3)对于bools,默认是false
    (4)对于数值类型,默认是0
    (5)对于枚举,默认是第一个定义的枚举值,必须为0;
    (6)对于消息类型(message),域没有被设置,确切的消息是根据语言确定的

  • option可选项

//是否运行生成多个java文件
option java_multiple_files = false;

//这个选项表明生成java类所在的包。如果在.proto文件中没有明确的声明java_package,就采用默认的包名
option java_package = “com.example.administrator.grpctest.proto”;

//生成的java类名字
option java_outer_classname = “TestProto”;

//生成方式:可以被设置为 SPEED, CODE_SIZE,or LITE_RUNTIME。这些值将通过如下的方式影响C++及java代码的生成:
option optimize_for = SPEED;

(1)SPEED (default): protocol buffer编译器将通过在消息类型上执行序列化、语法分析及其他通用的操作。这种代码是最优的。

(2)CODE_SIZE: protocol buffer编译器将会产生最少量的类,通过共享或基于反射的代码来实现序列化、语法分析及各种其它操作。采用该方式产生的代码将比SPEED要少得多, 但是操作要相对慢些。当然实现的类及其对外的API与SPEED模式都是一样的。这种方式经常用在一些包含大量的.proto文件而且并不盲目追求速度的 应用中。

(3)LITE_RUNTIME: protocol buffer编译器依赖于运行时核心类库来生成代码(即采用libprotobuf-lite 替代libprotobuf)。这种核心类库由于忽略了一 些描述符及反射,要比全类库小得多。这种模式经常在移动手机平台应用多一些。编译器采用该模式产生的方法实现与SPEED模式不相上下,产生的类通过实现 MessageLite接口,但它仅仅是Messager接口的一个子集

  • 导包使用
    场景:test.proto中调用test2.proto文件中的DeviceInfo这个message,test2.proto的包名是 test2package
    (1)import “test2.proto”; 添加你需要调用哪一个proto文件
    (2)在test.proto中调用: test2package.DeviceInfo deviceInfo = 3;
    例子:
syntax = "proto3";
package testpackge;

//导包
import "test2.proto"
message Info{
    string id = 1;

    //调用Message
    test2package.DeviceInfo deviceInfo = 2;
}
  • proto使用例子
    调用服务器sayHello函数为例:
syntax = "proto3";

option java_multiple_files = false;
option java_package = "com.example.administrator.grpctest.proto";
option java_outer_classname = "HelloWorldProto";
option optimize_for = CODE_SIZE;

//service package name
package proto;

//服务端中的HelloWorld这个接口,这个接口中可以包含多个方法
service HelloWorld {
  rpc sayHello (RequestP) returns (ReplyP) {}
}

message ReplyP{
    bool Result = 1;
    string UUID = 2;
    string Token = 3;
    string extra = 4;
}

message RequestP{
    string UUID = 1;
    string Token = 2;
    PersionInfo persionInfo = 3;
    reserved  15, 9 to 11;
}

message PersionInfo{
    string id = 1;
    string name = 2;
    int32 age = 3;
    enmu Sex{
        int32 m = 0;
        int32 w = 1;
    }
    Sex sex = 4;
}

grpc源码分析

以调用登录接口为例
1. 阻塞调用

HelloWorldProto.LoginReply reply = platformStub.login(loginRequest)

->  blockingUnaryCall(getChannel(), METHOD_LOGIN, getCallOptions(), request)

->      ListenableFuture<RespT> responseFuture = futureUnaryCall(call, param);  //然后线程一直在这里阻塞线程

->          asyncUnaryRequestCall(call, param, new UnaryStreamToFuture<RespT>(responseFuture), false);  //在这里等待数据返回到responseFuture中

->              startCall(call, responseListener, streamingResponse);   //开始调用
                call.sendMessage(param);                                //并且调用sendMessage将数据发送出去

->                  call.start(responseListener, new Metadata());       //调用服务端

->                      public abstract void start(Listener<RespT> responseListener, Metadata headers);  //服务端

2.非阻塞调用

sPlatformLoginStub.login(loginRequest, new StreamObserver<HelloWorldProto.LoginReply>() {
    @Override
    public void onNext(HelloWorldProto.LoginReply value) {
        Log.e("wnw", "next:" + System.currentTimeMillis());
    }

    @Override
    public void onError(Throwable t) {
        Log.e("wnw", "error");
    }

    @Override
    public void onCompleted() {
        Log.e("wnw", "complete:" + System.currentTimeMillis());
    }
});

->  asyncUnaryCall(getChannel().newCall(METHOD_LOGIN, getCallOptions()), request, responseObserver);

->      asyncUnaryRequestCall(call, param, observer, false);

->          asyncUnaryRequestCall(call,param,new StreamObserverToCallListenerAdapter<ReqT, RespT>(responseObserver,
            new CallToStreamObserverAdapter<ReqT>(call),streamingResponse),streamingResponse);                         //在这里将responseObserver传递进去

->              startCall(call, responseListener, streamingResponse);   //开始调用ClientCall的startCall     
                call.sendMessage(param);                                //并且调用sendMessage将数据发送出去                

->                  call.start(responseListener, new Metadata());      //调用服务端, 

->                      public abstract void start(Listener<RespT> responseListener, Metadata headers);  //服务端  

从上面跟踪的代码逻辑看,后面调用的逻辑是一致的,主要的区别在:

  • 阻塞方式传递的是:UnaryStreamToFuture 变量,其继承了ClientCall.Listener
  • 非阻塞方式传递的是:StreamObserverToCallListenerAdapter, 它也是继承了ClientCall.Listener
  • ClientCall.Listener是服务端返回数据调用的回调接口,将数据传递回来

下面研究UnaryStreamToFuture和StreamObserverToCallListenerAdapter的区别之处:
1. 先看 ClientCall.Listener文件中定义了什么

public abstract class ClientCall<ReqT, RespT> {

  public abstract static class Listener<T> {

    //服务器相应数据返回的头部信息
    public void onHeaders(Metadata headers) {}

    //服务器返回的message
    public void onMessage(T message) {}

    //ClientCall close,status == 0, 代表OK
    public void onClose(Status status, Metadata trailers) {}

    //ClientCall现在能够发送额外的消息
    public void onReady() {}
  }

    ........
}

2.再看StreamObserverToCallListenerAdapter, 在这里面,重写了ClientCall.Listener的这几个函数
(1)在 onMessage()函数中:调用了observer.onNext(message); //这里直接将数据交给了观察者
(2)在 onClose()函数中:

if (status.isOk()) {
        observer.onCompleted();                              //调用观察者结束
} else {
        observer.onError(status.asRuntimeException(trailers));  //调用观察者错误
}

3.在看UnaryStreamToFuture函数,在这里面,也重写了ClientCall.Listener的这几个函数
(1)在onMessage()函数中:this.value = value; 在这里把服务器返回的value存下来
(2)在onClose()函数中:

    if (status.isOk()) {
        if (value == null) {
            // No value received so mark the future as an error
            responseFuture.setException(
            Status.INTERNAL.withDescription("No value received for unary call")
                .asRuntimeException(trailers));
            }
            responseFuture.set(value);  //在这里将数据设置responseFuture返回
        } else {
            responseFuture.setException(status.asRuntimeException(trailers));
        }
    }

原来同步和异步的回调的差别就在这里,二者都是监听服务器返回的函数onMessage(), onClose()函数,如果是异步,就调用观察者将数据通过onNext()函数返回,如果是同步,就将数据直接返回。

参考

google protobuf 定义服务(service)
Protobuf3语言指南
RPC原理及RPC实例分析

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Android使用 gRPC,您需要进行以下步骤: 1. 在您的 Android 项目中添加 gRPC 和 protobuf 的依赖项。您可以在项目的 build.gradle 文件中添加以下代码: ``` implementation 'io.grpc:grpc-okhttp:1.40.1' implementation 'io.grpc:grpc-protobuf-lite:1.40.1' implementation 'io.grpc:grpc-stub:1.40.1' implementation 'com.google.protobuf:protobuf-java:3.18.0' ``` 2. 定义您的 gRPC 服务和消息类型的.proto 文件。您可以使用 Protocol Buffers 语言来定义它们。例如,创建一个名为 "example.proto" 的文件,并在其中定义服务和消息类型。 示例 "example.proto" 文件内容: ```protobuf syntax = "proto3"; package com.example.grpc; service ExampleService { rpc ExampleMethod (ExampleRequest) returns (ExampleResponse) {} } message ExampleRequest { string message = 1; } message ExampleResponse { string reply = 1; } ``` 3. 在您的项目中运行 Protocol Buffers 编译器,以生成 Java 类。您可以使用 `protoc` 命令行工具或 Gradle 插件来完成此操作。例如,使用 Gradle 插件可以在项目的 build.gradle 文件中添加以下代码: ``` protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.18.0' } plugins { grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.40.1' } } generateProtoTasks { all()*.plugins { grpc {} } } } ``` 然后,在命令行中运行 `./gradlew generateProto` 命令,以生成 Java 类。 4. 创建 gRPC 客户端代码。您可以使用生成的 Java 类来创建客户端代码,并与 gRPC 服务进行通信。在您的 Android 项目中,您可以创建一个 gRPC 客户端类,以处理与服务器的通信。 示例 gRPC 客户端类: ```java import com.example.grpc.ExampleRequest; import com.example.grpc.ExampleResponse; import com.example.grpc.ExampleServiceGrpc; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.grpc.stub.StreamObserver; public class GrpcClient { private final ManagedChannel channel; private final ExampleServiceGrpc.ExampleServiceStub stub; public GrpcClient(String host, int port) { channel = ManagedChannelBuilder.forAddress(host, port) .usePlaintext() .build(); stub = ExampleServiceGrpc.newStub(channel); } public void callExampleMethod(String message, StreamObserver<ExampleResponse> responseObserver) { ExampleRequest request = ExampleRequest.newBuilder() .setMessage(message) .build(); stub.exampleMethod(request, responseObserver); } public void shutdown() throws InterruptedException { channel.shutdown().awaitTermination(5, TimeUnit.SECONDS); } } ``` 这是一个简单的示例,演示了如何创建 gRPC 客户端并调用服务的方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值