Internamente o .NET Framework possui uma classe chamada ServicePoint que controla as conexões HTTP que são realizadas. A sua criação é feita e gerenciada por uma outra classe, chamada de ServicePointManager, e ambas estão debaixo do namespace System.Net. A criação não se dá de forma desordenada, ou seja, ela é criada para atender um determinado recurso (página, serviço, imagem, etc.), e ao invés de desprezá-la, ela é armazenada e compartilhada para quando precisarmos, novamente, acessar aquele mesmo servidor.
Para falarmos de classes de mais alto nível, um exemplo é a classe HttpWebRequest que utilizamos para realizar requisições HTTP, que recorre internamente à classe ServicePointManager, e que por sua vez, faz todas verificações necessárias, e devolve um objeto ServicePoint pronto para ser utilizado. Como há uma única instância desta classe com aquele servidor, podemos ter a necessidade de realizar conexões simultâneas para ele. Por questões de performance, a especificação do protocolo HTTP determina que um cliente não deve manter mais do que duas conexões com um mesmo servidor.
Apesar desta sugestão, podemos ter a necessidade de aumentar esse valor para que possamos executar mais requisições concorrentes a um mesmo servidor. Pensando nisso, a Microsoft disponibiliza um configuração global, qual nos permite determinar a quantidade de conexões concorrentes que podemos realizar à um mesmo servidor. Para isso, há várias propriedades estáticas, que estão expostas a partir da classe ServicePointManager, e a propriedade que controla a quantidade de conexões é a DefaultConnectionLimit, onde o padrão é apenas 2 conexões, assim como sugere o HTTP/1.1.
Quando consumimos serviços WCF ou até mesmo serviços do tipo ASMX, não lidamos – diretamente – com as classes que vimos aqui, mas se analisarmos os bastidores das classes do WCF (ClientBase<TChannel>) e do ASMX (SoapHttpClientProtocol), veremos que eles, em algum momento, vai recorrer à classe HttpWebRequest e, consequentemente, estaremos sendo limitados caso haja a necessidade de realizar mais do que duas conexões concorrentes ao mesmo servidor.
Supondo que temos um serviço WCF e queremos consumí-lo em uma aplicação cliente, e para efeito dos testes, vamos criar 100 requisições concorrentes para o mesmo. Um pequeno detalhe, é que já definimos a quantidade mínima de threads para que o ThreadPool já as crie, nos antecipando e dizendo a ele quantas threads serão necessárias para executarmos o trabalho.
const int QtdeDeRequisicoes = 100;
ServicePointManager.DefaultConnectionLimit = 2;
ThreadPool.SetMinThreads(QtdeDeRequisicoes, QtdeDeRequisicoes);
var tasks = new Task[QtdeDeRequisicoes];
var sw = Stopwatch.StartNew();
for (int i = 0; i < QtdeDeRequisicoes; i++)
{
tasks[i] =
Task.Factory.StartNew<string>(o =>
{
Console.WriteLine(“Inicio:t” + o);
using (var proxy = new ServiceReference1.ServiceClient())
return proxy.GetData((int)o);
}, i)
.ContinueWith(t => Console.WriteLine(“Fim:t” + t.Result));
}
Task.WaitAll(tasks);
Console.WriteLine(sw.Elapsed);
Note que no código acima estamos explicitamente definindo que a aplicação cliente pode somente estabelecer duas conexões concorrentes com um mesmo servidor. Para o serviço de teste, ele levará, na minha máquina, cerca de 48 segundos. O gráfico abaixo ilustra a execução destas requisições. Enquanto a linha vermelha mostra a quantidade de requisições que chegaram até o serviço, a linha verde exibe a quantidade de requisições que estão sendo executadas por ele. Tudo isso sendo extraído do servidor onde o mesmo está hospedado.
Se mudarmos ligeiramente o código acima, definindo para 15 a propriedade DefaultConnectionLimit, o tempo para executar as mesmas 100 requisições cai para 20 segundos, ou seja, temos um ganho considerável. Novamente, através do gráfico abaixo temos as mesmas requisições em um tempo bem mais reduzido:
Observação: Apesar de facilitar a vida do cliente, esta configuração poderá comprometer o desempenho do serviço/servidor, pois estamos atendendo várias requisições que partem de um mesmo cliente, exacerbando o serviço com requisições que poderiam estar distribuídas entre outros interessados em consumir este mesmo serviço.
É importante mencionar que o cliente criado para consumir o serviço WCF de exemplo, é uma aplicação Windows (Console). Quando a aplicação cliente é uma aplicação Web (ASP.NET), também podemos ser influenciado por esta configuração, mas neste caso, ele está dimensionado de uma forma diferente, ou seja, em sua configuração padrão, no interior da classe HttpRuntime, ele define a propriedade DefaultConnectionLimit como 12 * quantidade de CPUs da máquina atual. Sendo assim, qualquer conexão HTTP que você realize através desta aplicação Web, você estará “limitado” à esta quantidade de conexões.
Finalmente, para efeito de testes, você não pode ter o cliente e o serviço na mesma máquina, pois a classe ServicePointManager avalia se o serviço está hospedado localmente, e se estiver, retorna a constante Int32.MaxValue (2147483647) como quantidade de conexões simultâneas, sendo um valor que dificilmente será atingido.
Olá Israel, muito bom vê-lo postando novamente!
Só uma coisa, quando você escreve "a linha verde exibe a quantidade de requisições que estão pendentes de execução", você quer dizer *requisições em execução* não?
Ou seja, mostra as conexões que estão sendo servidas naquele momento, e não as conexões enfileiradas aguardando execução.
Boas Paulo,
Desculpe-me, mas eu me confundi quando escrevi o artigo. Já alterei. Obrigado pela dica.