Как я курсовую писал на gRPC+REST в связке с клиентом на Android+Jetpack Compose

Введение

Я — Денис, Middle Android-разработчик в «Лайв Тайпинге». В этой статье хочу немного отойти от стандартного обзора технических тем, которые обычно пишу на Хабре. Тут будет 40% обо мне и 60% технического материала. Если хотите сразу перейти к технической части. Прошу во главу «Что такое gRPC».

На первом курсе университета так вышло, что я попал работать в Лайв Тайпинг. Это топовая Омская студия разработки мобильных приложений, в которой я работаю и по сей день. Сейчас же я учусь на третьем курсе в университете путей сообщений в Омске.

Каждый семестр в университете мы пишем две курсовые работы. И этот семестр не стал исключением. В этой статье я расскажу о том как написал курсовую работу и успешно сдал её с первой попытки без защиты на «отлично». Чего у меня и моих одногруппников ранее никогда не удавалось.

Untitled

Задача курсовой работы разработать клиент-серверное приложение. И перед тем как начать писать курсовую нужно согласовать тему с преподавателей этой дисциплины. Для этого нужно описать в техническом задании тему и технические подробности работы.

До этого момента я никогда не писал ТЗ. Но работал с ним. Поэтому я знаю структуру и что в этом документе стоило описать для моей курсовой.

Прежде чем приступить к ТЗ, мне нужно было определить, над чем именно я буду работать в рамках курсовой работы. Я рассмотрел несколько вариантов и даже создал несколько набросков в фигме. Для вдохновения я изучал референсы современных мобильных приложений на Dribble. Сначала я задумал разработать мобильное приложение — для покупки продуктов в магазине без участия кассиров.

Untitled

Но по итогу я ни к чему не пришел и решил, что идея не сильно мне интересна. После небольшого раздумья мне пришла другая идея — мобильное приложение для кофейни. В прошлом году я наткнулся на статью на Хабре о том, как команда ДоДо создала потрясающий экран на Jetpack Compose. Мне захотелось сделать что-то подобное. Но быстро осознал, что для этого нужны красивые анимации и фотографии кофейных напитков. Поэтому тоже откинул мысль.

Ссылка на статью — https://habr.com/ru/companies/dododev/articles/764540/

Ссылка на статью — https://habr.com/ru/companies/dododev/articles/764540/

После нескольких дней размышлений о теме курсовой, я наткнулся на приложение Радио Arzamas. Мне очень понравился приглушенный в тёмных тонах интерфейс. Я закинул пару скриншотов из приложения в фигму. Повертел их туда-сюда и мне пришла идея. Я решил разработать мобильное приложение, где можно прочитать биографии и статьи про авторов русской литературы 18–19 веков. Но чтобы добавить хоть какую-то особенность, а не просто кинуть стену текста, я выдумал падать эту информацию в формате вертикальных клипов.

Буквально за один вечер я нашел все необходимые дополнительные референсы для своей работы и добавил их в фигму.

Мне сильно понравилась эта идея. Она объединила все мои любимые вещи на тот момент: романы русских классиков и рилсы, которые я часто смотрел до начала работы над курсовой.

Untitled

После прототипирования в фигме, я набросал ТЗ, уточнив что оно может меняться во время разработки. Преподаватель одобрил задумку и сказал, что этого будет более чем достаточно для курсовой.

Untitled

Я работал над курсовой с утра до вечера каждый день на протяжении месяца. Чтобы как можно глубже уйти в курсовую, и не сильно отвлекаться на другие работы в университете я их либо заморозил, либо передал на аутсорс одногруппникам.

Напомню, что тема курсовой работы разработать клиент-серверное приложение. Но так как я Android разработчик, то о каком-либо крутом опыте с серверной частью не стоит и говорить. Мне лишь довелось написать пару простых проектов с CRUD методами на Ktor в пет-проектах.

Для сервера в курсовой работе я решил выбрать ЯП Go. Мне он очень нравится за синтаксис и его лаконичность. Go я выучил ещё год назад, на зимних каникулах. Но снова писать простые REST CRUD штуки мне было не очень интересно. Я захотел разнообразия, поэтому решил попробовать протокол gRPC для небольшого куска моей курсовой. Сервис авторизации как раз для этого отлично подходил.

Что такое gRPC

Google Remote Procedure Call или же gRPC — это RPC-фреймворк с открытым исходным кодом. Он отлично подходит для создания масштабируемых и быстрых API. Протокол широко используется, например в Google, IBM, Netflix.

Untitled

gRPC использует HTTP/2 в качестве транспортного протокола. HTTP/2 обеспечивает более эффективное использование сетевых ресурсов по сравнению с HTTP/1.1, позволяя множеству запросов и ответов передаваться параллельно в рамках одного TCP-соединения. Это уменьшает задержки и увеличивает общую производительность.

HHTP 2 — новый протокол передачи данных по сети (вышел в 2015 году). Новшества:

  • бинарный формат передачи сообщений (в отличии от текстового HTTP 1.1);

  • более продвинутое сжатие HTTP сообщений — меньший размер, выше скорость;

  • потоки данных;

  • мультиплексирование, приоритизация потоков и т.д.

Когда использовать

  • монолитное приложение, к которому должны иметь доступ извне или через браузер — REST API;

  • микросервисы, которые общаются друг с другом — gRPC;

  • много разных ЯП — gRPC (или REST API);

  • нужен стриминг данных — gRPC;

  • критически важна скорость передачи данных (огромное количество запросов или узкий канал) — gRPC.

ProtoBuf

ProtoBuf — это язык описания интерфейса и система сериализации данных, разработанные Google. Они используются для сериализации структурированных данных, подобно XML, но более эффективны, быстры и меньше по размеру. Структура данных в ProtoBuf описывается в файлах с расширением .proto.

ProtoBuf поддерживает различные типы данных, например:  int32,  float,  bool,  string,  bytes. ProtoBuf сериализует данные в бинарный формат, что делает его очень эффективным как по размеру, так и по скорости сериализации/десериализации. После определения структуры данных в .proto файлах, используется компилятор protoc для генерации кода на различных языках программирования.

В своей курсовой работе я написал лишь сервис авторизации на gRPC для «пробы пера». Так .proto файл для сервиса выглядит как в коде ниже:

service Auth {
	rpc Register (RegisterRequest) returns (RegisterResponse);
	rpc Login (LoginRequest) returns (LoginResponse);
}

message RegisterRequest {
	string phone = 1;
	string password = 2;
}

message RegisterResponse {
	int64 user_id = 1;
}

message LoginRequest {
	string phone = 1;
	string password = 2;
}

message LoginResponse {
	string token = 1
}

Разница между gRPC и REST

Когда клиент и сервер взаимодействует через REST они передают друг-другу JSON сообщения.

Untitled

JSON — это текстовый формат обмена данными, основанный на JavaScript. Он используется для сериализации и передачи данных между сервером и веб-приложениями.

Untitled

gRPC по умолчанию использует ProtoBuf из-за его высокой производительности и эффективности. Однако, gRPC также поддерживает JSON и другие форматы, что делает его гибким для различных сценариев использования. Далее в таблице, сравним ProtoBuf и JSON.

Критерий

Protocol Buffers (ProtoBuf)

JSON

Формат данных

Бинарный

Текстовый

Размер сообщений

Обычно меньше, более компактные

Обычно больше из-за текстового формата

Скорость обработки

Быстрее из-за меньшего размера и бинарной природы

Медленнее, требует парсинга текста

Читаемость

Требует специальных инструментов для чтения и отладки

Легко читаем и отлаживаем человеком

Интероперабельность

Хорошая поддержка между различными ЯП

Отличная поддержка на всех платформах

Совместимость

Строгая совместимость, требует точного соответствия схемы данных

Гибкая, легко адаптируется к изменениям

Типизация данных

Строго типизированный, требует определения всех полей

Динамически типизированный

Использование

Предпочтительнее для высокопроизводительных и оптимизированных систем

Широко используется для веб-API и легкой интеграции

Если нужна производительность и компактность, то ProtoBuf будет предпочтительным выбором. Если важнее удобство отладки и широкая совместимость, то может быть умнее использовать JSON.

JSON

{ 
  "timestamp": 15030534477, 
  "url": "http://example.com/" 
}

Плюсы:

Минусы:

  • не сжимается при передаче, передается как текст (большой размер сообщения);

  • избыточный (ключи повторяются);

  • нет строгой типизации.

ProtoBuf

message RegisterRequest { 
  int64 timestamp = 1; 
  string url = 2; 
}

Плюсы:

Минусы:

gRPC поддерживает несколько типов взаимодействий:

Unary RPC:  это самая базовая и простая модель в gRPC. Клиент отправляет один запрос серверу и получает в ответ одно сообщение. Это аналогично традиционному вызову функции в программировании. Подходит для простых запросов и операций, где требуется однократное взаимодействие;

Server streaming RPC:  в этой модели клиент отправляет один запрос серверу, после чего сервер начинает отправлять поток ответов. Подходит для сценариев, где сервер должен отправить большое количество данных или постоянно обновляемую информацию;

Client streaming RPC:  клиент отправляет поток данных серверу. После завершения отправки потока клиент ожидает ответ от сервера. Этот тип подходит для сценариев, где клиенту необходимо отправить большое количество данных или серию сообщений;

Bidirectional streaming RPC:  в двунаправленном потоковом RPC клиент и сервер обмениваются потоками данных в обоих направлениях. Клиент может начать отправку серии сообщений, не дожидаясь ответов сервера, и наоборот.

Для проекта я выбрал самую простую модель — Unary RPC. Так как в проекте на данный момент нет логики, которая бы требовала сложной архитектуры или приемов взаимодействий.

Компилятор protoc, поставляемый с gRPC, совместим с широким спектром языков программирования. Это делает gRPC отличным средством для многоязычных сред, где вы подключаете множество различных микросервисов, написанных на разных языках и работающих на разных платформах.

Напротив, REST API не предлагает функций генерации собственного кода. Вы должны использовать сторонний инструмент, такой как Swagger, для генерации кода для вызовов API на разных языках. Это не доставляет неудобств, но стоит отметить, что gRPC не зависит от каких-либо внешних инструментов для генерации кода.

Согласно широко цитируемым тестам, соединения gRPC API значительно быстрее, чем соединения REST API. GRPC примерно в 7 раз быстрее REST при получении данных и примерно в 10 раз быстрее, чем REST при отправке данных для этой конкретной полезной нагрузки. В основном это связано с плотной упаковкой буферов протокола и использованием HTTP / 2 в gRPC.

Untitled

Несмотря на преимущества в скорости передачи сообщений, реализация gRPC немного медленнее, чем реализация REST. Внедрение простой службы gRPC заняло у меня примерно на 30% больше времени чем REST.

Вся остальная логика на серверной части будет реализована на REST. Получение статей, биографий, данных пользователя. Все это и прочее работает на REST в совокупности с базой данных на SQLite.

Практический пример сетевой архитектуры

Сперва я написал сервер на gRPC на Go. Но потом понял, что Retrofit не умеет в gRPC. Поискал альтернативы, увидел, что есть варианты с библиотекой Wire от разработчиков Retrofit. Но мне показалось излишним внедрение такой библиотеки для двух gRPC методов в свой курсовой проект. Поэтому продолжил поиск более простых альтернатив. По итогу я нашёл решение в виде стандартной библиотеки от Google.

Если вы используете gRPC на клиенте, то вам больше не нужно писать самостоятельно модели для сервера. Просто добавим .proto файлы. Они уже сами генерируют нужные модели. Эти файлы располагаются в модуле, в котором вы поместили .proto файлы в папке build.

Untitled

По итогу имеем два класса. Один служит как дата-модель, другой как контракт между сервером и клиентом. Но просто так эти два файла не сгененируются. Нужно сделать несколько вещей чтобы это заработало:

  1. добавить зависимости для gRPC и proto;

  2. добавить генерацию proto в gradle.

Сперва добавим зависимости в version catalog.

[versions]
protobuf = "3.25.2"
protobufPlugin = "0.9.4"
kotlinx-serialization = "1.6.3"
grpc = "1.62.2"

[libraries]
# gRPC
grpc-okhttp = { module = "io.grpc:grpc-okhttp", version.ref = "grpc" }
annotations-api = { module = "org.apache.tomcat:annotations-api", version.ref = "annotationApi" }
grpc-protoc-gen-java = { module = "io.grpc:protoc-gen-grpc-java", version.ref = "grpc" }
grpc-stub = { module = "io.grpc:grpc-stub", version.ref = "grpc" }
protobuf-lite = { module = "io.grpc:grpc-protobuf-lite", version.ref = "grpc" }

# Protobuf
protobuf-kotlin-lite = { group = "com.google.protobuf", name = "protobuf-kotlin-lite", version.ref = "protobuf" }
protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" }

[bundles]
grpc = [
    "grpc-stub",
    "grpc-okhttp",
    "grpc-protoc-gen-java",
    "annotations-api",
    "protobuf-lite",
]

[plugins]
protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" }
protobuf-classpath = { id = "com.google.protobuf", version.ref = "protobufGradlePlugin" }

Далее подключим кодо-генерацию в gradle. Учтите, что нужно подключать там, где расположен .proto файл.

import com.google.protobuf.gradle.id

plugins {
    alias(libs.plugins.app.feature.domain)
    alias(libs.plugins.protobuf)
}

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.19.2"
    }
    plugins {
        id("grpc") {
            artifact = "io.grpc:protoc-gen-grpc-java:1.47.0"
        }
    }
    generateProtoTasks {
        all().forEach { task ->
            task.builtins {
                create("java") {
                    option("lite")
                }
            }
            task.plugins {
                create("grpc") {
                    option("lite")
                }
            }
        }
    }
}

android {
    namespace = "ru.popkov.android.core.feature.domain"
}

dependencies {
    implementation(libs.bundles.grpc)
    api(libs.protobuf.kotlin.lite)
}

Ну и пожалуй самое главное — это репозиторий, в котором хранится реализация наших gRPC методов.

@AutoBind
@Singleton
class AuthRepository @Inject constructor(
    private val dataStore: Token,
) : AuthRepository {

    override suspend fun registerUser(registerRequest: AuthOuterClass.RegisterRequest): RegisterResponse {
        val channel =
            ManagedChannelBuilder.forAddress("10.0.2.2", 4040).usePlaintext().build()
        val client = AuthGrpc.newBlockingStub(channel)
        return client.register(registerRequest)
    }
}

Функция принимает запрос из сгенерированого proto-файлом классом, и возвращает proto модель-ответ. В самом методе в переменной channel нужно подключится к адресу сервера, в моем случае это localhost. А далее всё довольно просто, устанавливаем соединение и «дёргаем» нужную «ручку».

Сам запрос к репозиторию во ViewModel может выглядеть примерно так:

private suspend fun registerNewUser(
    phoneNumber: String,
    password: String
): Deferred {
    val request = AuthOuterClass.RegisterRequest.newBuilder()
        .setPhone(phoneNumber)
        .setPassword(password)
        .build()

    val handler = CoroutineExceptionHandler { _, throwable ->
        Timber.tag("Auth").d("exception: %s", throwable)
    }

    return viewModelScope.async(handler) {
        authRepository.registerUser(request)
    }
}

Эта вся логика, которую нужно написать для работы с gRPC в Android приложении.

Заключение

В итоге получился неплохой MVP проект. В нём есть не оптимальные моменты и пара багов. В дальнейшем, уже не в рамках курсовой, я хочу допилить проект и возможно расширить его функционал.

Из того, что хочу добавить:

  • поддержку анимированых гивок авторов;

  • короткие видео о литературе на экране вертикальных клипов

Посмотреть как работает приложение можно в этом шортс. Ознакомится с полным исходным кодом сервера и клиента можно по ссылкам ниже:

  1. исходный код мобильного приложения;

  2. исходный код сервера на gRPC+REST.

Также можете глянуть текст пояснительной записки к курсовой работе.

Благодарю за внимание!

dcddba896ec4c949dec4aa0bb59a0a4b.jpgДенис Попков

Middle Android разработчик в «Лайв Тайпинге»

Если вы нашли неточности/ошибки в статье или просто хотите дополнить её своим мнением — то прошу в комментарии! Или можете написать мне в Telegram — t.me/MolodoyDenis.

© Habrahabr.ru