gRPC on Kotlin Android

gRPC 是由 Google 开发的高性能、开源的远程过程调用(RPC)框架,它基于 HTTP/2 协议进行通信,并使用 Protocol Buffers 作为默认的序列化工具。gRPC 支持多种编程语言,包括 C、C++、Java、Python、Go、C# 等,使得开发者可以在不同的平台上轻松地构建分布式系统。

本文为您从 0 开始搭建 kotlin 语言下的 gRPC 运行环境。

Reference

官方文档:grpc.io/docs/langua…]

官方 Sample 的理解

proto

gRPC 与所有的 RPC 一样,是 C/S 架构 + 通信协议为组成部分。其中 gRPC 的协议是通过 proto 文件来定义的。

Protocol Buffers(protobuf)是一种轻量级、高效的数据序列化格式,由 Google 开发并开源。它被设计用于高效地序列化结构化数据,并支持多种编程语言。

Protocol Buffers 被广泛用于分布式系统中,例如用于定义 RPC 服务的消息格式、网络通信协议的数据交换格式等。它是一种通用且高效的数据序列化方案,适用于各种不同的场景。

以官方的 Sample 为例,下面的 .proto 文件描述了一个服务:

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

定义了一个 Greeter 服务,它提供一个 SayHello 方法,服务端和客户端的实现类都会实现该方法。

客户端侧,可以传递一个 HelloRequest 类型的请求,其中包含一个类型为字符串的参数 name;服务端侧会返回一个 HelloReply 类型的响应,其中包含一个字符串类型的 massge 属性值。

服务端

private class HelloWorldService : GreeterGrpcKt.GreeterCoroutineImplBase() {
  override suspend fun sayHello(request: HelloRequest) = helloReply {
    message = "Hello ${request.name}"
  }
}

grpc 会根据 proto 文件自动生成一些代码,例如上面这段代码中的 GreeterGrpcKt、GreeterCoroutineImplBase、HelloRequest 等。

在服务端的 sayHello 方法中,实现服务端内部的逻辑最终返回一个 HelloReply 类型的消息。

客户端

class HelloWorldClient(
    private val channel: ManagedChannel
) : Closeable {
  private val stub: GreeterCoroutineStub = GreeterCoroutineStub(channel)

  suspend fun greet(name: String) {
    val request = helloRequest { this.name = name }
    val response = stub.sayHello(request)
    println("Received: ${response.message}")
  }
}

客户端这边简单演示了如何调用自动生成的 GreeterCoroutineStub 对象的 sayHello 方法发出请求,并接受响应结果。

项目搭建

新建一个 Android 项目,准备开始搭建 grpc-kotlin 语言的实现。

添加依赖

在 app 的 build.gradle 模块下,增加以下依赖(如果是 library 建议使用 api 替换 implementation):

implementation("io.grpc:grpc-stub:grpc_version")
implementation("io.grpc:grpc-protobuf-lite:grpc_version")
implementation("io.grpc:grpc-kotlin-stub:XXX")
implementation("com.google.protobuf:protobuf-kotlin-lite:XXX")
// 根据实际需要可选
implementation("io.grpc:grpc-okhttp:grpc_version")
implementation("io.grpc:grpc-netty:grpc_version")

增加插件:

plugins {
    // ...
    id 'com.google.protobuf' version('X.X.X')
}

编译 proto 文件需要在 build.gradle 中增加以下内容,注意不用放在任何 block 下,与 plugin/android 平级 :

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.25.3"
    }
    plugins {
        create("java") {
            artifact = "io.grpc:protoc-gen-grpc-java:1.60.2"
        }
        create("grpc") {
            artifact = "io.grpc:protoc-gen-grpc-java:1.60.2"
        }
        create("grpckt") {
            artifact = "io.grpc:protoc-gen-grpc-kotlin:1.4.1" + ":jdk8@jar"
        }
    }
    generateProtoTasks {
        all().forEach {
            it.plugins {
                create("java") {
                    option("lite")
                }
                create("grpc") {
                    option("lite")
                }
                create("grpckt") {
                    option("lite")
                }
            }
            it.builtins {
                create("kotlin") {
                    option("lite")
                }
            }
        }
    }
}

需要注意的是,这里不能省略 “java”、“grpc” 这两部分内容,否则编译会报错缺少一些自动生成的类型。

增加 proto 文件

可以用多种形式在 Android 项目中导入 proto 文件,常见方式是在 app/src/main/ 下新建一个 proto 目录,然后把所有 .proto 文件放在该目录下,另一种方式就是参考 sample 代码,通过独立的一个 module,其他模块引用该 module 依赖。

这里添加了一个 sample 中的示例:

syntax = "proto3";
import "google/protobuf/empty.proto";

option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";

package helloworld;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}

  rpc SayGreet (google.protobuf.Empty) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

这里与 sample 不同的是,增加了一个 google.protobuf.Empty 作为参数,这个类型的定义在 Google 通用的 protobuf 文件中,通过

import "google/protobuf/empty.proto";

来引用,并通过完整包名来使用,这样构建时才不会出现问题。

在 proto 文件中,以下配置不会对 proto 文件跨平台产生影响,只是配置 java 环境下生成内容的一些属性,比如包名、类名等。

option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";

在 C++ 或其他语言会忽略 java 的这些配置。

此时,就可以运行 build 来构建项目了,但此时 build 项目,依然会报错:

10 files found with path 'META-INF/INDEX.LIST'.
Adding a packaging block may help, please refer to
https://developer.android.com/reference/tools/gradle-api/8.3/com/android/build/api/dsl/Packaging
for more information

build.gradleandroid 块中增加以下配置:

android {
		// ... 
		packagingOptions {
        resources.excludes.add("META-INF/*")
    }
}

到此,整个项目的构建就完成了。

代码实现

本地服务器实现

实现本地服务器代码分为以下部分:

  1. 定义一个服务;
  2. 服务中实现 proto 定义的方法,返回 proto 中定义的返回值;
  3. 构建一个 Server;
  4. 为 Server 实现生命周期逻辑:启动、停止、阻塞等。

第一步,定义服务:

class HelloWorldServer {
    internal class HelloWorldService : GreeterGrpcKt.GreeterCoroutineImplBase() {
        override suspend fun sayHello(request: HelloRequest) = helloReply {
						// TODO
				}

        override suspend fun sayGreet(request: Empty) = helloReply {
            // TODO
        }
    }
}

这里的 GreeterGrpcKt.GreeterCoroutineImplBase 是 grpc 自动生成的代码类型。

第二步,实现返回逻辑:

    internal class HelloWorldService : GreeterGrpcKt.GreeterCoroutineImplBase() {
        override suspend fun sayHello(request: HelloRequest) =
            helloReply {
                message = "Hello ${request.name}"
            }

        override suspend fun sayGreet(request: Empty) = helloReply {
            message = "Hello Empty"
        }
    }

只需要定义返回值即可。

第三步,构建 Server:

    val server: Server =
        ServerBuilder
            .forPort(port)
            .addService(HelloWorldService())
            .build()

第四步,server 的生命周期:

    fun start() {
        server.start()
        println("Server started, listening on $port")
        Runtime.getRuntime().addShutdownHook(
            Thread {
                println("*** shutting down gRPC server since JVM is shutting down")
                this@HelloWorldServer.stop()
                println("*** server shut down")
            },
        )
    }

    private fun stop() {
        server.shutdown()
    }

    fun blockUntilShutdown() {
        server.awaitTermination()
    }

服务器启动
fun main() {
    val port = System.getenv("PORT")?.toInt() ?: 50051
    val server = HelloWorldServer(port)
    server.start()
    server.blockUntilShutdown()
}

在启动服务器时,应该会报错,需要添加以下依赖:

implementation("io.grpc:grpc-netty:1.60.2")
implementation("com.squareup.okio:okio:3.8.0")
implementation("io.perfmark:perfmark-api:0.27.0")

并更改服务端代码:

    private val server: Server =
        NettyServerBuilder
            .forPort(port)
            .addService(HelloWorldService())
            .build()

本地客户端实现

实现本地客户端实现非常简单:

class HelloWorldClient(private val channel: ManagedChannel) {
    private val stub: GreeterGrpcKt.GreeterCoroutineStub =
        GreeterGrpcKt.GreeterCoroutineStub(channel)

    suspend fun sayHello(name: String) {
        val request = helloRequest { this.name = name }
        val response = stub.sayHello(request)
        println("Received: ${response.message}")
    }

    suspend fun sayGreet() {
        val request = helloRequest { this.name = name }
        val response = stub.sayGreet(Empty.newBuilder().build())
        println("Received: ${response.message}")
    }
}

通过 ManagedChannel 对象,构建 grpc 自动生成的对应类的 Stub 对象,然后通过 Stub 对象调用 proto 服务中定义的 rpc 方法,接收返回值即可。

grpc-kotlin,会根据 proto 定义的 server 自动生成 XXXCoroutineStub,配合协程使用。

java 可以使用 XXXStub(异步)、XXXBlockingStub(同步)来实现客户端请求、接收响应的逻辑。

客户端发起请求
fun main() {
    runBlocking {
        val port = 50051
        val channel = ManagedChannelBuilder.forAddress("localhost", port).usePlaintext().build()
        val client = HelloWorldClient(channel)
        println("call sayHello")
        client.sayHello("user")
        while (true) {
            Thread.sleep(3000)
            println("time next")
        }
    }
}

Android 客户端实现

首先,上面构建本地客户端的形式适用于 Android 客户端直接去实现,另一种是使用 grpc-android,增加以下依赖:

implementation("io.grpc:grpc-android:1.62.2")

通过这个库中的 AndroidChannelBuilder 来构建 channel:

// val channel = ManagedChannelBuilder.forAddress("localhost", port).usePlaintext().build()
val androidChannel = AndroidChannelBuilder
            .forAddress("localhost", port)
            .context(this@MainActivity)
            .usePlaintext()
            .build()

AndroidChannelBuilder 用来构建一个 ManagedChannel,当提供了一个 Context 时,它会自动监控 Android 设备的网络状态,以平滑处理间歇性的网络故障。 目前仅兼容 gRPC 的 OkHttp 传输,在运行时必须可用。 需要 Android 的 ACCESS_NETWORK_STATE 权限。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        findViewById<Button>(R.id.hwBtn).setOnClickListener {
            sayHello()
        }
    }

    private fun sayHello() {
        val port = System.getenv("PORT")?.toInt() ?: 50051
        val androidChannel = AndroidChannelBuilder
            .forAddress("localhost", port)
            .context(this@MainActivity)
            .usePlaintext()
            .build()
        val client = HelloWorldClient(androidChannel)
        CoroutineScope(Dispatchers.IO).launch {
            client.sayHello("from android")
        }
    }
}

单元测试

单元测试首先要添加 gRPC 测试依赖:

testImplementation("io.grpc:grpc-testing:1.60.2")

为 Server 创建测试
class HelloWorldServerTest {
    @get:Rule
    val grpcServerRule: GrpcServerRule = GrpcServerRule().directExecutor()

    @Test
    fun sayHello() = runBlocking {
            val service = HelloWorldServer.HelloWorldService()
            grpcServerRule.serviceRegistry.addService(service)

            val stub = GreeterGrpcKt.GreeterCoroutineStub(grpcServerRule.channel)
            val testName = "test user"
            val reply = stub.sayHello(helloRequest { name = testName })
            assertEquals("Hello $testName", reply.message)
        }

    @Test
    fun sayGreet() = runBlocking {
        val service = HelloWorldServer.HelloWorldService()
        grpcServerRule.serviceRegistry.addService(service)

        val stub = GreeterGrpcKt.GreeterCoroutineStub(grpcServerRule.channel)
        val reply = stub.sayGreet(Empty.newBuilder().build())
        assertEquals("Hello Empty", reply.message)
    }

    @Test
    fun start() {
        val port = System.getenv("PORT")?.toInt() ?: 50051
        val server = HelloWorldServer(port)
        CoroutineScope(Dispatchers.IO).launch {
            delay(1000)
            val stub = GreeterGrpcKt.GreeterCoroutineStub(grpcServerRule.channel)
            val testName = "test user"
            val reply = stub.sayGreet(Empty.newBuilder().build())
            assertEquals("Hello Empty", reply.message)
        }
        server.start()
    }
}

使用 Junit4 和 grpc-testing 来构建测试代码,GrpcServerRule 是 grpc-testing 中的工具,GrpcServerRule 是一个 JUnit TestRule,对于测试基于 gRPC 的客户端和服务非常有用。

前两个方法是测试请求响应的逻辑;最后一个对服务生命周期的测试则需要考虑更多问题,例如服务器启动后,会一直处于阻塞状态,要确保请求是在服务器启动后才发出的,需要开发者模拟实际情境构建测试。

为客户端添加测试

首先为了方便把 HelloWorldClient 中的 sayHello 和 sayGreet 方法无返回值改为返回 HelloRely:

    suspend fun sayHello(name: String): HelloReply {
        val request = helloRequest { this.name = name }
        val response = stub.sayHello(request)
        println("Received: ${response.message}")
        return response
    }

    suspend fun sayGreet(): HelloReply {
        val response = stub.sayGreet(Empty.newBuilder().build())
        println("Received: ${response.message}")
        return response
    }

然后为这两个方法创建测试:

class HelloWorldClientKtTest {
    @get:Rule
    val grpcServerRule: GrpcServerRule = GrpcServerRule().directExecutor()

    @Before
    fun setup() {
        val service = HelloWorldServer.HelloWorldService()
        grpcServerRule.serviceRegistry.addService(service)
    }

    @Test
    fun sayHello() = runBlocking {
            val client = HelloWorldClient(grpcServerRule.channel)
            val testName = "test user"
            val reply = client.sayHello(testName)
            assertEquals("Hello $testName", reply.message)
        }

    @Test
    fun sayGreet() = runBlocking {
        val client = HelloWorldClient(grpcServerRule.channel)
        val reply = client.sayGreet()

        assertEquals("Hello Empty", reply.message)
    }
}

首先,在创建客户端测试之前要启动一个服务端,才能验证客户端的请求和返回结果的正确性。所以这里在所有测试方法之前添加 @Before 注解的 setup 方法先运行服务端。

然后构建客户端对象,调用请求,最后通过断言验证结果。

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
img
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓(文末还有ChatGPT机器人小福利哦,大家千万不要错过)

PS:群里还设有ChatGPT机器人,可以解答大家在工作上或者是技术上的问题
图片

  • 15
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值