Streaming Bidireccional con gRPC: Implementación y Pruebas

Clase 17 de 22Curso de Go Avanzado: Protobuffers y gRPC

Resumen

¿Cómo implementar un streaming bidireccional en gRPC?

En el desarrollo de aplicaciones modernas, la transmisión eficiente de datos entre el cliente y el servidor es crucial. El protocolo gRPC ofrece técnicas avanzadas para lograr una comunicación fluida, permitiendo incluso el streaming bidireccional. En esta sección, exploraremos cómo implementar esta técnica combinando tanto el streaming del cliente como del servidor, logrando una interacción simultánea entre ambas partes.

¿Qué es un streaming bidireccional?

Un streaming bidireccional en gRPC permite que el cliente y el servidor envíen y reciban flujos de datos de manera simultánea y continua. Esto es especialmente útil para aplicaciones que requieren una comunicación en tiempo real y de baja latencia, como en el caso de un examen en línea donde el cliente envía respuestas mientras el servidor genera las preguntas.

¿Cómo se define un RPC para streaming bidireccional?

Para implementar un RPC que soporte streaming bidireccional, comenzamos definiéndolo en el archivo proto:

rpc takeTest (stream TakeTestRequest) returns (stream Question) {}

Aquí, takeTest recibe un flujo de solicitudes TakeTestRequest y devuelve un flujo de preguntas Question, indicando que ambos procesos ocurrirán a través de data streaming.

¿Cómo agregar las peticiones y respuestas necesarias?

Debemos definir mensajes en el archivo proto:

message TakeTestRequest {
  string answer = 1;
}

message Question {
  string id = 1;
  string question = 2;
}

Luego, adaptamos la lógica en el código base para manejar estos nuevos tipos de mensajes y procurar que el servidor y cliente puedan intercambiar esta información.

¿Cómo estructurar el repositorio para preguntas del test?

Es fundamental agrupar las preguntas del test desde la base de datos con una función dentro del repositorio:

func (repo *PostgresRepository) GetQuestionsPerTest(ctx context.Context, testId string) ([]models.Question, error) {
    rows, err := repo.db.QueryContext(ctx,
        "SELECT id, question FROM questions WHERE test_id = $1", testId)
    // Manejo de error y cierre de filas
    // Crear y devolver la lista de preguntas
}

La función consulta todas las preguntas asociadas a un test específico, permitiendo futuras consultas eficaces durante la ejecución del streaming.

¿Cómo implementar la lógica del servidor para el RPC?

La lógica del servidor en Go, define cómo el servidor recibe y envía datos durante el streaming:

func (s *testServer) TakeTest(stream testpb.TestService_TakeTestServer) error {
    questions, err := s.repo.GetQuestionsPerTest(context.Background(), "t1")
    if err != nil {
        return err
    }

    var i int
    for {
        if i < len(questions) {
            currentQuestion := questions[i]
            questionToSend := testpb.Question{
                Id:       currentQuestion.Id,
                Question: currentQuestion.Question,
            }
            if err := stream.Send(&questionToSend); err != nil {
                return err
            }
            i++
        }
        
        answer, err := stream.Recv()
        if err == io.EOF {
            return nil
        }
        if err != nil {
            return err
        }
        
        log.Println("Received answer:", answer.GetAnswer())
    }
}

Este método permite recibir respuestas del cliente y enviar preguntas, verificando y controlando errores durante el proceso.

¿Qué destacar en esta implementación?

  • Streaming Unificado: Utilizar un solo stream para enviar y recibir datos, maximizando la eficiencia del ancho de banda.
  • Código Limpio: Separar la lógica de negocio y la capa de acceso a datos para mantener un código organizado y modular.
  • Pruebas y Seguridad: Probar exhaustivamente y manejar errores aseguran que la aplicación responde adecuadamente ante fallos.

Este enfoque proporciona una comunicación rápida y eficiente en aplicaciones modernas que requieren transmisiones de datos complejas, manteniendo una sincronización perfecta entre las partes involucradas.