Kotlin basics tutorial update due to code reorg (#476)

- Contributes to #465
- Also did some copyediting
This commit is contained in:
Patrice Chalin 2020-10-29 13:01:48 -04:00 committed by GitHub
parent 91309767d5
commit 0e23f24484
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 159 additions and 183 deletions

View File

@ -1,6 +1,7 @@
--- ---
title: Basics tutorial title: Basics tutorial
description: A basic tutorial introduction to gRPC in Kotlin/JVM. description: A basic tutorial introduction to gRPC in Kotlin/JVM.
spelling: cSpell:ignore grpckt Mendham millis println
weight: 50 weight: 50
--- ---
@ -9,59 +10,51 @@ working with gRPC.
By walking through this example you'll learn how to: By walking through this example you'll learn how to:
- Define a service in a .proto file. - Define a service in a `.proto` file.
- Generate server and client code using the protocol buffer compiler. - Generate server and client code using the protocol buffer compiler.
- Use the Kotlin gRPC API to write a simple client and server for your service. - Use the Kotlin gRPC API to write a simple client and server for your service.
It assumes that you have read the [Introduction to gRPC](/docs/what-is-grpc/introduction/) and are familiar You should already be familiar gRPC and protocol buffers; if not, see
with [protocol buffers](https://developers.google.com/protocol-buffers/docs/overview). Note that the [Introduction to gRPC][] and the proto3 [Language guide][proto3].
example in this tutorial uses the proto3 version of the protocol buffers
language: you can find out more in the
[proto3 language
guide](https://developers.google.com/protocol-buffers/docs/proto3).
### Why use gRPC? ### Why use gRPC?
Our example is a simple route mapping application that lets clients get {{< why-grpc >}}
information about features on their route, create a summary of their route, and
exchange route information such as traffic updates with the server and other
clients.
With gRPC we can define our service once in a .proto file and implement clients ### Setup
and servers in any of gRPC's supported languages, which in turn can be run in
environments ranging from servers inside Google to your own tablet - all the
complexity of communication between different languages and environments is
handled for you by gRPC. We also get all the advantages of working with protocol
buffers, including efficient serialization, a simple IDL, and easy interface
updating.
### Example code and setup This tutorial has the same [prerequisites][] as the [Quick start][]. Install the
necessary SDKs and tools before proceeding.
The example code for our tutorial is in ### Get the example code
[grpc/grpc-kotlin/examples/src/main/kotlin/io/grpc/examples/routeguide](https://github.com/grpc/grpc-kotlin/tree/master/examples/src/main/kotlin/io/grpc/examples/routeguide).
To download the example, clone the latest release in `grpc-kotlin` repository by
running the following command:
```sh The example code is part of the [grpc-kotlin][] repo.
$ git clone https://github.com/grpc/grpc-kotlin.git
```
Then change to the example's main source folder: 1. [Download the repo as a zip file][download] and unzip it, or clone
the repo:
```sh ```sh
$ cd grpc-kotlin/examples/src/main/kotlin/io/grpc/examples/routeguide $ git clone https://github.com/grpc/grpc-kotlin
``` ```
2. Change to the examples directory:
```sh
$ cd grpc-kotlin/examples
```
### Defining the service ### Defining the service
Our first step (as you'll know from the [Introduction to gRPC](/docs/what-is-grpc/introduction/)) is to Your first step (as you'll know from the [Introduction to gRPC][]) is to define
define the gRPC *service* and the method *request* and *response* types using the gRPC _service_ and the method _request_ and _response_ types using [protocol
[protocol buffers][proto3].
buffers](https://developers.google.com/protocol-buffers/docs/overview). You can
see the complete .proto file in
[grpc-kotlin/examples/src/main/proto/route_guide.proto](https://github.com/grpc/grpc-kotlin/blob/master/examples/src/main/proto/route_guide.proto).
To define a service, you specify a named `service` in the .proto file: If you'd like to follow along by looking at the complete `.proto` file, see
`routeguide/route_guide.proto` from the
[protos/src/main/proto/io/grpc/examples][protos-src] folder.
To define a service, you specify a named `service` in the `.proto` file like
this:
```proto ```proto
service RouteGuide { service RouteGuide {
@ -122,8 +115,8 @@ all of which are used in the `RouteGuide` service:
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {} rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
``` ```
Our `.proto` file also contains protocol buffer message type definitions for all The `.proto` file also contains protocol buffer message type definitions for all
the request and response types used in our service methods - for example, here's the request and response types used by the service methods -- for example, here's
the `Point` message type: the `Point` message type:
```proto ```proto
@ -139,51 +132,54 @@ message Point {
### Generating client and server code ### Generating client and server code
Next we need to generate the gRPC client and server interfaces from our .proto Next, you need to generate the gRPC client and server interfaces from the `.proto`
service definition. We do this using the protocol buffer compiler `protoc` with service definition. You do this using the protocol buffer compiler `protoc` with
a special gRPC Kotlin and Java plugins. You need to use the special gRPC Kotlin and Java plugins.
[proto3](https://github.com/google/protobuf/releases) compiler (which supports
both proto2 and proto3 syntax) in order to generate gRPC services.
When using Gradle or Maven, the protoc build plugin can generate the necessary When using Gradle or Maven, the `protoc` build plugin will generate the
code as part of the build. See the [grpc-kotlin README][] for details. necessary code as part of the build process. For a Gradle example, see
[stub/build.gradle.kts][].
[grpc-kotlin README]: https://github.com/grpc/grpc-kotlin/blob/master/README.md If you run `./gradlew installDist` from the examples folder, the following files
are generated from the service definition -- you'll find the generated files in
subdirectories below `stub/build/generated/source/proto/main`:
The following classes are generated from our service definition: - `Feature.java`, `Point.java`, `Rectangle.java`, and others, which contain all
- `Feature.java`, `Point.java`, `Rectangle.java`, and others which contain all
the protocol buffer code to populate, serialize, and retrieve our request and the protocol buffer code to populate, serialize, and retrieve our request and
response message types. response message types.
- `RouteGuideGrpcKt.kt`, which contains, among other things:
- A base class for `RouteGuide` servers to implement, You'll find these files in the `java/io/grpc/examples/routeguide`
`RouteGuideGrpcKt.RouteGuideCoroutineImplBase`, with all the methods defined subdirectory.
in the `RouteGuide` service. - `RouteGuideOuterClassGrpcKt.kt`, which contains, among other things:
- The `RouteGuideCoroutineStub` class that clients use to talk to a - `RouteGuideGrpcKt.RouteGuideCoroutineImplBase`, an abstract base class for
`RouteGuide` server. `RouteGuide` servers to implement, with all the methods defined in the
`RouteGuide` service.
- `RouteGuideGrpcKt.RouteGuideCoroutineStub`, a class that clients use to talk
to a `RouteGuide` server.
You'll find this Kotlin file under `grpckt/io/grpc/examples/routeguide`.
### Creating the server {#server} ### Creating the server {#server}
First let's look at how we create a `RouteGuide` server. If you're only First consider how to create a `RouteGuide` server. If you're only interested in
interested in creating gRPC clients, you can skip this section and go straight creating gRPC clients, skip ahead to [Creating the client](#client) -- though
to [Creating the client](#client) (though you might find it interesting you might find this section interesting anyway!
anyway!).
There are two parts to making our `RouteGuide` service do its job: There are two main things that you need to do when creating a `RouteGuide`
server:
- Overriding the service base class generated from our service definition: doing - Extend the `RouteGuideCoroutineImplBase` service base class to do the actual
the actual "work" of our service. service work.
- Running a gRPC server to listen for requests from clients and return the - Create and run a gRPC server to listen for requests from clients and return
service responses. the service responses.
You can find our example `RouteGuide` server in Open the example `RouteGuide` server code in `routeguide/RouteGuideServer.kt`
[grpc-kotlin/examples/src/main/kotlin/io/grpc/examples/routeguide/RouteGuideServer.kt](https://github.com/grpc/grpc-kotlin/blob/master/examples/src/main/kotlin/io/grpc/examples/routeguide/RouteGuideServer.kt). under the [server/src/main/kotlin/io/grpc/examples][server-src] folder.
Let's take a closer look at how it works.
#### Implementing RouteGuide #### Implementing RouteGuide
As you can see, our server has a `RouteGuideService` class that extends the As you can see, the server has a `RouteGuideService` class that extends the
generated `RouteGuideGrpcKt.RouteGuideCoroutineImplBase` abstract class: generated service base class:
```kotlin ```kotlin
class RouteGuideService( class RouteGuideService(
@ -196,10 +192,9 @@ class RouteGuideService(
#### Simple RPC #### Simple RPC
`RouteGuideService` implements all our service methods. Let's look at the `RouteGuideService` implements all the service methods. Consider the simplest
simplest method first, `GetFeature()`, which just gets a `Point` from the client method first, `GetFeature()`, which gets a `Point` from the client and returns a
and returns the corresponding feature information from its database in a `Feature` built from the corresponding feature information in the database.
`Feature`.
```kotlin ```kotlin
override suspend fun getFeature(request: Point): Feature = override suspend fun getFeature(request: Point): Feature =
@ -209,32 +204,31 @@ override suspend fun getFeature(request: Point): Feature =
``` ```
The method accepts a client's `Point` message request as a parameter, and it The method accepts a client's `Point` message request as a parameter, and it
returns a `Feature` message as a response. In the method we populate the returns a `Feature` message as a response. The method populates the `Feature`
`Feature` with the appropriate information, and then `return` it to the gRPC with the appropriate information, and then returns it to the gRPC framework,
framework, which sends it back to the client. which sends it back to the client.
##### Server-side streaming RPC ##### Server-side streaming RPC
Next let's look at one of our streaming RPCs. `ListFeatures` is a server-side Next, consider one of the streaming RPCs. `ListFeatures()` is a server-side
streaming RPC, so we need to send back multiple `Feature`s to our client. streaming RPC, so the server gets to send back multiple `Feature` messages to
the client.
```kotlin ```kotlin
override fun listFeatures(request: Rectangle): Flow<Feature> = override fun listFeatures(request: Rectangle): Flow<Feature> =
features.asFlow().filter { it.exists() && it.location in request } features.asFlow().filter { it.exists() && it.location in request }
``` ```
Like the simple RPC, this method gets a request object (the `Rectangle` in which The request object is a `Rectangle`. The server collects, and returns to the
our client wants to find `Feature`s). client, all the `Feature` objects in its collection that are inside the given
`Rectangle`.
This time, we get as many `Feature` objects as we need to return to the client
-- in this case, we select them from the service's feature collection based on
whether they're inside our request `Rectangle`.
##### Client-side streaming RPC ##### Client-side streaming RPC
Now let's look at something a little more complicated: the client-side streaming Now consider something a little more complicated: the client-side streaming
method `RecordRoute()`, where we get a stream of `Point`s from the client and method `RecordRoute()`, where the server gets a stream of `Point` objects from
return a single `RouteSummary` with information about their trip. the client, and returns a single `RouteSummary` with information about their
trip through the given points.
```kotlin ```kotlin
override suspend fun recordRoute(requests: Flow<Point>): RouteSummary { override suspend fun recordRoute(requests: Flow<Point>): RouteSummary {
@ -267,11 +261,9 @@ The request parameter is a stream of client request messages represented as a
Kotlin [Flow][]. The server returns a single response just like in the simple Kotlin [Flow][]. The server returns a single response just like in the simple
RPC case. RPC case.
[Flow]: https://kotlinlang.org/docs/reference/coroutines/flow.html#flows
##### Bidirectional streaming RPC ##### Bidirectional streaming RPC
Finally, let's look at our bidirectional streaming RPC `RouteChat()`. Finally, consider the bidirectional streaming RPC `RouteChat()`.
```kotlin ```kotlin
override fun routeChat(requests: Flow<RouteNote>): Flow<RouteNote> = override fun routeChat(requests: Flow<RouteNote>): Flow<RouteNote> =
@ -289,90 +281,69 @@ override fun routeChat(requests: Flow<RouteNote>): Flow<RouteNote> =
} }
``` ```
This time we get a stream of `RouteNote` objects that, as in our client-side Similar to the client-side streaming example, for this method, the server gets a
streaming example, can be used to access messages. stream of `RouteNote` objects as a `Flow`. However, this time the server returns
`RouteNote` instances via the method's returned stream _while_ the client is still
<!-- However, this time we return writing messages to _its_ message stream.
values via our method's returned stream, while the client is still writing
messages to *their* message stream. -->
#### Starting the server #### Starting the server
Once we've implemented all our methods, we also need to start up a gRPC server Once all the server's methods are implemented, you need code to create a gRPC
so that clients can actually use our service. The following snippet shows how we server instance, something like this:
do this for our `RouteGuide` service:
```kotlin ```kotlin
class RouteGuideServer private constructor( class RouteGuideServer(
val port: Int, val port: Int,
val server: Server val features: Collection<Feature> = Database.features(),
val server: Server =
ServerBuilder.forPort(port)
.addService(RouteGuideService(features)).build()
) { ) {
constructor(port: Int) : this(port, defaultFeatureSource())
constructor(port: Int, featureData: ByteSource) :
this(
serverBuilder = ServerBuilder.forPort(port),
port = port,
features = featureData.parseJsonFeatures()
)
constructor(
serverBuilder: ServerBuilder<*>,
port: Int,
features: Collection<Feature>
) : this(
port = port,
server = serverBuilder.addService(RouteGuideService(features)).build()
)
fun start() { fun start() {
server.start() server.start()
println("Server started, listening on $port") println("Server started, listening on $port")
/* ... */ /* ... */
} }
/* ... */
}
companion object { fun main(args: Array<String>) {
@JvmStatic val port = 8980
fun main(args: Array<String>) { val server = RouteGuideServer(port)
val port = 8980 server.start()
val server = RouteGuideServer(port)
server.start()
/* ... */
}
}
/* ... */ /* ... */
} }
``` ```
As you can see, we build and start our server using a `ServerBuilder`.
To do this, we: A server instance is built and started using a `ServerBuilder` as follows:
1. Specify the address and port we want to use to listen for client requests 1. Specify the port, that the server will listen for client requests on, using
using the builder's `forPort()` method. `forPort()`.
1. Create an instance of our service implementation class `RouteGuideService` 1. Create an instance of the service implementation class `RouteGuideService`
and pass it to the builder's `addService()` method. and pass it to the builder's `addService()` method.
1. Call `build()` and `start()` on the builder to create and start an RPC server 1. Call `build()` and `start()` on the builder to create and start an RPC server
for our service. for the route guide service.
### Creating the client {#client} ### Creating the client {#client}
In this section, we'll look at creating a client for our `RouteGuide` In this section, you'll look at a client for the `RouteGuide` service.
service. You can see our complete example client code in
[grpc-kotlin/examples/src/main/kotlin/io/grpc/examples/routeguide/RouteGuideClient.kt](https://github.com/grpc/grpc-kotlin/blob/master/examples/src/main/kotlin/io/grpc/examples/routeguide/RouteGuideClient.kt). For the complete client code, open `routeguide/RouteGuideClient.kt`
under the [client/src/main/kotlin/io/grpc/examples][client-src] folder.
#### Instantiating a stub #### Instantiating a stub
To call service methods, we first need to create a gRPC *channel* to communicate To call service methods, you first need to create a gRPC _channel_ using a
with the server. We use a `ManagedChannelBuilder` to create the channel: `ManagedChannelBuilder`. You'll use this channel to communicate with the server.
```kotlin ```kotlin
val channel = ManagedChannelBuilder.forAddress("localhost", 8980).usePlaintext() val channel = ManagedChannelBuilder.forAddress("localhost", 8980).usePlaintext().build()
``` ```
Once the gRPC *channel* is setup, we need a client _stub_ to perform RPCs. We Once the gRPC channel is setup, you need a client _stub_ to perform RPCs. Get it
get it by instantiating `RouteGuideCoroutineStub`, which is available from the by instantiating `RouteGuideCoroutineStub`, which is available from the package
package that we generated from our .proto file. that was generated from the `.proto` file.
```kotlin ```kotlin
val stub = RouteGuideCoroutineStub(channel) val stub = RouteGuideCoroutineStub(channel)
@ -380,12 +351,12 @@ val stub = RouteGuideCoroutineStub(channel)
#### Calling service methods #### Calling service methods
Now let's look at how we call our service methods. Now consider how you'll call service methods.
##### Simple RPC ##### Simple RPC
Calling the simple RPC `GetFeature()` is as straightforward as calling a local Calling the simple RPC `GetFeature()` is as straightforward as calling a local
method. method:
```kotlin ```kotlin
val request = point(latitude, longitude) val request = point(latitude, longitude)
@ -393,13 +364,10 @@ val feature = stub.getFeature(request)
``` ```
The stub method `getFeature()` executes the corresponding RPC, suspending until The stub method `getFeature()` executes the corresponding RPC, suspending until
the RPC completes. We want the client to await (or block) until the RPC the RPC completes:
completes, so we make the stub method call inside a [runBlocking][] coroutine
builder. We wrap the entire body of the client class's `getFeature()` helper
method like this:
```kotlin ```kotlin
fun getFeature(latitude: Int, longitude: Int) = runBlocking { suspend fun getFeature(latitude: Int, longitude: Int) {
val request = point(latitude, longitude) val request = point(latitude, longitude)
val feature = stub.getFeature(request) val feature = stub.getFeature(request)
if (feature.exists()) { /* ... */ } if (feature.exists()) { /* ... */ }
@ -408,11 +376,11 @@ fun getFeature(latitude: Int, longitude: Int) = runBlocking {
##### Server-side streaming RPC ##### Server-side streaming RPC
Next, let's look at a server-side streaming `ListFeatures()` RPC, which returns Next, consider the server-side streaming `ListFeatures()` RPC, which returns a
a stream of geographical `Feature`s: stream of geographical features:
```kotlin ```kotlin
fun listFeatures(lowLat: Int, lowLon: Int, hiLat: Int, hiLon: Int) = runBlocking { suspend fun listFeatures(lowLat: Int, lowLon: Int, hiLat: Int, hiLon: Int) {
val request = Rectangle.newBuilder() val request = Rectangle.newBuilder()
.setLo(point(lowLat, lowLon)) .setLo(point(lowLat, lowLon))
.setHi(point(hiLat, hiLon)) .setHi(point(hiLat, hiLon))
@ -425,16 +393,16 @@ fun listFeatures(lowLat: Int, lowLon: Int, hiLat: Int, hiLon: Int) = runBlocking
``` ```
The stub `listFeatures()` method returns a stream of features in the form of a The stub `listFeatures()` method returns a stream of features in the form of a
`Flow<Feature>` instance. The flow `collect` method allows the client to `Flow<Feature>` instance. The flow `collect()` method allows the client to
processes the server-provided features as they become available. processes the server-provided features as they become available.
##### Client-side streaming RPC ##### Client-side streaming RPC
With the client-side streaming `RecordRoute()` RPC, we send a stream of The client-side streaming `RecordRoute()` RPC sends a stream of `Point` messages
`Point` messages to the server and get back a single `RouteSummary`. to the server and gets back a single `RouteSummary`.
```kotlin ```kotlin
fun recordRoute(points: Flow<Point>) = runBlocking { suspend fun recordRoute(points: Flow<Point>) {
println("*** RecordRoute") println("*** RecordRoute")
val summary = stub.recordRoute(points) val summary = stub.recordRoute(points)
println("Finished trip with ${summary.pointCount} points.") println("Finished trip with ${summary.pointCount} points.")
@ -445,9 +413,9 @@ fun recordRoute(points: Flow<Point>) = runBlocking {
} }
``` ```
We generate the route points from the points associated with a randomly selected The method generates the route points from the points associated with a randomly
list of features. The random selection is taken from a previously loaded feature selected list of features. The random selection is taken from a previously
collection: loaded feature collection:
```kotlin ```kotlin
fun generateRoutePoints(features: List<Feature>, numPoints: Int): Flow<Point> = flow { fun generateRoutePoints(features: List<Feature>, numPoints: Int): Flow<Point> = flow {
@ -466,16 +434,15 @@ until the server requests the next point.
##### Bidirectional streaming RPC ##### Bidirectional streaming RPC
Finally, let's look at our bidirectional streaming RPC `RouteChat()`. As in the Finally, consider the bidirectional streaming RPC `RouteChat()`. As in the case
case of `RecordRoute()`, we pass to the method a stream that we'll use to write of `RecordRoute()`, you pass to the stub method a stream that you use to write
the request messages to; like in `ListFeatures()`, we get back a stream that we the request messages to; like in `ListFeatures()`, you get back a stream that
can use to read response messages from. However, this time we'll send values via you can use to read response messages from. However, this time you send values
our method's stream while the server is also writing messages to _its_ message via our method's stream while the server is also writing messages to _its_
stream. message stream.
```kotlin ```kotlin
fun routeChat() = runBlocking { suspend fun routeChat() {
println("*** RouteChat")
val requests = generateOutgoingNotes() val requests = generateOutgoingNotes()
stub.routeChat(requests).collect { note -> stub.routeChat(requests).collect { note ->
println("Got message \"${note.message}\" at ${note.location.toStr()}") println("Got message \"${note.message}\" at ${note.location.toStr()}")
@ -487,44 +454,41 @@ private fun generateOutgoingNotes(): Flow<RouteNote> = flow {
val notes = listOf(/* ... */) val notes = listOf(/* ... */)
for (note in notes) { for (note in notes) {
println("Sending message \"${note.message}\" at ${note.location.toStr()}") println("Sending message \"${note.message}\" at ${note.location.toStr()}")
emit(note); emit(note)
delay(500) delay(500)
} }
} }
``` ```
The syntax for reading and writing here is very similar to our client-side and The syntax for reading and writing here is very similar to the client-side and
server-side streaming methods. Although each side will always get the other's server-side streaming methods. Although each side will always get the other's
messages in the order they were written, both the client and server can read and messages in the order they were written, both the client and server can read and
write in any order —- the streams operate completely independently. write in any order —- the streams operate completely independently.
### Try it out! ### Try it out!
1. Work from the example directory: Run the following commands from the `grpc-kotlin/examples` directory:
```sh 1. Compile the client and server
$ cd examples/route_guide
```
2. Compile the client and server
```sh ```sh
$ ./gradlew installDist $ ./gradlew installDist
``` ```
3. Run the server: 2. Run the server:
```sh ```sh
$ ./build/install/examples/bin/route-guide-server $ ./server/build/install/server/bin/route-guide-server
Server started, listening on 8980
``` ```
4. From another terminal, run the client: 3. From another terminal, run the client:
```sh ```sh
$ ./build/install/examples/bin/route-guide-client $ ./client/build/install/client/bin/route-guide-client
``` ```
You'll see output like this: You'll see client output like this:
```nocode ```nocode
*** GetFeature: lat=409146138 lon=-746188906 *** GetFeature: lat=409146138 lon=-746188906
@ -563,3 +527,15 @@ Got message "First message" at 0.0, 0.0
Got message "Second message" at 0.0, 0.0 Got message "Second message" at 0.0, 0.0
Finished RouteChat Finished RouteChat
``` ```
[client-src]: https://github.com/grpc/grpc-kotlin/tree/master/examples/client/src/main/kotlin/io/grpc/examples
[download]: https://github.com/grpc/grpc-kotlin/archive/master.zip
[Flow]: https://kotlinlang.org/docs/reference/coroutines/flow.html#flows
[grpc-kotlin]: https://github.com/grpc/grpc-kotlin
[Introduction to gRPC]: /docs/what-is-grpc/introduction/
[proto3]: https://developers.google.com/protocol-buffers/docs/proto3
[Prerequisites]: ../quickstart/#prerequisites
[protos-src]: https://github.com/grpc/grpc-kotlin/tree/master/examples/protos/src/main/proto/io/grpc/examples
[Quick start]: ../quickstart/
[server-src]: https://github.com/grpc/grpc-kotlin/tree/master/examples/server/src/main/kotlin/io/grpc/examples
[stub/build.gradle.kts]: https://github.com/grpc/grpc-kotlin/blob/master/examples/stub/build.gradle.kts