Pierwsze kroki z gRPC-Go

1. Wprowadzenie

W tym module nauczysz się tworzyć klienta i serwer za pomocą gRPC-Go, które będą stanowić podstawę aplikacji do mapowania tras napisanej w Go.

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.

Usługa jest zdefiniowana 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 Go, 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

Sprawdź, czy masz zainstalowane te elementy:

  • Narzędzia Go w wersji 1.24.5 lub nowszej. Instrukcje instalacji znajdziesz w przewodniku dla początkujących dotyczącym języka Go.
  • kompilator bufora protokołu protoc w wersji 3.27.1 lub nowszej; Instrukcje instalacji znajdziesz w przewodniku instalacji kompilatora.
  • Wtyczki kompilatora bufora protokołu dla Go i gRPC. Aby zainstalować te wtyczki, uruchom te polecenia:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

Zaktualizuj zmienną PATH, aby kompilator bufora protokołu mógł znaleźć wtyczki:

export PATH="$PATH:$(go env GOPATH)/bin"

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.

Pobierz ten kod źródłowy jako archiwum ZIP z GitHub i rozpakuj jego zawartość.

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 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.

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 wygeneruj kod gRPC dla klienta i serwera z pliku .proto za pomocą kompilatora bufora protokołu. W katalogu routeguide uruchom:

protoc --go_out=. --go_opt=paths=source_relative \
       --go-grpc_out=. --go-grpc_opt=paths=source_relative \
       route_guide.proto

To polecenie generuje te pliki:

  • route_guide.pb.go, który zawiera funkcje tworzenia typów wiadomości aplikacji i uzyskiwania dostępu do ich danych.
  • route_guide_grpc.pb.go, który zawiera funkcje używane przez klienta do wywoływania zdalnej metody gRPC usługi oraz funkcje używane przez serwer do udostępniania tej zdalnej usługi.

Następnie zaimplementujemy metodę GetFeature po stronie serwera, aby gdy klient wyśle żądanie, serwer mógł odpowiedzieć.

5. Wdrażanie usługi

Funkcja GetFeature po stronie serwera wykonuje główną pracę: pobiera wiadomość Point od klienta i zwraca w wiadomości Feature odpowiednie informacje o lokalizacji z listy znanych miejsc. Oto implementacja funkcji w języku server/server.go:

func (s *routeGuideServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) {
  for _, feature := range s.savedFeatures {
    if proto.Equal(feature.Location, point) {
      return feature, nil
    }
  }
  // No feature was found, return an unnamed feature
  return &pb.Feature{Location: point}, nil
}

Gdy ta metoda jest wywoływana w odpowiedzi na żądanie klienta zdalnego, funkcja otrzymuje obiekt Context opisujący wywołanie RPC oraz obiekt Point protokołu buforowania z tego żądania klienta. Funkcja zwraca obiekt bufora protokołu Feature dla wyszukiwanej lokalizacji i w razie potrzeby obiekt error.

W metodzie wypełnij obiekt Feature odpowiednimi informacjami dla danego obiektu Point, a następnie return go wraz z błędem nil, aby poinformować gRPC, że zakończono obsługę RPC i że obiekt Feature można zwrócić do klienta.

Metoda GetFeature wymaga utworzenia i zarejestrowania obiektu routeGuideServer, aby żądania klientów dotyczące wyszukiwania lokalizacji mogły być kierowane do tej funkcji. Możesz to zrobić w main():

func main() {
  flag.Parse()
  lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
  if err != nil {
    log.Fatalf("failed to listen: %v", err)
  }

  var opts []grpc.ServerOption
  grpcServer := grpc.NewServer(opts...)

  s := &routeGuideServer{}
  s.loadFeatures()
  pb.RegisterRouteGuideServer(grpcServer, s)
  grpcServer.Serve(lis)
}

Oto, co dzieje się w main() krok po kroku:

  1. Określ port TCP, który ma być używany do nasłuchiwania żądań klientów zdalnych, używając lis, err := net.Listen(...). Domyślnie aplikacja używa portu TCP 50051 określonego przez zmienną port lub przez przekazanie przełącznika --port w wierszu poleceń podczas uruchamiania serwera. Jeśli nie można otworzyć portu TCP, aplikacja kończy się błędem krytycznym.
  2. Utwórz instancję serwera gRPC za pomocą grpc.NewServer(...) i nadaj jej nazwę grpcServer.
  3. Utwórz wskaźnik do routeGuideServer, struktury reprezentującej usługę API aplikacji, i nadaj mu nazwę s..
  4. Użyj s.loadFeatures(), aby wypełnić tablicę s.savedFeatures lokalizacjami, które można wyszukać za pomocą GetFeature.
  5. Zarejestruj usługę API na serwerze gRPC, aby wywołania RPC do GetFeature były kierowane do odpowiedniej funkcji.
  6. Wywołaj funkcję Serve() na serwerze, podając szczegóły portu, aby zablokować oczekiwanie na żądania klienta. Będzie to trwało do momentu zakończenia procesu lub wywołania funkcji Stop().

Funkcja loadFeatures() pobiera mapowania współrzędnych na lokalizacje z server/testdata.go.

6. Tworzenie klienta

Teraz edytuj plik client/client.go, w którym zaimplementujesz kod klienta.

Aby wywołać metody usługi zdalnej, musimy najpierw utworzyć kanał gRPC do komunikacji z serwerem. Tworzymy go, przekazując ciąg URI serwera docelowego (w tym przypadku jest to po prostu adres i numer portu) do funkcji grpc.NewClient() w funkcji main() klienta w ten sposób:

conn, err := grpc.NewClient("dns:///"+*serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
        log.Fatalf("fail to dial: %v", err)
}
defer conn.Close()

Adres serwera, zdefiniowany przez zmienną serverAddr, domyślnie ma wartość localhost:50051 i może zostać zastąpiony przełącznikiem --addr w wierszu poleceń podczas uruchamiania klienta.

Jeśli klient musi połączyć się z usługą, która wymaga danych logowania, np. danych logowania TLS lub JWT, może przekazać obiekt DialOptions jako parametr do grpc.NewClient, który zawiera wymagane dane logowania. Usługa RouteGuide nie wymaga żadnych danych logowania.

Po skonfigurowaniu kanału gRPC potrzebujemy namiastek klienta, aby wykonywać wywołania RPC za pomocą wywołań funkcji Go. Stub uzyskujemy za pomocą metody NewRouteGuideClient udostępnianej przez plik route_guide_grpc.pb.go wygenerowany z pliku .proto aplikacji.

import (pb "github.com/grpc-ecosystem/codelabs/getting_started_unary/routeguide")

client := pb.NewRouteGuideClient(conn)

Wywoływanie metod usługi

W gRPC-Go wywołania RPC działają w trybie blokującym/synchronicznym, co oznacza, że wywołanie RPC czeka na odpowiedź serwera i zwraca odpowiedź lub błąd.

Simple RPC

Wywołanie prostego wywołania RPC GetFeature jest prawie tak proste jak wywołanie metody lokalnej, w tym przypadku client.GetFeature:

point := &pb.Point{Latitude: 409146138, Longitude: -746188906}
log.Printf("Getting feature for point (%d, %d)", point.Latitude, point.Longitude)

// Call GetFeature method on the client.
feature, err := client.GetFeature(context.TODO(), point)
if err != nil {
  log.Fatalf("client.GetFeature failed: %v", err)
}

Klient wywołuje metodę w utworzonym wcześniej module pośredniczącym. W przypadku parametrów metody klient tworzy i wypełnia obiekt bufora protokołu żądania Point. Przekazujesz też obiekt context.Context, który w razie potrzeby pozwala nam zmienić działanie RPC, np. określić limit czasu wywołania lub anulować RPC w trakcie wykonywania. Jeśli wywołanie nie zwróci błędu, klient może odczytać informacje o odpowiedzi z serwera z pierwszej wartości zwracanej:

log.Println(feature)

Funkcja main() klienta powinna wyglądać tak:

func main() {
        flag.Parse()

        // Set up a connection to the gRPC server.
        conn, err := grpc.NewClient("dns:///"+*serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
        if err != nil {
                log.Fatalf("fail to dial: %v", err)
        }
        defer conn.Close()

        // Create a new RouteGuide stub.
        client := pb.NewRouteGuideClient(conn)

        point := &pb.Point{Latitude: 409146138, Longitude: -746188906}
        log.Printf("Getting feature for point (%d, %d)", point.Latitude, point.Longitude)

        // Call GetFeature method on the client.
        feature, err := client.GetFeature(context.TODO(), point)
        if err != nil {
                log.Fatalf("client.GetFeature failed: %v", err)
        }
        log.Println(feature)
}

7. Wypróbuj

Aby sprawdzić, czy serwer i klient działają prawidłowo, wykonaj te polecenia w katalogu roboczym aplikacji:

  1. Uruchom serwer w jednym terminalu:
cd server
go run .
  1. Uruchom klienta w innym terminalu:
cd client
go run .

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

Getting feature for point (409146138, -746188906)
name:"Berkshire Valley Management Area Trail, Jefferson, NJ, USA" location:<latitude:409146138 longitude:-746188906 >
Getting feature for point (0, 0)
location:<>

8. Co dalej?