Neuigkeiten von trion.
Immer gut informiert.

Angular und gRPC Web

gRPC hat bisher vor allem im Backend oder bei der direkten Kommunikation zwischen Anwendungen auf dem Endgrät und dem Backend eine Rolle gespielt. Aufgrund der zunehmenden Rolle von Browseranwendungen sollte gRPC auch in clientseitigen Webanwendungen genutzt werden können.
Das zugehörige Projekt ist gRPC-Web: https://github.com/grpc/grpc-web

In diesem Beitrag wurde die Erstellung eines Spring Boot gRPC Backends demonstriert. Nun get es darum, das Backend in einer Angular Webanwendung mit gRPC-Web anzubinden.

Der erste Schritt ist die Erzeugung der JavaScript oder TypeScript Objekte und Client-Bindings. Falls TypeScript eine Option im Projekt darstellt, sollte darauf zurückgegriffen werden: Durch die Typinformationen ist die Werkzeugunterstützung, beispielsweise in IDEs oder bei statischer Codeanalyse auf potentielle Fehler deutlich besser.

TypeScript Protobuf Bindings

Aus Protobuf Definitionen lassen sich sowohl TypeScript Interfaces, als auch eine grpc-web Zugriffsschicht für die Nutzung in Browser Anwendungen generieren. Am einfachsten erfolgt dies mit einem Docker Image, in dem die benötigten Werkzeuge enthalten sind.

Dockerfile für grpc-web-builder
# docker build -t trion/grpc-web-builder .
FROM ubuntu:18.04

RUN apt-get update && apt-get install -y \
  automake \
  build-essential \
  git \
  libtool \
  make && \
  apt-get clean; rm -rf /var/lib/apt/lists/*

RUN git clone https://github.com/grpc/grpc-web /github/grpc-web && \
    cd /github/grpc-web && \
    ./scripts/init_submodules.sh && \
    cd /github/grpc-web/third_party/grpc && \
    make && make install  && \
    cd /github/grpc-web/third_party/grpc/third_party/protobuf && \
    make install && \
    cd /github/grpc-web && \
    make install-plugin && \
    cd / && rm -rf /github

ENV MAKEFLAGS=-j4 import_style=commonjs grpc_web_import_style=commonjs mode=grpcwebtext protofile=echo.proto output=/protofile/generated
VOLUME /protofile

CMD rm -rf $output && \
  mkdir $output && \
  protoc \
  -I=/protofile \
  /protofile/$protofile \
  --js_out=import_style=$import_style:$output \
  --grpc-web_out=import_style=$grpc_web_import_style,mode=$mode:$output

Alternativ kann natürlich ein fertiges Docker Image verwendet werden, oder die Werkzeuge nativ installiert werden. Nun können die Protocol Buffer Definitionen übersetzt werden. Bei Verwendung des Docker Image werden die notwendigen Einstellungen am einfachsten mit Umgebungsvariablen definiert. Dabei wird festgelegt, ob JavaScript oder TypeScript Code erzeugt werden soll, wo sich die Protobuf Beschreibung findet, und welches Transportprotokoll verwendet werden soll. Aktuell wird lediglich grpcwebtext und kein rein binäres Transportformat unterstützt.

Generierung von TypeScript Definitionen für Protobuf Strukturen
$ docker run --rm -u $(id -u) \
  -v "$PWD:/protofile" \
  -e protofile=Greeter.proto \
  -e mode=grpcwebtext \
  -e grpc_web_import_style=typescript \
  trion/grpc-web-builder

Als Ergebnis finden sich dann im generated Ordner folgende Dateien

  1. Greeter_pb.d.ts

  2. Greeter_pb.js

  3. GreeterServiceClientPb.ts

Die ersten beiden sind JavaScript und zugehörige TypeScript Informationen zu den Objekten, der GreeterServiceClientPb.ts ist der Client für den tatsächlichen Zugriff über gRPC Web. Mit den so erzeugten Dateien kann nun in einem Angular Projekt gearbeitet werden.

Angular Anwendung

Bei der Erstellung der Angular Anwendung kann ebenfalls auf ein Docker Image zurückgegriffen werden, um die Installation von lokalen Werkzeugen zu vermeiden. Alternativ kann natürlich mit Angular-CLI oder einem Angular Seed-Projekt gearbeitet werden.

Erzeugung der Angular Anwendung mit Docker
$ docker run --rm -u $(id -u)\
 -v $PWD:/app \
 trion/ng-cli ng new GrpcDemo
CREATE GrpcDemo/README.md (1025 bytes)
CREATE GrpcDemo/angular.json (3786 bytes)
...

Um mit Protocol Buffers zu arbeiten, muss die Abhängigkeit google-protobuf und für gRPC Web die Abhängigkeit grpc-web ergänzt werden. Das geschieht wie gewohnt mit npm - auch hier, falls gewünscht, mit einem Docker Image.

Ergänzung der Abhängigkeit auf grpc-web und google-protobuf durch npm
$ cd GrpcDemo
$ docker run --rm -u $(id -u)\
 -v $PWD:/app \
 trion/ng-cli npm install grpc-web
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.4 (node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.4: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})

+ grpc-web@1.0.2
added 1 package from 1 contributor and audited 39129 packages in 7.842s
found 0 vulnerabilities
$ docker run --rm -u $(id -u)\
 -v $PWD:/app \
 trion/ng-cli npm i google-protobuf

npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.4 (node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.4: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})

+ google-protobuf@3.6.1
added 1 package from 1 contributor and audited 39130 packages in 7.217s
found 0 vulnerabilities

Die Infrastruktur der Angular Anwendung für gRPC-Web ist damit eingerichtet. Nun fehlt noch eine Einbindung.
In Angular werden sogenannte Services verwendet, um Funktionalität - wie den Zugriff auf eine Remote-API - bereitzustellen, und Components in der View Schicht zur Darstellung und Interaktion. Im nächsten Schritt wird daher ein Greeting-Service und eine Greeter-Component erzeugt. Bei Angular kann der erforderliche Code und die Einbindung in das Angular Modul komfortabel durch Angular-CLI erfolgen.

Erzeugung der Greeter-Komponente und Service in Angular
$ docker run --rm -u $(id -u)\
 -v $PWD:/app \
 trion/ng-cli ng g s greeter
CREATE src/app/greeter.service.spec.ts (338 bytes)
CREATE src/app/greeter.service.ts (136 bytes)
$ docker run --rm -u $(id -u)\
 -v $PWD:/app \
 trion/ng-cli ng g c greeter
CREATE src/app/greeter/greeter.component.css (0 bytes)
CREATE src/app/greeter/greeter.component.html (26 bytes)
CREATE src/app/greeter/greeter.component.spec.ts (635 bytes)
CREATE src/app/greeter/greeter.component.ts (273 bytes)
UPDATE src/app/app.module.ts (400 bytes)

Verwendung gRPC in Angular

Die mit grpc-web-builder erzeugten Sourcen werden in die Angular Anwendung nach src/app kopiert. Dazu kann ruhig der generated Ordner als solches genutzt werden.
Anschließend können die generierten Klasen im Angular Service regulär importiert werden.

Im Service wird dann auch konfiguriert, wo der gRPC Web Endpunkt angesprochen werden kann. Natürlich ist hier eine dynamischere Konfiguration, je nach Umebung, als Erweiterung denkbar möglich. Für das Beispiel wird auf einen festen Port auf localhost zugegriffen.

gRPC Web Angular Service
import { Injectable } from '@angular/core';
import { Greeting, Person } from './generated/Greeter_pb';
import { GreeterServiceClient } from './generated/GreeterServiceClientPb';
import { Error, Status } from 'grpc-web';
import { Observable } from 'rxjs';


@Injectable({
  providedIn: 'root'
})
export class GreeterService {

  private greeterServiceClient: GreeterServiceClient;
  constructor() {
    this.greeterServiceClient =
      new GreeterServiceClient('http://localhost:8090', null, null);
  }

  callGreet(firstName: string, lastName: string) {
    const person = new Person();

    person.setFirstName(firstName);
    person.setLastName(lastName);

    const obs = new Observable<string>(s => {
      const call = this.greeterServiceClient.greet(person, null,
        (err: Error, greeting: Greeting) => {
          s.next(greeting.getMessage());
      });
      call.on('status', (status: Status) => {
        // handle status code
      });
    });
    return obs;
  }
}

In der Angular Komponente kann der so erzeugte Angular gRPC Service nun angebunden werden. Die Beispielkomponente erhält ein einfaches Eingabefeld und einen Knopf, um den den gRPC Greeting Dienst Aufruf zu starten.

Komponente
import { Component } from '@angular/core';
import {GreeterService} from '../greeter.service';
import {Observable} from 'rxjs';

@Component({
  selector: 'app-greeter',
  template: `
        First name: <input type="text" #firstName>
        Last name: <input type="text" #lastName>
        <button (click)="greet(firstName.value, lastName.value)">Greet!</button>
        Greeting: {{greeting | async }}`
})
export class GreeterComponent {
  private greeting: Observable<string>;

  constructor(private greeterService: GreeterService) { }

  greet(firstName: string, lastName: string) {
    this.greeting = this.greeterService.callGreet(firstName, lastName);
  }
}

Damit die nun erzeugte Komponente auch angezeigt wird, wird diese in dem app.component.html Template als <app-greeter></app-greeter> eingebunden. Anschließend kann mit ng serve die Angular Anwendung lokal gestartet werden.

Start der Angular Anwendung mit Docker
$ docker run --rm -u $(id -u)\
 -v $PWD:/app \
 -p 4200:4200 \
 trion/ng-cli ng serve --host 0.0.0.0

Ein Zugriff auf den gRPC Dienst ist jedoch noch nicht direkt möglich: Das gRPC-Web Protokoll ist aktuell noch nicht in den typischen Frameworks integriert.

gRPC Web Gateway Proxy für Spring Boot

Damit ein Zugriff von Web Clients via gRPC Web auf einen gRPC Dienst möglich ist, bedarf es aktuell noch einem zusätzlichen Gateway-Proxy. Diesem obliegt die Konvertierung von gRPC-Web Aufrufen auf das reguläre gRPC Protokoll. Es ist davon auszugehen, dass in absehbarer Zeit eine direkte Unterstützung des gRPC-Web Protokolls in populären Frameworks, wie Spring, implementiert wird.

Als Gateway-Proxy eignet sich beispielsweise Envoy, der vor allem im Kubernetes Umfeld als Teil von Istio Popularität erlangt hat. Envoy bringt gRPC-Web Support von Haus aus mit, lediglich eine Konfiguration zum Mapping von Eingangsparametern auf das richtige gRPC Backend muss vorgenommen werden.

Beispielkonfiguration für Envoy gRPC-Web und Spring Boot Anwendung
admin:
  access_log_path: /tmp/admin_access.log
  address:
    socket_address: { address: 0.0.0.0, port_value: 8091 }

static_resources:
  listeners:
  - name: listener_0
    address:
      socket_address: { address: 0.0.0.0, port_value: 8090 }
    filter_chains:
    - filters:
      - name: envoy.http_connection_manager
        config:
          codec_type: auto
          stat_prefix: ingress_http
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              domains: ["*"]
              routes:
              - match: { prefix: "/" }
                route: { cluster: greeting_service }
              cors:
                allow_origin: ["*"]
                allow_methods: GET, PUT, DELETE, POST, OPTIONS
                allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web
                max_age: "1728000"
                expose_headers: grpc-status,grpc-message
                enabled: true
          http_filters:
          - name: envoy.grpc_web
          - name: envoy.cors
          - name: envoy.router
  clusters:
  - name: greeting_service
    connect_timeout: 0.25s
    type: logical_dns
    http2_protocol_options: {}
    lb_policy: round_robin
    hosts: [{ socket_address: { address: 127.0.0.1, port_value: 6565 }}]

Am einfachsten lässt sich Envoy als Docker Container starten. Wichtig ist im Test dabei die Konfiguration --net=host, damit der Zugriff auf die Spring-Boot Anwendung ermöglicht wird, die auf localhost läuft.

Envoy als Docker Container unter Linux mit Host-Netzwerk für Zugriff auf localhost
$ docker run --rm --net=host \
  -v $PWD/envoy-config.yaml:/envoy-config.yaml \
  envoyproxy/envoy \
  /usr/local/bin/envoy -c /envoy-config.yaml
[main] [source/server/server.cc:206] initializing epoch 0 (hot restart version=10.200.16384.127.options=capacity=16384, num_slots=8209 hash=228984379728933363 size=2654312)
[main] [source/server/server.cc:208] statically linked extensions:
[main] [source/server/server.cc:210]   access_loggers: envoy.file_access_log,envoy.http_grpc_access_log
[main] [source/server/server.cc:213]   filters.http: envoy.buffer,envoy.cors,envoy.ext_authz,envoy.fault,envoy.filters.http.header_to_metadata,envoy.filters.http.jwt_authn,envoy.filters.http.rbac,envoy.grpc_http1_bridge,envoy.grpc_json_transcoder,envoy.grpc_web,envoy.gzip,envoy.health_check,envoy.http_dynamo_filter,envoy.ip_tagging,envoy.lua,envoy.rate_limit,envoy.router,envoy.squash
[main] [source/server/server.cc:216]   filters.listener: envoy.listener.original_dst,envoy.listener.proxy_protocol,envoy.listener.tls_inspector
[main] [source/server/server.cc:219]   filters.network: envoy.client_ssl_auth,envoy.echo,envoy.ext_authz,envoy.filters.network.dubbo_proxy,envoy.filters.network.rbac,envoy.filters.network.sni_cluster,envoy.filters.network.thrift_proxy,envoy.http_connection_manager,envoy.mongo_proxy,envoy.ratelimit,envoy.redis_proxy,envoy.tcp_proxy
[main] [source/server/server.cc:221]   stat_sinks: envoy.dog_statsd,envoy.metrics_service,envoy.stat_sinks.hystrix,envoy.statsd
[main] [source/server/server.cc:223]   tracers: envoy.dynamic.ot,envoy.lightstep,envoy.tracers.datadog,envoy.zipkin
[main] [source/server/server.cc:226]   transport_sockets.downstream: envoy.transport_sockets.alts,envoy.transport_sockets.capture,raw_buffer,tls
[main] [source/server/server.cc:229]   transport_sockets.upstream: envoy.transport_sockets.alts,envoy.transport_sockets.capture,raw_buffer,tls
[main] [source/server/server.cc:271] admin address: 0.0.0.0:8091
[config] [source/server/configuration_impl.cc:51] loading 0 static secret(s)
[config] [source/server/configuration_impl.cc:57] loading 1 cluster(s)
[upstream] [source/common/upstream/cluster_manager_impl.cc:136] cm init: all clusters initialized
[config] [source/server/configuration_impl.cc:62] loading 1 listener(s)
[config] [source/server/configuration_impl.cc:95] loading tracing configuration
[config] [source/server/configuration_impl.cc:115] loading stats sink configuration
[main] [source/server/server.cc:429] all clusters initialized. initializing init manager
[config] [source/server/listener_manager_impl.cc:908] all dependencies initialized. starting workers
[main] [source/server/server.cc:457] starting main dispatch loop

Die Spring Boot Anwendung, die den gRPC Dienst bereitstellt, wird im Beispiel über Maven gestartet. (Details zur Anwendung finden sich in In diesem Beitrag zu gRPC mit Spring Boot.)

Spring Boot Anwendung mit gRPC
$ ./mvwn spring-boot:run
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Detecting the operating system and CPU architecture
[INFO] ------------------------------------------------------------------------
[INFO] os.detected.name: linux
[INFO] os.detected.arch: x86_64
[INFO] os.detected.version: 4.18
[INFO] os.detected.version.major: 4
[INFO] os.detected.version.minor: 18
[INFO] os.detected.release: ubuntu
[INFO] os.detected.release.version: 18.10
[INFO] os.detected.release.like.ubuntu: true
[INFO] os.detected.release.like.debian: true
[INFO] os.detected.classifier: linux-x86_64
[INFO]
[INFO] -------------------------< de.trion:grpcdemo >--------------------------
[INFO] Building grpc-demo 1.0.0.CI-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
...

Nun kann ein Aufruf der Angular Anwendung über http://localhost:4200 erfolgen. Nachdem durch die Eingabefelder ein Vorname und Nachname eingegeben wurde, kann der Greeting Dienst aufgerufen werden.

Um sich ein Bild der übermittelten Daten zu machen, kann im Browser die Netzwerkkonsole geöffnet werden. Ein Beispiel für die Person "Demo User" bekommt als Antwort folgende gRPC Payload: QUFBQUFCSUtFRWhsYkd4dk9pQkVaVzF2 SUZWelpYST1nQUFBQUE5bmNuQmpMWE4wWVhSMWN6b3dEUW89.

Fazit und gRPC Beispiel Projekt

Das gRPC-Web Projekt ist ein deutlicher Schritt nach vorne und eröffnet neue Architekturoptionen. In diesem Beitrag wurde gezeigt, wie komfortabel die Einbindung in eine Angular Anwendung aussehen kann und dank TypeScript und Protobuf ein typsicherer Umgang mit der API möglich ist. Leider unterstützt gRPC-Web derzeit noch kein Streaming, so dass lediglich Request-Response Abläufe abgebildet werden können. Außerdem fehlt noch die direkte Unterstützung für gRPC-Web in den Anwendungsframeworks, so dass auf ein Gateway-Proxy zurückgegriffen werden muss.




Zu den Themen Spring Boot, Angular und Cloud Architektur bieten wir sowohl Beratung, Entwicklungsunterstützung als auch passende Schulungen an:

Auch für Ihren individuellen Bedarf können wir Workshops und Schulungen anbieten. Sprechen Sie uns gerne an.

Feedback oder Fragen zu einem Artikel - per Twitter @triondevelop oder E-Mail freuen wir uns auf eine Kontaktaufnahme!

Zur Desktop Version des Artikels