Часть 1 — сервер gRPC и клиент Android в Kotlin
Если вы никогда раньше не знали о микросервисах в Kotlin и Grpc, я рекомендую вам выполнить поиск в Интернете по словам «Grpc» и «Kotlin», а затем просмотреть некоторые документы, которые помогут вам сначала понять их. Впоследствии в этой статье будет показано, как шаг за шагом создавать микросервисы с помощью Kotlin и gRPC.
Я разделю эту статью на 2 части:
Часть 1:
- Построить 1 сервер gRPC
- Собрать Android-клиент
Часть 2:
- Создавайте несколько серверов gRPC в одном проекте
- Собрать iOS-клиент
Теперь давайте начнем.
Для этого проекта я буду использовать Android Studio и установить Kotlin Multiplatform Mobile.
После установки плагина создайте новый проект и выберите «Kotlin Multiplatform App» -> «Далее» -> назовите проект KotlinGrpc
.
Примечание. Если вы не даете одно и то же имя пакета dandelion.net.kotlingrpc
, каждый раз, когда вы копируете и вставляете мой код, пожалуйста, позаботьтесь об имени вашего пакета, потому что легко получить ошибку.
Структура проекта KotlinGrpc
будет выглядеть так, как показано на рисунке ниже.
- Создание сервера gRPC
а. Конфигурация build.gradle
проекта.
Перейдите к KotlinGrpc/build.gradle.kts
(проект build.gradle)
plugins { id("com.google.protobuf") version "0.8.15" apply false kotlin("jvm") version "1.5.31" apply false id("com.android.application") version "7.2.1" apply false id("org.jetbrains.kotlin.android") version "1.5.31" apply false } ext["grpcVersion"] = "1.46.0" ext["grpcKotlinVersion"] = "1.3.0" // CURRENT_GRPC_KOTLIN_VERSION ext["protobufVersion"] = "3.20.1" allprojects { repositories { mavenLocal() mavenCentral() google() } }
ПРИМЕЧАНИЕ.Вы должны нажать "Синхронизировать сейчас"(уведомление вверху справа)после того, как вы отредактируете что-то в gradle.build.kts
б. Добавление нового модуля PROTOS
Щелкните правой кнопкой мыши корневую папку (папка KotlinGrpc) -> Создать -> Модуль
Выбор «Библиотека Java или Kotlin» — › имя библиотеки как «protos
»
Вы можете удалить некоторые лишние файлы, такие как MyClass
Перейдите к protos/build.gradle.kts
, добавьте конфигурацию, затем нажмите «Синхронизировать сейчас».
plugins { `java-library` } java { sourceSets.getByName("main").resources.srcDir("src/main/java") }
Затем перейдите к protos/src/main/java/dandelion.net.protos
, добавьте новый файл и назовите его hello_service.proto
.
Мы получим образец HelloWorldService из документа gRPC, он включает 1 запрос от клиента и 1 ответ от сервера.
Это определение прото-файла:
Сериализованные данные определяются в файлах конфигурации, называемых прото-файлами (.proto). В этих файлах будут храниться конфигурации, известные как сообщения. При компиляции протофайлов генерируется код на языке программирования пользователя.
syntax = "proto3"; option java_multiple_files = true; option java_package = "dandelion.net.protos"; option java_outer_classname = "HelloWorldService"; // 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; bool received = 2; }
в. Добавление нового модуля STUB
Щелкните правой кнопкой мыши корневую папку (папка KotlinGrpc) -> Создать -> Модуль -> выбор «Библиотека Java или Kotlin» -> имя библиотеки stub
Этот шаг — сила gRPC. Когда мы запустим проект, Google сгенерирует для нас код и сохранит его в модуле stub
. Пойдем:
stub/build.gradle.kts
:
Настройте зависимости API и установите sourceSets
(где файлы создаются автоматически)
import com.google.protobuf.gradle.generateProtoTasks
import com.google.protobuf.gradle.id
import com.google.protobuf.gradle.plugins
import com.google.protobuf.gradle.protobuf
import com.google.protobuf.gradle.protoc
plugins {
kotlin("jvm")
id("com.google.protobuf")
}
dependencies {
protobuf(project(":protos"))
api(kotlin("stdlib"))
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.2")
api("io.grpc:grpc-protobuf:${rootProject.ext["grpcVersion"]}")
api("com.google.protobuf:protobuf-java-util:${rootProject.ext["protobufVersion"]}")
api("io.grpc:grpc-kotlin-stub:${rootProject.ext["grpcKotlinVersion"]}")
}
java {
sourceCompatibility = JavaVersion.VERSIONKotlinGrpc
8
}
sourceSets {
val main by getting { }
main.java.srcDirs("build/generated/source/proto/main/grpc")
main.java.srcDirs("build/generated/source/proto/main/grpckt")
main.java.srcDirs("build/generated/source/proto/main/java")
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:${rootProject.ext["protobufVersion"]}"
}
plugins {
id("grpc") {
artifact = "io.grpc:protoc-gen-grpc-java:${rootProject.ext["grpcVersion"]}"
}
id("grpckt") {
artifact = "io.grpc:protoc-gen-grpc-kotlin:0.2.0:jdk7@jar"
}
}
generateProtoTasks {
all().forEach {
it.plugins {
id("grpc")
id("grpckt")
}
}
}
}
Теперь посмотрим на модуль «заглушки», когда мы еще не построили проект.
Затем нажмите «Создать проект» или нажмите значок «молоток» вверху, чтобы создать приложение, после чего у нас будет нужный файл.
д. Добавление нового модуля СЕРВЕР
Щелкните правой кнопкой мыши корневую папку (папка KotlinGrpc) -> Создать -> Модуль -> выбор «Библиотека Java или Kotlin» -> имя библиотеки Server
Как и в этих шагах выше, мы собираемся добавить конфигурацию в build.gradle.kts
.
Go to server/build.gradle.kts
:
plugins { application kotlin("jvm") } dependencies { implementation(project(":stub")) implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0") implementation("org.jetbrains.kotlinx", "kotlinx-coroutines-core", "1.5.2") runtimeOnly("io.grpc:grpc-netty:${rootProject.ext["grpcVersion"]}") } tasks.register<JavaExec>("HelloServer") { dependsOn("classes") classpath = sourceSets["main"].runtimeClasspath mainClass.set("dandelion.net.server.HelloServerKt") } val helloServerStartScripts = tasks.register<CreateStartScripts>("helloServerStartScripts") { mainClass.set("dandelion.net.server.HelloServerKt") applicationName = "hello-server" outputDir = tasks.named<CreateStartScripts>("startScripts").get().outputDir classpath = tasks.named<CreateStartScripts>("startScripts").get().classpath } tasks.named("startScripts") { dependsOn(helloServerStartScripts) } tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> { kotlinOptions { freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" } }
Затем перейдите к server/src/main/java/dandelion/net/server
, добавьте новый файл Kotlin и назовите его HelloServer.kt
. Обратите внимание, что вы можете удалить некоторые лишние файлы, такие как MyClass
.
package dandelion.net.server import dandelion.net.protos.GreeterGrpcKt import dandelion.net.protos.HelloReply import dandelion.net.protos.HelloRequest import io.grpc.Server import io.grpc.ServerBuilder import kotlinx.coroutines.asCoroutineDispatcher import java.util.concurrent.Executors class HelloServer(private val port: Int) { val server: Server = ServerBuilder .forPort(port) .addService(HelloService()) .build() fun start() { server.start() println("Server started, listening on $port") Runtime.getRuntime().addShutdownHook( Thread { [email protected]() println("*** server shut down") } ) } private fun stop() { server.shutdown() } fun blockUntilShutdown() { server.awaitTermination() } private class HelloService : GreeterGrpcKt.GreeterCoroutineImplBase( coroutineContext = Executors.newFixedThreadPool( 1 ).asCoroutineDispatcher()) { override suspend fun sayHello(request: HelloRequest) : HelloReply { return HelloReply.newBuilder() .setMessage("Hello " + request.name) .build() } } } fun main() { val port = System.getenv("PORT")?.toInt() ?: 8080 val server = HelloServer(port) server.start() server.blockUntilShutdown() }
Объяснять:
class HelloServer(private val port: Int)
— › этот класс строит сервер портов.private class HelloService : GreeterGrpcKt.GreeterCoroutineImplBase(…)
— › этот класс использует Kotlin-Coroutine для вызова функции, которую мы создаем в файле proto.GreeterGrpcKt
— это класс, который Google генерирует для нас внутри модуляstub
.
Теперь нажмите «кнопку запуска», чтобы запустить сервер, мы увидим, что сервер запускается на порту 8080.
2. Создание клиента Android
Давайте добавим зависимости в файл androidApp/build.gradle.kts
, здесь мы определим IP-адрес, к которому мы хотим подключиться с сервера (serverUrl
)
plugins { id("com.android.application") id("org.jetbrains.kotlin.android") } android { sourceSets["main"].java.srcDir("src/main/java") compileOptions { sourceCompatibility = JavaVersion.VERSIONKotlinGrpc
8 targetCompatibility = JavaVersion.VERSIONKotlinGrpc
8 } buildFeatures { compose = true } kotlinOptions { jvmTarget = "1.8" } composeOptions { kotlinCompilerExtensionVersion = composeVersion } packagingOptions { resources.excludes += "META-INF/kotlinx_coroutines_core.version" } compileSdk = 32 defaultConfig { applicationId = "dandelion.mobile.android" minSdk = 21 targetSdk = 32 versionCode = 1 versionName = "1.0" val serverUrl: String? by project if (serverUrl != null) { resValue("string", "server_url", serverUrl!!) } else { resValue("string", "server_url", "http://10.0.2.2:8080/") } testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } compileOptions { sourceCompatibility = JavaVersion.VERSIONKotlinGrpc
8 targetCompatibility = JavaVersion.VERSIONKotlinGrpc
8 } kotlinOptions { jvmTarget = "1.8" } } val composeVersion = "1.1.0" dependencies { implementation(project(":stub")) implementation(kotlin("stdlib")) implementation("androidx.activity:activity-compose:1.5.1") implementation("androidx.appcompat:appcompat:1.4.2") implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.2") implementation("androidx.compose.foundation:foundation-layout:$composeVersion") implementation("androidx.compose.material:material:$composeVersion") implementation("androidx.compose.runtime:runtime:$composeVersion") implementation("androidx.compose.ui:ui:$composeVersion") runtimeOnly("io.grpc:grpc-okhttp:${rootProject.ext["grpcVersion"]}") }
Далее начинаем строить UI с @Composable
, поэтому layout
нам не нужен, и удаляем папку layout
внутри androidApp/src/main/res
Go to androidApp/src/main/res/values/styles.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="name_hint">Name:</string> <string name="send_request">Send gRPC Request</string> <string name="app_label">gRPC Kotlin Android</string> <string name="server_response">Server response: </string> </resources>
Перейдите на страницу androidApp/src/main/AndroidManifest.xml
и добавьте разрешение INTERNET
.
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="dandelion.net.kotlingrpc.android"> <uses-permission android:name="android.permission.INTERNET"/> <application android:allowBackup="true" android:fullBackupContent="true" android:icon="@android:drawable/btn_star" android:label="@string/app_label"> <activity android:theme="@style/Theme.AppCompat.NoActionBar" android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application> </manifest>
Наконец, перейдите к androidApp/src/main/java/dandelion/net/kotlingrpc/android/MainActivity.kt
:
package dandelion.net.kotlingrpc.android import android.net.Uri import android.os.Bundle import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.Button import androidx.compose.material.MaterialTheme import androidx.compose.material.OutlinedTextField import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import dandelion.net.kotlingrpc.android.R import dandelion.net.protos.GreeterGrpcKt import dandelion.net.protos.HelloRequest import io.grpc.ManagedChannelBuilder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.asExecutor import kotlinx.coroutines.launch import java.io.Closeable class MainActivity : AppCompatActivity() { private val uri by lazy { Uri.parse((resources.getString(R.string.server_url))) } private val greeterService by lazy { GreeterRCP(uri) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Surface(color = MaterialTheme.colors.background) { Greeter(greeterService) } } } override fun onDestroy() { super.onDestroy() greeterService.close() } } class GreeterRCP(uri: Uri) : Closeable { val responseState = mutableStateOf("") private val channel = let { println("Connecting to ${uri.host}:${uri.port}") val builder = ManagedChannelBuilder.forAddress(uri.host, uri.port) if (uri.scheme == "https") { builder.useTransportSecurity() } else { builder.usePlaintext() } builder.executor(Dispatchers.IO.asExecutor()).build() } private val greeter = GreeterGrpcKt.GreeterCoroutineStub(channel) suspend fun sayHello(name: String) { try { val request = HelloRequest.newBuilder().setName(name).build() val response = greeter.sayHello(request) responseState.value = response.message } catch (e: Exception) { responseState.value = e.message ?: "Unknown Error" e.printStackTrace() } } override fun close() { channel.shutdownNow() } } @Composable fun Greeter(greeterRCP: GreeterRCP) { val scope = rememberCoroutineScope() val nameState = remember { mutableStateOf(TextFieldValue()) } Column(Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Top, Alignment.CenterHorizontally) { Text(stringResource(R.string.name_hint), modifier = Modifier.padding(top = 10.dp)) OutlinedTextField(nameState.value, { nameState.value = it }) Button({ scope.launch { greeterRCP.sayHello(nameState.value.text) } }, Modifier.padding(10.dp)) { Text(stringResource(R.string.send_request)) } if (greeterRCP.responseState.value.isNotEmpty()) { Text(stringResource(R.string.server_response), modifier = Modifier.padding(top = 10.dp)) Text(greeterRCP.responseState.value) } } }
Объяснять:
class GreeterRCP(uri: Uri): Closeable
— › этот класс создаетchannel
для подключения к IP-адресу сервера и вызывает классGreeterGrpcKt
внутри модуляstub
.fun Greeter(greeterRCP: GreeterRCP)
— › эта функция для создания@Composable
пользовательского интерфейса для Android-приложения.
Теперь наслаждайтесь тем, что мы сделали.
Сначала запустите HelloServer
, затем запустите клиент Android.
Если вы что-то пропустили в моем коде, этот репозиторий GitHub для вас:
Несколько полезных источников для вас: