Pierwsze kroki z gRPC-Java

1. Wprowadzenie

W tym module praktycznym użyjesz gRPC-Java do utworzenia klienta i serwera, które będą stanowić podstawę aplikacji do mapowania tras napisanej w języku Java.

Po ukończeniu tego samouczka będziesz mieć klienta, który łączy się z serwerem zdalnym za pomocą gRPC, aby uzyskać nazwę lub adres pocztowy miejsca znajdującego się pod określonymi współrzędnymi na mapie. W pełni funkcjonalna aplikacja może korzystać z tego modelu klient-serwer, aby wyliczać lub podsumowywać ważne miejsca na trasie.

Interfejs API serwera jest zdefiniowany w pliku Protocol Buffers, który będzie używany do generowania kodu szablonowego dla klienta i serwera, aby mogły się ze sobą komunikować. Dzięki temu zaoszczędzisz czas i wysiłek potrzebny na wdrożenie tej funkcji.

Wygenerowany kod obsługuje nie tylko złożoność komunikacji między serwerem a klientem, ale także serializację i deserializację danych.

Czego się nauczysz

  • Jak używać buforów protokołu do definiowania interfejsu API usługi.
  • Jak utworzyć klienta i serwer oparte na gRPC na podstawie definicji Protocol Buffers za pomocą automatycznego generowania kodu.
  • Znajomość komunikacji klient-serwer z użyciem gRPC.

Te warsztaty są przeznaczone dla programistów Java, którzy dopiero zaczynają korzystać z gRPC lub chcą sobie przypomnieć podstawy tej technologii, a także dla wszystkich innych osób zainteresowanych tworzeniem systemów rozproszonych. Nie musisz mieć wcześniejszego doświadczenia z gRPC.

2. Zanim zaczniesz

Wymagania wstępne

  • JDK w wersji 8 lub nowszej

Pobierz kod

Aby nie trzeba było zaczynać od zera, w tym ćwiczeniu znajdziesz szkielet kodu źródłowego aplikacji, który możesz uzupełnić. Z tych instrukcji dowiesz się, jak dokończyć aplikację, w tym jak użyć wtyczek kompilatora buforów protokołów do wygenerowania kodu gRPC.

Najpierw utwórz katalog roboczy z ćwiczeniami i przejdź do niego:

mkdir grpc-java-getting-started && cd grpc-java-getting-started

Pobierz i rozpakuj ćwiczenia z programowania:

curl -sL https://github.com/grpc-ecosystem/grpc-codelabs/archive/refs/heads/v1.tar.gz \
  | tar xvz --strip-components=4 \
  grpc-codelabs-1/codelabs/grpc-java-getting-started/start_here

Możesz też pobrać plik ZIP zawierający tylko katalog z instrukcjami i rozpakować go ręcznie.

Jeśli nie chcesz wpisywać implementacji, gotowy kod źródłowy jest dostępny na GitHubie.

3. Określ usługę

Pierwszym krokiem jest zdefiniowanie usługi gRPC aplikacji, jej metody RPC oraz typów wiadomości żądania i odpowiedzi za pomocą buforów protokołu. Twoja usługa będzie zapewniać:

  • Metoda RPC o nazwie GetFeature, która jest zaimplementowana na serwerze i wywoływana przez klienta.
  • Typy wiadomości PointFeature to struktury danych wymieniane między klientem a serwerem podczas korzystania z metody GetFeature. Klient podaje współrzędne mapy jako Point w żądaniu GetFeature wysyłanym do serwera, a serwer odpowiada, przesyłając odpowiedni Feature, który opisuje wszystko, co znajduje się pod tymi współrzędnymi.

Ta metoda RPC i jej typy wiadomości będą zdefiniowane w pliku src/main/proto/routeguide/route_guide.proto podanego kodu źródłowego.

Protokoły buforów są powszechnie znane jako protobufy. Więcej informacji o terminologii gRPC znajdziesz w artykule Podstawowe koncepcje, architektura i cykl życia.

W tym przykładzie generujemy kod w języku Java, dlatego w pliku .proto określiliśmy opcję pliku java_package i nazwę klasy Java:

option java_package = "io.grpc.examples.routeguide";
option java_outer_classname = "RouteGuideProto";

Rodzaje wiadomości

routeguide/route_guide.proto pliku kodu źródłowego najpierw zdefiniuj Point typ wiadomości. Symbol Point reprezentuje parę współrzędnych szerokości i długości geograficznej na mapie. W tym ćwiczeniu używaj liczb całkowitych jako współrzędnych:

message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

Numery 12 to unikalne identyfikatory poszczególnych pól w strukturze message.

Następnie określ Featuretyp wiadomości. Feature używa pola string na nazwę lub adres pocztowy czegoś w lokalizacji określonej przez Point:

message Feature {
  // The name or address of the feature.
  string name = 1;

  // The point where the feature is located.
  Point location = 2;
}

Metoda usługi

Plik route_guide.proto ma strukturę service o nazwie RouteGuide, która definiuje co najmniej 1 metodę udostępnianą przez usługę aplikacji.

Dodaj metodę rpc GetFeature w definicji RouteGuide. Jak wspomnieliśmy wcześniej, ta metoda wyszukuje nazwę lub adres lokalizacji na podstawie podanego zestawu współrzędnych, więc w przypadku danego Point zwraca GetFeature Feature:

service RouteGuide {
  // Definition of the service goes here

  // Obtains the feature at a given position.
  rpc GetFeature(Point) returns (Feature) {}
}

Jest to metoda RPC typu unary: prosta procedura RPC, w której klient wysyła żądanie do serwera i czeka na odpowiedź, podobnie jak w przypadku lokalnego wywołania funkcji.

4. Generowanie kodu klienta i serwera

Następnie musimy wygenerować interfejsy klienta i serwera gRPC z naszej definicji usługi .proto. W tym celu używamy kompilatora buforów protokołu protoc ze specjalną wtyczką gRPC Java. Aby wygenerować usługi gRPC, musisz użyć kompilatora proto3 (który obsługuje składnię proto2 i proto3).

Jeśli używasz Gradle lub Maven, protocwtyczka do kompilacji może wygenerować niezbędny kod w ramach kompilacji. Więcej informacji o generowaniu kodu z własnych plików .proto znajdziesz w pliku README pakietu grpc-java.

W kodzie źródłowym laboratorium udostępniliśmy środowisko i konfigurację Gradle, które umożliwiają utworzenie tego projektu.

W katalogu grpc-java-getting-started uruchom to polecenie:

$ chmod +x gradlew
$ ./gradlew generateProto

Z definicji usługi generowane są te klasy:

  • Feature.java, Point.java i inne, które zawierają cały kod bufora protokołu do wypełniania, serializowania i pobierania typów wiadomości żądań i odpowiedzi.
  • RouteGuideGrpc.java, który zawiera (oprócz innego przydatnego kodu) klasę bazową do implementacji serwerów RouteGuide, RouteGuideGrpc.RouteGuideImplBase ze wszystkimi metodami zdefiniowanymi w klasach usług i klasach stubów RouteGuide, z których mogą korzystać klienci.

5. Wdrażanie serwera

Najpierw zobaczmy, jak utworzyć RouteGuideserwerRouteGuide. Aby usługa RouteGuide działała prawidłowo, musisz wykonać 2 czynności:

  • Implementacja interfejsu usługi wygenerowanego na podstawie definicji usługi, który wykonuje rzeczywistą „pracę” usługi.
  • Uruchomienie serwera gRPC, który nasłuchuje żądań od klientów i przekazuje je do właściwej implementacji usługi.

Implementacja RouteGuide

Jak widać, nasz serwer ma klasę RouteGuideService, która rozszerza wygenerowaną klasę abstrakcyjną RouteGuideGrpc.RouteGuideImplBase:

private static class RouteGuideService extends RouteGuideGrpc.RouteGuideImplBase {
...
}

Aby zainicjować serwer za pomocą funkcji, udostępniliśmy te 2 pliki:

./src/main/java/io/grpc/examples/routeguide/RouteGuideUtil.java

./src/main/resources/io/grpc/examples/routeguide/route_guide_db.json

Przyjrzyjmy się szczegółowo prostej implementacji RPC.

Wywołanie RPC typu unary

RouteGuideService wdraża wszystkie nasze metody obsługi. W tym przypadku jest to tylko GetFeature(), które pobiera wiadomość Point od klienta i zwraca w wiadomości Feature odpowiednie informacje o lokalizacji z listy znanych miejsc.

@Override
public void getFeature(Point request, StreamObserver<Feature> responseObserver) {
  responseObserver.onNext(checkFeature(request));
  responseObserver.onCompleted();
}

Metoda getFeature() przyjmuje 2 parametry:

  • Point: prośba.
  • StreamObserver<Feature>: obserwator odpowiedzi, czyli specjalny interfejs, za pomocą którego serwer może wywołać odpowiedź.

Aby zwrócić odpowiedź do klienta i zakończyć połączenie:

  1. Tworzymy i wypełniamy obiekt odpowiedzi Feature, aby zwrócić go do klienta zgodnie z definicją usługi. W tym przykładzie robimy to w osobnej prywatnej metodzie checkFeature().
  2. Używamy metody onNext() obserwatora odpowiedzi, aby zwrócić Feature.
  3. Używamy metody onCompleted() obserwatora odpowiedzi, aby określić, że zakończyliśmy obsługę RPC.

Uruchamianie serwera

Po zaimplementowaniu wszystkich metod usługi musimy uruchomić serwer gRPC, aby klienci mogli z niej korzystać. W naszym szablonie uwzględniamy tworzenie obiektu ServerBuilder:

ServerBuilder.forPort(port), port, RouteGuideUtil.parseFeatures(featureFile)

Usługę tworzymy w konstruktorze:

  1. Określ port, którego chcemy używać do nasłuchiwania żądań klientów, za pomocą metody forPort() narzędzia do tworzenia (będzie ono używać adresu wieloznacznego).
  2. Utwórz instancję klasy implementacji usługi RouteGuideService i przekaż ją do metody addService() konstruktora.
  3. Wywołaj build() na konstruktorze, aby utworzyć serwer RPC dla naszej usługi.

Poniższy fragment kodu pokazuje, jak tworzymy obiekt ServerBuilder.

/** Create a RouteGuide server listening on {@code port} using {@code featureFile} database. */
public RouteGuideServer(int port, URL featureFile) throws IOException {
    this(Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create()),
        port, RouteGuideUtil.parseFeatures(featureFile));
  }

Poniższy fragment kodu pokazuje, jak utworzyć obiekt serwera dla usługi RouteGuide.

/** Create a RouteGuide server using serverBuilder as a base and features as data. */
public RouteGuideServer(ServerBuilder<?> serverBuilder, int port, Collection<Feature> features) {
  this.port = port;
  server = serverBuilder.addService(new RouteGuideService(features))
      .build();
}

Zaimplementuj metodę start, która wywołuje start na serwerze utworzonym powyżej.

public void start() throws IOException {
  server.start();
  logger.info("Server started, listening on " + port);
}

Zaimplementuj metodę oczekiwania na zakończenie działania serwera, aby nie zamykał się on od razu.

/** Await termination on the main thread since the grpc library uses daemon threads. */
private void blockUntilShutdown() throws InterruptedException {
  if (server != null) {
    server.awaitTermination();
  }
}

Jak widać, tworzymy i uruchamiamy serwer za pomocą ServerBuilder.

W metodzie głównej:

  1. Utwórz instancję RouteGuideServer.
  2. Zadzwoń pod numer start(), aby aktywować serwer RPC dla naszej usługi.
  3. Poczekaj, aż usługa zostanie zatrzymana przez wywołanie funkcji blockUntilShutdown().
 public static void main(String[] args) throws Exception {
    RouteGuideServer server = new RouteGuideServer(8980);
    server.start();
    server.blockUntilShutdown();
  }

6. Tworzenie klienta

W tej sekcji zajmiemy się tworzeniem klienta dla usługi RouteGuide.

Utwórz obiekt zastępczy

Aby wywoływać metody usługi, musimy najpierw utworzyć stub. Istnieją 2 rodzaje atrap, ale w tym samouczku potrzebujemy tylko atrapy blokującej. Wyróżniamy 2 rodzaje:

  • blokujący/synchroniczny element zastępczy, który wykonuje wywołanie RPC i czeka na odpowiedź serwera, a następnie zwraca odpowiedź lub zgłasza wyjątek;
  • nieblokujący/asynchroniczny stub, który wykonuje nieblokujące wywołania serwera, a odpowiedź jest zwracana asynchronicznie. Niektóre typy połączeń strumieniowych można wykonywać tylko za pomocą asynchronicznego elementu zastępczego.

Najpierw musimy utworzyć kanał gRPC, a potem użyć go do utworzenia naszego stuba.

Mogliśmy użyć bezpośrednio ManagedChannelBuilder, aby utworzyć kanał.

ManagedChannelBuilder.forAddress(host, port).usePlaintext().build

Użyjmy jednak metody narzędziowej, która przyjmuje ciąg znaków z symbolem hostname:port.

Grpc.newChannelBuilder(target, InsecureChannelCredentials.create()).build();

Teraz możemy użyć kanału do utworzenia bloku zastępczego. W tym ćwiczeniu w Codelabs mamy tylko blokujące wywołania RPC, więc używamy metody newBlockingStub udostępnionej w klasie RouteGuideGrpc wygenerowanej na podstawie pliku .proto.

blockingStub = RouteGuideGrpc.newBlockingStub(channel);

Wywoływanie metod usługi

Zobaczmy teraz, jak wywołujemy metody usługi.

Simple RPC

Wywołanie prostego RPC GetFeature jest prawie tak proste jak wywołanie metody lokalnej.

Tworzymy i wypełniamy obiekt bufora protokołu żądania (w naszym przypadku Point), przekazujemy go do metody getFeature() w naszym blokującym stubie i otrzymujemy odpowiedź Feature.

Jeśli wystąpi błąd, jest on kodowany jako Status, który możemy uzyskać z StatusRuntimeException.

Point request = Point.newBuilder().setLatitude(lat).setLongitude(lon).build();

Feature feature;
try {
  feature = blockingStub.getFeature(request);
} catch (StatusRuntimeException e) {
  logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
  return;
}

Kod standardowy rejestruje wiadomość zawierającą treści w zależności od tego, czy w określonym punkcie występuje funkcja.

7. Spróbuj!

  1. W katalogu start_here uruchom to polecenie:
$ ./gradlew installDist

Spowoduje to skompilowanie kodu, spakowanie go w plik JAR i utworzenie skryptów, które uruchamiają przykład. Zostaną one utworzone w katalogu build/install/start_here/bin/. Są to skrypty route-guide-serverroute-guide-client.

Przed uruchomieniem klienta serwer musi być włączony.

  1. Uruchom serwer:
$ ./build/install/start_here/bin/route-guide-server
  1. Uruchom klienta:
$ ./build/install/start_here/bin/route-guide-client

Zobaczysz dane wyjściowe podobne do tych (sygnatury czasowe zostały pominięte dla przejrzystości):

INFO: *** GetFeature: lat=409,146,138 lon=-746,188,906
INFO: Found feature called "Berkshire Valley Management Area Trail, Jefferson, NJ, USA" at 40.915, -74.619
INFO: *** GetFeature: lat=0 lon=0
INFO: Found no feature at 0, 0

8. Co dalej?