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 办法先运转服务端。

然后构建客户端目标,调用恳求,最后经过断言验证成果。

Demo 仓库

github.com/JChunyu/Grp…