Chúng ta có nên tạo một phiên bản mới của httpClient cho tất cả các yêu cầu không?


57

Gần đây tôi tình cờ thấy bài đăng trên blog này từ quái vật asp.net nói về các vấn đề với việc sử dụng HttpClienttheo cách sau:

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

Theo bài đăng trên blog, nếu chúng tôi loại bỏ HttpClientsau mỗi yêu cầu, nó có thể giữ các kết nối TCP mở. Điều này có khả năng có thể dẫn đến System.Net.Sockets.SocketException.

Cách chính xác theo bài đăng là tạo một ví dụ duy nhất HttpClientvì nó giúp giảm lãng phí ổ cắm.

Từ bài viết:

Nếu chúng ta chia sẻ một ví dụ duy nhất của httpClient thì chúng ta có thể giảm lãng phí ổ cắm bằng cách sử dụng lại chúng:

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

Tôi đã luôn vứt bỏ HttpClientđồ vật sau khi sử dụng vì tôi cảm thấy đây là cách tốt nhất để sử dụng nó. Nhưng bài viết trên blog này bây giờ làm cho tôi cảm thấy tôi đã làm nó sai từ lâu.

Chúng ta có nên tạo một bản sao mới HttpClientcho tất cả các yêu cầu không? Có bất kỳ cạm bẫy của việc sử dụng cá thể tĩnh?


Bạn đã gặp phải bất kỳ vấn đề nào bạn quy cho cách bạn đang sử dụng nó?
whatsisname

Có thể kiểm tra câu trả lời này và cũng thế này .
John Wu

@whatsisname không Tôi không có nhưng nhìn vào blog tôi cảm thấy rằng tôi có thể sử dụng sai điều này mọi lúc. Do đó, muốn hiểu từ các nhà phát triển đồng nghiệp nếu họ thấy bất kỳ vấn đề nào trong cả hai cách tiếp cận.
Ankit Vijay

3
Tôi đã không tự mình thử nó (vì vậy không cung cấp câu trả lời này), nhưng theo microsoft kể từ .NET Core 2.1, bạn phải sử dụng HttpClientFactory như được mô tả trên docs.microsoft.com/en-us/dotnet/stiteria/ Tiết
kiệm

(Như đã nêu trong câu trả lời của tôi, chỉ muốn làm cho nó rõ hơn, vì vậy tôi đang viết một bình luận ngắn.) Ví dụ tĩnh sẽ xử lý đúng cách bắt tay kết nối tcp, khi bạn thực hiện Close()hoặc bắt đầu một cái mới Get(). Nếu bạn chỉ loại bỏ ứng dụng khách khi bạn hoàn thành với nó, sẽ không có ai xử lý cái bắt tay đóng đó và tất cả các cổng của bạn sẽ có trạng thái TIME_WAIT, vì điều đó.
Mladen B.

Câu trả lời:


39

Có vẻ như một bài viết blog hấp dẫn. Tuy nhiên, trước khi đưa ra quyết định, trước tiên tôi sẽ chạy các bài kiểm tra tương tự mà người viết blog đã chạy, nhưng trên mã của riêng bạn. Tôi cũng sẽ thử và tìm hiểu thêm một chút về HTTPClient và hành vi của nó.

Bài đăng này nêu:

Một cá thể HTTPClient là một tập hợp các cài đặt được áp dụng cho tất cả các yêu cầu được thực hiện bởi thể hiện đó. Ngoài ra, mọi phiên bản HTTPClient đều sử dụng nhóm kết nối riêng, tách biệt các yêu cầu của nó khỏi các yêu cầu được thực hiện bởi các phiên bản httpClient khác.

Vì vậy, điều có thể xảy ra khi chia sẻ một httpClient là các kết nối đang được sử dụng lại, sẽ rất tốt nếu bạn không yêu cầu các kết nối liên tục. Cách duy nhất bạn sẽ biết chắc chắn liệu điều này có quan trọng với tình huống của bạn hay không là chạy các bài kiểm tra hiệu suất của riêng bạn.

Nếu bạn đào, bạn sẽ tìm thấy một số tài nguyên khác giải quyết vấn đề này (bao gồm cả bài viết Thực tiễn tốt nhất của Microsoft), vì vậy có lẽ nên thực hiện bằng mọi cách (với một số biện pháp phòng ngừa).

Người giới thiệu

Bạn đang sử dụng sai httpclient và nó đang làm mất ổn định phần mềm của bạn
Singleton HttpClient? Cảnh giác với hành vi nghiêm trọng này và cách khắc phục
các Mô hình và Thực tiễn của Microsoft - Tối ưu hóa hiệu suất: Khởi tạo không đúng cách
Một ví dụ sử dụng lại httpClient trên Đánh giá mã
Singleton HttpClient không tôn trọng các thay đổi DNS (CoreFX)
Lời khuyên chung cho việc sử dụng HttpClient


1
Đó là một danh sách rộng rãi tốt. Đây là cuối tuần của tôi đọc.
Ankit Vijay

"Nếu bạn đào, bạn sẽ tìm thấy một số tài nguyên khác giải quyết vấn đề này ..." bạn muốn nói vấn đề mở kết nối TCP?
Ankit Vijay

Câu trả lời ngắn: sử dụng một httpClient tĩnh . Nếu bạn cần hỗ trợ thay đổi DNS (của máy chủ web hoặc các máy chủ khác), thì bạn cần phải lo lắng về cài đặt thời gian chờ.
Jess

3
Đó là một bằng chứng cho thấy httpClient đã gây rối như thế nào khi sử dụng nó là "đọc cuối tuần" như nhận xét của @AnkitVijay.
usr

@Jess bên cạnh các thay đổi DNS - ném tất cả lưu lượng truy cập của khách hàng của bạn qua một ổ cắm sẽ làm rối loạn cân bằng tải?
Iain

16

Tôi đến bữa tiệc muộn, nhưng đây là hành trình học hỏi của tôi về chủ đề khó khăn này.

1. Chúng ta có thể tìm thấy người ủng hộ chính thức về việc tái sử dụng HTTPClient ở đâu?

Ý tôi là, nếu việc sử dụng lại httpClient được dự địnhlàm như vậy là rất quan trọng , thì người ủng hộ đó được ghi lại tốt hơn trong tài liệu API của riêng mình, thay vì bị ẩn trong nhiều "Chủ đề nâng cao", "Mẫu hiệu suất (chống)" hoặc các bài đăng trên blog khác ngoài đó . Nếu không thì làm thế nào một người học mới phải biết nó trước khi quá muộn?

Kể từ bây giờ (tháng 5 năm 2018), kết quả tìm kiếm đầu tiên khi googling "c # httpclient" trỏ đến trang tham chiếu API này trên MSDN , hoàn toàn không đề cập đến ý định đó. Vâng, bài học 1 ở đây cho người mới là, luôn nhấp vào liên kết "Phiên bản khác" ngay sau tiêu đề trang trợ giúp MSDN, bạn có thể sẽ tìm thấy các liên kết đến "phiên bản hiện tại" ở đó. Trong trường hợp httpClient này, nó sẽ đưa bạn đến tài liệu mới nhất ở đây có chứa mô tả ý định đó .

Tôi nghi ngờ nhiều nhà phát triển mới tham gia chủ đề này cũng không tìm thấy trang tài liệu chính xác, đó là lý do tại sao kiến ​​thức này không được phổ biến rộng rãi và mọi người đã ngạc nhiên khi họ phát hiện ra sau đó , có thể là một cách khó khăn .

2. Quan niệm (mis?) Của using IDisposable

Cái này là hơi lạc đề nhưng vẫn có giá trị chỉ ra rằng, nó không phải là một trùng hợp ngẫu nhiên khi thấy mọi người trong những bài đăng trên blog nói trên đổ lỗi như thế nào HttpClient's IDisposablegiao diện làm cho họ có xu hướng sử dụng các using (var client = new HttpClient()) {...}mô hình và sau đó dẫn đến các vấn đề.

Tôi tin rằng điều đó dẫn đến một quan niệm bất thành văn (mis?): "Một đối tượng IDis Dùng được dự kiến ​​sẽ tồn tại trong thời gian ngắn" .

TUY NHIÊN, trong khi nó chắc chắn trông giống như một thứ tồn tại ngắn khi chúng ta viết mã theo phong cách này:

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

các tài liệu chính thức về IDisposable không bao giờ đề cập đến IDisposableđối tượng phải là ngắn ngủi. Theo định nghĩa, IDis Dùng chỉ là một cơ chế để cho phép bạn giải phóng các tài nguyên không được quản lý. Chỉ có bấy nhiêu thôi. Theo nghĩa đó, bạn được MỞ RỘNG để cuối cùng kích hoạt xử lý, nhưng nó không yêu cầu bạn phải làm như vậy trong một thời gian ngắn.

Do đó, công việc của bạn là chọn đúng thời điểm để kích hoạt xử lý, dựa trên yêu cầu vòng đời của đối tượng thực của bạn. Không có gì ngăn bạn sử dụng IDis Dùng một cách lâu dài:

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

Với cách hiểu mới này, bây giờ chúng tôi xem lại bài đăng trên blog đó , chúng tôi có thể nhận thấy rõ rằng "sửa chữa" khởi tạo HttpClientmột lần nhưng không bao giờ loại bỏ nó, đó là lý do tại sao chúng tôi có thể thấy từ đầu ra netstat của nó, kết nối vẫn ở trạng thái THÀNH LẬP có nghĩa là nó có KHÔNG được đóng đúng cách. Nếu nó bị đóng, thay vào đó, trạng thái của nó sẽ ở TIME_WAIT. Trong thực tế, không phải là vấn đề lớn khi chỉ rò rỉ một kết nối mở sau khi toàn bộ chương trình của bạn kết thúc và người đăng blog vẫn thấy hiệu suất tăng sau khi sửa lỗi; tuy nhiên, về mặt khái niệm không chính xác để đổ lỗi cho IDis Dùng và chọn KHÔNG loại bỏ nó.

3. Chúng ta có phải đặt httpClient vào một thuộc tính tĩnh hay thậm chí đặt nó dưới dạng đơn lẻ không?

Dựa trên sự hiểu biết của phần trước, tôi nghĩ rằng câu trả lời ở đây trở nên rõ ràng: "không nhất thiết". Nó thực sự phụ thuộc vào cách bạn tổ chức mã của mình, miễn là bạn sử dụng lại một HTTPClient VÀ (lý tưởng) để loại bỏ nó.

Vui vẻ, ngay cả ví dụ trong phần Ghi chú của tài liệu chính thức hiện tại cũng hoàn toàn đúng. Nó định nghĩa một lớp "GoodContoder", chứa thuộc tính httpClient tĩnh sẽ không bị loại bỏ; không tuân theo những gì một ví dụ khác trong phần Ví dụ nhấn mạnh: "cần gọi xử lý ... vì vậy ứng dụng không bị rò rỉ tài nguyên".

Và cuối cùng, singleton không phải không có những thách thức riêng.

"Có bao nhiêu người nghĩ biến toàn cầu là một ý tưởng tốt? Không ai cả.

Có bao nhiêu người nghĩ singleton là một ý tưởng tốt? Một vài

Đưa cái gì? Singletons chỉ là một loạt các biến toàn cầu. "

- Trích dẫn từ bài nói chuyện đầy cảm hứng này, "Nhà nước toàn cầu và người độc thân"

PS: Kết nối Sql

Điều này không liên quan đến Q & A hiện tại, nhưng nó có lẽ là một điều cần biết. Mô hình sử dụng SqlConnection là khác nhau. Bạn KHÔNG cần phải sử dụng lại SqlConnection , bởi vì nó sẽ xử lý nhóm kết nối của nó tốt hơn theo cách đó.

Sự khác biệt được gây ra bởi phương pháp thực hiện của họ. Mỗi phiên bản httpClient sử dụng nhóm kết nối riêng của nó (trích dẫn từ đây ); nhưng bản thân SqlConnection được quản lý bởi một nhóm kết nối trung tâm, theo điều này .

Và bạn vẫn cần phải loại bỏ SqlConnection, giống như bạn phải làm với HttpClient.


14

Tôi đã làm một số thử nghiệm thấy cải thiện hiệu suất với tĩnh HttpClient. Tôi đã sử dụng mã dưới đây để thử nghiệm của mình:

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

Để thử nghiệm:

  • Tôi đã chạy mã với 10, 100, 1000 và 1000 kết nối.
  • Ran mỗi bài kiểm tra 3 lần để tìm ra trung bình.
  • Thực hiện một phương pháp tại một thời điểm

Tôi tìm thấy sự cải thiện hiệu suất từ ​​40% đến 60% uon bằng cách sử dụng tĩnh HttpClientthay vì xử lý HttpClienttheo yêu cầu. Tôi đã đặt các chi tiết của kết quả kiểm tra hiệu suất trong bài viết blog ở đây .


1

Để đóng đúng kết nối TCP , chúng ta cần hoàn thành chuỗi gói FIN - FIN + ACK - ACK (giống như SYN - SYN + ACK - ACK, khi mở kết nối TCP ). Nếu chúng ta chỉ gọi một phương thức .Close () (thường xảy ra khi một httpClient đang xử lý) và chúng ta không đợi phía từ xa xác nhận yêu cầu đóng của mình (với FIN + ACK), chúng ta sẽ kết thúc với trạng thái TIME_WAIT cổng TCP cục bộ, vì chúng tôi đã loại bỏ trình lắng nghe của chúng tôi (HttpClient) và chúng tôi không bao giờ có cơ hội đặt lại trạng thái cổng về trạng thái đóng thích hợp, một khi thiết bị ngang hàng gửi cho chúng tôi gói FIN + ACK.

Cách thích hợp để đóng kết nối TCP là gọi phương thức .Close () và chờ đợi sự kiện đóng từ phía bên kia (FIN + ACK) đến bên chúng tôi. Chỉ sau đó chúng tôi mới có thể gửi ACK cuối cùng của mình và loại bỏ HTTPClient.

Chỉ cần thêm, sẽ rất hợp lý khi giữ các kết nối TCP mở, nếu bạn đang thực hiện các yêu cầu HTTP, vì tiêu đề HTTP "Kết nối: Giữ nguyên". Hơn nữa, bạn có thể yêu cầu đồng nghiệp từ xa đóng kết nối cho bạn, thay vào đó, bằng cách đặt tiêu đề HTTP "Kết nối: Đóng". Bằng cách đó, các cổng cục bộ của bạn sẽ luôn được đóng đúng cách, thay vì ở trạng thái TIME_WAIT.


1

Đây là một ứng dụng API cơ bản sử dụng httpClient và HttpClientHandler một cách hiệu quả. Khi bạn tạo một httpClient mới để thực hiện một yêu cầu, có rất nhiều chi phí. KHÔNG tạo lại httpClient cho mỗi yêu cầu. Tái sử dụng httpClient càng nhiều càng tốt ...

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

Sử dụng:

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

Không có cách nào để sử dụng lớp HttpClient. Điều quan trọng là kiến ​​trúc ứng dụng của bạn theo cách có ý nghĩa đối với môi trường và các ràng buộc của nó.

HTTP là một giao thức tuyệt vời để sử dụng khi bạn cần trưng ra các API công khai. Nó cũng có thể được sử dụng hiệu quả cho các dịch vụ nội bộ có độ trễ thấp trọng lượng nhẹ - mặc dù mẫu hàng đợi tin nhắn RPC thường là lựa chọn tốt hơn cho các dịch vụ nội bộ.

Có rất nhiều phức tạp trong việc làm tốt HTTP.

Hãy xem xét những điều sau đây:

  1. Tạo một ổ cắm và thiết lập kết nối TCP sử dụng băng thông mạng và thời gian.
  2. HTTP / 1.1 hỗ trợ các yêu cầu đường ống trên cùng một ổ cắm. Gửi nhiều yêu cầu lần lượt, không cần chờ phản hồi trước đó - điều này có thể chịu trách nhiệm cho việc cải thiện tốc độ được báo cáo bởi bài đăng trên Blog.
  3. Bộ cân bằng bộ đệm và tải - nếu bạn có bộ cân bằng tải trước máy chủ, thì việc đảm bảo các yêu cầu của bạn có các tiêu đề bộ đệm phù hợp có thể giảm tải cho máy chủ của bạn và nhận phản hồi nhanh hơn cho máy khách.
  4. Đừng bao giờ thăm dò tài nguyên, sử dụng phân đoạn HTTP để trả về các phản hồi định kỳ.

Nhưng trên hết, kiểm tra, đo lường và xác nhận. Nếu nó không hoạt động như thiết kế, thì chúng tôi có thể trả lời các câu hỏi cụ thể về cách đạt được kết quả mong đợi của bạn.


4
Điều này không thực sự trả lời bất cứ điều gì được hỏi.
whatsisname

Bạn dường như cho rằng có MỘT cách chính xác. Tôi không nghĩ là có. Tôi biết bạn phải sử dụng nó theo cách phù hợp, sau đó kiểm tra và đo lường cách nó hoạt động, và sau đó điều chỉnh cách tiếp cận của bạn cho đến khi bạn hài lòng.
Michael Shaw

Bạn đã viết một chút về việc sử dụng HTTP hay không để giao tiếp. OP đã hỏi về cách tốt nhất để sử dụng một thành phần thư viện cụ thể.
whatsisname

1
@MichaelShaw: HttpClientthực hiện IDisposable. Do đó, không phải là không có lý khi hy vọng nó là một đối tượng tồn tại trong thời gian ngắn, biết cách tự dọn dẹp, phù hợp để gói trong một usingtuyên bố mỗi khi bạn cần. Thật không may, đó không phải là cách nó thực sự hoạt động. Bài đăng trên blog mà OP liên kết chứng minh rõ ràng rằng có các tài nguyên (cụ thể là các kết nối ổ cắm TCP) tồn tại rất lâu sau khi usingtuyên bố vượt quá phạm vi và HttpClientđối tượng có lẽ đã bị xử lý.
Robert Harvey

1
Tôi hiểu quá trình suy nghĩ đó. Chỉ là nếu bạn đang nghĩ về HTTP theo quan điểm kiến ​​trúc và đang có ý định thực hiện nhiều yêu cầu cho cùng một dịch vụ - thì bạn sẽ nghĩ về bộ nhớ đệm và đường ống, và sau đó suy nghĩ về việc biến httpClient thành một đối tượng tồn tại ngắn Đơn giản là cảm thấy sai. Tương tự như vậy, nếu bạn đang thực hiện các yêu cầu đến các máy chủ khác nhau và sẽ không nhận được lợi ích gì từ việc giữ cho ổ cắm hoạt động, thì việc xử lý đối tượng HttpClient sau khi sử dụng nó có ý nghĩa.
Michael Shaw
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.