Comienza a usar gRPC-Python: transmisión

1. Introducción

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

Al final del instructivo, tendrás un cliente que se conecta a un servidor remoto con gRPC para obtener información sobre las funciones en la ruta de un cliente, crear un resumen de la ruta de un cliente y compartir información de la ruta, como actualizaciones de tráfico, con el servidor y otros clientes.

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 de transmisión cliente-servidor con gRPC

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

2. Antes de comenzar

Requisitos

  • Python 3.9 o una versión posterior Te recomendamos Python 3.13. Para obtener instrucciones de instalación específicas de la plataforma, consulta Configuración y uso de Python. Como alternativa, instala un Python que no sea del sistema con herramientas como uv o pyenv.
  • pip para instalar paquetes de Python
  • venv para crear entornos virtuales de Python

Los paquetes ensurepip y venv forman parte de la biblioteca estándar de Python y, por lo general, están disponibles de forma predeterminada.

Sin embargo, algunas distribuciones basadas en Debian (incluido Ubuntu) optan por excluirlos cuando redistribuyen Python. Para instalar los paquetes, ejecuta el siguiente comando:

sudo apt install python3-pip python3-venv

Obtén el código

Para optimizar tu aprendizaje, este codelab ofrece un código fuente precompilado que te ayudará a comenzar. En los siguientes pasos, se te guiará para completar la aplicación, incluida la generación de código gRPC con el complemento del compilador de Protocol Buffer grpc_tools.protoc.

grpc-codelabs

El código fuente del scaffold para este codelab está disponible en el directorio codelabs/grpc-python-streaming/start_here. Si prefieres no implementar el código por tu cuenta, el código fuente completo está disponible en el directorio completed.

Primero, crea el directorio de trabajo del codelab y cámbiate a él con el comando cd:

mkdir grpc-python-streaming && cd grpc-python-streaming

Descarga y extrae el 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-python-streaming/start_here

También puedes descargar el archivo .zip que contiene solo el directorio del codelab y descomprimirlo de forma manual.

3. Define mensajes y servicios

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:

  • Métodos de RPC llamados ListFeatures, RecordRoute y RouteChat que el servidor implementa y el cliente llama.
  • Los tipos de mensajes Point, Feature, Rectangle, RouteNote y RouteSummary, que son estructuras de datos que se intercambian entre el cliente y el servidor cuando se llaman a los métodos de RPC.

Estos métodos de RPC y sus tipos de mensajes se definirán en el archivo protos/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.

Define los tipos de mensajes

En el archivo protos/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;
}

Para que se puedan transmitir varios puntos dentro de un área a un cliente, necesitarás un mensaje Rectangle que represente un rectángulo de latitud y longitud, representado como dos puntos opuestos diagonalmente lo y hi:

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

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

Además, un mensaje de RouteNote que representa un mensaje enviado en un punto determinado:

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

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

Por último, necesitarás un mensaje de RouteSummary. Este mensaje se recibe en respuesta a una RPC de RecordRoute, que se explica en la siguiente sección. Contiene la cantidad de puntos individuales recibidos, la cantidad de atributos detectados y la distancia total recorrida como la suma acumulativa de la distancia entre cada 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;
}

Cómo definir métodos de servicio

Para definir un servicio, especifica un servicio con nombre en tu archivo .proto. 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.

Cuando defines métodos RPC dentro de la definición de tu servicio, especificas sus tipos de solicitud y respuesta. En esta sección del codelab, definiremos lo siguiente:

ListFeatures

Obtiene los objetos Feature disponibles en el Rectangle determinado. Los resultados se transmiten en lugar de devolverse de una vez, ya que el rectángulo puede abarcar un área grande y contener una gran cantidad de entidades.

Para esta aplicación, usarás una RPC de transmisión del servidor: el cliente envía una solicitud al servidor y obtiene una transmisión para leer una secuencia de mensajes. El cliente lee la transmisión que se muestra hasta que no haya más mensajes. Como puedes ver en nuestro ejemplo, para especificar un método de transmisión del servidor, debes colocar la palabra clave stream antes del tipo de respuesta.

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

RecordRoute

Acepta un flujo de puntos en una ruta que se recorre y devuelve un RouteSummary cuando se completa el recorrido.

En este caso, es adecuada una RPC de transmisión del cliente: el cliente escribe una secuencia de mensajes y los envía al servidor, nuevamente a través de una transmisión proporcionada. Una vez que el cliente termina de escribir los mensajes, espera a que el servidor los lea todos y muestre la respuesta. Para especificar un método de transmisión por Internet del cliente, coloca la palabra clave stream antes del tipo de solicitud.

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

RouteChat

Acepta un flujo de RouteNotes que se envía mientras se recorre una ruta y recibe otros RouteNotes (p.ej., de otros usuarios).

Este es exactamente el caso de uso para la transmisión bidireccional. Es una RPC de transmisión bidireccional en la que ambos extremos envían una secuencia de mensajes a través de una transmisión de lectura y escritura. Las dos transmisiones operan de forma independiente, por lo que los clientes y los servidores pueden leer y escribir en el orden que deseen. Por ejemplo, el servidor podría esperar a recibir todos los mensajes del cliente antes de escribir sus respuestas, o podría leer un mensaje y, luego, escribir uno, o alguna otra combinación de lecturas y escrituras. Se conserva el orden de los mensajes en cada transmisión. Para especificar este tipo de método, coloca la palabra clave stream antes de la solicitud y la respuesta.

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

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.

Para la generación de código de Python de gRPC, creamos grpcio-tools. Incluye lo siguiente:

  1. El compilador protoc normal que genera código de Python a partir de definiciones de message
  2. Es un complemento de protobuf de gRPC que genera código de Python (stubs de cliente y servidor) a partir de las definiciones de service.

Instalaremos el paquete de Python grpcio-tools con pip. Creemos un nuevo entorno virtual de Python (venv) para aislar las dependencias de tu proyecto de los paquetes del sistema:

python3 -m venv --upgrade-deps .venv

Para activar el entorno virtual en el shell de bash/zsh, haz lo siguiente:

source .venv/bin/activate

Para Windows y shells no estándar, consulta la tabla en https://docs.python.org/3/library/venv.html#how-venvs-work.

A continuación, instala grpcio-tools (esto también instala el paquete grpcio):

pip install grpcio-tools

Usa el siguiente comando para generar el código de plantilla de Python:

python -m grpc_tools.protoc --proto_path=./protos  \
 --python_out=. --pyi_out=. --grpc_python_out=. \
 ./protos/route_guide.proto

Esto generará los siguientes archivos para las interfaces que definimos en route_guide.proto:

  1. route_guide_pb2.py contiene el código que crea clases de forma dinámica generadas a partir de las definiciones de message.
  2. route_guide_pb2.pyi es un "archivo de código auxiliar" o "archivo de sugerencias de tipo" que se genera a partir de las definiciones de message. Solo contiene las firmas sin implementación. Los IDE pueden usar archivos de código auxiliar para proporcionar una mejor detección de errores y autocompletado.
  3. route_guide_pb2_grpc.py se genera a partir de las definiciones de service y contiene clases y funciones específicas de gRPC.

El código específico de gRPC contiene lo siguiente:

  1. RouteGuideStub, que puede usar un cliente de gRPC para invocar RPC de RouteGuide.
  2. RouteGuideServicer, que define la interfaz para las implementaciones del servicio RouteGuide.
  3. Función add_RouteGuideServicer_to_server que se usa para registrar un RouteGuideServicer en un servidor de gRPC.

5. Crea el servidor

Primero, veamos cómo crear un servidor RouteGuide. La creación y ejecución de un servidor RouteGuide se divide en dos elementos de trabajo:

  • Implementar la interfaz del servidor generada a partir de la definición del servicio con funciones que realizan el "trabajo" real del servicio
  • Ejecutar un servidor de gRPC para escuchar las solicitudes de los clientes y transmitir las respuestas

Veamos route_guide_server.py.

Implementa RouteGuide

route_guide_server.py tiene una clase RouteGuideServicer que es una subclase de la clase generada route_guide_pb2_grpc.RouteGuideServicer:

# RouteGuideServicer provides an implementation of the methods of the RouteGuide service.
class RouteGuideServicer(route_guide_pb2_grpc.RouteGuideServicer):

RouteGuideServicer implementa todos los métodos de servicio de RouteGuide.

RPC de transmisión del servidor

ListFeatures es una RPC de transmisión de respuestas que envía varios Features al cliente:

def ListFeatures(self, request, context):
    """List all features contained within the given Rectangle."""
    left = min(request.lo.longitude, request.hi.longitude)
    right = max(request.lo.longitude, request.hi.longitude)
    top = max(request.lo.latitude, request.hi.latitude)
    bottom = min(request.lo.latitude, request.hi.latitude)
    for feature in self.db:
        lat, lng = feature.location.latitude, feature.location.longitude
        if left <= lng <= right and bottom <= lat <= top:
            yield feature

Aquí, el mensaje de solicitud es un route_guide_pb2.Rectangle dentro del cual el cliente desea encontrar Features. En lugar de devolver una sola respuesta, el método genera cero o más respuestas.

RPC de transmisión del cliente

El método de transmisión de solicitudes RecordRoute usa un iterador de valores de solicitud y devuelve un solo valor de respuesta.

def RecordRoute(self, request_iterator, context):
    """Calculate statistics about the trip composed of Points."""
    point_count = 0
    feature_count = 0
    distance = 0.0
    prev_point = None

    start_time = time.time()
    for point in request_iterator:
        point_count += 1
        if get_feature(self.db, point):
            feature_count += 1
        if prev_point:
            distance += get_distance(prev_point, point)
        prev_point = point

    elapsed_time = time.time() - start_time
    return route_guide_pb2.RouteSummary(
        point_count=point_count,
        feature_count=feature_count,
        distance=int(distance),
        elapsed_time=int(elapsed_time),
    )

RPC de transmisión bidireccional

Por último, veamos nuestro RPC de transmisión bidireccional RouteChat():

def RouteChat(self, request_iterator, context):
    """
    Receive a stream of message/location pairs, and responds with
    a stream of all previous messages for the given location.
    """
    prev_notes = []
    for new_note in request_iterator:
        for prev_note in prev_notes:
            if prev_note.location == new_note.location:
                yield prev_note
        prev_notes.append(new_note)

La semántica de este método es una combinación de la del método de transmisión de solicitudes y la del método de transmisión de respuestas. Se le pasa un iterador de valores de solicitud y, en sí mismo, es un iterador de valores de respuesta.

Inicia el servidor

Una vez que implementaste todos los métodos de RouteGuide, el siguiente paso es iniciar un servidor de gRPC para que los clientes puedan usar tu servicio:

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    route_guide_pb2_grpc.add_RouteGuideServicer_to_server(
        RouteGuideServicer(),
        server,
    )
    listen_addr = "localhost:50051"
    server.add_insecure_port(listen_addr)
    print(f"Starting server on {listen_addr}")
    server.start()
    server.wait_for_termination()

El método start() del servidor no genera bloqueos. Se creará una instancia de un nuevo subproceso para controlar las solicitudes. El subproceso que llama a server.start() a menudo no tendrá ningún otro trabajo que hacer mientras tanto. En este caso, puedes llamar a server.wait_for_termination() para bloquear de forma limpia el subproceso de llamada hasta que finalice el servidor.

6. Crea el cliente

Veamos route_guide_client.py.

Cómo crear un código auxiliar

Para llamar a los métodos del servicio, primero debemos crear un stub.

Creamos una instancia de la clase RouteGuideStub del módulo route_guide_pb2_grpc, que se genera a partir de nuestro método .proto. en run():

with grpc.insecure_channel("localhost:50051") as channel:
    stub = route_guide_pb2_grpc.RouteGuideStub(channel)

Ten en cuenta que aquí channel se usa como administrador de contexto y se cerrará automáticamente una vez que el intérprete salga del bloque with.

Llama a métodos de servicio

En el caso de los métodos de RPC que devuelven una sola respuesta (métodos "response-unary"), gRPC Python admite la semántica de flujo de control síncrona (bloqueante) y asíncrona (no bloqueante). En el caso de los métodos RPC de transmisión de respuestas, las llamadas muestran de inmediato un iterador de valores de respuesta. Las llamadas al método next() de ese iterador se bloquean hasta que la respuesta que se debe generar desde el iterador esté disponible.

RPC de transmisión del servidor

Llamar a la transmisión de respuestas ListFeatures es similar a trabajar con tipos de secuencia:

def guide_list_features(stub):
    _lo = route_guide_pb2.Point(latitude=400000000, longitude=-750000000)
    _hi = route_guide_pb2.Point(latitude=420000000, longitude=-730000000)
    rectangle = route_guide_pb2.Rectangle(
        lo=_lo,
        hi=_hi,
    )
    print("Looking for features between 40, -75 and 42, -73")

    features = stub.ListFeatures(rectangle)
    for feature in features:
        print(
            f"Feature called '{feature.name}'"
            f" at {format_point(feature.location)}"
        )

RPC de transmisión del cliente

Llamar al RecordRoute de transmisión de solicitudes es similar a pasar un iterador a un método local. Al igual que la RPC simple anterior que también devuelve una sola respuesta, se puede llamar de forma síncrona:

def guide_record_route(stub):
    feature_list = route_guide_resources.read_route_guide_database()
    route_iterator = generate_route(feature_list)

    route_summary = stub.RecordRoute(route_iterator)
    print(f"Finished trip with {route_summary.point_count} points")
    print(f"Passed {route_summary.feature_count} features")
    print(f"Traveled {route_summary.distance} meters")
    print(f"It took {route_summary.elapsed_time} seconds")

RPC de transmisión bidireccional

Llamar a RouteChat con transmisión bidireccional tiene (como es el caso en el servidor) una combinación de la semántica de transmisión de solicitudes y de transmisión de respuestas.

Genera los mensajes de solicitud y envíalos uno por uno con yield.

def generate_notes():
    home = route_guide_pb2.Point(latitude=1, longitude=1)
    work = route_guide_pb2.Point(latitude=2, longitude=2)
    notes = [
        make_route_note("Departing from home", home),
        make_route_note("Arrived at work", work),
        make_route_note("Having lunch at work", work),
        make_route_note("Departing from work", work),
        make_route_note("Arrived home", home),
    ]
    for note in notes:
        print(
            f"Sending RouteNote for {format_point(note.location)}:"
            f" {note.message}"
        )
        yield note
        # Sleep to simulate moving from one point to another.
        # Only for demonstrating the order of the messages.
        time.sleep(0.1)

Recibe y procesa las respuestas del servidor:

def guide_route_chat(stub):
    responses = stub.RouteChat(generate_notes())
    for response in responses:
        print(
            "< Found previous note at"
            f" {format_point(response.location)}: {response.message}"
        )

Llama a los métodos auxiliares

En run, ejecuta los métodos que acabamos de crear y pásales el stub.

print("-------------- ListFeatures --------------")
guide_list_features(stub)
print("-------------- RecordRoute --------------")
guide_record_route(stub)
print("-------------- RouteChat --------------")
guide_route_chat(stub)

7. Probar

Ejecuta el servidor:

python route_guide_server.py

Desde otra terminal, activa el entorno virtual de nuevo (source .venv/bin/activate)) y, luego, ejecuta el cliente:

python route_guide_client.py

Veamos el resultado.

ListFeatures

Primero, encontrarás la lista de funciones. Cada característica se transmite desde el servidor (RPC de transmisión del servidor) a medida que se descubre que se encuentra dentro del rectángulo solicitado:

-------------- ListFeatures --------------
Looking for features between 40, -75 and 42, -73
Feature called 'Patriots Path, Mendham, NJ 07945, USA' at (lat=407838351, lng=-746143763)
Feature called '101 New Jersey 10, Whippany, NJ 07981, USA' at (lat=408122808, lng=-743999179)
Feature called 'U.S. 6, Shohola, PA 18458, USA' at (lat=413628156, lng=-749015468)
Feature called '5 Conners Road, Kingston, NY 12401, USA' at (lat=419999544, lng=-740371136)
...

RecordRoute

En segundo lugar, RecordRoute muestra la lista de puntos visitados de forma aleatoria que se transmiten del cliente al servidor (RPC de transmisión del cliente):

-------------- RecordRoute --------------
Visiting point (lat=410395868, lng=-744972325)
Visiting point (lat=404310607, lng=-740282632)
Visiting point (lat=403966326, lng=-748519297)
Visiting point (lat=407586880, lng=-741670168)
Visiting point (lat=406589790, lng=-743560121)
Visiting point (lat=410322033, lng=-747871659)
Visiting point (lat=415464475, lng=-747175374)
Visiting point (lat=407586880, lng=-741670168)
Visiting point (lat=402647019, lng=-747071791)
Visiting point (lat=414638017, lng=-745957854)

Después de que el cliente termine de transmitir todos los puntos visitados, recibirá una respuesta sin transmisión (una RPC unary) del servidor. Esta respuesta contendrá un resumen de los cálculos realizados en la ruta completa del cliente.

Finished trip with 10 points
Passed 10 features
Traveled 654743 meters
It took 0 seconds

RouteChat

Por último, el resultado de RouteChat demuestra la transmisión bidireccional. Cuando el cliente "visita" los puntos home o work, envía un RouteNote al servidor para registrar una nota sobre el punto. Cuando ya se visitó un punto, el servidor transmite todas las notas anteriores de ese punto.

-------------- RouteChat --------------
Sending RouteNote for (lat=1, lng=1): Departing from home
Sending RouteNote for (lat=2, lng=2): Arrived at work
Sending RouteNote for (lat=2, lng=2): Having lunch at work
< Found previous note at (lat=2, lng=2): Arrived at work
Sending RouteNote for (lat=2, lng=2): Departing from work
< Found previous note at (lat=2, lng=2): Arrived at work
< Found previous note at (lat=2, lng=2): Having lunch at work
Sending RouteNote for (lat=1, lng=1): Arrived home
< Found previous note at (lat=1, lng=1): Departing from home

8. ¿Qué sigue?