Comienza a usar gRPC-Go

1. Introducción

En este codelab, usarás gRPC-Go para crear un cliente y un servidor que formen la base de una aplicación de asignación de rutas escrita en Go.

Al final del instructivo, tendrás un cliente que se conecta a un servidor remoto con gRPC para obtener el nombre o la dirección postal de lo que se encuentra en coordenadas específicas en un mapa. Una aplicación completa podría usar este diseño cliente-servidor para enumerar o resumir los puntos de interés a lo largo de una ruta.

El servicio se define en un archivo de Protocol Buffers, que se usará para generar código estándar para el cliente y el servidor, de modo que puedan comunicarse entre sí, lo que te ahorrará tiempo y esfuerzo en la implementación de esa funcionalidad.

Este código generado se encarga no solo de las complejidades de la comunicación entre el servidor y el cliente, sino también de la serialización y deserialización de datos.

Qué aprenderás

  • Cómo usar los búferes de protocolo para definir una API de servicio
  • Cómo compilar un cliente y un servidor basados en gRPC a partir de una definición de Protocol Buffers con la generación de código automatizada
  • Conocimiento de la comunicación cliente-servidor con gRPC

Este codelab está dirigido a desarrolladores de Go que no conocen gRPC o que desean repasar sus conceptos, o a cualquier otra persona interesada en crear sistemas distribuidos. No se requiere experiencia previa con gRPC.

2. Antes de comenzar

Requisitos previos

Asegúrate de haber instalado lo siguiente:

  • La versión 1.24.5 o posterior de la cadena de herramientas de Go Para obtener instrucciones de instalación, consulta la sección Comenzar de Go.
  • El compilador de búferes de protocolo, protoc, versión 3.27.1 o posterior Para obtener instrucciones de instalación, consulta la guía de instalación del compilador.
  • Son los complementos del compilador de búferes de protocolo para Go y gRPC. Para instalar estos complementos, ejecuta los siguientes comandos:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

Actualiza tu variable PATH para que el compilador de búferes de protocolo pueda encontrar los complementos:

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

Obtén el código

Para que no tengas que empezar desde cero, este codelab proporciona un andamio del código fuente de la aplicación para que lo completes. En los siguientes pasos, se muestra cómo finalizar la aplicación, incluido el uso de los complementos del compilador de búfer de protocolo para generar el código gRPC estándar.

Descarga este código fuente como un archivo .ZIP de GitHub y descomprime su contenido.

Como alternativa, el código fuente completo está disponible en GitHub si deseas omitir la escritura de una implementación.

3. Define el servicio

El primer paso es definir el servicio de gRPC de la aplicación, su método de RPC y sus tipos de mensajes de solicitud y respuesta con búferes de protocolo. Tu servicio proporcionará lo siguiente:

  • Un método de RPC llamado GetFeature que el servidor implementa y el cliente llama.
  • Son los tipos de mensajes Point y Feature que son estructuras de datos que se intercambian entre el cliente y el servidor cuando se usa el método GetFeature. El cliente proporciona coordenadas de mapa como un Point en su solicitud GetFeature al servidor, y el servidor responde con un Feature correspondiente que describe lo que se encuentra en esas coordenadas.

Este método de RPC y sus tipos de mensajes se definirán en el archivo routeguide/route_guide.proto del código fuente proporcionado.

Los búferes de protocolo se conocen comúnmente como protobufs. Para obtener más información sobre la terminología de gRPC, consulta los conceptos básicos, la arquitectura y el ciclo de vida de gRPC.

Tipos de mensajes

En el archivo routeguide/route_guide.proto del código fuente, primero define el tipo de mensaje Point. Un objeto Point representa un par de coordenadas de latitud y longitud en un mapa. En este codelab, usa números enteros para las coordenadas:

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

Los números 1 y 2 son números de ID únicos para cada uno de los campos de la estructura message.

A continuación, define el tipo de mensaje Feature. Un Feature usa un campo string para el nombre o la dirección postal de algo en una ubicación especificada por un Point:

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

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

Método de servicio

El archivo route_guide.proto tiene una estructura service llamada RouteGuide que define uno o más métodos proporcionados por el servicio de la aplicación.

Agrega el método rpc GetFeature dentro de la definición de RouteGuide. Como se explicó anteriormente, este método buscará el nombre o la dirección de una ubicación a partir de un conjunto de coordenadas determinado, por lo que GetFeature devolverá un Feature para un Point determinado:

service RouteGuide {
  // Definition of the service goes here

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

Este es un método de RPC unario: una RPC simple en la que el cliente envía una solicitud al servidor y espera a que vuelva una respuesta, al igual que una llamada a función local.

4. Genera el código del cliente y del servidor

A continuación, genera el código gRPC estándar para el cliente y el servidor desde el archivo .proto con el compilador de búfer de protocolo. En el directorio routeguide, ejecuta lo siguiente:

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

Este comando genera los siguientes archivos:

  • route_guide.pb.go, que contiene funciones para crear los tipos de mensajes de la aplicación y acceder a sus datos
  • route_guide_grpc.pb.go, que contiene funciones que el cliente usa para llamar al método gRPC remoto del servicio y funciones que el servidor usa para proporcionar ese servicio remoto.

A continuación, implementaremos el método GetFeature en el servidor para que, cuando el cliente envíe una solicitud, el servidor pueda responder con una respuesta.

5. Implementa el servicio

La función GetFeature del servidor es donde se realiza el trabajo principal: toma un mensaje Point del cliente y devuelve en un mensaje Feature la información de ubicación correspondiente de una lista de lugares conocidos. A continuación, se muestra la implementación de la función en 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
}

Cuando se invoca este método después de una solicitud de un cliente remoto, se pasa a la función un objeto Context que describe la llamada a RPC y un objeto de búfer de protocolo Point de esa solicitud del cliente. La función devuelve un objeto de búfer de protocolo Feature para la ubicación buscada y un error según sea necesario.

En el método, completa un objeto Feature con la información adecuada para el Point determinado y, luego, returnlo junto con un error nil para indicarle a gRPC que terminaste de controlar la RPC y que el objeto Feature se puede devolver al cliente.

El método GetFeature requiere que se cree y registre un objeto routeGuideServer para que las solicitudes de los clientes para las búsquedas de ubicación se puedan enrutar a esa función. Esto se hace en 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)
}

Esto es lo que sucede en main(), paso a paso:

  1. Especifica el puerto TCP que se usará para escuchar las solicitudes de clientes remotos con lis, err := net.Listen(...). De forma predeterminada, la aplicación usa el puerto TCP 50051, como se especifica en la variable port o pasando el parámetro --port en la línea de comandos cuando se ejecuta el servidor. Si no se puede abrir el puerto TCP, la aplicación finaliza con un error fatal.
  2. Crea una instancia del servidor de gRPC con grpc.NewServer(...) y asígnale el nombre grpcServer.
  3. Crea un puntero a routeGuideServer, una estructura que representa el servicio de API de la aplicación, y nómbralo s..
  4. Usa s.loadFeatures() para completar el array s.savedFeatures con ubicaciones que se pueden buscar a través de GetFeature.
  5. Registra el servicio de API con el servidor de gRPC para que las llamadas a RPC a GetFeature se enruten a la función adecuada.
  6. Llama a Serve() en el servidor con los detalles de nuestro puerto para realizar una espera de bloqueo de las solicitudes del cliente. Esto continúa hasta que se detiene el proceso o se llama a Stop().

La función loadFeatures() obtiene sus asignaciones de coordenadas a ubicación de server/testdata.go.

6. Crea el cliente

Ahora, edita client/client.go, que es donde implementarás el código del cliente.

Para llamar a los métodos del servicio remoto, primero debemos crear un canal de gRPC para comunicarnos con el servidor. Para ello, pasamos la cadena URI de destino del servidor (que, en este caso, es simplemente la dirección y el número de puerto) a grpc.NewClient() en la función main() del cliente de la siguiente manera:

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

La dirección del servidor, definida por la variable serverAddr, es localhost:50051 de forma predeterminada y se puede anular con el parámetro --addr en la línea de comandos cuando se ejecuta el cliente.

Si el cliente necesita conectarse a un servicio que requiere credenciales de autenticación, como credenciales de TLS o JWT, puede pasar un objeto DialOptions como parámetro a grpc.NewClient que contenga las credenciales requeridas. El servicio RouteGuide no requiere credenciales.

Una vez que se configura el canal de gRPC, necesitamos un stub del cliente para realizar RPC a través de llamadas a funciones de Go. Obtenemos ese stub con el método NewRouteGuideClient que proporciona el archivo route_guide_grpc.pb.go generado a partir del archivo .proto de la aplicación.

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

client := pb.NewRouteGuideClient(conn)

Llama a métodos de servicio

En gRPC-Go, las RPC operan en un modo de bloqueo o síncrono, lo que significa que la llamada a la RPC espera a que responda el servidor y mostrará una respuesta o un error.

RPC simple

Llamar a la RPC simple GetFeature es casi tan sencillo como llamar a un método local, en este caso 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)
}

El cliente llama al método en el stub creado anteriormente. Para los parámetros del método, el cliente crea y completa un objeto de búfer de protocolo de solicitud Point. También pasas un objeto context.Context que nos permite cambiar el comportamiento de nuestra RPC si es necesario, como definir un límite de tiempo para la llamada o cancelar una RPC en curso. Si la llamada no devuelve un error, el cliente puede leer la información de respuesta del servidor desde el primer valor de devolución:

log.Println(feature)

En total, la función main() del cliente debería verse de la siguiente manera:

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

Ejecuta los siguientes comandos en el directorio de trabajo de la aplicación para confirmar que el servidor y el cliente funcionen correctamente entre sí:

  1. Ejecuta el servidor en una terminal:
cd server
go run .
  1. Ejecuta el cliente desde otra terminal:
cd client
go run .

Verás un resultado como este, con marcas de tiempo omitidas para mayor claridad:

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. ¿Qué sigue?