Часть 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 будет выглядеть так, как показано на рисунке ниже.

  1. Создание сервера 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.VERSIONKotlinGrpc8
}

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.VERSIONKotlinGrpc8
        targetCompatibility = JavaVersion.VERSIONKotlinGrpc8
    }
    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.VERSIONKotlinGrpc8
        targetCompatibility = JavaVersion.VERSIONKotlinGrpc8
    }
    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 для вас:



Несколько полезных источников для вас:

Часть 2: Несколько серверов gRPC для клиентов Android и iOS