Premiers pas avec gRPC-Go : streaming

1. Introduction

Dans cet atelier de programmation, vous allez utiliser gRPC-Go pour créer un client et un serveur qui constituent la base d'une application de cartographie d'itinéraire écrite en Go.

À la fin du tutoriel, vous disposerez d'un client qui se connecte à un serveur distant à l'aide de gRPC pour obtenir des informations sur les caractéristiques d'un itinéraire client, créer un récapitulatif d'un itinéraire client et échanger des informations sur l'itinéraire, telles que les mises à jour du trafic, avec le serveur et d'autres clients.

Le service est défini dans un fichier Protocol Buffers, qui sera utilisé pour générer du code passe-partout pour le client et le serveur afin qu'ils puissent communiquer entre eux. Vous gagnerez ainsi du temps et des efforts pour implémenter cette fonctionnalité.

Ce code généré gère non seulement les complexités de la communication entre le serveur et le client, mais aussi la sérialisation et la désérialisation des données.

Points abordés

  • Utiliser Protocol Buffers pour définir une API de service.
  • Comment créer un client et un serveur basés sur gRPC à partir d'une définition Protocol Buffers à l'aide de la génération de code automatisée.
  • Comprendre la communication de streaming client-serveur avec gRPC.

Cet atelier de programmation s'adresse aux développeurs Go qui débutent avec gRPC ou qui souhaitent se rafraîchir la mémoire sur gRPC, ou à toute personne intéressée par la création de systèmes distribués. Aucune expérience préalable avec gRPC n'est requise.

2. Avant de commencer

Prérequis

Assurez-vous d'avoir installé les éléments suivants :

  • La version 1.24.5 ou ultérieure de la chaîne d'outils Go. Pour obtenir des instructions d'installation, consultez la page Getting started (Premiers pas) de Go.
  • Le compilateur de tampons de protocole protoc, version 3.27.1 ou ultérieure. Pour obtenir des instructions d'installation, consultez le guide d'installation du compilateur.
  • Plug-ins du compilateur de tampon de protocole pour Go et gRPC. Pour installer ces plug-ins, exécutez les commandes suivantes :
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

Mettez à jour votre variable PATH afin que le compilateur de tampon de protocole puisse trouver les plug-ins :

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

Obtenir le code

Pour que vous n'ayez pas à partir de zéro, cet atelier de programmation fournit un échafaudage du code source de l'application que vous devez compléter. Les étapes suivantes vous montreront comment finaliser l'application, y compris en utilisant les plug-ins du compilateur de tampon de protocole pour générer le code gRPC standard.

Commencez par créer le répertoire de travail de l'atelier de programmation et accédez-y :cd

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

Téléchargez et extrayez l'atelier de programmation :

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

Vous pouvez également télécharger le fichier .zip contenant uniquement le répertoire de l'atelier de programmation et le décompresser manuellement.

Le code source complet est disponible sur GitHub si vous ne souhaitez pas saisir d'implémentation.

3. Définir les messages et les services

La première étape consiste à définir le service gRPC de l'application, ses méthodes RPC, ainsi que ses types de messages de requête et de réponse à l'aide de Protocol Buffers. Votre service fournira :

  • Méthodes RPC appelées ListFeatures, RecordRoute et RouteChat que le serveur implémente et que le client appelle.
  • Les types de messages Point, Feature, Rectangle, RouteNote et RouteSummary, qui sont des structures de données échangées entre le client et le serveur lors de l'appel des méthodes ci-dessus.

Ces méthodes RPC et leurs types de messages seront tous définis dans le fichier routeguide/route_guide.proto du code source fourni.

Les tampons de protocole sont communément appelés "protobufs". Pour en savoir plus sur la terminologie gRPC, consultez Concepts fondamentaux, architecture et cycle de vie de gRPC.

Définir les types de messages

Dans le fichier routeguide/route_guide.proto du code source, définissez d'abord le type de message Point. Un Point représente une paire de coordonnées (latitude et longitude) sur une carte. Pour cet atelier de programmation, utilisez des nombres entiers pour les coordonnées :

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

Les nombres 1 et 2 sont des ID uniques pour chacun des champs de la structure message.

Ensuite, définissez le type de message Feature. Un Feature utilise un champ string pour le nom ou l'adresse postale d'un élément à un emplacement spécifié par un Point :

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

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

Ensuite, un message Rectangle représentant un rectangle de latitude-longitude, représenté par deux points diagonalement opposés "lo" et "hi".

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

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

Il s'agit également d'un message RouteNote qui représente un message envoyé à un moment donné.

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

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

Nous aurons également besoin d'un message RouteSummary. Ce message est reçu en réponse à un RPC RecordRoute, qui est expliqué dans la section suivante. Il contient le nombre de points individuels reçus, le nombre de caractéristiques détectées et la distance totale parcourue, qui correspond à la somme cumulée de la distance entre chaque point.

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

Définir les méthodes de service

Pour définir un service, vous devez spécifier un service nommé dans votre fichier .proto. Le fichier route_guide.proto possède une structure service nommée RouteGuide qui définit une ou plusieurs méthodes fournies par le service de l'application.

Définissez les méthodes RPC dans la définition de votre service, en spécifiant leurs types de requête et de réponse. Dans cette section de l'atelier de programmation, définissons les éléments suivants :

ListFeatures

Obtient les Feature disponibles dans le Rectangle spécifié. Les résultats sont diffusés en flux continu plutôt que renvoyés en une seule fois (par exemple, dans un message de réponse avec un champ répété), car le rectangle peut couvrir une grande surface et contenir un grand nombre d'entités.

Un type approprié pour ce RPC est un RPC de streaming côté serveur : le client envoie une requête au serveur et obtient un flux pour lire une séquence de messages en retour. Le client lit le flux renvoyé jusqu'à ce qu'il n'y ait plus de messages. Comme vous pouvez le voir dans notre exemple, vous spécifiez une méthode de streaming côté serveur en plaçant le mot clé "stream" avant le type de réponse.

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

RecordRoute

Accepte un flux de Point sur un itinéraire parcouru et renvoie un RouteSummary lorsque le parcours est terminé.

Un RPC de streaming côté client semble approprié dans ce cas : le client écrit une séquence de messages et les envoie au serveur, à nouveau à l'aide d'un flux fourni. Une fois que le client a terminé d'écrire les messages, il attend que le serveur les lise tous et renvoie sa réponse. Pour spécifier une méthode de streaming côté client, placez le mot clé "stream" avant le type de requête.

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

RouteChat

Accepte un flux de RouteNote envoyés lors du parcours d'un itinéraire, tout en recevant d'autres RouteNote (par exemple, d'autres utilisateurs).

C'est exactement le type de cas d'utilisation pour le streaming bidirectionnel. Dans un RPC de streaming bidirectionnel, les deux parties envoient une séquence de messages à l'aide d'un flux en lecture-écriture. Les deux flux fonctionnent indépendamment, de sorte que les clients et les serveurs peuvent lire et écrire dans l'ordre de leur choix.

Par exemple, le serveur peut attendre de recevoir tous les messages du client avant d'écrire ses réponses, ou il peut lire un message, puis en écrire un, ou encore effectuer une autre combinaison de lectures et d'écritures.

L'ordre des messages dans chaque flux est conservé. Pour spécifier ce type de méthode, placez le mot clé "stream" avant la requête et la réponse.

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

4. Générer le code client et serveur

Ensuite, générez le code gRPC standard pour le client et le serveur à partir du fichier .proto à l'aide du compilateur de tampons de protocole. Dans le répertoire routeguide, exécutez la commande suivante :

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

Cette commande génère les fichiers suivants :

  • route_guide.pb.go, qui contient des fonctions permettant de créer les types de messages de l'application et d'accéder à leurs données, ainsi que la définition des types qui représentent les messages.
  • route_guide_grpc.pb.go, qui contient les fonctions utilisées par le client pour appeler la méthode gRPC à distance du service, ainsi que les fonctions utilisées par le serveur pour fournir ce service à distance.

Ensuite, nous implémenterons les méthodes côté serveur afin que le serveur puisse répondre lorsqu'un client envoie une requête.

5. Implémenter le service

Commençons par examiner comment créer un serveur RouteGuide. Pour que notre service RouteGuide fonctionne correctement, deux éléments sont nécessaires :

  • Implémenter l'interface de service générée à partir de notre définition de service : effectuer le "travail" réel de notre service.
  • Exécuter un serveur gRPC pour écouter les requêtes des clients et les distribuer à la bonne implémentation de service.

Implémentons RouteGuide dans server/server.go.

Implémenter RouteGuide

Nous devons implémenter l'interface RouteGuideService générée. Voici à quoi ressemblerait l'implémentation.

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

Examinons en détail chaque implémentation RPC.

RPC de streaming côté serveur

Commencez par l'un de nos RPC de streaming. ListFeatures est un RPC de streaming côté serveur. Nous devons donc renvoyer plusieurs Feature à notre 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
}

Comme vous pouvez le voir, au lieu d'obtenir de simples objets de requête et de réponse dans les paramètres de notre méthode, nous obtenons cette fois un objet de requête (le Rectangle dans lequel notre client souhaite trouver Features) et un objet RouteGuide_ListFeaturesServer spécial pour écrire nos réponses. Dans la méthode, nous remplissons autant d'objets Feature que nécessaire pour le renvoi, en les écrivant dans RouteGuide_ListFeaturesServer à l'aide de sa méthode Send(). Enfin, comme dans notre RPC simple, nous renvoyons une erreur nil pour indiquer à gRPC que nous avons terminé d'écrire les réponses. En cas d'erreur lors de cet appel, nous renvoyons une erreur non nulle. La couche gRPC la traduira en un état RPC approprié à envoyer sur le réseau.

RPC de streaming côté client

Examinons maintenant quelque chose d'un peu plus compliqué : la méthode de streaming côté client RecordRoute, où nous obtenons un flux de Points du client et renvoyons un seul RouteSummary avec des informations sur son trajet. Comme vous pouvez le voir, cette fois, la méthode ne comporte aucun paramètre de requête. Au lieu de cela, il obtient un flux RouteGuide_RecordRouteServer, que le serveur peut utiliser pour lire et écrire des messages. Il peut recevoir des messages client à l'aide de sa méthode Recv() et renvoyer sa réponse unique à l'aide de sa méthode 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
  }
}

Dans le corps de la méthode, nous utilisons la méthode Recv() de RouteGuide_RecordRouteServer pour lire à plusieurs reprises les requêtes de notre client dans un objet de requête (dans ce cas, un Point) jusqu'à ce qu'il n'y ait plus de messages : le serveur doit vérifier l'erreur renvoyée par Recv() après chaque appel. Si la valeur est nil, le flux est toujours valide et la lecture peut se poursuivre. Si la valeur est io.EOF, le flux de messages est terminé et le serveur peut renvoyer son RouteSummary. Si elle a une autre valeur, nous renvoyons l'erreur "telle quelle" afin qu'elle soit traduite en état RPC par la couche gRPC.

RPC de streaming bidirectionnel

Enfin, examinons notre RPC de streaming bidirectionnel 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
      }
    }
  }
}

Cette fois, nous obtenons un flux RouteGuide_RouteChatServer qui, comme dans notre exemple de streaming côté client, peut être utilisé pour lire et écrire des messages. Cette fois, nous renvoyons des valeurs via le flux de notre méthode pendant que le client continue d'écrire des messages dans son flux de messages. La syntaxe de lecture et d'écriture est très semblable à celle de notre méthode de streaming client, sauf que le serveur utilise la méthode send() du flux plutôt que SendAndClose(), car il écrit plusieurs réponses. Bien que chaque côté reçoive toujours les messages de l'autre dans l'ordre dans lequel ils ont été écrits, le client et le serveur peuvent lire et écrire dans n'importe quel ordre. Les flux fonctionnent de manière totalement indépendante.

Démarrer le serveur

Une fois que nous avons implémenté toutes nos méthodes, nous devons également démarrer un serveur gRPC pour que les clients puissent réellement utiliser notre service. L'extrait suivant montre comment nous procédons pour notre service 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)

Voici ce qui se passe dans main(), étape par étape :

  1. Spécifiez le port TCP à utiliser pour écouter les requêtes des clients distants à l'aide de lis, err := net.Listen(...). Par défaut, l'application utilise le port TCP 50051, comme spécifié par la variable port ou en transmettant le commutateur --port sur la ligne de commande lors de l'exécution du serveur. Si le port TCP ne peut pas être ouvert, l'application se termine avec une erreur fatale.
  2. Créez une instance du serveur gRPC à l'aide de grpc.NewServer(...), en nommant cette instance grpcServer.
  3. Créez un pointeur vers routeGuideServer, une structure représentant le service d'API de l'application, en nommant le pointeur s..
  4. Utilisez s.loadFeatures() pour remplir le tableau s.savedFeatures.
  5. Enregistrez l'implémentation de notre service auprès du serveur gRPC.
  6. Appelez Serve() sur le serveur avec les détails de notre port pour effectuer une attente bloquante des requêtes client. Cette opération se poursuit jusqu'à ce que le processus soit arrêté ou que Stop() soit appelé.

La fonction loadFeatures() obtient ses mappages de coordonnées vers des lieux à partir de server/testdata.go.

6. Créer le client

Modifiez maintenant client/client.go, qui est l'endroit où vous implémenterez le code client.

Pour appeler les méthodes du service à distance, nous devons d'abord créer un canal gRPC pour communiquer avec le serveur. Pour ce faire, nous transmettons la chaîne URI cible du serveur (qui, dans ce cas, est simplement l'adresse et le numéro de port) à grpc.NewClient() dans la fonction main() du client comme suit :

// 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'adresse du serveur, définie par la variable serverAddr, est localhost:50051 par défaut. Elle peut être remplacée par le commutateur --addr sur la ligne de commande lors de l'exécution du client.

Si le client doit se connecter à un service qui nécessite des identifiants d'authentification, tels que des identifiants TLS ou JWT, il peut transmettre un objet DialOptions en tant que paramètre à grpc.NewClient contenant les identifiants requis. Le service RouteGuide ne nécessite aucun identifiant.

Une fois le canal gRPC configuré, nous avons besoin d'un stub client pour effectuer des RPC via des appels de fonction Go. Nous obtenons ce stub à l'aide de la méthode NewRouteGuideClient fournie par le fichier route_guide_grpc.pb.go généré à partir du fichier .proto de l'application.

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

client := pb.NewRouteGuideClient(conn)

Appeler les méthodes de service

Voyons maintenant comment appeler les méthodes de service. Dans gRPC-Go, les RPC fonctionnent en mode bloquant/synchrone, ce qui signifie que l'appel RPC attend la réponse du serveur et renvoie une réponse ou une erreur.

RPC de streaming côté serveur

C'est ici que nous appelons la méthode de streaming côté serveur ListFeatures, qui renvoie un flux d'objets Feature géographiques.

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

Comme dans le RPC simple, nous transmettons à la méthode un contexte et une requête. Toutefois, au lieu d'obtenir un objet de réponse, nous obtenons une instance de RouteGuide_ListFeaturesClient. Le client peut utiliser le flux RouteGuide_ListFeaturesClient pour lire les réponses du serveur. Nous utilisons la méthode Recv() de RouteGuide_ListFeaturesClient pour lire à plusieurs reprises les réponses du serveur dans un objet de tampon de protocole de réponse (dans ce cas, un Feature) jusqu'à ce qu'il n'y ait plus de messages : le client doit vérifier l'erreur renvoyée par Recv() après chaque appel. Si la valeur est nil, le flux est toujours valide et la lecture peut se poursuivre. Si la valeur est io.EOF, le flux de messages est terminé. Sinon, il doit y avoir une erreur RPC, qui est transmise via err.

RPC de streaming côté client

La méthode de streaming côté client RecordRoute est semblable à la méthode côté serveur, sauf que nous ne transmettons à la méthode qu'un contexte et obtenons en retour un flux RouteGuide_RecordRouteClient, que nous pouvons utiliser pour écrire et lire des messages.

// 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 dispose d'une méthode Send() que nous pouvons utiliser pour envoyer des requêtes au serveur. Une fois que nous avons terminé d'écrire les requêtes de notre client dans le flux à l'aide de Send(), nous devons appeler CloseAndRecv() sur le flux pour indiquer à gRPC que nous avons terminé d'écrire et que nous attendons de recevoir une réponse. Nous obtenons l'état RPC à partir de l'erreur renvoyée par CloseAndRecv(). Si l'état est nil, la première valeur renvoyée par CloseAndRecv() sera une réponse de serveur valide.

RPC de streaming bidirectionnel

Enfin, examinons notre RPC de streaming bidirectionnel RouteChat(). Comme dans le cas de RecordRoute, nous ne transmettons à la méthode qu'un objet de contexte et nous récupérons un flux que nous pouvons utiliser pour écrire et lire des messages. Cette fois, nous renvoyons des valeurs via le flux de notre méthode pendant que le serveur écrit encore des messages dans son flux de messages.

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 syntaxe de lecture et d'écriture est très semblable à celle de notre méthode de streaming côté client, sauf que nous utilisons la méthode CloseSend() du flux une fois l'appel terminé. Bien que chaque côté reçoive toujours les messages de l'autre dans l'ordre dans lequel ils ont été écrits, le client et le serveur peuvent lire et écrire dans n'importe quel ordre. Les flux fonctionnent de manière totalement indépendante.

7. Essayer

Vérifiez que le serveur et le client fonctionnent correctement ensemble en exécutant les commandes suivantes dans le répertoire de travail de l'application :

  1. Exécutez le serveur dans un terminal :
cd server
go run .
  1. Exécutez le client à partir d'un autre terminal :
cd client
go run .

Un résultat semblable à celui-ci s'affiche (les codes temporels sont omis pour plus de clarté) :

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. Étape suivante