2. gRPC in Quarkus
2.1. Dependencies
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-grpc</artifactId>
</dependency>
2.2. Protobuf
In den Folder proto werden alle benötigten proto Dateien abgelegt.

Mit dem shell command
mvn compile
werden alle benötigten Dateien generiert.
Für dieses Beispiel wird ein Service mit folgenden Methoden definiert.
syntax = "proto3";
service Greeter {
rpc SayHello(HelloRequest) returns (HelloReply) {}
rpc StreamHello(stream HelloRequest) returns (stream HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
2.3. Einen Stub verwenden
Um einen Stub in einem Quarkus Projekt zu verwenden, muss nur der Name des Services "Injected" werden.
@GrpcClient//(client-name) (1)
Greeter greeter;
1 | Wenn der client-name des Stubs nicht angegeben wird, wird stattdessen automatisch der Name der Variable benutzt. In diesem Fall wäre dieser greeter . |
Dann können alle Methoden, die in den Protofiles definiert worden sind, aufgerufen werden.
2.3.1. Konfiguration
Der Port und Host des gRPC Service kann mit folgender Konfiguration in den application.properties definiert werden.
quarkus.grpc.clients.<client-name>.host=<host>
quarkus.grpc.clients.<client-name>.port=<port>
Der Host ist dabei per Default "localhost" und der Port ist per Default 9000.
Der client-name wäre in unserem Fall greeter also würde die Konfiguration folgendermaßen aussehen.
quarkus.grpc.clients.greeter.host=localhost
quarkus.grpc.clients.greeter.port=8080
2.4. Einen Service Implementieren
2.4.1. Konfiguration
Falls man den gRPC Service nicht auf einem separaten Server laufen lassen möchte, muss man folgendes in den application.properties eintragen.
quarkus.grpc.server.use-separate-server=false
2.4.2. Implementierung
Um unseren GreeterService nun zu implementieren, müssen wir eine Klasse GreeterService erstellen die unseren gRPC Service implementiert.
@GrpcService
public class GreeterService implements Greeter {
}
Nun müssen die definierten Methoden ausprogrammiert werden.
Die Methode SayHello soll eine Begrüßung für einen mitgegebenen Namen generieren und zurückschicken.
@Override
public Uni<HelloReply> sayHello(HelloRequest request) {
return Uni.createFrom()
.item(
HelloReply.newBuilder() (1)
.setMessage(greet(request.getName())) (2)
.build() (3)
);
}
In diesem Beispiel müssen wir drei Sachen machen, um unser gRPC Objekt zu kreieren:
1 | Einen neuen Builder des HelloReply typen erstellen |
2 | Unsere Begrüßung in die Eigenschaft message speichern |
3 | Das Objekt bauen |
Die Methode StreamHello soll jede Sekunde eine generierte Begrüßung schicken. Es wird immer der zuletzt geschickte Name verwendet.
public Multi<HelloReply> streamHello(Multi<HelloRequest> incomingStream) {
AtomicReference<String> name = new AtomicReference<>(""); (1)
incomingStream (2)
.subscribe()
.with(request -> {
name.set(request.getName());
});
return Multi.createFrom() (3)
.ticks()
.every(Duration.ofSeconds(1))
.map(ignored ->
HelloReply.newBuilder().setMessage(greet(name.get())).build()
);
}
Hier gibt es 3 grobe Abschnitte
1 | Eine Referenz auf einen String erstellen, auf die man aus mehreren Threads zugreifen kann |
2 | Diese Referenz immer dann neu setzten, wenn der Stub/Client einen neuen Namen schickt |
3 | Alle Sekunden eine Begrüßung mit dem Momentan gesetzten Namen an den Stub/Client schicken |
3. Chat Stub
In diesem Abschnitt wir ein Stub für ein bereits implementiertes Backend geschrieben.
Der vorgezeigte Programmflow ist folgender:

3.2. Dependencies
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-grpc</artifactId>
</dependency>
<dependency>
<groupId>org.jline</groupId>
<artifactId>jline-reader</artifactId>
<version>3.29.0</version>
</dependency>
</dependencies>
Neben der Quarkus gRPC Dependency brauchen wir auch JLine.
JLine ermöglicht die OS-spezifische Interaktion mit Konsolen.
3.3. Konfiguration
quarkus.package.jar.type=uber-jar
quarkus.grpc.clients.chat.host=winnie.at
quarkus.grpc.clients.chat.port=80
Für den Stub (chat) muss sowohl der Port als auch der Host des Backends spezifiziert werden.
3.4. Protocol Buffer
syntax = "proto3";
option go_package = "/proto";
option java_package = "at.htl.grpc";
service Chat {
rpc ClaimName(ClaimNameRequest) returns (ClaimNameResponse) {} (1)
rpc Connect(stream OutgoingMessage) returns (stream IncomingMessage) {} (2)
}
message ClaimNameRequest {
string name = 1;
}
message ClaimNameResponse {
string token = 1;
}
message OutgoingMessage {
string message = 1;
}
message IncomingMessage {
string name = 1;
string response = 2;
}
In diesem Proto file sind 2 Prozeduren definiert:
1 | Einen Namen reservieren ⇒ JWT als response |
2 | JWT im Header ⇒ Am Chat teilnehmen |
3.5. Implementierung
3.5.1. Statische Message
Da wir in mit mehreren Threads arbeiten, die zugleich auf die Variable message zugreifen, müssen wir diese Variable in dem Globalen Scope definieren
private static final StringBuilder message = new StringBuilder();
3.5.2. Stub Injection
In unserem Programm muss als erstes der gRPC Stub Injected werden.
// Inject gRPC stub
@GrpcClient
Chat chat;
3.5.3. JWT Abfragen
Dann muss ein Name reserviert werden.
@Override
public int run(String... args) {
// claim name from server
String token = chat
.claimName(
ClaimNameRequest.newBuilder()
.setName(String.join(" ", args))
.build()
)
.await()
.indefinitely()
.getToken();
3.5.4. JWT in den Header
Um den JWT dem gRPC konform zu übermitteln, muss er in den Header gespeichert werden.
// append token in header
Metadata headers = new Metadata();
headers.put(
Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER), (1)
String.format("Bearer %s", token) (2)
);
1 | Den Header Key spezifizieren |
2 | Den Token als Value angeben |
Dieser Header muss nun an den Stub gebunden werden.
// attach headers to stub
Chat authorizedStub = GrpcClientUtils.attachHeaders(chat, headers);
Von nun an wird nur mehr der authorizedStub
verwendet, da der normale Stub keine Authentifizierung hat.
3.5.5. Output Stream erstellen
Um die Verbindung zu öffnen, muss ein Multi erstellt werden.
// create output stream
Multi<OutgoingMessage> outgoingStream = Multi.createFrom()
.<OutgoingMessage>emitter(ChatMain::sendMessages);
Dieser Multi wird von unserer Methode SendMessages befüllt (seeded).
3.5.6. Verbinden
// connect to service and start printing incoming messages
printMessages(authorizedStub.connect(outgoingStream));
Nun wird sich mit dem gRPC Service verbunden und der Stream mit den empfangenen Nachrichten wird direkt an die Methode PrintMessages weitergegeben.
3.5.7. Nachrichten Ausgeben
Da der User auf der Konsole seine eigene Nachricht eingibt, soll nicht über die Nachricht des Users mit anderen Nachrichten geschrieben werden.
/**
* Print incoming messages without overwriting the current input line.
*
* @param incomingStream stream with incoming messages
*/
public static void printMessages(Multi<IncomingMessage> incomingStream) {
incomingStream
.subscribe()
.with(incomingMessage ->
System.out.printf(
"\033[2K\r%s: %s\n\rWrite message: %s",
incomingMessage.getName(),
incomingMessage.getResponse(),
message
)
);
}
Wenn eine Nachricht empfangen wird, wird zuerst die momentane Zeile komplett gelöscht. Dann wird die empfangene Nachricht ausgegeben und schlussendlich wird in der nächsten Zeile der Prompt an den User wieder hergestellt.
3.5.8. Nachrichten Senden
/**
* Read input from CLI and send it to service.
*
* @param emitter the emitter that sends messages back to the service
*/
public static void sendMessages(
MultiEmitter<? super OutgoingMessage> emitter
) {
try (Terminal terminal = TerminalBuilder.terminal()) {
// strip flags from terminal
terminal.enterRawMode(); (1)
while (true) {
try {
// fill the message with user input
readMessage(terminal); (2)
} catch (IOException e) {
throw new RuntimeException(e);
}
// send the message to service
emitter.emit(
(3)
OutgoingMessage.newBuilder()
.setMessage(message.toString())
.build()
);
// print finished message line
System.out.printf("\033[2K\rYou wrote: %s\n\r", message); (4)
// empty the message
message.setLength(0); (5)
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
1 | Bestimmte Flags werden vom Terminal entfernt, sodass der Input, der in die Konsole geschrieben wird:
|
2 | Als Erstes wird unsere statische message Variable befüllt. |
3 | Mit dieser Variable wird dann durch den Emitter unsere Nachricht an den gRPC Service geschickt. |
4 | Die input Zeile wird nun gelöscht und anstatt von `Write Message: ` wird die Zeile mit `You wrote: ` befüllt. |
5 | Zuletzt wird unsere Variable message wieder zurückgesetzt. |
3.5.9. Nachrichten Einlesen
Implementierung
/**
* Read string from CLI without control characters.
*
* @throws IOException if the connection to the terminal cannot be established
*/
private static void readMessage(Terminal terminal) throws IOException {
System.out.print("Write message: ");
int controlCharCounter = 0;
while (true) {
// read one character from the console
char character = (char) terminal.reader().read();
if (
(controlCharCounter == 2 && character == '[') ||
controlCharCounter == 1
) {
controlCharCounter--;
continue;
}
// stop reading and return
if (character == '\n' || character == '\r') {
return;
}
// delete single character
if (character == 127 && !message.isEmpty()) {
System.out.print("\b \b");
message.deleteCharAt(message.length() - 1);
}
controlCharCounter = 0;
if (character == 27) {
controlCharCounter = 2;
}
// do not display/send control characters
if (Character.isISOControl(character)) {
continue;
}
// print character to console and append it to message
System.out.print(character);
message.append(character);
}
}
In dieser Methode wird jeweils eine Nachricht eingelesen. Die eingabe wird mit Enter beendet.
Der Input des Users gelesen und in die Variable message gespeichert.
Bei einem Enter wird die aus dieser Methode returned.