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.gradle
的 android
块中增加以下装备:
android {
// ...
packagingOptions {
resources.excludes.add("META-INF/*")
}
}
到此,整个项目的构建就完成了。
代码完成
本地服务器完成
完成本地服务器代码分为以下部分:
- 界说一个服务;
- 服务中完成 proto 界说的办法,回来 proto 中界说的回来值;
- 构建一个 Server;
- 为 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 办法先运转服务端。
然后构建客户端目标,调用恳求,最后经过断言验证成果。