Conectando múltiplos clientes da forma certa 🙂

Clientes assíncronos

Já falamos sobre como utilizar os padrões request-response, pub-sub, que nos permitem criar filas com tópicos, distribuir mensagens para processamento por múltiplos workers ou simplementes conectar dois nós para execução de procedimentos.

Tudo isso funciona, é prático e nos ajuda no dia a dia, só tem um porém, todos os serviços que comentamos são síncronos.

Para exemplificar, vamos analisar o código de um server simples:

using (var server = new ResponseSocket())
{
    server.Bind("tcp://localhost:10000");

    while (true)
    {
        var msg = server.ReceiveFrameString();
        server.SendFrame($"Olá, meu querido(a) {msg}.");
    }
}

É bem simples, nosso servidor recebe um nome e responde com a mensagem "Olá, meu querido(a) {msg}.".

O código do cliente é igualmente simples:

using (var client = new RequestSocket())
{
    client.Connect("tcp://localhost:10000");
    client.SendFrame("Douglas");

    Console.WriteLine(client.ReceiveFrameString());
}

Ao executarmos, somos saudados pelo servidor com a mensagem "Olá, meu querido(a) Douglas". Mas o que acontece se adicionarmos uma segunda chamada logo após a primeira?

using (var client = new RequestSocket())
{
    client.Connect("tcp://localhost:10000");
    client.SendFrame("Douglas");
    client.SendFrame("Raquel");

    Console.WriteLine(client.ReceiveFrameString());
}

Ao executar, uma uma bela exception vai ser disparada na nossa frente. A questão é que um request sempre precisa ser seguido de um response, sempre!

Ou seja, não é possível fazer duas chamadas no servidor consecultivas com o par de sockets request-response. Mas podemos reverter esta situação com um tipo diferente de socket, chamado Dealer.

Se alterarmos o socket utilizado pelo cliente para DealerSocket podemos fazer múltiplas chamadas, veja:

using (var client = new DealerSocket())
{
    client.Connect("tcp://localhost:10000");
    client.SendFrame("Douglas");
    client.SendFrame("Raquel");

    Console.WriteLine(client.ReceiveFrameString());
}

Ao debugar, você vai perceber que a thread não é mais bloqueada e a aplicação segue seu fluxo de execução!

E esta é a grande função do socket Dealer, adicionar capacidades assíncronas as aplicações, executando procedimentos que não bloqueam a thread principal do servidor.

Roteamento de mensagens

Como já vimos, o ZeroMQ nos permite a partir de uma combinação de sockets criar diversas dinâmicas, tudo depende de como vai ser organizada a topologia da nossa aplicação.

Podemos redirecionar mensagens para outros serviços, e até mesmo criar mecanismos de balanceamento de carga.

Mas, se temos um serviço online sendo acessado por diversos clientes externos, como saber qual request responder para cada cliente?

Em cenários reais, devemos sempre manter a preocupação do contexto da requisição, e quando começamos a trocar mensagens em múltiplos serviços cria-se a necessidade de manter uma rastreabilidade, com o objetivo de responder corretamente, auditar e atribuir certos comportamentos.

Para essa finalidade, temos o últimos tipo de socket ainda não apresentado, o RouterSocket!

Segundo a documentação do ZeroMQ:

The ROUTER socket, unlike other sockets, tracks every connection it has, and tells the caller about these. The way it tells the caller is to stick the connection identity in front of each message received. An identity, sometimes called an address, is just a binary string with no meaning except "this is a unique handle to the connection". Then, when you send a message via a ROUTER socket, you first send an identity frame.

Ou seja, podemos utilizar o router como ferramenta para identificar as requisições e rotear corretamente as mensagens para os clientes.

Cada vez que o router recebe uma mensagem, ele passa a gerenciar o endereço do cliente que enviou para poder enviar o response, isso é controlado automaticamente pelo ZMQ que basicamente mantem
um dicionário de endereços.

Vamos ver o uso do RouterSocket reescrevendo o exemplo anterior.

Server

using (var server = new RouterSocket())
{
    server.Options.Identity = Encoding.UTF8.GetBytes(Guid.NewGuid().ToString());
    server.Options.RouterMandatory = true;
    server.Bind("tcp://localhost:10000");

    while (true)
    {
        var msg = server.ReceiveMultipartMessage();

        var messageToClient = new NetMQMessage();
        messageToClient.Append(msg[0]);
        messageToClient.AppendEmptyFrame();
        messageToClient.Append($"Resposta da mensagem: {msg[3].ConvertToString()}");

        server.SendMultipartMessage(messageToClient);
    }
}

Começamos instanciando o RouterSocket, e na sequencia atribuimos uma identificação a ele com a propriedade Identity, neste caso estamos criando uma identity para o próprio RouterSocket.

A pripriedade RouterMandatory é responsável pelo comportamento a ser tomado quando não é possível rotear uma mensagem, marcando como "true" nós informamos que caso a mensagem não seja roteável, a aplicação precisa disparar uma exception do tipo HostUnreachableException.

Client

using (var client = new DealerSocket())
{
    client.Connect("tcp://localhost:10000");

    var messageToRouter = new NetMQMessage();
    messageToRouter.Append(Guid.NewGuid().ToString());
    messageToRouter.AppendEmptyFrame();
    messageToRouter.Append("Douglas");

    client.SendMultipartMessage(messageToRouter);

    var messageFromRouter = client.ReceiveMultipartMessage();

    Console.WriteLine(messageFromRouter[1].ConvertToString());
}

Nosso cliente se comporta da mesma forma como no exemplo anterior, única diferença é o uso do NetMQMessage para criar a mensagem a ser enviada.
Utilizar o NetMQMessage nos permite ter um pequeno ganho de performance pelo uso da memória do netmq, e também nos permite criar contrados para criação de objetos de mensagem.

Existem ainda diversas outras combinações de sockets que podemos utilizar, seguem alguns exemplos retirados diretamente do guia do ZeroMQ:

  • REQ to REP
  • DEALER to REP
  • REQ to ROUTER
  • DEALER to ROUTER
  • DEALER to DEALER
  • ROUTER to ROUTER

Podemos criar diversas combinações, mais ou menos complexas de acordo com as necessidades do software.

Com isso, temos exemplificados todos os padrões de comunicação do ZeroMQ, nós próximos artigos vamos explorar o uso do ZeroMQ em aplicações reais, por exemplo, podemos abolir totalmente o uso de locks e Monitor para gerenciar concorrência em ambientes multi-threads, podemos inclusive criar bibliotecas de notificação de eventos utilizando apenas a lib do zmq.

Um mundo totalmente novo para nós! Até a próxima 😉

#### Créditos
Abertura: Photo by John Barkiple on Unsplash

Tags: | | | | | | | | | | | |

Sobre o Autor

Douglas Ramalho
Douglas Ramalho

Pesquisador apaixonado por filmes, música e bits, muitos bits. Sou um profissional viciado em tecnologia, com 11 anos de experiência em TI e um caminho repleto de suor, sangue e bugs. Gosto não apenas de estudar, mas também de promover o conhecimento, dessa forma meu objetivo é compartilhar toda a minha experiência para que possamos evoluir juntos, e também ajudar outras pessoas neste processo, para que em um grande grupo de apaixonados possamos elevar o nível de excelência a um patamar ainda maior.

0 Comentários

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Solicitar exportação de dados

Utilize este formulário para solicitar uma cópia dos seus dados neste site.

Solicitar remoção de dados

Utilize este formulário para solicitar a remoção dos seus dados neste site.