gRPC — это современный высокопроизводительный фреймворк удаленного вызова процедур (RPC) с открытым исходным кодом, запускаемый в любой среде.
Применяется для эффективного подключения сервисов в ЦОДах и между ними со встраиваемой балансировкой нагрузки, проверкой работоспособности, поддержкой трассировки, аутентификации.
Для сервиса в gRPC автоматически генерируются идиоматические «заглушки» клиента и сервера на различных языках и платформах. Поддерживается двунаправленная потоковая передача между клиентом и сервером по протоколу HTTP/2.
RPC — это форма взаимодействия клиента и сервера с применением не обычного HTTP-вызова, а вызова функции.
Буферы протокола
В gRPC используются буферы протокола — это всеязычный, платформенно-независимый, расширяемый механизм Google для сериализации структурированных данных с прямой и обратной совместимостью.
Он аналогичен JSON, только меньше и быстрее, и им генерируются привязки к нативному языку. Позже мы создадим файл с расширением proto.
Буферы протокола — самый распространенный формат данных в gRPC.
message Person {
string name = 1;
int32 id = 2;
}
Сообщение message
можно считать объектом.
Сервисы gRPC создаются так:
// Определение сервиса приветствия.
service Greeter {
// Отправка приветствия
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
Чтобы из файла proto генерировался код, в gRPC используется protoc
со специальным плагином gRPC: получается сгенерированный код для клиента gRPC и серверный код, а также обычный код буфера протокола для заполнения, сериализации и извлечения типов сообщений.
В gRPC имеется 4 вида сервисов.
- Одиночные RPC: клиентом отправляется запрос — с сервера возвращается ответ. Похоже на обычный вызов функции.
- RPC потоковой передачи данных с сервера: клиентом отправляется запрос — с сервера возвращается поток для чтения последовательности сообщений. Пока в возвращаемом потоке есть сообщения, они считываются клиентом. В gRPC гарантируется их упорядочение в отдельном RPC-вызове.
- RPC потоковой передачи данных от клиента: клиентом записывается последовательность сообщений, которые отправляются им на сервер, опять же по предоставляемому потоку. На сервере они считываются, ответ возвращается клиенту. В gRPC опять же гарантируется упорядочение сообщений в отдельном RPC-вызове.
- RPC двунаправленной потоковой передачи данных: обеими сторонами по потоку чтения-записи отправляется последовательность сообщений.
Подключаем сервер gRPC к приложению Android
Этап 1. Создаем новый проект Android:
Этап 2. Добавляем в сборке файла Gradle уровня проекта вот это:
buildscript {
...
dependencies {
classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.18"
}
}
Переходим к файлу gradle уровня приложения и добавляем в блок плагинов это:
id 'com.google.protobuf'
Выше блока зависимостей добавляем это:
protobuf {
protoc { artifact = 'com.google.protobuf:protoc:3.19.2' }
plugins {
grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.47.0'
}
}
generateProtoTasks {
all().each { task ->
task.builtins {
java { option 'lite' }
}
task.plugins {
grpc {
option 'lite' }
}
}
}
}
А в блоке зависимостей — это:
// Зависимость grpc
implementation 'io.grpc:grpc-okhttp:1.47.0'
implementation 'io.grpc:grpc-protobuf-lite:1.47.0'
implementation 'io.grpc:grpc-stub:1.47.0'
implementation 'org.apache.tomcat:annotations-api:6.0.53'
Нажимаем Sync Now («Синхронизировать»).
Этап 3. Создаем файл Proto.
Переходим в root -> app -> src -> main -> proto (создаем эту папку).
Добавляем файл greeter.proto
:
syntax = "proto3";
option java_multiple_files = true;
option java_package = "com.example.grpc_app_demo";
option java_outer_classname = "GreeterProto";
option objc_class_prefix = "GRT";
package greeter;
// Определение сервиса приветствия.
service Greeter {
// Одиночный вызов
rpc SayHello (HelloRequest) returns (HelloResponse) {}
// Потоковая передача с сервера
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);
// Потоковая передача от клиента
rpc LotsOfRequests(stream HelloRequest) returns (HelloResponse);
// Двунаправленная потоковая передача
rpc BidirectionalHello(stream HelloRequest) returns (stream HelloResponse);
}
// Сообщение-запрос с именем пользователя.
message HelloRequest {
string name = 1;
}
// Сообщение-ответ с приветствиями
message HelloResponse {
string message = 1;
}
Делаем повторную сборку проекта, сгенерируются файлы — это «заглушки» клиента для подключения к серверу grpc.
Этап 4. Создаем канал для обмена данными между клиентом и сервером:
channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build()
Адрес хоста и номер порта укажем позже.
А сейчас отправим простой одиночный вызов:
fun sendMessage(
message: String,
channel: ManagedChannel?
):String? {
return try {
val stub = GreeterGrpc.newBlockingStub(channel)
val request = HelloRequest.newBuilder().setName(message).build()
val reply = stub.sayHello(request)
reply.message
} catch (e: Exception) {
e.message
}
}
Здесь используется сгенерированная «заглушка» GreeterGrpcStub, создаются входные данные — HelloRequest и получается ответ.
Прежде чем вызывать этот метод, настроим локальный сервер.
Этап 5. Загружаем отсюда Node.js и настраиваем.
Создаем папку grpc-server
, запускаем npm init -y
. Переопределяем файл package.json
:
{
...
"dependencies": {
"@grpc/proto-loader": "⁰.5.0",
"async": "¹.5.2",
"google-protobuf": "³.0.0",
"@grpc/grpc-js": "¹.1.0",
"lodash": "⁴.6.1",
"minimist": "¹.2.0"
}
}
Запускаем npm install
. Отправляем тот же greeter.proto
в эту папку.
Создаем файл index.js
и помещаем в него этот код:
var PROTO_PATH = __dirname+'/greeter.proto';
var grpc = require('@grpc/grpc-js');
var protoLoader = require('@grpc/proto-loader');
var packageDefinition = protoLoader.loadSync(
PROTO_PATH,
{keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
var greeter_proto =
grpc.loadPackageDefinition(packageDefinition).greeter;
function lotsOfReplies(call) {
console.log("Request:"+call.request);
var name = call.request.name
for(let i=0;i<10;++i){
call.write({message: "Yo "+name+" "+i});
}
call.end();
}
function sayHello(call, callback) {
callback(null, {message: 'Yo ' + call.request.name});
}
function lotsOfRequests(call, callback) {
var input = [];
var index = 0;
call.on('data',function(request){
console.log("Request:"+request.name);
input.push("Hello "+request.name+" "+index+"\n");
index++;
});
call.on('end',function(){
callback(null, {message: input.toString()});
});
}
function bidirectionalHello(call) {
var input = [];
var index = 0;
call.on('data',function(request){
console.log("Request: "+request.name);
if(index < 2){
call.write({message: "Hello "+request.name+" "+(index*2)});
}
else{
call.write({message: "Yo "+request.name+" "+(index*3)});
}
input.push(request.name+"\n");
index++;
});
call.on('end',function(){
call.write({message: "\n"+input.toString()+" "+(index*index)});
call.end();
});
}
function main() {
var server = new grpc.Server();
server.addService(greeter_proto.Greeter.service, {
sayHello: sayHello,
lotsOfReplies: lotsOfReplies,
lotsOfRequests: lotsOfRequests,
bidirectionalHello: bidirectionalHello
});
server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
server.start();
});
}
main();
Запускаем узел index.js
: localhost готов к работе.
Этап 6. Переходим к MainActivity.kt
:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val mainViewModel: MainViewModel by viewModels()
super.onCreate(savedInstanceState)
setContent {
GRPC_APP_DEMOTheme {
// Контейнер surface с цветом background из темы
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
Scaffold(
topBar = {
TopAppBar(
title = {
Text("GRPC Demo")
}
)
}
) {
Column(
modifier = Modifier
.padding(24.dp)
){
Row(
modifier = Modifier.fillMaxWidth()
){
OutlinedTextField(
enabled = mainViewModel.hostEnabled.value,
value = mainViewModel.ip.value,
onValueChange = {
mainViewModel.onIpChange(it)
},
modifier = Modifier
.fillMaxWidth()
.weight(1f),
placeholder = {
Text("IP address")
},
label = {
Text("Server")
}
)
Spacer(modifier = Modifier.size(16.dp))
OutlinedTextField(
enabled = mainViewModel.portEnabled.value,
value = mainViewModel.port.value,
onValueChange = {
mainViewModel.onPortChange(it)
},
modifier = Modifier
.fillMaxWidth()
.weight(1f),
placeholder = {
Text("Port")
},
label = {
Text("Port")
}
)
}
Row(
modifier = Modifier.fillMaxWidth()
){
Button(
enabled = mainViewModel.startEnabled.value,
onClick = {
mainViewModel.start()
},
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
Text("Start")
}
Spacer(modifier = Modifier.size(16.dp))
Button(
enabled = mainViewModel.endEnabled.value,
onClick = {
mainViewModel.exit()
},
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
Text("End")
}
}
Button(
enabled = mainViewModel.buttonsEnabled.value,
onClick = {
mainViewModel.sayHello("Arun")
},
modifier = Modifier.fillMaxWidth()
) {
Text("Simple RPC: Say Hello")
}
Text("Result: ${mainViewModel.result.value}")
}
}
}
}
}
}
}
В MainViewModel.kt
помещаем этот метод:
fun sayHello(name: String) {
viewModelScope.launch(context = Dispatchers.IO) {
try {
updateResult(sendMessage(name,channel) ?: "")
} catch (e: Exception) {
updateResult(e.message?:"")
}
}
}
А в grpc.kt
— этот:
fun sendMessage(
message: String,
channel: ManagedChannel?
):String? {
return try {
val stub = GreeterGrpc.newBlockingStub(channel)
val request = HelloRequest.newBuilder().setName(message).build()
val reply = stub.sayHello(request)
reply.message
} catch (e: Exception) {
e.message
}
}
Запускаем приложение.
Добавляем сервер 10.0.2.2 и порт 50051, нажимаем Start и Simple RPC и получаем ответ с локального сервера:
Ответ с сервера соответствует коду.
Переходим к потоковой передаче с сервера. В MainViewModel.kt
помещаем этот код:
fun sendMessageWithReplies(message: String) {
viewModelScope.launch(Dispatchers.IO) {
try {
updateResult(sendMessageWithReplies(message,channel).toString())
} catch (e: Exception) {
updateResult(e.message?:"")
}
}
}
А в grpc.kt
— этот:
fun sendMessageWithReplies(
message: String,
channel: ManagedChannel?
):Any? {
return try {
val stub = GreeterGrpc.newBlockingStub(channel)
val request = HelloRequest.newBuilder().setName(message).build()
val reply = stub.lotsOfReplies(request)
reply.asSequence().toList().map { it -> it.message+"\n" }
} catch (e: Exception) {
e
}
}
Получаем итератор:
Нажимаем Server Streaming и получаем поток сообщений.
Переходим к потоковой передаче от клиента, создаем асинхронную «заглушку»:
fun sendMessageWithRequests(
channel: ManagedChannel?
):Any {
return try {
val stub = GreeterGrpc.newStub(channel)
var failed: Throwable? = null
val finishLatch = CountDownLatch(1)
val responseList = mutableListOf<HelloResponse>()
val requestObserver = stub.lotsOfRequests(object : StreamObserver<HelloResponse> {
override fun onNext(response: HelloResponse) {
responseList.add(response)
}
override fun onError(t: Throwable) {
failed = t
finishLatch.countDown()
override fun onCompleted() {
finishLatch.countDown()
}
})
try {
val requests = arrayOf(
newHelloResponse("TOM"),
newHelloResponse("ANDY"),
newHelloResponse("MANDY"),
newHelloResponse("John")
)
for (request in requests) {
requestObserver.onNext(request)
}
} catch (e: java.lang.RuntimeException) {
requestObserver.onError(e)
return e.message?:""
}
requestObserver.onCompleted()
if (!finishLatch.await(1, TimeUnit.MINUTES)) {
return "Timeout error"
}
if (failed != null) {
return failed?.message?:""
}
return responseList.map { it.message }
} catch (e: Exception) {
e
}
}
Нужно создать 2 средства наблюдения за потоками — по одному для ответа и запроса.
Сделаем «защелку» с обратным отсчетом для ожидания текущего потока:
Нажимаем Client Streaming и получаем результат.
То же самое делаем с двунаправленной потоковой передачей:
fun sendMessageBiDirectional(channel: ManagedChannel?): Any{
return try {
val stub = GreeterGrpc.newStub(channel)
var failed: Throwable? = null
val finishLatch = CountDownLatch(1)
val responseList = mutableListOf<HelloResponse>()
val requestObserver = stub.bidirectionalHello(object : StreamObserver<HelloResponse> {
override fun onNext(response: HelloResponse) {
responseList.add(response)
}
override fun onError(t: Throwable) {
failed = t
finishLatch.countDown()
}
override fun onCompleted() {
finishLatch.countDown()
}
})
try {
val requests = arrayOf(
newHelloResponse("TOM"),
newHelloResponse("ANDY"),
newHelloResponse("MANDY"),
newHelloResponse("John")
)
for (request in requests) {
requestObserver.onNext(request)
}
} catch (e: java.lang.RuntimeException) {
requestObserver.onError(e)
return e.message?:""
}
requestObserver.onCompleted()
if (!finishLatch.await(1, TimeUnit.MINUTES)) {
return "Timeout error"
}
if (failed != null) {
return failed?.message?:""
}
return responseList.map { it.message }
} catch (e: Exception) {
e
}
}
Нажимаем Bi-directional. Вот и все.
Поясним использование методов на сервере Node.js:
var PROTO_PATH = __dirname+'/greeter.proto';
var grpc = require('@grpc/grpc-js');
var protoLoader = require('@grpc/proto-loader');
var packageDefinition = protoLoader.loadSync(
PROTO_PATH,
{keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
var greeter_proto =
grpc.loadPackageDefinition(packageDefinition).greeter;
function lotsOfReplies(call) {
console.log("Request:"+call.request);
var name = call.request.name
for(let i=0;i<10;++i){
call.write({message: "Yo "+name+" "+i});
}
call.end();
}
function sayHello(call, callback) {
callback(null, {message: 'Yo ' + call.request.name});
}
function lotsOfRequests(call, callback) {
var input = [];
var index = 0;
call.on('data',function(request){
console.log("Request:"+request.name);
input.push("Hello "+request.name+" "+index+"\n");
index++;
});
call.on('end',function(){
callback(null, {message: input.toString()});
});
}
function bidirectionalHello(call) {
var input = [];
var index = 0;
call.on('data',function(request){
console.log("Request: "+request.name);
if(index < 2){
call.write({message: "Hello "+request.name+" "+(index*2)});
}
else{
call.write({message: "Yo "+request.name+" "+(index*3)});
}
input.push(request.name+"\n");
index++;
});
call.on('end',function(){
call.write({message: "\n"+input.toString()+" "+(index*index)});
call.end();
});
}
function main() {
var server = new grpc.Server();
server.addService(greeter_proto.Greeter.service, {
sayHello: sayHello,
lotsOfReplies: lotsOfReplies,
lotsOfRequests: lotsOfRequests,
bidirectionalHello: bidirectionalHello
});
server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
server.start();
});
}
main();
В методе main()
запускается сервер grpc с IP-адресом 0.0.0.0 и портом 50051 и добавляется сервис Greeter.
Для демонстрационных целей в различные RPC-вызовы добавлено логики.
Для потоковых запросов — потоковой передачи от клиента и двунаправленной потоковой передачи — нужны обратные вызовы, например call.on(‘data’,null)
и call.on(‘end’,null)
.
Когда получаются запросы из потока, объектом call
реализуется Readable
. Когда в потоке отправляется ответ, объектом call
реализуется Writable
.
gRPC — весьма высокопроизводительный способ взаимодействия с серверами. Еще больше о нем — здесь.
Читайте также:
- Что такое закрепление сертификата в Android
- Магия совместимости XML и Jetpack Compose
- Android Networking с Kotlin Coroutines
Читайте нас в Telegram, VK и Дзен
Перевод статьи Shaik Ahron: Connecting Android Apps with Server using gRPC