Como parte do projeto SegmentedEncoder, é necessário um servidor que suporte HTTP2, para o gRPC, e capaz de oferecer um servidor para a interface web e para a API REST.

Para isso será utilizado tower para criar um serviço que diferencie as requisições gRPC, e as encaminhe para o serviço apropriado, enquanto as outras são servidas pelo outro.

Como muitos serviços web são baseados no hyper, seria interessante criar um serviço compatível.

Primeiro, o servidor hyper utiliza um serviço chamado MakeService, isso é um serviço que cria outro serviço. Nesse caso, ele recebe uma referência para AddrStream, que possui os endereços do cliente, e servidor, e também, a conexão TCP. Resumindo, hyper recebe um MakeService, que implementa Service<&'a AddrStream> (a referência é anotada).

Esse serviço cria outro serviço para cada conexão, e esse serviço é utilizado para responder às requisições, ele implementa Service<Request<Body>>. Esse serviço tem como resposta o tipo Response<HttpBody>. Onde HttpBody é implementado por Body, esse trait permite que a resposta seja um tipo diferente de Body.
Você pode conferir a documentação do módulo service no hyper, e um exemplo com a implementação de um serviço.

Como estamos utilizando dois serviços diferentes, precisamos garantir que eles implementem como resposta HttpBody, e precisamos retornar um objeto que implemente HttpBody e utilize os métodos dos serviços que estamos roteando.

Inicialmente há duas formas de utilizar o módulo Multiplexer, com dois MakeService para criar um MakeMultiplexer, ou com dois Services para criar um Multiplexer.
Para obter um MakeService a partir do tonic, podemos usar tower::make::Shared::new(), que pode gerar um, a partir de um serviço do tonic.
Posteriormente, podemos criar um Router para utilizar com os serviços do tonic, utilizando axum para rotear para os serviços gRPC.

Implementação

Foi criado uma struct para armazenar os dois Services. E foi implementado Service<Request<Body>> nessa struct. Ao implementar Service, poll_ready foi bem simples, se realizar o poll_ready nos serviços internos, retornamos Ready(Ok(())) se ambos forem Ready(Ok(())), se um deles for um erro, retornamos o erro. Para isso é preciso garantir que o erro dos dois serviços implementem Into<> para o mesmo tipo de erro de Multiplexer.

Porém, o servidor do hyper tem como restrição que o valor retornado de call seja Response<B>, e tem como restrição para B:

where
B: HttpBody + 'static,
B::Error: Into<Box<dyn StdError + Send + Sync> >,

Como os serviços podem retornar dois tipos diferentes de HttpBody, precisamos transformá-los num único tipo que consiga adaptar os dois. Logo, como o valor retornado de call() de cada serviço é depende do tipo Output do trait Future. Também é preciso adaptar.

Assim, foi criado um enum, EncapsulatedFuture, que implementa Future para retornar os errors transformados em BoxedError, e retornar as respostas dos dois serviços. Para isso foi criado outro enum, EncapsulatedBody. Esse enum apenas chama os métodos de HttpBody no valor contido. Ainda existem alguns métodos que podem ser implementados, porém, ficarão para uma próxima vez.

Para implementar Future, foi utilizado outro pacote, pin_project, pois é utilizado Pin<self> no lugar de self. Pin<> serve para garantir que um objeto ou variável mantenha os seus dados na mesma localização na memória, mas tem algumas restrições no acesso aos campos do objeto. Confira a documentação do módulo ‘pin’.

Usando esses dois enums, podemos implementar call(). Essa função verifica o header Content-Type do request, e caso comece com application/grpc, é repassado ao serviço grpc, se não, para web.

E com isso já temos o nosso Multiplexer funcionando, faltando apenas um MakeService para utilizarmos com o hyper. Para isso, MakeMultiplexer foi criado. Isso é um Service que cria um Multiplexer. Para isso, MakeMultiplexer utiliza dois MakeServices, um para o gRPC, e outro para web.
Esse serviço é similar ao anterior, porem mais simples, pois a resposta é apenas uma instância de Multiplexer.

Esse Service<T> possui um tipo genérico como request, e o seu valor é simplesmente passado para os serviços internos.

Agora temos um pacote funcional, disponível no meu github.

Agora devo focar no projeto SegmentedEncoder, mas inda existem algumas mudanças que planejo fazer:

  • Melhorar os requisitos dos tipos
  • Adicionar mais exemplos
  • Melhorar a documentação