quarta-feira, 12 de dezembro de 2012

MSMQ: Request/Response

Dando continuidade à série de tutoriais básicos sobre como usar o MSMQ com C# para integrar sistemas, neste post vou mostrar o padrão Request/Response ou Request/Reply. É um padrão muito básico e essencial numa arquitetura de integrações usando sistemas de mensagem.


Pré-Requisitos

Assim como no post anterior, você vai precisar ter o MSMQ habilitado e usar o Visual Studio (ou um compilador para .NET).

Prática

Novamente, assim como no tutorial anterior, vamos criar um modelo canônico que será compartilhado pelas aplicações participantes. Crie um projeto do tipo class library com o nome de Eip.Messages em C#. Declare as seguintes interfaces:

public interface IRequestMessage
{
 string Text { get; set; }
}

e
public interface IResponseMessage
{
 string Text { get; set; }
}

Implemente-as como à seguir:
[Serializable]
public class RequestMessageImpl
 : IRequestMessage
{
 public RequestMessageImpl(string text)
 {
  Text = text;
 }
 public string Text { get; set; }
}
e
[Serializable]
public class ResponseMessageImpl
 : IResponseMessage
{
 public ResponseMessageImpl(string text)
 {
  Text = text;
 }
 public string Text { get; set; }
}

Agora, vamos criar o projeto que faz a requisição. Adicione à solução um novo projeto do tipo Console em C# com o nome Eip.Request. Adicione também referências ao projeto Eip.Messages e a System.Messaging. Depois disso, cole o código seguinte dentro do método static void Main(string[] args).

Console.Title = "Request Endpoint";
Console.WindowHeight = 10;
Console.WindowWidth = 50;

// Instancia a fila da requisição (request)
MessageQueue requestQueue = new MessageQueue(@".\private$\request_queue");

// Instancia a fila da resposta (response)
MessageQueue responseQueue = new MessageQueue(@".\private$\response_queue");

// Configura o que será lido das mensagens.
// Para simplificar o exemplo, usamos o SetAll()
MessagePropertyFilter filter = new MessagePropertyFilter();
filter.SetAll();
requestQueue.MessageReadPropertyFilter = filter;
responseQueue.MessageReadPropertyFilter = filter;

// Define qual formatador a fila está usando
responseQueue.Formatter = new BinaryMessageFormatter();

while (true)
{
 Console.WriteLine("Entre com a requisição:");
 string inputRequest = Console.ReadLine();

 if (inputRequest == "sair")
  break;

 // Cria o objeto requisição com a entrada do usuário
 IRequestMessage request = new RequestMessageImpl(inputRequest);

 // Instancia a mensagem que encapsula nossa requisição e define como ela será formatada 
 // para entrar na fila
 Message requestMessage = new Message(request, new BinaryMessageFormatter());

 // Atribui a propriedade de fila de resposta que é onde vamos receber a resposta
 requestMessage.ResponseQueue = responseQueue;
 
 // Envia a requisição
 requestQueue.Send(requestMessage);
 
 // Armazena o Id de correlação
 string correlationId = requestMessage.Id;

 // Bloqueia até receber a mensagem de retorno ou expirar o timeout (5 segundos: new TimeSpan(0, 0, 5))
 // Note que estamos aguardando uma resposta com o Id que relacionamos no passo anteior
 Message responseMessage = responseQueue.ReceiveByCorrelationId(correlationId, new TimeSpan(0, 0, 5));

 // Recupera o objeto resposta
 IResponseMessage responseObject = (IResponseMessage)responseMessage.Body;
 
 Console.WriteLine(responseObject.Text);

 // E a vida segue...
}


Precisamos criar as duas filas que são referenciadas no código acima: .\private$\request_queue.\private$\response_queue. Crie-as como sugerido no tutorial anterior, caso contrário, a aplicação não vai funcionar.

Inicie a aplicação e entre com uma requisição. Uma exceção será lançada após 5 segundos sem a resposta; vamos consertar isso.



Após as filas terem sido criadas, crie um novo projeto também do tipo Console com o nome Eip.Response para adicioná-lo à solução. Adicione referências ao projeto Eip.Messages e a System.Messaging. Depois disso, cole o código seguinte dentro do método static void Main(string[] args).

Console.Title = "Response Endpoint";
Console.WindowHeight = 10;
Console.WindowWidth = 70;

// Instancia a fila da requisição (request).
// Esta fila deve ser a mesma do lado da requisição -- os dois endpoints (request/response) precisam entrar num acordo aqui
MessageQueue requestQueue = new MessageQueue(@".\private$\request_queue");

// Configura o que será lido das mensagens.
// Para simplificar o exemplo, usamos o SetAll()
MessagePropertyFilter filter = new MessagePropertyFilter();
filter.SetAll();
requestQueue.MessageReadPropertyFilter = filter;

// Temos que trabalhar com algum formato, escolhemos o binário por ser o mais rápido
// É muito comum usar o XmlMessageFormatter por ser legível.
requestQueue.Formatter = new BinaryMessageFormatter();

Console.WriteLine("Aguardando requisições...");

while (true)
{
 // Bloqueia até receber a requisição
 Message requestMessage = requestQueue.Receive();

 // Recupera o objeto requisição
 IRequestMessage requestObject = (IRequestMessage)requestMessage.Body;

 // Processa a requisição e gera um objeto de resposta
 IResponseMessage responseObject = new ResponseMessageImpl(requestObject.Text.ToUpper());

 Console.WriteLine("Requisição recebida: '{0}'", requestObject.Text);

 // Encapsula o objeto resposta dentro de uma mensagem
 Message responseMessage = new Message(responseObject, new BinaryMessageFormatter());
 
 // Passo fundamental! Correlaciona os objetos de resposta e requisição.
 // Sem isso, o outro lado nunca vai receber a resposta
 responseMessage.CorrelationId = requestMessage.Id;

 // Quem define a fila da resposta é quem faz a requisição
 // A responsta poderia ir para qualquer lugar, inclusive um terceito endpoint
 MessageQueue responseQueue = requestMessage.ResponseQueue;

 // Envia a mensagem de resposta (sério?!)
 responseQueue.Send(responseMessage);

 Console.WriteLine("Responsta enviada: '{0}'", responseObject.Text);
}

Inicie as duas aplicações console e veja as requisições sendo respondidas. As respostas nada mais são do que o retorno da string da requisição em maiúsculas.



Pronto! Nossa solução com suporte a requisição e resposta está terminada. Use sua criatividade e faça experimentos com ela.

Balanceamento de Carga Nativo

Uma das vantagens de uma arquitetura baseada em eventos com sistemas de mensagem é a facilidade para o escalonamento. Se você iniciar duas instâncias da aplicação Eip.Receive, vai notar que o consumo das mensagens será dividido por igual em termos de quantidade. Isso é chamado de Round Robin -- cada um consume uma mensagem da fila em modo concorrente e comportado (sem furar a fila).

Conclusão

Note que além do balanceamento de carga nativo, poderíamos serializar as mensagens em algum formato mais facilmente interoperável (XML ou JSON) para que outras aplicações em diferentes plataformas também pudessem participar dessa solução. Reflita também na riqueza de se trocar objetos como mensagens e na possibilidade de definirmos o endereço de resposta dinamicamente. Essas são apenas algumas vantagens desse tipo de arquitetura para integração entre sistemas. É claro que, como qualquer coisa na vida, há desavantagens e vantagens nessa abordagem -- ela não é a solução ideal para todas as integrações.

Nenhum comentário:

Postar um comentário