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 uzyskiwać informacje o funkcjach na trasie klienta, tworzyć podsumowanie trasy klienta i wymieniać informacje o trasie, takie jak aktualizacje ruchu, z serwerem i innymi klientami.
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 strumieniowej klient-serwer za pomocą 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.
Najpierw utwórz katalog roboczy codelabu i przejdź do niego:cd
mkdir streaming-grpc-go-getting-started && cd streaming-grpc-go-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-go-streaming/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ślanie komunikatów i usług
Pierwszym krokiem jest zdefiniowanie usługi gRPC aplikacji, jej metod RPC oraz typów wiadomości żądań i odpowiedzi za pomocą buforów protokołu. Twoja usługa będzie zapewniać:
- Metody RPC wywoływane przez serwer i klienta:
ListFeatures
,RecordRoute
iRouteChat
. - Typy wiadomości
Point
,Feature
,Rectangle
,RouteNote
iRouteSummary
, które są strukturami danych wymienianymi między klientem a serwerem podczas wywoływania powyższych metod.
Te metody RPC i ich 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.
Definiowanie typów wiadomości
W 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 1
i 2
to unikalne identyfikatory poszczególnych pól w strukturze message
.
Następnie określ Feature
typ 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;
}
Następnie Rectangle
, czyli wiadomość reprezentująca prostokąt szerokości i długości geograficznej, przedstawiony jako 2 przeciwległe punkty „lo” i „hi”.
message Rectangle {
// One corner of the rectangle.
Point lo = 1;
// The other corner of the rectangle.
Point hi = 2;
}
Jest to też wiadomość RouteNote
, która reprezentuje wiadomość wysłaną w danym momencie.
message RouteNote {
// The location from which the message is sent.
Point location = 1;
// The message to be sent.
string message = 2;
}
Wymagamy też wiadomości RouteSummary
. Ten komunikat jest odbierany w odpowiedzi na wywołanie RPC RecordRoute
, które opisujemy w następnej sekcji. Zawiera liczbę otrzymanych punktów, liczbę wykrytych cech i całkowity przebyty dystans jako sumę odległości między poszczególnymi punktami.
message RouteSummary {
// The number of points received.
int32 point_count = 1;
// The number of known features passed while traversing the route.
int32 feature_count = 2;
// The distance covered in metres.
int32 distance = 3;
// The duration of the traversal in seconds.
int32 elapsed_time = 4;
}
Określanie metod usługi
Aby zdefiniować usługę, w pliku .proto musisz określić nazwaną usługę. Plik route_guide.proto
ma strukturę service
o nazwie RouteGuide
, która definiuje co najmniej 1 metodę udostępnianą przez usługę aplikacji.
Zdefiniuj RPC
metody w definicji usługi, określając typy żądań i odpowiedzi. W tej części ćwiczenia zdefiniujemy:
ListFeatures
Pobiera Feature
dostępne w danym Rectangle
. Wyniki są przesyłane strumieniowo, a nie zwracane od razu (np. w wiadomości z odpowiedzią zawierającej pole powtarzane), ponieważ prostokąt może obejmować duży obszar i zawierać ogromną liczbę elementów.
Odpowiednim typem tego wywołania RPC jest strumieniowe wywołanie RPC po stronie serwera: klient wysyła żądanie do serwera i otrzymuje strumień, z którego może odczytywać sekwencję komunikatów. Klient odczytuje zwrócony strumień, dopóki nie będzie już żadnych wiadomości. Jak widać w naszym przykładzie, metodę przesyłania strumieniowego po stronie serwera określa się, umieszczając słowo kluczowe stream przed typem odpowiedzi.
rpc ListFeatures(Rectangle) returns (stream Feature) {}
RecordRoute
Akceptuje strumień obiektów Point
na pokonywanej trasie i zwraca obiekt RouteSummary
po zakończeniu przejazdu.
W tym przypadku odpowiednie wydaje się wywołanie RPC strumieniowania po stronie klienta: klient zapisuje sekwencję wiadomości i wysyła je na serwer, ponownie używając udostępnionego strumienia. Gdy klient skończy pisać wiadomości, czeka, aż serwer je wszystkie odczyta i zwróci odpowiedź. Metodę przesyłania strumieniowego po stronie klienta określa się, umieszczając słowo kluczowe stream przed typem żądania.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
RouteChat
Akceptuje strumień RouteNote
wysyłanych podczas pokonywania trasy, a także odbiera inne RouteNote
(np. od innych użytkowników).
To jest właśnie przypadek użycia strumieniowania dwukierunkowego. Dwukierunkowe przesyłanie strumieniowe RPC polega na tym, że obie strony wysyłają sekwencję wiadomości za pomocą strumienia odczytu i zapisu. Oba strumienie działają niezależnie, więc klienci i serwery mogą odczytywać i zapisywać dane w dowolnej kolejności.
Na przykład serwer może poczekać na otrzymanie wszystkich wiadomości od klienta, zanim napisze odpowiedzi, lub może odczytać wiadomość, a następnie napisać odpowiedź albo zastosować inną kombinację odczytów i zapisów.
Kolejność wiadomości w każdym strumieniu jest zachowana. Ten typ metody określa się, umieszczając słowo kluczowe stream przed żądaniem i odpowiedzią.
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
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 oraz uzyskiwania dostępu do ich danych i definicji typów reprezentujących wiadomości.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 metody po stronie serwera, aby gdy klient wyśle żądanie, serwer mógł odpowiedzieć.
5. Wdrażanie usługi
Najpierw zobaczmy, jak utworzyć RouteGuide
serwerRouteGuide
. Aby usługa RouteGuide
działała prawidłowo, musisz wykonać 2 czynności:
- Implementacja interfejsu usługi wygenerowanego na podstawie definicji usługi: wykonywanie rzeczywistej „pracy” usługi.
- Uruchomienie serwera gRPC, który nasłuchuje żądań od klientów i przekazuje je do właściwej implementacji usługi.
Wdróżmy RouteGuide w server/server.go
.
Implementacja RouteGuide
Musimy wdrożyć wygenerowany interfejs RouteGuideService
. Tak wyglądałaby implementacja.
type routeGuideServer struct {
...
}
...
func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
...
}
...
func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error {
...
}
...
func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
...
}
Przyjrzyjmy się szczegółowo każdej implementacji RPC.
Strumieniowe wywołanie RPC po stronie serwera
Zacznij od jednego z naszych strumieniowych wywołań RPC. ListFeatures
to strumieniowe RPC po stronie serwera, więc musimy odesłać do klienta wiele obiektów Feature
.
func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
for _, feature := range s.savedFeatures {
if inRange(feature.Location, rect) {
if err := stream.Send(feature); err != nil {
return err
}
}
}
return nil
}
Jak widać, zamiast prostych obiektów żądań i odpowiedzi w parametrach metody otrzymujemy tym razem obiekt żądania (Rectangle
, w którym klient chce znaleźć Features
) i specjalny obiekt RouteGuide_ListFeaturesServer
do zapisywania odpowiedzi. W tej metodzie wypełniamy tyle obiektów Feature
, ile potrzebujemy, aby zwrócić je do obiektu RouteGuide_ListFeaturesServer
za pomocą metody Send()
. Na koniec, podobnie jak w przypadku prostego wywołania RPC, zwracamy nil
błąd, aby poinformować gRPC, że zakończyliśmy pisanie odpowiedzi. Jeśli podczas tego wywołania wystąpi błąd, zwrócimy błąd inny niż nil. Warstwa gRPC przetłumaczy go na odpowiedni stan RPC, który zostanie wysłany przez sieć.
RPC przesyłania strumieniowego po stronie klienta
Przyjrzyjmy się teraz nieco bardziej skomplikowanej metodzie przesyłania strumieniowego po stronie klienta RecordRoute
, w której otrzymujemy strumień Points
od klienta i zwracamy pojedynczy RouteSummary
z informacjami o jego podróży. Jak widać, tym razem metoda nie ma w ogóle parametru żądania. Zamiast tego otrzymuje strumień RouteGuide_RecordRouteServer
, którego serwer może używać do odczytywania i zapisywania wiadomości – może odbierać wiadomości od klienta za pomocą metody Recv()
i zwracać pojedynczą odpowiedź za pomocą metody SendAndClose()
.
func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error {
var pointCount, featureCount, distance int32
var lastPoint *pb.Point
startTime := time.Now()
for {
point, err := stream.Recv()
if err == io.EOF {
endTime := time.Now()
return stream.SendAndClose(&pb.RouteSummary{
PointCount: pointCount,
FeatureCount: featureCount,
Distance: distance,
ElapsedTime: int32(endTime.Sub(startTime).Seconds()),
})
}
if err != nil {
return err
}
pointCount++
for _, feature := range s.savedFeatures {
if proto.Equal(feature.Location, point) {
featureCount++
}
}
if lastPoint != nil {
distance += calcDistance(lastPoint, point)
}
lastPoint = point
}
}
W treści metody używamy metody RouteGuide_RecordRouteServer
Recv()
, aby wielokrotnie odczytywać żądania klienta do obiektu żądania (w tym przypadku Point
), dopóki nie będzie już więcej wiadomości: serwer musi sprawdzać błąd zwracany przez Recv()
po każdym wywołaniu. Jeśli jest to nil
, strumień jest nadal prawidłowy i można go dalej odczytywać. Jeśli jest to io.EOF
, strumień wiadomości został zakończony i serwer może zwrócić RouteSummary
. Jeśli ma inną wartość, zwracamy błąd „w takiej postaci”, aby warstwa gRPC przetłumaczyła go na stan RPC.
Dwukierunkowe wywołanie RPC strumieniowania
Na koniec przyjrzyjmy się dwukierunkowemu strumieniowemu wywołaniu RPC RouteChat()
.
func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
for {
in, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
key := serialize(in.Location)
s.mu.Lock()
s.routeNotes[key] = append(s.routeNotes[key], in)
// Note: this copy prevents blocking other clients while serving this one.
// We don't need to do a deep copy, because elements in the slice are
// insert-only and never modified.
rn := make([]*pb.RouteNote, len(s.routeNotes[key]))
copy(rn, s.routeNotes[key])
s.mu.Unlock()
for _, note := range rn {
if err := stream.Send(note); err != nil {
return err
}
}
}
}
Tym razem otrzymujemy strumień RouteGuide_RouteChatServer
, który, podobnie jak w przykładzie przesyłania strumieniowego po stronie klienta, może służyć do odczytywania i zapisywania wiadomości. Tym razem jednak zwracamy wartości za pomocą strumienia metody, gdy klient nadal pisze wiadomości do swojego strumienia wiadomości. Składnia odczytu i zapisu jest tu bardzo podobna do naszej metody przesyłania strumieniowego przez klienta, z tym że serwer używa metody send()
strumienia zamiast SendAndClose()
, ponieważ zapisuje wiele odpowiedzi. Chociaż każda ze stron zawsze będzie otrzymywać wiadomości drugiej strony w kolejności, w jakiej zostały napisane, zarówno klient, jak i serwer mogą odczytywać i zapisywać dane w dowolnej kolejności – strumienie działają całkowicie niezależnie.
Uruchom serwer
Po zaimplementowaniu wszystkich metod musimy też uruchomić serwer gRPC, aby klienci mogli korzystać z naszej usługi. Poniższy fragment kodu pokazuje, jak to robimy w przypadku naszej usługi RouteGuide
:
lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
grpcServer := grpc.NewServer()
s := &routeGuideServer{routeNotes: make(map[string][]*pb.RouteNote)}
s.loadFeatures()
pb.RegisterRouteGuideServer(grpcServer, s)
grpcServer.Serve(lis)
Oto, co dzieje się w main()
krok po kroku:
- 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 TCP50051
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. - Utwórz instancję serwera gRPC za pomocą
grpc.NewServer(...)
i nadaj jej nazwęgrpcServer
. - Utwórz wskaźnik do
routeGuideServer
, struktury reprezentującej usługę API aplikacji, i nadaj mu nazwęs.
. - Użyj
s.loadFeatures()
, aby wypełnić tablicęs.savedFeatures
. - Zarejestruj implementację usługi na serwerze gRPC.
- 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 funkcjiStop()
.
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:
// 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()
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_streaming/routeguide")
client := pb.NewRouteGuideClient(conn)
Metody usługi połączeń
Zobaczmy teraz, jak wywołujemy metody 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.
Strumieniowe wywołanie RPC po stronie serwera
W tym miejscu wywołujemy metodę strumieniowania po stronie serwera ListFeatures
, która zwraca strumień obiektów geograficznych Feature
.
rect := &pb.Rectangle{ ... } // initialize a pb.Rectangle
log.Printf("Looking for features within %v", rect)
stream, err := client.ListFeatures(context.Background(), rect)
if err != nil {
log.Fatalf("client.ListFeatures failed: %v", err)
}
for {
// For server-to-client streaming RPCs, you call stream.Recv() until it
// returns io.EOF.
feature, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("client.ListFeatures failed: %v", err)
}
log.Printf("Feature: name: %q, point:(%v, %v)", feature.GetName(),
feature.GetLocation().GetLatitude(), feature.GetLocation().GetLongitude())
}
Podobnie jak w przypadku prostego wywołania RPC przekazujemy do metody kontekst i żądanie. Zamiast obiektu odpowiedzi otrzymujemy jednak instancję RouteGuide_ListFeaturesClient
. Klient może używać strumienia RouteGuide_ListFeaturesClient
do odczytywania odpowiedzi serwera. Używamy metody RouteGuide_ListFeaturesClient
Recv()
, aby wielokrotnie odczytywać odpowiedzi serwera do obiektu bufora protokołu odpowiedzi (w tym przypadku Feature
), dopóki nie będzie już więcej wiadomości. Po każdym wywołaniu klient musi sprawdzić błąd err zwrócony przez Recv()
. Jeśli nil
, strumień nadal działa prawidłowo i może kontynuować odczytywanie. Jeśli io.EOF
, strumień wiadomości został zakończony. W przeciwnym razie musi wystąpić błąd RPC, który jest przekazywany przez err
.
RPC przesyłania strumieniowego po stronie klienta
Metoda przesyłania strumieniowego po stronie klienta RecordRoute
jest podobna do metody po stronie serwera, z tym wyjątkiem, że przekazujemy do niej tylko kontekst i otrzymujemy z powrotem strumień RouteGuide_RecordRouteClient
, którego możemy używać zarówno do zapisywania, jak i do odczytywania wiadomości.
// Create a random number of random points
r := rand.New(rand.NewSource(time.Now().UnixNano()))
pointCount := int(r.Int31n(100)) + 2 // Traverse at least two points
var points []*pb.Point
for i := 0; i < pointCount; i++ {
points = append(points, randomPoint(r))
}
log.Printf("Traversing %d points.", len(points))
c2sStream, err := client.RecordRoute(context.TODO())
if err != nil {
log.Fatalf("client.RecordRoute failed: %v", err)
}
// Stream each point to the server.
for _, point := range points {
if err := c2sStream.Send(point); err != nil {
log.Fatalf("client.RecordRoute: stream.Send(%v) failed: %v", point, err)
}
}
// Close the stream and receive the RouteSummary from the server.
reply, err := c2sStream.CloseAndRecv()
if err != nil {
log.Fatalf("client.RecordRoute failed: %v", err)
}
log.Printf("Route summary: %v", reply)
Obiekt RouteGuide_RecordRouteClient
ma metodę Send()
, której możemy użyć do wysyłania żądań do serwera. Gdy skończymy zapisywać żądania klienta w strumieniu za pomocą narzędzia Send()
, musimy wywołać w strumieniu funkcję CloseAndRecv()
, aby poinformować gRPC, że zakończyliśmy zapisywanie i oczekujemy odpowiedzi. Stan RPC pobieramy z błędu zwróconego przez CloseAndRecv()
. Jeśli stan to nil
, pierwsza wartość zwracana przez CloseAndRecv()
będzie prawidłową odpowiedzią serwera.
Dwukierunkowe wywołanie RPC strumieniowania
Na koniec przyjrzyjmy się dwukierunkowemu strumieniowemu wywołaniu RPC RouteChat()
. Podobnie jak w przypadku RecordRoute
, przekazujemy do metody tylko obiekt kontekstu i otrzymujemy strumień, którego możemy używać do zapisywania i odczytywania wiadomości. Tym razem jednak zwracamy wartości za pomocą strumienia metody, gdy serwer nadal zapisuje wiadomości w swoim strumieniu wiadomości.
biDiStream, err := client.RouteChat(context.Background())
if err != nil {
log.Fatalf("client.RouteChat failed: %v", err)
}
// this channel is used to wait for the receive goroutine to finish.
recvDoneCh := make(chan struct{})
// receive goroutine.
go func() {
for {
in, err := biDiStream.Recv()
if err == io.EOF {
// read done.
close(recvDoneCh)
return
}
if err != nil {
log.Fatalf("client.RouteChat failed: %v", err)
}
log.Printf("Got message %s at point(%d, %d)", in.Message, in.Location.Latitude, in.Location.Longitude)
}
}()
// send messages simultaneously.
for _, note := range notes {
if err := biDiStream.Send(note); err != nil {
log.Fatalf("client.RouteChat: stream.Send(%v) failed: %v", note, err)
}
}
biDiStream.CloseSend()
// wait for the receive goroutine to finish.
<-recvDoneCh
Składnia odczytu i zapisu jest tu bardzo podobna do naszej metody przesyłania strumieniowego po stronie klienta, z tym że po zakończeniu wywołania używamy metody CloseSend()
strumienia. Chociaż każda ze stron zawsze będzie otrzymywać wiadomości drugiej strony w kolejności, w jakiej zostały napisane, zarówno klient, jak i serwer mogą odczytywać i zapisywać dane w dowolnej kolejności – strumienie działają całkowicie niezależnie.
7. Wypróbuj
Aby sprawdzić, czy serwer i klient działają prawidłowo, wykonaj te polecenia w katalogu roboczym aplikacji:
- Uruchom serwer w jednym terminalu:
cd server go run .
- 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):
Looking for features within lo:<latitude:400000000 longitude:-750000000 > hi:<latitude:420000000 longitude:-730000000 > name:"Patriots Path, Mendham, NJ 07945, USA" location:<latitude:407838351 longitude:-746143763 > ... name:"3 Hasta Way, Newton, NJ 07860, USA" location:<latitude:410248224 longitude:-747127767 > Traversing 56 points. Route summary: point_count:56 distance:497013163 Got message First message at point(0, 1) Got message Second message at point(0, 2) Got message Third message at point(0, 3) Got message First message at point(0, 1) Got message Fourth message at point(0, 1) Got message Second message at point(0, 2) Got message Fifth message at point(0, 2) Got message Third message at point(0, 3) Got message Sixth message at point(0, 3)
8. Co dalej?
- Dowiedz się, jak działa gRPC, w artykułach Wprowadzenie do gRPC i Podstawowe pojęcia.
- Zapoznaj się z samouczkiem dotyczącym podstaw.
- Zapoznaj się z dokumentacją interfejsu API.