Tüm istekler için yeni bir tek HttpClient örneği oluşturmalı mıyız?


57

Son zamanlarda, buHttpClient şekilde kullanmakla ilgili konular hakkında konuşan asp.net canavarlarından bu blog yazısına rastladım :

using(var client = new HttpClient())
{
}

Blog postasına göre, HttpClienther isteğin ardından elden çıkarırsak TCP bağlantılarını açık tutabilir. Bu potansiyel olarak yol açabilir System.Net.Sockets.SocketException.

Gönderi başına doğru yol HttpClient, soket israfını azaltmaya yardımcı olduğu için tek bir örnek oluşturmaktır .

Gönderiden:

Tek bir HttpClient örneği paylaşırsak, yeniden kullanarak soket israfını azaltabiliriz:

namespace ConsoleApplication
{
    public class Program
    {
        private static HttpClient Client = new HttpClient();
        public static void Main(string[] args)
        {
            Console.WriteLine("Starting connections");
            for(int i = 0; i<10; i++)
            {
                var result = Client.GetAsync("http://aspnetmonsters.com").Result;
                Console.WriteLine(result.StatusCode);
            }
            Console.WriteLine("Connections done");
            Console.ReadLine();
        }
    }
}

Kullandıktan HttpClientsonra, onu kullanmanın en iyi yolu olduğunu düşündüğümden sonra her zaman nesneyi imha ettim. Ancak bu blog yazısı şimdi beni bu kadar yanlış yaptığımı hissettiriyor.

HttpClientTüm istekler için yeni bir örnek oluşturmalı mıyız ? Statik örnek kullanmanın herhangi bir tuzağı var mı?


Kullandığınız şekilde atfedilen herhangi bir sorunla karşılaştınız mı?
whatsisname

Belki bu cevabı ve ayrıca bunu kontrol edin .
John Wu

@whatsisname hayır Bilmiyorum ama bloga bakıyorum, her zaman bu yanlışı kullandığımı hissediyorum. Dolayısıyla, her iki yaklaşımda da herhangi bir sorun görürlerse, diğer geliştiricilerden anlamak istedim.
Ankit Vijay

3
Kendim denemedim (bunu bir cevap olarak vermedim
Joeri Sebrechts

(Cevabımda belirtildiği gibi, sadece daha görünür hale getirmek istedim, bu yüzden kısa bir yorum yazıyorum.) Statik bir örnek tcp bağlantısının kapanış anlaşmasını düzgün bir şekilde halledecektir, bir kez yaptığınızda Close()veya yeni başlattığınızda Get(). Müşteriyi işiniz bittiğinde elden çıkarırsanız, bu kapanış anlaşmasını kaldıracak kimse olmayacak ve bu nedenle bağlantı noktalarınızın hepsinde TIME_WAIT durumu olacak.
Mladen B.

Yanıtlar:


39

Zorlayıcı bir blog yazısı gibi görünüyor. Ancak, bir karar vermeden önce, ilk olarak blog yazarının yaptığı testlerin aynısını yapardım ama kendi kodunuzla. Ayrıca HttpClient ve davranışı hakkında biraz daha fazla bilgi edinmeye çalışırdım.

Bu gönderi şöyle diyor:

HttpClient örneği, bu örnek tarafından yürütülen tüm isteklere uygulanan bir ayarlar topluluğudur. Ek olarak, her HttpClient örneği, isteklerini diğer HttpClient örnekleri tarafından yürütülen isteklerden izole ederek kendi bağlantı havuzunu kullanır.

Öyleyse, bir HttpClient paylaşıldığı zaman muhtemelen olan, bağlantıların tekrar kullanılmasıdır, bu da kalıcı bağlantı gerektirmiyorsa sorun değil. Durumunuz için bunun önemli olup olmadığından emin olmanın tek yolu kendi performans testlerinizi yapmaktır.

Kazarsanız, bu konuyu ele alan başka kaynakları da bulacaksınız (Microsoft Best Practices makalesi dahil), bu nedenle yine de uygulamak (bazı önlemlerle birlikte) iyi bir fikirdir.

Referanslar

Httpclient'i Yanlış Kullanıyorsunuz ve Yazılımınızı Kararsızlaştırıyor
Singleton HttpClient? Bu ciddi davranışın ve nasıl çözüleceğine dikkat edin
Microsoft Desenler ve Uygulamalar - Performans Optimizasyonu: Yanlış Örnekleme
Kod İncelemesinde tek bir yeniden kullanılabilir HttpClient örneği
Singleton HttpClient, DNS değişikliklerine saygı göstermiyor (CoreFX)
HttpClient kullanmak için genel öneri


1
Bu iyi bir kapsamlı liste. Bu benim hafta sonu okumam.
Ankit Vijay

"Kazarsanız, bu konuyu ele alan başka kaynakları da bulacaksınız ..."
Ankit Vijay

Kısa cevap: Statik bir HttpClient kullanın . DNS değişikliklerini (web sunucunuzdan veya diğer sunucularınızdan) desteklemeniz gerekiyorsa, zaman aşımı ayarları konusunda endişelenmeniz gerekir.
Jess,

3
HttpClient'in ne kadar berbat olduğunun bir kanıtı, onu kullanmanın @AnkitVijay tarafından yorumlandığı gibi bir "hafta sonu okuması" olduğudur.
usr

@ DNS değişikliklerinin yanı sıra - tüm müşterinizin trafiğini tek bir soketten atmak da yük dengelemesini engeller mi?
Iain

16

Partiye geç kaldım, ama işte bu zor konuyla ilgili öğrenme yolculuğum.

1. HttpClient'i tekrar kullanmakla ilgili resmi savunucuyu nerede bulabiliriz?

Demek istediğim, eğer HttpClient'i yeniden kullanmak amaçlanıyorsa ve bunu yapmak önemliyse , bu tür bir savunucu birçok "Gelişmiş Konular", "Performans (anti) modeli" veya diğer blog yayınlarında saklanmak yerine, kendi API dokümantasyonunda daha iyi belgelenmiştir. . Aksi halde, yeni bir öğrencinin çok geç olmadan nasıl bilmesi gerekiyor?

Şu andan itibaren (Mayıs 2018), "c # httpclient" googlingi yapıldığında ilk arama sonucu , bu niyetten hiç bahsetmeyen MSDN'deki bu API referans sayfasına işaret ediyor . Yeni başlayanlar için buradaki 1. derste, MSDN yardım sayfası başlığından hemen sonra "Diğer Sürümler" bağlantısını tıklayın. Muhtemelen orada "mevcut sürümün" bağlantılarını bulacaksınız. Bu HttpClient durumunda, sizi bu niyet tanımını içeren en son belgeye götürecektir .

Bu konuda yeni olan birçok geliştiricinin de doğru dokümantasyon sayfasını bulamadığından şüpheliyim, bu bilgi yaygın bir şekilde yayılmıyor ve insanlar daha sonra , muhtemelen zor bir şekilde bulduğunda şaşırdı .

2. (yanlış?) Anlayışı using IDisposable

Bu seferki biraz konu kapalı ama nasıl suçlayarak bu anılan blog yayınlarında insanları görmek için bir tesadüf değil, işaret hala değer HttpClient'ın IDisposableonları kullanma eğiliminde yapar arayüz using (var client = new HttpClient()) {...}desen ve ardından soruna neden olmaktadır.

Bunun söylenmemiş (yanlış?) Bir anlayışa dayandığına inanıyorum: “yeniden kullanılabilir bir nesnenin kısa ömürlü olması bekleniyor” .

NASIL, bu tarzda kod yazarken kesinlikle kısa süreli bir şeye benziyor olsa da:

using (var foo = new SomeDisposableObject())
{
    ...
}

IDisposal resmi belgeler asla bahseder IDisposablenesneler kısa ömürlü olmak zorunda. Tanım gereği, IDisposable yalnızca yönetilmeyen kaynakları serbest bırakmanıza izin veren bir mekanizmadır. Daha fazlası değil. Bu anlamda, sonunda elden çıkarmayı tetiklemek için BEKLENİRSİNİZ, ancak bunu kısa ömürlü bir şekilde yapmanız gerekmez.

Bu nedenle, elden çıkarmanın ne zaman tetikleneceğini doğru bir şekilde seçmek, gerçek nesnenin yaşam döngüsü gereksinimini temel almak sizin işinizdir. Sizi uzun ömürlü bir şekilde kullanılamaz bir ID kullanmanızdan alıkoyacak hiçbir şey yoktur:

using System;
namespace HelloWorld
{
    class Hello
    {
        static void Main()
        {
            Console.WriteLine("Hello World!");

            using (var client = new HttpClient())
            {
                for (...) { ... }  // A really long loop

                // Or you may even somehow start a daemon here

            }

            // Keep the console window open in debug mode.
            Console.WriteLine("Press any key to exit.");
            Console.ReadKey();
        }
    }
}

Bu yeni anlayışla, şimdi blog gönderisini tekrar görüyoruz, "düzeltmenin" bir HttpClientkez başlatıldığını ama hiçbir zaman elden çıkarmadığını açıkça görebiliyoruz, bu yüzden netstat çıktısından görebiliyoruz ki bağlantı ESTABLISHED durumunda kaldı. Doğru kapatılmadı. Kapatılsaydı, durumu TIME_WAIT'de olurdu. Uygulamada, tüm programınız sona erdikten sonra açık olan sadece bir bağlantıyı sızdırmak önemli değil ve blog posteri düzeltmeden sonra hala bir performans artışı görüyor; ama yine de, tanımlanabilir şekilde suçlamak ve imha etmemeyi seçmek kavramsal olarak yanlıştır.

3. HttpClient'i statik bir özelliğe mi, hatta bir singleton'a mı koymalıyız?

Önceki bölümün anlayışına dayanarak, bence buradaki cevap açıktır: "zorunlu değil". Gerçekten, bir HttpClient AND (ve sonunda ideal olarak) elden çıkardığınız sürece kodunuzu nasıl düzenlediğinize bağlıdır.

Çok komik bir şekilde, mevcut resmi belgenin Açıklamalar bölümündeki örnek bile tam olarak doğru yapmıyor. İmha edilmeyecek statik bir HttpClient özelliği içeren bir "GoodController" sınıfını tanımlar; Bu da, Örnekler bölümündeki başka bir örnekte vurguladığı şeye uymuyor: "Dispose'i çağırmanız gerekir ... bu yüzden uygulama kaynakları sızdırmaz".

Ve son olarak, singleton kendi zorlukları olmadan değildir.

“Kaç kişi küresel değişkenin iyi bir fikir olduğunu düşünüyor? Hiç kimse.

Kaç kişi singleton'ın iyi bir fikir olduğunu düşünüyor? Birkaç.

Ne oluyor? Singletons, sadece bir sürü küresel değişkendir. ”

- Bu ilham verici konuşmadan, “Global State and Singletons” dan alıntılandı.

PS: SqlConnection

Bu, şu anki soru-cevap ile ilgisiz, ancak muhtemelen bilmesi gereken bir şey. SqlConnection kullanım şekli farklıdır. Sen SqlConnection yeniden gerekmediğini onun bağlantı havuzu böyle daha iyi idare edecek, çünkü.

Fark, uygulama yaklaşımlarından kaynaklanmaktadır. Her HttpClient örneği, kendi bağlantı havuzunu kullanır ( buradan alıntılanır ); ama SqlConnection kendisine göre bir merkezi bağlantı havuzu tarafından yönetilen bu .

Ve hala SqlConnection'ı atmanız gerekiyor, HttpClient için yapmanız gerekenle aynı.


14

Statik ile performans iyileştirmeleri görmek bazı testler yaptım HttpClient. Testlerim için aşağıdaki kodu kullandım:

namespace HttpClientTest
{
    using System;
    using System.Net.Http;

    class Program
    {
        private static readonly int _connections = 10;
        private static readonly HttpClient _httpClient = new HttpClient();

        private static void Main()
        {
            TestHttpClientWithStaticInstance();
            TestHttpClientWithUsing();
        }

        private static void TestHttpClientWithUsing()
        {
            try
            {
                for (var i = 0; i < _connections; i++)
                {
                    using (var httpClient = new HttpClient())
                    {
                        var result = httpClient.GetAsync(new Uri("http://bing.com")).Result;
                    }
                }
            }
            catch (Exception exception)
            {
                Console.WriteLine(exception);
            }
        }

        private static void TestHttpClientWithStaticInstance()
        {
            try
            {
                for (var i = 0; i < _connections; i++)
                {
                    var result = _httpClient.GetAsync(new Uri("http://bing.com")).Result;
                }
            }
            catch (Exception exception)
            {
                Console.WriteLine(exception);
            }
        }
    }
}

Test için:

  • Kodu 10, 100, 1000 ve 1000 bağlantıyla çalıştırdım.
  • Ortalamayı bulmak için her testi 3 kez yaptınız.
  • Bir seferde bir yöntem yürütüldü

Talep HttpClientiçin atmak yerine statik kullanarak% 40 ila% 60 arasında performans artışı buldum HttpClient. Buradaki blog yazısına performans testi sonucunun ayrıntılarını koydum .


1

TCP bağlantısını düzgün bir şekilde kapatmak için , bir FIN - FIN + ACK - ACK paket dizisini tamamlamamız gerekir (tıpkı bir TCP bağlantısını açarken , SYN - SYN + ACK - ACK gibi ). Eğer sadece bir .Close () yöntemini çağırırsak (genellikle bir HttpClient atılırken olur ) ve uzak tarafın yakın isteğimizi (FIN + ACK ile) onaylamasını beklemiyoruz, TIME_WAIT durumunda kalıyoruz yerel TCP portu, çünkü dinleyicimizi (HttpClient) elden çıkardık ve uzak eşler bize FIN + ACK paketini gönderdiğinde, port durumunu uygun bir kapalı duruma getirme şansımız olmadı.

TCP bağlantısını kapatmanın doğru yolu .Close () yöntemini çağırmak ve close olayının diğer taraftan (FIN + ACK) tarafımıza ulaşmasını beklemektir. Ancak o zaman nihai ACK'mızı gönderebilir ve HttpClient'i elden çıkarabiliriz.

Eklemek gerekirse, "Bağlantı: Canlı Tut" HTTP başlığı nedeniyle HTTP istekleri gerçekleştiriyorsanız, TCP bağlantılarını açık tutmak mantıklıdır. Dahası, uzaktaki kişiden sizin için bağlantıyı kapatmasını isteyebilirsiniz, bunun yerine, "Bağlantı: Kapat" HTTP başlığını ayarlayarak. Bu şekilde, yerel bağlantı noktalarınız TIME_WAIT durumunda olmak yerine her zaman uygun şekilde kapatılır.


1

İşte HttpClient ve HttpClientHandler'ı verimli bir şekilde kullanan temel bir API istemcisi. Bir istek yapmak için yeni bir HttpClient oluşturduğunuzda, çok fazla ek yükü vardır. Her istek için HttpClient'i yeniden yaratmayın. HttpClient'i mümkün olduğunca tekrar kullanın ...

using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
//You need to install package Newtonsoft.Json > https://www.nuget.org/packages/Newtonsoft.Json/
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;


public class MyApiClient : IDisposable
{
    private readonly TimeSpan _timeout;
    private HttpClient _httpClient;
    private HttpClientHandler _httpClientHandler;
    private readonly string _baseUrl;
    private const string ClientUserAgent = "my-api-client-v1";
    private const string MediaTypeJson = "application/json";

    public MyApiClient(string baseUrl, TimeSpan? timeout = null)
    {
        _baseUrl = NormalizeBaseUrl(baseUrl);
        _timeout = timeout ?? TimeSpan.FromSeconds(90);    
    }

    public async Task<string> PostAsync(string url, object input)
    {
        EnsureHttpClientCreated();

        using (var requestContent = new StringContent(ConvertToJsonString(input), Encoding.UTF8, MediaTypeJson))
        {
            using (var response = await _httpClient.PostAsync(url, requestContent))
            {
                response.EnsureSuccessStatusCode();
                return await response.Content.ReadAsStringAsync();
            }
        }
    }

    public async Task<TResult> PostAsync<TResult>(string url, object input) where TResult : class, new()
    {
        var strResponse = await PostAsync(url, input);

        return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }

    public async Task<TResult> GetAsync<TResult>(string url) where TResult : class, new()
    {
        var strResponse = await GetAsync(url);

        return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }

    public async Task<string> GetAsync(string url)
    {
        EnsureHttpClientCreated();

        using (var response = await _httpClient.GetAsync(url))
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }

    public async Task<string> PutAsync(string url, object input)
    {
        return await PutAsync(url, new StringContent(JsonConvert.SerializeObject(input), Encoding.UTF8, MediaTypeJson));
    }

    public async Task<string> PutAsync(string url, HttpContent content)
    {
        EnsureHttpClientCreated();

        using (var response = await _httpClient.PutAsync(url, content))
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }

    public async Task<string> DeleteAsync(string url)
    {
        EnsureHttpClientCreated();

        using (var response = await _httpClient.DeleteAsync(url))
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }

    public void Dispose()
    {
        _httpClientHandler?.Dispose();
        _httpClient?.Dispose();
    }

    private void CreateHttpClient()
    {
        _httpClientHandler = new HttpClientHandler
        {
            AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip
        };

        _httpClient = new HttpClient(_httpClientHandler, false)
        {
            Timeout = _timeout
        };

        _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(ClientUserAgent);

        if (!string.IsNullOrWhiteSpace(_baseUrl))
        {
            _httpClient.BaseAddress = new Uri(_baseUrl);
        }

        _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeJson));
    }

    private void EnsureHttpClientCreated()
    {
        if (_httpClient == null)
        {
            CreateHttpClient();
        }
    }

    private static string ConvertToJsonString(object obj)
    {
        if (obj == null)
        {
            return string.Empty;
        }

        return JsonConvert.SerializeObject(obj, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }

    private static string NormalizeBaseUrl(string url)
    {
        return url.EndsWith("/") ? url : url + "/";
    }
}

Kullanımı:

using (var client = new MyApiClient("http://localhost:8080"))
{
    var response = client.GetAsync("api/users/findByUsername?username=alper").Result;
    var userResponse = client.GetAsync<MyUser>("api/users/findByUsername?username=alper").Result;
}

-5

HttpClient sınıfını kullanmanın bir yolu yoktur. Önemli olan, uygulamanızı çevresi ve kısıtlamaları için anlamlı olacak şekilde tasarlamaktır.

HTTP, genel API’leri göstermeniz gerektiğinde kullanmak için harika bir protokoldür. Aynı zamanda hafif düşük gecikmeli Dahili servisler için de etkili bir şekilde kullanılabilir - RPC mesaj sırası paterni genellikle dahili servisler için daha iyi bir seçimdir.

HTTP işleminde çok fazla karmaşıklık var.

Aşağıdakileri göz önünde bulundur:

  1. Bir soket oluşturmak ve bir TCP bağlantısı kurmak, ağ bant genişliğini ve zamanını kullanır.
  2. HTTP / 1.1, aynı soketteki boru hattı isteklerini destekler. Önceki yanıtları beklemenize gerek kalmadan birbiri ardına birden fazla istek gönderme - Blog postası tarafından bildirilen hız iyileştirmesinden muhtemelen bu sorumludur.
  3. Önbellekleme ve yük dengeleyici - sunucular önünde bir yük dengeleyiciniz varsa, isteklerinizin uygun önbellek başlıklarına sahip olmasını sağlamak, sunucularınızdaki yükü azaltabilir ve istemcilerin yanıtlarını daha hızlı alabilir.
  4. Asla bir kaynağı yoklama, düzenli yanıtları döndürmek için HTTP öbürünü kullan.

Ancak her şeyden önce, test edin, ölçün ve onaylayın. Tasarlandığı gibi davranmıyorsa, beklenen sonuçlarınıza nasıl ulaşacağınızla ilgili özel soruları cevaplayabiliriz.


4
Bu aslında sorulan hiçbir şeye cevap vermiyor.
whatsisname,

ONE doğru yol olduğunu varsayıyor gibisiniz. Ben olduğunu sanmıyorum. Uygun şekilde kullanmanız gerektiğini biliyorum, sonra nasıl davrandığını test edin ve ölçün, sonra mutlu olana kadar yaklaşımınızı ayarlayın.
Michael Shaw,

İletişim kurmak için HTTP kullanıp kullanmama konusunda biraz yazdınız. OP, belirli bir kütüphane bileşenini en iyi nasıl kullanacağını sordu.
whatsisname,

1
@MichaelShaw: HttpClientuygular IDisposable. Bu nedenle, kendisinden sonra nasıl temizleneceğini bilen, usingher ihtiyacınız olduğunda bir ifadeye sarılmaya uygun, kısa ömürlü bir nesne olmasını beklemek mantıksız değildir . Ne yazık ki, aslında bu şekilde çalışmaz. OP'nin bağlantılı olduğu blog yazısı, usingifadenin kapsam dışında kalmasından ve HttpClientnesnenin imha edilmesinden çok sonra devam eden kaynaklar (özellikle TCP soket bağlantıları) olduğunu açıkça göstermektedir .
Robert Harvey,

1
Bu düşünce sürecini anlıyorum. Sadece mimarlık açısından HTTP'yi düşünüyorsanız ve aynı servise birçok talepte bulunmayı planlıyor olsaydınız - o zaman önbellekleme ve boru hatları ve ardından HttpClient'i kısa ömürlü bir nesne yapma düşüncesi olurdu. Sadece yanlış hissediyorum. Aynı şekilde, farklı sunuculara taleplerde bulunuyorsanız ve soketi canlı tutmaktan hiçbir fayda elde edemezseniz, HttpClient nesnesini kullandıktan sonra elden çıkarmanız mantıklı olacaktır.
Michael Shaw,
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.