REQUIREMENTS:
-
Java 11
-
Uses the gradle build system, gradle downloads itself!
./gradlew <command>
-
Tip for Intellij users: install protocol buffers plugin
Resources:
- Workshop Presentation
- Protocol Buffers 3 docs <- make sure to look for proto3!
- Protocol Buffers Java generated code guide <- make sure to look for proto3!
- gRPC homepage
- gRPC docs <- select Java or directly go to Java generated code reference
- gRPC videos
The workshop contains a simple tic-tac-toe game (boter kaas en eieren). Multiple players can join and play against each other.
There are two components: a client and and a server. Clients connect to the server using gRPC and execute calls such as testConnection()
, createPlayer()
, listPlayers()
and playGame()
.
Most of the code is already finished. The only thing that needs to be implemented is the client side gRPC layer.
Schematic overview of the interactions between client and server:
Run ServerMain.java
from your IDE or
./gradlew runServer
Run ClientMain.java
from your IDE or
./gradlew runClient
The client will open a JavaFX window.
Take a look at tic_tac_toe.proto This file already contains the protocol buffer message types that will be used, and a service definition. The server is already implemented using this definition.
The next exercises will require you to implement the client side, step by step.
During the workshop, the host will run a gRPC server instance that everybody can use.
During development you can also start the server yourself from the IDE by running ServerMain.java
or
./gradlew runServer
The next assignments assume you keep the server running.
Open the file GrpcController.java and
implement the connectToServer()
method.
Step 1: create a ManagedChannel
using a ManagedChannelBuilder
to build a channel that connects to the given host and port.
The builder has a method usePlainText()
that you must call, because we are not setting up a secure connection for this workshop.
Once you have channel, you can create a client for a service defined in a .proto file. We only have one service defined and it is called TicTacToe. The clients are called stubs and only need a channel. All calls are send over that channel.
Step 2: create a blocking stub using the TicTacToeGrpc.newBlockingStub()
class and call testConnection()
on that stub. This will execute a call to the server!
If anything goes wrong, an exception will be thrown and a visual dialog will pop-up. Otherwise everything is OK.
Step 3: Store the ManagedChannel
instance in the state, using state.setGrpcConnection(grpcConnection)
.
Run the client and test your code by running ClientMain.java or
./gradlew runClient
And click the connect button!
Open the file GrpcController.java and
implement the createPlayer()
method.
Step 1 Build a CreatePlayerRequest
the given playerName in it.
Step 2 Create a TicTacToeGrpc.newBlockingstub()
, use the channel from exercise 1 which can be retrieved from the state: state.getGrpcConnection()
Step 3 Call the create player method on the stub. You should, get a player object back, store this object in the state using state.setMyself(returnedPlayer)
.
Test it out running the game and click the Join button, you should see your name appear next to it.
Open the file GrpcController.java and
implement the initializeGameStream()
method.
You might have noticed that the blockingstub you created in the previous exercises does not contain the playGame()
method from our TicTacToe service. That's because playGame()
is a bi-directional streaming call, which is by definition not blocking.
Step 1 Create a TicTacToeGrpc.newStub()
instead of a blockingStub(). This stub exposes the PlayGame method.
Step 2 The stubs have many sort of builder methods. The one you should use now is .withCallCredentials()
and pass it a new PlayerIdCredentials
object. The ID passed into the PlayerIdCredentials should equal state.getMyself().getId()
. Call credentials will be sent as metadata before each call. The server will use it to identify which player is joining the game.
Step 3 Issue a call to playGame()
The input parameter is a StreamObserver. You can create an anonymous class of the StreamObserver interface, giving you 3 methods to implement onNext()
, onError()
, onCompleted()
. This methods can be implemented with only one line each:
onNext() should be implemented: state.onGameEvent(gameEvent)
onError() should be implemented: state.onGameStreamError(throwable)
onCompleted() should be implemented: state.onGameStreamCompleted()
Step 4 Store the stream returned by playGame()
in the state by using state.setGameCommandStream()
. The game uses this stream to send game commands to the server.
Test it out running the game and click the Join button. If you see your name next to it, and no error pops up, everythin is ok!
Open the file GrpcController.java and
implement the listPlayers()
method.
Step 1 Create a stub (whichever you like) and call listPlayers. Remember you always need an input parameter, even if it is empty. Make sure to also send the PlayerIdCredentials!
Step 2 Store the list of players that gets returned in state.setPlayers()
Test it out running the game, join the game. You can now click Fetch Players. A list of all players that are joined should be displayed.
Open the file GrpcController.java and
implement the startGameAgainstPlayer()
method.
This method receives the opponent you want to challenge. You can get yourself using state.getMyself()
.
Step 1 Create GameCommand and StartGame command.
Step 2 Send this game command to the server using state.getGameCommandStream().onNext()
Test it out by:
- Run the client 2 times:
- In each client join the game using a different name.
- In both windows, now click 'Fetch Players'. You should see 2 players in the list.
- In the first client, select the other player and click the 'Start game' button (button is located) below the list.
- In both windows, you should see a grid on the right side!
This is the last exercise, then we will be able to play the game.
Open the file GrpcController.java and
implement the makeBoardMove()
method.
Step 1 Create GameCommand with the BoardMove parameter.
Step 2 Send this game command to the server using state.getGameCommandStream().onNext()
Test it out by:
- Run the client 2 times:
- In each client join the game using a different name.
- In both windows, now click 'Fetch Players'. You should see 2 players in the list.
- In the first client, select the other player and click the 'Start game' button (button is located) below the list.
- In both windows, you should see a grid on the right side!
- The window that initiated the game can click somewhere in the grid to make a move.
- Both windows should update, the other player can make the next move.
Test this out by playing against someone else! The game itself does not detect when a game has ended yet. So this is it :)
Step 1 Add an RPC to the TicTacToe service definition called ListPlayersStream
in the tic_tac_toe.proto file. Make the return type a stream
of Player objects. The request type can be the same as the other listPlayers()
method.
Step 2 Regenerate regenerate the code using:
./gradlew generateProto
The new method is now available on the stubs, and you can override a new method in TicTacToeServerController
Step 3 Implement the server side by calling onNext
multiple times for each player. When all
players are sent, then call onCompleted()
to signal that no more items are to be expected.
Step 4 Update the listPlayers() method in GrpcController.java
to use listPlayersStream()
method.
If you are running linux and you have a 4K display, the UI might look really small. Gnome based distro's can use this command to scale the window:
gsettings set org.gnome.desktop.interface scaling-factor 2
See https://stackoverflow.com/questions/26182460/javafx-8-hidpi-support