Inizia a utilizzare gRPC-Go - Streaming

1. Introduzione

In questo codelab utilizzerai gRPC-Go per creare un client e un server che costituiscono la base di un'applicazione di mappatura di itinerari scritta in Go.

Al termine del tutorial, avrai un client che si connette a un server remoto utilizzando gRPC per ottenere informazioni sulle funzionalità di un percorso del client, creare un riepilogo di un percorso del client e scambiare informazioni sul percorso, come gli aggiornamenti sul traffico, con il server e altri client.

Il servizio è definito in un file Protocol Buffers, che verrà utilizzato per generare il codice boilerplate per il client e il server in modo che possano comunicare tra loro, risparmiando tempo e fatica nell'implementazione di questa funzionalità.

Questo codice generato si occupa non solo delle complessità della comunicazione tra il server e il client, ma anche della serializzazione e della deserializzazione dei dati.

Obiettivi didattici

  • Come utilizzare i buffer di protocollo per definire un'API di servizio.
  • Come creare un client e un server basati su gRPC da una definizione di Protocol Buffers utilizzando la generazione automatica del codice.
  • Comprensione della comunicazione di streaming client-server con gRPC.

Questo codelab è rivolto agli sviluppatori Go che non hanno familiarità con gRPC o che vogliono ripassare gRPC, o a chiunque sia interessato a creare sistemi distribuiti. Non è richiesta alcuna esperienza precedente con gRPC.

2. Prima di iniziare

Prerequisiti

Assicurati di aver installato quanto segue:

  • La toolchain Go versione 1.24.5 o successive. Per istruzioni sull'installazione, consulta la sezione Guida introduttiva di Go.
  • Il compilatore del buffer di protocollo, protoc, versione 3.27.1 o successive. Per le istruzioni di installazione, consulta la guida all'installazione del compilatore.
  • I plug-in del compilatore di buffer di protocollo per Go e gRPC. Per installare questi plug-in, esegui i seguenti comandi:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

Aggiorna la variabile PATH in modo che il compilatore del buffer del protocollo possa trovare i plug-in:

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

Ottieni il codice

Per non dover iniziare da zero, questo codelab fornisce una struttura del codice sorgente dell'applicazione da completare. I passaggi seguenti mostrano come completare l'applicazione, incluso l'utilizzo dei plug-in del compilatore di protocol buffer per generare il codice gRPC boilerplate.

Innanzitutto, crea la directory di lavoro del codelab e cd al suo interno:

mkdir streaming-grpc-go-getting-started && cd streaming-grpc-go-getting-started

Scarica ed estrai il codelab:

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

In alternativa, puoi scaricare il file .zip contenente solo la directory del codelab e decomprimerlo manualmente.

Il codice sorgente completato è disponibile su GitHub se vuoi evitare di digitare un'implementazione.

3. Definisci messaggi e servizi

Il primo passaggio consiste nel definire il servizio gRPC dell'applicazione, i relativi metodi RPC e i tipi di messaggi di richiesta e risposta utilizzando Protocol Buffers. Il tuo servizio fornirà:

  • Metodi RPC chiamati ListFeatures, RecordRoute e RouteChat che il server implementa e il client chiama.
  • I tipi di messaggi Point, Feature, Rectangle, RouteNote e RouteSummary, che sono strutture di dati scambiate tra il client e il server quando vengono chiamati i metodi precedenti.

Questi metodi RPC e i relativi tipi di messaggio verranno tutti definiti nel file routeguide/route_guide.proto del codice sorgente fornito.

Protocol Buffers sono comunemente noti come protobuf. Per ulteriori informazioni sulla terminologia gRPC, consulta Concetti fondamentali, architettura e ciclo di vita di gRPC.

Definisci i tipi di messaggi

Nel file routeguide/route_guide.proto del codice sorgente, definisci innanzitutto il tipo di messaggio Point. Un Point rappresenta una coppia di coordinate di latitudine e longitudine su una mappa. Per questo codelab, utilizza numeri interi per le coordinate:

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

I numeri 1 e 2 sono numeri ID univoci per ciascuno dei campi nella struttura message.

Successivamente, definisci il tipo di messaggio Feature. Un Feature utilizza un campo string per il nome o l'indirizzo postale di un elemento in una località specificata da un Point:

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

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

Poi un messaggio Rectangle che rappresenta un rettangolo di latitudine e longitudine, rappresentato da due punti diagonalmente opposti "lo" e "hi".

message Rectangle {
  // One corner of the rectangle.
  Point lo = 1;

  // The other corner of the rectangle.
  Point hi = 2;
}

Inoltre, un messaggio RouteNote che rappresenta un messaggio inviato in un determinato momento.

message RouteNote {
  // The location from which the message is sent.
  Point location = 1;

  // The message to be sent.
  string message = 2;
}

Richiederemo anche un messaggio su RouteSummary. Questo messaggio viene ricevuto in risposta a una RPC RecordRoute, che viene spiegata nella sezione successiva. Contiene il numero di punti individuali ricevuti, il numero di caratteristiche rilevate e la distanza totale percorsa come somma cumulativa della distanza tra ogni punto.

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;
}

Definisci i metodi di servizio

Per definire un servizio, specifica un servizio denominato nel file .proto. Il file route_guide.proto ha una struttura service denominata RouteGuide che definisce uno o più metodi forniti dal servizio dell'applicazione.

Definisci i metodi RPC all'interno della definizione del servizio, specificando i tipi di richiesta e risposta. In questa sezione del codelab, definiamo:

ListFeatures

Recupera gli Feature disponibili all'interno del Rectangle specificato. I risultati vengono trasmessi in streaming anziché restituiti contemporaneamente (ad es. in un messaggio di risposta con un campo ripetuto), poiché il rettangolo potrebbe coprire un'area di grandi dimensioni e contenere un numero elevato di funzionalità.

Un tipo appropriato per questa RPC è una RPC di streaming lato server: il client invia una richiesta al server e riceve un flusso per leggere una sequenza di messaggi. Il client legge dallo stream restituito finché non ci sono più messaggi. Come puoi vedere nel nostro esempio, devi specificare un metodo di streaming lato server inserendo la parola chiave stream prima del tipo di risposta.

rpc ListFeatures(Rectangle) returns (stream Feature) {}

RecordRoute

Accetta un flusso di Point su un percorso attraversato, restituendo un RouteSummary al termine dell'attraversamento.

In questo caso, una RPC streaming lato client sembra appropriata: il client scrive una sequenza di messaggi e li invia al server, sempre utilizzando un flusso fornito. Una volta che il client ha finito di scrivere i messaggi, attende che il server li legga tutti e restituisca la risposta. Specifichi un metodo di streaming lato client inserendo la parola chiave stream prima del tipo di richiesta.

rpc RecordRoute(stream Point) returns (RouteSummary) {}

RouteChat

Accetta un flusso di RouteNote inviati durante il percorso di un itinerario, mentre riceve altri RouteNote (ad es. da altri utenti).

Questo è esattamente il tipo di caso d'uso per lo streaming bidirezionale. Una RPC di streaming bidirezionale fa sì che entrambe le parti inviino una sequenza di messaggi utilizzando uno stream di lettura/scrittura. I due flussi operano in modo indipendente, quindi client e server possono leggere e scrivere nell'ordine che preferiscono.

Ad esempio, il server potrebbe attendere di ricevere tutti i messaggi del client prima di scrivere le risposte oppure potrebbe leggere un messaggio e poi scriverne un altro o una combinazione di letture e scritture.

L'ordine dei messaggi in ogni stream viene mantenuto. Specifichi questo tipo di metodo inserendo la parola chiave stream prima della richiesta e della risposta.

rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}

4. Genera codice client e server

A questo punto, genera il codice gRPC boilerplate sia per il client che per il server dal file .proto utilizzando il compilatore del buffer del protocollo. Nella directory routeguide, esegui:

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

Questo comando genera i seguenti file:

  • route_guide.pb.go, che contiene funzioni per creare i tipi di messaggi dell'applicazione e accedere ai relativi dati e alla definizione dei tipi che rappresentano i messaggi.
  • route_guide_grpc.pb.go, che contiene le funzioni utilizzate dal client per chiamare il metodo gRPC remoto del servizio e le funzioni utilizzate dal server per fornire il servizio remoto.

Successivamente, implementeremo i metodi lato server, in modo che quando il client invia una richiesta, il server possa rispondere.

5. Implementare il servizio

Per prima cosa, vediamo come creare un server RouteGuide. Il funzionamento del nostro servizio RouteGuide si basa su due elementi:

  • Implementazione dell'interfaccia di servizio generata dalla nostra definizione di servizio: esecuzione del "lavoro" effettivo del nostro servizio.
  • Esecuzione di un server gRPC per ascoltare le richieste dei client e inviarle all'implementazione del servizio corretta.

Implementiamo RouteGuide in server/server.go.

Implementa RouteGuide

Dobbiamo implementare l'interfaccia RouteGuideService generata. Ecco come apparirebbe l'implementazione.

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

Esaminiamo in dettaglio ogni implementazione RPC.

RPC di streaming lato server

Inizia con una delle nostre RPC di streaming. ListFeatures è una RPC di streaming lato server, quindi dobbiamo inviare più Feature al nostro client.

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
}

Come puoi vedere, anziché ottenere semplici oggetti di richiesta e risposta nei parametri del nostro metodo, questa volta otteniamo un oggetto di richiesta (il Rectangle in cui il nostro cliente vuole trovare Features) e uno speciale oggetto RouteGuide_ListFeaturesServer per scrivere le nostre risposte. Nel metodo, inseriamo tutti gli oggetti Feature necessari per la restituzione, scrivendoli in RouteGuide_ListFeaturesServer utilizzando il metodo Send(). Infine, come nella nostra semplice RPC, restituiamo un errore nil per comunicare a gRPC che abbiamo terminato di scrivere le risposte. Se si verifica un errore in questa chiamata, restituiamo un errore non nullo; il livello gRPC lo traduce in uno stato RPC appropriato da inviare via cavo.

RPC di streaming lato client

Ora esaminiamo qualcosa di un po' più complicato: il metodo di streaming lato client RecordRoute, in cui riceviamo un flusso di Points dal client e restituiamo un singolo RouteSummary con informazioni sul viaggio. Come puoi vedere, questa volta il metodo non ha alcun parametro di richiesta. Riceve invece un flusso RouteGuide_RecordRouteServer, che il server può utilizzare per leggere e scrivere messaggi. Può ricevere messaggi client utilizzando il metodo Recv() e restituire la singola risposta utilizzando il metodo 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
  }
}

Nel corpo del metodo utilizziamo il metodo Recv() di RouteGuide_RecordRouteServer per leggere ripetutamente le richieste del nostro client in un oggetto richiesta (in questo caso un Point) finché non ci sono più messaggi: il server deve controllare l'errore restituito da Recv() dopo ogni chiamata. Se è nil, lo stream è ancora valido e può continuare a leggere; se è io.EOF, lo stream di messaggi è terminato e il server può restituire il relativo RouteSummary. Se ha un altro valore, restituiamo l'errore "così com'è" in modo che venga tradotto in uno stato RPC dal livello gRPC.

RPC di streaming bidirezionale

Infine, diamo un'occhiata alla nostra RPC di streaming bidirezionale 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
      }
    }
  }
}

Questa volta otteniamo uno stream RouteGuide_RouteChatServer che, come nell'esempio di streaming lato client, può essere utilizzato per leggere e scrivere messaggi. Tuttavia, questa volta restituiamo i valori tramite lo stream del nostro metodo mentre il client sta ancora scrivendo messaggi nel suo stream di messaggi. La sintassi per la lettura e la scrittura qui è molto simile al nostro metodo di streaming lato client, tranne per il fatto che il server utilizza il metodo send() dello stream anziché SendAndClose() perché scrive più risposte. Sebbene ogni parte riceva sempre i messaggi dell'altra nell'ordine in cui sono stati scritti, sia il client che il server possono leggere e scrivere in qualsiasi ordine. I flussi operano in modo completamente indipendente.

Avvia il server

Una volta implementati tutti i nostri metodi, dobbiamo anche avviare un server gRPC in modo che i client possano effettivamente utilizzare il nostro servizio. Il seguente snippet mostra come lo facciamo per il nostro servizio 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)

Ecco cosa succede in main(), passo dopo passo:

  1. Specifica la porta TCP da utilizzare per l'ascolto delle richieste del client remoto, utilizzando lis, err := net.Listen(...). Per impostazione predefinita, l'applicazione utilizza la porta TCP 50051 come specificato dalla variabile port o passando l'opzione --port nella riga di comando durante l'esecuzione del server. Se non è possibile aprire la porta TCP, l'applicazione termina con un errore irreversibile.
  2. Crea un'istanza del server gRPC utilizzando grpc.NewServer(...), assegnando a questa istanza il nome grpcServer.
  3. Crea un puntatore a routeGuideServer, una struttura che rappresenta il servizio API dell'applicazione, assegnando al puntatore il nome s.
  4. Utilizza s.loadFeatures() per compilare l'array s.savedFeatures.
  5. Registra l'implementazione del servizio con il server gRPC.
  6. Chiama Serve() sul server con i dettagli della porta per eseguire un'attesa di blocco per le richieste del client; questa operazione continua finché il processo non viene interrotto o viene chiamato Stop().

La funzione loadFeatures() ottiene le mappature da coordinate a posizione da server/testdata.go.

6. Crea il client

Ora modifica client/client.go, dove implementerai il codice client.

Per chiamare i metodi del servizio remoto, dobbiamo prima creare un canale gRPC per comunicare con il server. Lo creiamo passando la stringa URI di destinazione del server (che in questo caso è semplicemente l'indirizzo e il numero di porta) a grpc.NewClient() nella funzione main() del client nel seguente modo:

// 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()

L'indirizzo del server, definito dalla variabile serverAddr, è localhost:50051 per impostazione predefinita e può essere sostituito dall'opzione --addr nella riga di comando quando esegui il client.

Se il client deve connettersi a un servizio che richiede credenziali di autenticazione, come credenziali TLS o JWT, può passare un oggetto DialOptions come parametro a grpc.NewClient che contiene le credenziali richieste. Il servizio RouteGuide non richiede credenziali.

Una volta configurato il canale gRPC, abbiamo bisogno di uno stub client per eseguire RPC tramite chiamate di funzioni Go. Otteniamo questo stub utilizzando il metodo NewRouteGuideClient fornito dal file route_guide_grpc.pb.go generato dal file .proto dell'applicazione.

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

client := pb.NewRouteGuideClient(conn)

Metodi di servizio di chiamata

Ora vediamo come chiamiamo i metodi del servizio. In gRPC-Go, le RPC operano in modalità di blocco/sincrona, il che significa che la chiamata RPC attende la risposta del server e restituisce una risposta o un errore.

RPC di streaming lato server

Qui chiamiamo il metodo di streaming lato server ListFeatures, che restituisce un flusso di oggetti geografici 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())
}

Come nella semplice RPC, passiamo al metodo un contesto e una richiesta. Tuttavia, anziché ricevere un oggetto di risposta, riceviamo un'istanza di RouteGuide_ListFeaturesClient. Il client può utilizzare lo stream RouteGuide_ListFeaturesClient per leggere le risposte del server. Utilizziamo il metodo Recv() di RouteGuide_ListFeaturesClient per leggere ripetutamente le risposte del server a un oggetto buffer di protocollo di risposta (in questo caso un Feature) finché non ci sono più messaggi: il client deve controllare l'errore err restituito da Recv() dopo ogni chiamata. Se nil, lo stream è ancora valido e può continuare a leggere; se è io.EOF, lo stream di messaggi è terminato; in caso contrario, deve esserci un errore RPC, che viene passato tramite err.

RPC di streaming lato client

Il metodo di streaming lato client RecordRoute è simile al metodo lato server, tranne per il fatto che passiamo al metodo solo un contesto e riceviamo un flusso RouteGuide_RecordRouteClient, che possiamo utilizzare sia per scrivere che per leggere i messaggi.

// 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)

RouteGuide_RecordRouteClient ha un metodo Send() che possiamo utilizzare per inviare richieste al server. Una volta terminata la scrittura delle richieste del cliente nel flusso utilizzando Send(), dobbiamo chiamare CloseAndRecv() nel flusso per comunicare a gRPC che abbiamo terminato la scrittura e che ci aspettiamo di ricevere una risposta. Otteniamo lo stato RPC dall'errore restituito da CloseAndRecv(). Se lo stato è nil, il primo valore restituito da CloseAndRecv() sarà una risposta valida del server.

RPC di streaming bidirezionale

Infine, diamo un'occhiata alla nostra RPC di streaming bidirezionale RouteChat(). Come nel caso di RecordRoute, passiamo al metodo solo un oggetto contesto e riceviamo un flusso che possiamo utilizzare per scrivere e leggere i messaggi. Tuttavia, questa volta restituiamo i valori tramite lo stream del nostro metodo mentre il server sta ancora scrivendo messaggi nel suo stream di messaggi.

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

La sintassi per la lettura e la scrittura qui è molto simile al nostro metodo di streaming lato client, tranne per il fatto che utilizziamo il metodo CloseSend() dello stream una volta terminata la chiamata. Sebbene ogni parte riceva sempre i messaggi dell'altra nell'ordine in cui sono stati scritti, sia il client che il server possono leggere e scrivere in qualsiasi ordine. I flussi operano in modo completamente indipendente.

7. Prova

Verifica che il server e il client funzionino correttamente tra loro eseguendo i seguenti comandi nella directory di lavoro dell'applicazione:

  1. Esegui il server in un terminale:
cd server
go run .
  1. Esegui il client da un altro terminale:
cd client
go run .

Vedrai un output simile a questo, con i timestamp omessi per chiarezza:

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