Haruskah kita membuat satu instance baru dari HttpClient untuk semua permintaan?

58

Baru-baru ini saya menemukan posting blog ini dari monster asp.net yang berbicara tentang masalah penggunaan dengan HttpClientcara sebagai berikut:

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

Sesuai dengan posting blog, jika kita membuang HttpClientsetelah setiap permintaan itu dapat membuat koneksi TCP tetap terbuka. Ini berpotensi menyebabkan System.Net.Sockets.SocketException.

Cara yang benar sesuai pos adalah membuat satu contoh HttpClientkarena membantu mengurangi pemborosan soket.

Dari pos:

Jika kami membagikan satu instance HttpClient maka kami dapat mengurangi pemborosan soket dengan menggunakannya kembali:

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();
        }
    }
}

Saya selalu membuang HttpClientobjek setelah menggunakannya karena saya merasa ini adalah cara terbaik untuk menggunakannya. Tapi postingan blog ini sekarang membuat saya merasa saya melakukan kesalahan selama ini.

Haruskah kita membuat instance tunggal baru HttpClientuntuk semua permintaan? Apakah ada kesulitan menggunakan instance statis?

Ankit Vijay
sumber
Pernahkah Anda menemukan masalah yang dikaitkan dengan cara Anda menggunakannya?
whatsisname
Mungkin periksa jawaban ini dan juga ini .
John Wu
@whatsisname no Saya hanya melihat blog saya merasa bahwa saya mungkin menggunakan kesalahan ini sepanjang waktu. Oleh karena itu, ingin memahami dari sesama pengembang jika mereka melihat masalah dalam pendekatan mana pun.
Ankit Vijay
3
Saya belum mencobanya sendiri (jadi tidak memberikan ini sebagai jawaban), tetapi menurut microsoft pada. NET Core 2.1 Anda seharusnya menggunakan HttpClientFactory seperti yang dijelaskan pada docs.microsoft.com/en-us/dotnet/standard/ ...
Joeri Sebrechts
(Seperti yang dinyatakan dalam jawaban saya, hanya ingin membuatnya lebih terlihat, jadi saya menulis komentar singkat.) Contoh statis akan menangani handshake penutup koneksi tcp dengan benar, setelah Anda melakukan Close()atau memulai yang baru Get(). Jika Anda hanya membuang klien setelah selesai, tidak akan ada yang menangani jabat tangan penutup dan port Anda semua akan memiliki status TIME_WAIT, karena itu.
Mladen B.

Jawaban:

40

Sepertinya posting blog yang menarik. Namun, sebelum membuat keputusan, pertama-tama saya akan menjalankan tes yang sama dengan yang dilakukan penulis blog, tetapi dengan kode Anda sendiri. Saya juga akan mencoba dan mencari tahu lebih banyak tentang HttpClient dan perilakunya.

Pos ini menyatakan:

Instance HttpClient adalah kumpulan pengaturan yang diterapkan untuk semua permintaan yang dijalankan oleh instance tersebut. Selain itu, setiap instance HttpClient menggunakan kumpulan koneksi sendiri, mengisolasi permintaannya dari permintaan yang dieksekusi oleh instance HttpClient lainnya.

Jadi apa yang mungkin terjadi ketika HttpClient dibagikan adalah bahwa koneksi sedang digunakan kembali, yang baik-baik saja jika Anda tidak memerlukan koneksi terus-menerus. Satu-satunya cara Anda akan tahu pasti apakah ini penting atau tidak untuk situasi Anda adalah dengan menjalankan tes kinerja Anda sendiri.

Jika Anda menggali, Anda akan menemukan beberapa sumber daya lain yang mengatasi masalah ini (termasuk artikel Praktik Terbaik Microsoft), jadi mungkin ide yang baik untuk diterapkan pula (dengan beberapa tindakan pencegahan).

Referensi

Anda Menggunakan Httpclient Salah dan
Mengacaukan Perangkat Lunak Anda Singleton HttpClient? Waspadalah terhadap perilaku serius ini dan cara memperbaikinya
Pola dan Praktik Microsoft - Optimalisasi Kinerja: Instansiasi yang Tidak Benar
Satu contoh HttpClient yang dapat digunakan kembali pada Tinjauan Kode
Singleton HttpClient tidak menghargai perubahan DNS (CoreFX)
Saran umum untuk menggunakan HttpClient

Robert Harvey
sumber
1
Itu daftar lengkap yang bagus. Ini adalah akhir pekan saya baca.
Ankit Vijay
"Jika Anda menggali, Anda akan menemukan beberapa sumber daya lain yang mengatasi masalah ini ..." Anda bermaksud mengatakan masalah koneksi terbuka TCP?
Ankit Vijay
Jawaban singkat: gunakan HttpClient statis . Jika Anda perlu mendukung perubahan DNS (dari server web Anda atau server lain), maka Anda perlu khawatir tentang pengaturan batas waktu.
Jess
3
Ini adalah bukti betapa kacau HttpClient adalah bahwa menggunakannya adalah "membaca akhir pekan" seperti yang dikomentari oleh @AnkitVijay.
usr
@ Jess selain perubahan DNS - membuang semua lalu lintas klien Anda melalui satu soket akan mengacaukan penyeimbangan muatan juga?
Iain
16

Saya terlambat ke pesta, tapi inilah perjalanan belajar saya tentang topik rumit ini.

1. Di mana kami dapat menemukan advokat resmi untuk menggunakan kembali HttpClient?

Maksud saya, jika menggunakan kembali HttpClient dimaksudkan dan melakukannya adalah penting , advokat seperti itu lebih baik didokumentasikan dalam dokumentasi API-nya sendiri, daripada disembunyikan di banyak "Topik Lanjut", "Pola kinerja (anti)" atau posting blog lainnya di luar sana . Kalau tidak, bagaimana mungkin seorang pelajar baru mengetahuinya sebelum terlambat?

Sampai sekarang (Mei 2018), hasil pencarian pertama ketika googling "c # httpclient" menunjuk ke halaman referensi API ini di MSDN , yang tidak menyebutkan maksud itu sama sekali. Nah, pelajaran 1 di sini untuk pemula adalah, selalu klik tautan "Versi Lain" tepat setelah judul halaman bantuan MSDN, Anda mungkin akan menemukan tautan ke "versi saat ini" di sana. Dalam kasus HttpClient ini, ini akan membawa Anda ke dokumen terbaru di sini yang berisi uraian niat tersebut .

Saya curiga banyak pengembang yang baru mengenal topik ini juga tidak menemukan halaman dokumentasi yang benar, itu sebabnya pengetahuan ini tidak tersebar luas, dan orang-orang terkejut ketika mereka menemukannya nanti , mungkin dengan cara yang sulit .

2. Konsepsi (salah?) Dari using IDisposable

Satu ini sedikit di luar topik tapi masih layak menunjuk bahwa, itu bukan kebetulan melihat orang-orang pada mereka posting blog tersebut menyalahkan bagaimana HttpClient's IDisposableantarmuka membuat mereka cenderung menggunakan using (var client = new HttpClient()) {...}pola dan kemudian menyebabkan masalah.

Saya percaya bahwa konsepsi tak terucap (salah?): "Sebuah objek IDisposable diharapkan berumur pendek" .

NAMUN, sementara itu terlihat seperti hal yang berumur pendek ketika kita menulis kode dengan gaya ini:

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

yang dokumentasi resmi pada IDisposable tidak pernah menyebutkan IDisposablebenda harus berumur pendek. Menurut definisi, IDisposable hanyalah sebuah mekanisme untuk memungkinkan Anda melepaskan sumber daya yang tidak dikelola. Tidak ada lagi. Dalam hal itu, Anda HARUS pada akhirnya memicu pembuangan, tetapi itu tidak mengharuskan Anda untuk melakukannya dalam waktu singkat.

Karena itu, tugas Anda adalah memilih dengan tepat kapan akan memicu pembuangan, berdasarkan kebutuhan siklus hidup objek nyata Anda. Tidak ada yang menghentikan Anda menggunakan IDisposable dengan cara yang tahan lama:

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();
        }
    }
}

Dengan pemahaman baru ini, sekarang kita meninjau kembali posting blog itu , kita dapat dengan jelas melihat bahwa "perbaikan" diinisialisasi HttpClientsekali tetapi tidak pernah membuangnya, itu sebabnya kita dapat melihat dari output netstatnya bahwa, koneksi tetap pada keadaan ESTABLISHED yang berarti ia memiliki TIDAK ditutup dengan benar. Jika ditutup, kondisinya akan menjadi TIME_WAIT sebagai gantinya. Dalam praktiknya, tidak masalah untuk membocorkan hanya satu koneksi yang terbuka setelah seluruh program Anda berakhir, dan poster blog masih melihat peningkatan kinerja setelah perbaikan; tapi tetap saja, secara konseptual salah untuk menyalahkan IDisposable dan memilih untuk TIDAK membuangnya.

3. Apakah kita harus meletakkan HttpClient ke properti statis, atau bahkan menjadikannya sebagai singleton?

Berdasarkan pemahaman dari bagian sebelumnya, saya pikir jawabannya di sini menjadi jelas: "belum tentu". Ini benar-benar tergantung pada bagaimana Anda mengatur kode Anda, selama Anda menggunakan kembali HttpClient AND (idealnya) buang pada akhirnya.

Meriah, bahkan contoh di bagian Keterangan dari dokumen resmi saat ini tidak benar. Ini mendefinisikan kelas "GoodController", yang mengandung properti HttpClient statis yang tidak akan dibuang; yang tidak mematuhi apa contoh lain di bagian Contoh menekankan: "perlu memanggil buang ... sehingga aplikasi tidak membocorkan sumber daya".

Dan terakhir, singleton bukan tanpa tantangannya sendiri.

"Berapa banyak orang berpikir variabel global adalah ide yang bagus? Tidak ada.

Berapa banyak orang berpikir bahwa singleton adalah ide yang bagus? Beberapa.

Apa yang menyebabkannya? Lajang hanya sekelompok variabel global. "

- Dikutip dari ceramah yang menginspirasi ini, "Negara Global dan Lajang"

PS: Koneksi Sql

Yang ini tidak relevan dengan T&J saat ini, tetapi ini mungkin baik untuk diketahui. Pola penggunaan SqlConnection berbeda. Anda TIDAK perlu menggunakan kembali SqlConnection , karena ia akan menangani kelompok koneksi dengan lebih baik.

Perbedaan tersebut disebabkan oleh pendekatan implementasi mereka. Setiap instance HttpClient menggunakan kumpulan koneksi sendiri (dikutip dari sini ); tetapi SqlConnection sendiri dikelola oleh pool koneksi sentral, menurut ini .

Dan Anda masih perlu membuang SqlConnection, sama seperti yang seharusnya Anda lakukan untuk HttpClient.

RayLuo
sumber
14

Saya melakukan beberapa tes melihat peningkatan kinerja dengan statis HttpClient. Saya menggunakan kode di bawah ini untuk pengujian saya:

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);
            }
        }
    }
}

Untuk pengujian:

  • Saya menjalankan kode dengan 10, 100, 1000 dan 1000 koneksi.
  • Berlari setiap tes 3 kali untuk mengetahui rata-rata.
  • Dilaksanakan satu metode pada suatu waktu

Saya menemukan peningkatan kinerja antara 40% hingga 60% menggunakan statis HttpClientdaripada membuangnya untuk HttpClientpermintaan. Saya telah memasukkan rincian hasil tes kinerja di posting blog di sini .

Ankit Vijay
sumber
1

Untuk menutup koneksi TCP dengan benar , kita perlu menyelesaikan urutan paket FIN - FIN + ACK - ACK (seperti SYN - SYN + ACK - ACK, saat membuka koneksi TCP ). Jika kami hanya memanggil metode .Close () (biasanya terjadi ketika HttpClient sedang membuang), dan kami tidak menunggu sisi jarak jauh untuk mengonfirmasi permintaan tutup kami (dengan FIN + ACK), kami berakhir dengan status TIME_WAIT pada port TCP lokal, karena kami membuang pendengar kami (HttpClient) dan kami tidak pernah mendapat kesempatan untuk mengatur ulang status port ke keadaan tertutup yang tepat, setelah rekan jauh mengirimkan paket FIN + ACK.

Cara yang tepat untuk menutup koneksi TCP adalah dengan memanggil metode .Close () dan menunggu acara tutup dari sisi lain (FIN + ACK) untuk tiba di pihak kami. Hanya dengan begitu kita dapat mengirim ACK terakhir kita dan membuang HttpClient.

Hanya untuk menambahkan, masuk akal untuk menjaga koneksi TCP tetap terbuka, jika Anda melakukan permintaan HTTP, karena header HTTP "Connection: Keep-Alive". Lebih jauh lagi, Anda mungkin meminta rekan jauh untuk menutup koneksi untuk Anda, sebagai gantinya, dengan mengatur header HTTP "Connection: Close". Dengan begitu, porta lokal Anda akan selalu ditutup dengan benar, alih-alih dalam keadaan TIME_WAIT.

Mladen B.
sumber
1

Berikut adalah klien API dasar yang menggunakan HttpClient dan HttpClientHandler secara efisien. Saat Anda membuat HttpClient baru untuk membuat permintaan, ada banyak overhead. JANGAN buat kembali HttpClient untuk setiap permintaan. Gunakan kembali HttpClient sebanyak mungkin ...

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 + "/";
    }
}

Pemakaian:

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;
}
Alper Ebicoglu
sumber
-5

Tidak ada satu cara untuk menggunakan kelas HttpClient. Kuncinya adalah merancang aplikasi Anda dengan cara yang masuk akal untuk lingkungan dan batasannya.

HTTP adalah protokol yang bagus untuk digunakan ketika Anda perlu mengekspos API publik. Itu juga dapat digunakan secara efektif untuk layanan internal latensi rendah ringan - meskipun pola antrian pesan RPC sering merupakan pilihan yang lebih baik untuk layanan internal.

Ada banyak kerumitan dalam melakukan HTTP dengan baik.

Pertimbangkan yang berikut ini:

  1. Membuat soket dan membangun koneksi TCP menggunakan bandwidth dan waktu jaringan.
  2. HTTP / 1.1 mendukung permintaan saluran pipa pada soket yang sama. Mengirim beberapa permintaan satu demi satu, tanpa perlu menunggu tanggapan sebelumnya - ini mungkin bertanggung jawab atas peningkatan kecepatan yang dilaporkan oleh posting Blog.
  3. Caching dan memuat penyeimbang - jika Anda memiliki penyeimbang beban di depan server, maka memastikan permintaan Anda memiliki header cache yang tepat dapat mengurangi beban di server Anda, dan mendapatkan respons ke klien lebih cepat.
  4. Jangan pernah polling sumber daya, gunakan chunking HTTP untuk mengembalikan respons berkala.

Namun yang terpenting, uji, ukur dan konfirmasi. Jika tidak berperilaku seperti yang dirancang, maka kami dapat menjawab pertanyaan spesifik tentang bagaimana mencapai hasil yang Anda harapkan.

Michael Shaw
sumber
4
Ini sebenarnya tidak menjawab apa pun yang ditanyakan.
whatsisname
Anda tampaknya menganggap bahwa ada SATU cara yang benar. Saya kira tidak ada. Saya tahu Anda harus menggunakannya dengan cara yang sesuai, kemudian menguji dan mengukur bagaimana perilakunya, dan kemudian menyesuaikan pendekatan Anda sampai Anda bahagia.
Michael Shaw
Anda menulis sedikit tentang menggunakan apakah akan menggunakan HTTP atau tidak untuk berkomunikasi. OP bertanya tentang cara terbaik untuk menggunakan komponen perpustakaan tertentu.
whatsisname
1
@MichaelShaw: HttpClientmengimplementasikan IDisposable. Oleh karena itu tidak masuk akal untuk mengharapkannya menjadi objek berumur pendek yang tahu cara membersihkan setelah itu sendiri, cocok untuk membungkus dalam usingpernyataan setiap kali Anda membutuhkannya. Sayangnya, itu bukan cara kerjanya yang sebenarnya. Posting blog yang tertaut OP jelas menunjukkan bahwa ada sumber daya (khususnya, koneksi soket TCP) yang hidup lama setelah usingpernyataan itu keluar dari ruang lingkup dan HttpClientobjek mungkin telah dibuang.
Robert Harvey
1
Saya mengerti proses berpikir itu. Hanya jika Anda berpikir tentang HTTP dari sudut pandang arsitektur, dan berniat untuk membuat banyak permintaan ke layanan yang sama - maka Anda akan berpikir tentang caching dan pipelining, dan kemudian pemikiran menjadikan HttpClient objek yang berumur pendek akan hanya merasa salah. Demikian juga, jika Anda membuat permintaan ke server yang berbeda dan tidak akan mendapat manfaat dari menjaga soket tetap hidup, maka membuang objek HttpClient setelah penggunaannya masuk akal.
Michael Shaw