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