Objetivo
Nesta parte veremos mais uma abordagem para transferência de informações que é através de um request HTTP.
Setup
O servidor para nosso teste foi implementado como uma aplicação Web (IntegrationTests.WebApp), num handler http genérico. Para utilizá-lo, precisamos configurar nosso IIS. Para isso, seguiremos os passos:
- Abrir a console do IIS (inetmgr)
- Adicionar novo aplicativo, nome “integrationtests2”, Application Pool DefaultAppPool (v. 4.0, Integrated)
- Caminho físico: apontar para o diretório IntegrationTests\IntegrationTests.WebApp
- Configurar a connection string do banco de dados, no arquivo web.config existente no diretório IntegrationTests\IntegrationTests.WebApp
Código do teste
O teste segue a seguinte abordagem:
- O cliente executa um request http no servidor, baseado na stream do request, escreve o xml com a requisição para o servidor
- O servidor, recebe o request, enquanto vai escrevendo a resposta, vai dando “Flushes” no request, de forma que a stream de resposta é devolvida em pedaços para o cliente
- O cliente, vem lendo a stream de resposta enquanto o servidor ainda está processando o request.
Segue o fonte do servidor, escrito como um generic handler (.ashx) numa aplicação web padrão:
public class GenericHandler : IHttpHandler { public void ProcessRequest(HttpContext context) { if (String.IsNullOrEmpty(connString)) GetConnString(); if (context.Request.Headers["Flush"] == "true") flush = true; context.Response.ContentType = "text/xml"; this.context = context; StreamUtil u = new StreamUtil(); u.ProcessClientBigRequest(connString, context.Request.InputStream, context.Response.OutputStream, true, Flush); context.Response.Flush(); context.Response.End(); } private void Flush() { if (flush) context.Response.Flush(); }
Mais uma implementação simplista. A idéia aqui é obter a connection string, posteriormente, ler o header pra saber se os flushs parciais serão dados ou não. O método Flush, é dado como um callback para ProcessClientBigRequest. No meio do loop dele, ele executa o callback que vem dando Response.Flush, para cada pedacinho de xml tratado.
O código do cliente (task MSBuild) segue:
public override bool Execute() { Stopwatch watch = new Stopwatch(); watch.Start(); HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(new Uri(Uri)); request.Method = "POST"; request.ContentType = "text/xml"; request.BeginGetRequestStream(GetRequestStreamCallback, request); while (!finished) Thread.Sleep(250); watch.Stop(); Log.LogMessage("Total processing time: " + watch.Elapsed.TotalSeconds.ToString("0.00") + " seconds"); return true; } private void GetRequestStreamCallback(IAsyncResult result) { HttpWebRequest request = (HttpWebRequest)result.AsyncState; if (Flush) request.Headers.Add("Flush:true"); Stream requestStream = request.EndGetRequestStream(result); Log.LogMessage("Writing request with " + BigRequestSize.ToString() + " itens. Flush " + Flush.ToString()); StreamUtil util = new StreamUtil(); util.GenerateBigRequest(requestStream, true, BigRequestSize); request.BeginGetResponse(GetResponseCallback, request); } private void GetResponseCallback(IAsyncResult result) { HttpWebRequest request = (HttpWebRequest)result.AsyncState; HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(result); Stream responseStream = response.GetResponseStream(); Log.LogMessage("Reading response"); _util = new StreamUtil(); _util.ImportarStream(ConnString, responseStream); finished = true; }
O método Execute dispara o request (usando BeginGetRequestStream) e fica aguardando o retorno. O callback do BeginGetRequestStream é o método GetRequestStreamCallback. Este método adiciona os headers, e escreve na stream de request o xml com o request, item a item. Em seguida, executa o método BeginGetResponse, que por sua vez chama o callback GetResponseCallback.
O método GetResponseCallback, interpreta a stream contida na resposta, inserindo os resultados na base.
Conclusão
A idéia inicial que eu tinha ao construir este teste era vir devolvendo o response enquanto recebia o request. Percebi que por uma característica do protocolo http, isso é impossível. Não é possível começar a escrever a resposta enquanto o request ainda está sendo lido. Isso faz com que ocorra uma bufferização do request do lado do servidor, ou seja, como ele precisa esperar chegar todo o request antes de começar a gerar a resposta, ele vai alocando memória para esta stream (no lado do servidor), até que comece a paginação. A partir do momento que ele começa a liberar a resposta, esta memória vai sendo liberada.
Apesar de muito eficiente, ao executarmos este método com um request grande, percebemos um consumo alto de memória. É um método relativamente simples de transferir informações, tem esse ponto de atenção da memória e mesmo assim, não aproveita muito bem os recursos das duas máquinas, visto que acaba serializando o processamento (precisa esperar todo o request para começar a gerar a resposta). O único ponto que ele torna mais eficiente é que à medida que a resposta vai sendo escrita, o cliente já vai processando ela.
Código fonte da solução
O código fonte está disponível no git hub: https://github.com/ericlemes/IntegrationTests.