grpc-image
gRPC is a modern open-source high-performance Remote Procedure Call (RPC) framework that can run in any environment. It can efficiently connect services in and across data centers with pluggable support for load balancing, tracing, health checking, and authentication. It Automatically generates idiomatic client and server stubs for your service in a variety of languages and platforms. It supports Bidirectional streaming between the client and server with HTTP/2-based transport.
A RPC is a form of Client-Server Communication that uses a function call rather than a usual HTTP call.
gRPC uses ProtocolBuffers which is google’s mechanism of serializing structured data.
Protocol Buffers:
Protocol buffers provide a language-neutral, platform-neutral, extensible mechanism for serializing structured data in a forward-compatible and backward-compatible way. It’s like JSON, except it’s smaller and faster, and it generates native language bindings.
You have to create a file with an extension proto
Protocol buffers are the most commonly-used data format used by gRPC.
message Person { string name = 1; int32 id = 2; }
You can consider the message as an object.
To create gRPC services.
follow this:
// The greeter service definition. service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply) {} }
gRPC uses protoc with a special gRPC plugin to generate code from your proto file: you get generated gRPC client and server code, as well as the regular protocol buffer code for populating, serializing, and retrieving your message types.
gRPC provides four kinds of services.
- Unary RPCs where the client sends a single request to the server and gets a single response back, just like a normal function call.
- Server streaming RPCs where the client sends a request to the server and gets a stream to read a sequence of messages back. The client reads from the returned stream until there are no more messages. gRPC guarantees message ordering within an individual RPC call.
- Client streaming RPCs where the client writes a sequence of messages and sends them to the server, again using a provided stream. Once the client has finished writing the messages, it waits for the server to read them and return its response. Again gRPC guarantees message ordering within an individual RPC call.
- Bidirectional streaming RPCs where both sides send a sequence of messages using a read-write stream.
Let’s connect the gRPC server with our Android App.
checkout my app code here.
checkout my gRPC server here
Step 1: Create a new android project.
Step 2:
Add this in project level Gradle file’s build
buildscript { ... dependencies { classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.18" } }
Now, go to app level gradle file.
Add this in plugins block
id 'com.google.protobuf'
Above dependencies block add this.
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' } } } } }
Now, in the dependencies block
//grpc dependency 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'
click Sync Now
Step 3:
Now, you need to create a Proto file.
go to
root -> app -> src -> main -> proto (create this folder)
Add this file 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; | |
// The greeting service definition. | |
service Greeter { | |
//unary call | |
rpc SayHello (HelloRequest) returns (HelloResponse) {} | |
//server_streaming | |
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse); | |
//client_streaming | |
rpc LotsOfRequests(stream HelloRequest) returns (HelloResponse); | |
//bi-directional_streaming | |
rpc BidirectionalHello(stream HelloRequest) returns (stream HelloResponse); | |
} | |
// The request message containing the user's name. | |
message HelloRequest { | |
string name = 1; | |
} | |
// The response message containing the greetings | |
message HelloResponse { | |
string message = 1; | |
} |
Now rebuild the project. you will find a few generated files.
these are the client stubs using which we will connect to grpc server.
Step 4:
Before, exchanging data between client and server. we need to create a channel that is basically connecting the client and server.
channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build()
we will later provide host address and port number.
Now, to send a simple unary call.
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 | |
} | |
} |
Here,
we are using generated stub GreeterGrpcStub
and creating a HelloRequest which is our input.
and fetching the reply.
before calling this method we need to set up our local server.
Step 5:
You need Node.js.
So download from here and setup
Now, create a folder grpc-server
and run npm init -y
you need to override package.json file with this.
{ | |
... | |
"dependencies": { | |
"@grpc/proto-loader": "⁰.5.0", | |
"async": "¹.5.2", | |
"google-protobuf": "³.0.0", | |
"@grpc/grpc-js": "¹.1.0", | |
"lodash": "⁴.6.1", | |
"minimist": "¹.2.0" | |
} | |
} |
Now, run npm install
Now, put the same greeter.proto here in this folder.
and now create an index.js file
and put this code
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(); |
Job Offers
Now, run node index.js
Your localhost is up and running
Step 6:
Go to MainActivity.kt
class MainActivity : ComponentActivity() { | |
override fun onCreate(savedInstanceState: Bundle?) { | |
val mainViewModel: MainViewModel by viewModels() | |
super.onCreate(savedInstanceState) | |
setContent { | |
GRPC_APP_DEMOTheme { | |
// A surface container using the 'background' color from the theme | |
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}") | |
} | |
} | |
} | |
} | |
} | |
} | |
} |
In the MainViewModel.kt
put this method
fun sayHello(name: String) { | |
viewModelScope.launch(context = Dispatchers.IO) { | |
try { | |
updateResult(sendMessage(name,channel) ?: "") | |
} catch (e: Exception) { | |
updateResult(e.message?:"") | |
} | |
} | |
} |
and in 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 | |
} | |
} |
Now, run the app.
In the field, Server add 10.0.2.2 and Port 50051
and click the Start button and click Simple RPC button. you will get a response from your local server.
So, you will get a reply from Server as per your code.
Now, for server streaming.
in MainViewModel.kt put this code.
fun sendMessageWithReplies(message: String) { viewModelScope.launch(Dispatchers.IO) { try { updateResult(sendMessageWithReplies(message,channel).toString()) } catch (e: Exception) { updateResult(e.message?:"") } } }
and in 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 } }
you will receive an iterator.
Click on Server Streaming Button and you will get a stream of messages.
Now, for Client Streaming
You need to create an asynchronous stub.
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 | |
} | |
} |
You need to create 2 stream observers one for response and request.
I have created a countdown latch to wait on the current thread.
So, click on Client Streaming and you will get the output.
The same way you have to do for Bi-directional 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 | |
} | |
} |
Click on the Bi-directional button.
That’s it.
Now, I will explain a few methods used in the Node.js server.
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(); |
Here in the main() method, we are starting a grpc server at IP 0.0.0.0 and port 50051.
and adding Greeter service.
I have added some logic in different RPC calls for demonstration purposes.
for streaming requests like in the case of client streaming and bidirectional streaming. we need to have callbacks such as call.on(‘data’,null) and call.on(‘end’,null).
In the case of getting requests from stream, call object implements Readable.
In the case of sending a response on a stream, call object implements Writable.
gRPC is a very high-performing way to communicate with servers.
explore more about it here
Thank You for Reading
This article was originally published on proandroiddev.com on December 26, 2022