Выбор между HttpClient и WebClient


219

Наше веб-приложение работает в .Net Framework 4.0. Пользовательский интерфейс вызывает методы контроллера через вызовы ajax.

Нам нужно воспользоваться услугой REST от нашего поставщика. Я оцениваю лучший способ вызвать службу REST в .Net 4.0. Службе REST требуется базовая схема аутентификации, и она может возвращать данные как в формате XML, так и в формате JSON. Нет необходимости загружать / скачивать огромные данные, и я ничего не вижу в будущем. Я взглянул на несколько проектов с открытым исходным кодом для потребления REST и не нашел в них никакой ценности, чтобы оправдать дополнительную зависимость в проекте. Начал оценивать WebClientи HttpClient. Я скачал HttpClient для .Net 4.0 с NuGet.

Я искал различия между WebClientи, HttpClientи на этом сайте упоминалось, что один HttpClient может обрабатывать одновременные вызовы и может повторно использовать разрешенный DNS, конфигурацию cookie и аутентификацию. Мне еще предстоит увидеть практические ценности, которые мы можем получить из-за различий.

Я сделал быстрый тест производительности, чтобы найти, как WebClient(синхронизировать вызовы), HttpClient(синхронизировать и асинхронно) выполняют. и вот результаты:

Использование одного и того же HttpClientэкземпляра для всех запросов (мин. - макс.)

Синхронизация WebClient: 8 мс - 167 мс
Синхронизация HttpClient: 3 мс - 7228 мс
Асинхронность HttpClient: 985 - 10405 мс

Использование нового HttpClientдля каждого запроса (мин. - макс.)

Синхронизация WebClient: 4 мс - 297 мс
Синхронизация HttpClient: 3 мс - 7953 мс
Асинхронность HttpClient: 1027 - 10834 мс

Код

public class AHNData
{
    public int i;
    public string str;
}

public class Program
{
    public static HttpClient httpClient = new HttpClient();
    private static readonly string _url = "http://localhost:9000/api/values/";

    public static void Main(string[] args)
    {
       #region "Trace"
       Trace.Listeners.Clear();

       TextWriterTraceListener twtl = new TextWriterTraceListener(
           "C:\\Temp\\REST_Test.txt");
       twtl.Name = "TextLogger";
       twtl.TraceOutputOptions = TraceOptions.ThreadId | TraceOptions.DateTime;

       ConsoleTraceListener ctl = new ConsoleTraceListener(false);
       ctl.TraceOutputOptions = TraceOptions.DateTime;

       Trace.Listeners.Add(twtl);
       Trace.Listeners.Add(ctl);
       Trace.AutoFlush = true;
       #endregion

       int batchSize = 1000;

       ParallelOptions parallelOptions = new ParallelOptions();
       parallelOptions.MaxDegreeOfParallelism = batchSize;

       ServicePointManager.DefaultConnectionLimit = 1000000;

       Parallel.For(0, batchSize, parallelOptions,
           j =>
           {
               Stopwatch sw1 = Stopwatch.StartNew();
               GetDataFromHttpClientAsync<List<AHNData>>(sw1);
           });
       Parallel.For(0, batchSize, parallelOptions,
            j =>
            {
                Stopwatch sw1 = Stopwatch.StartNew();
                GetDataFromHttpClientSync<List<AHNData>>(sw1);
            });
       Parallel.For(0, batchSize, parallelOptions,
            j =>
            {
                using (WebClient client = new WebClient())
                {
                   Stopwatch sw = Stopwatch.StartNew();
                   byte[] arr = client.DownloadData(_url);
                   sw.Stop();

                   Trace.WriteLine("WebClient Sync " + sw.ElapsedMilliseconds);
                }
           });

           Console.Read();
        }

        public static T GetDataFromWebClient<T>()
        {
            using (var webClient = new WebClient())
            {
                webClient.BaseAddress = _url;
                return JsonConvert.DeserializeObject<T>(
                    webClient.DownloadString(_url));
            }
        }

        public static void GetDataFromHttpClientSync<T>(Stopwatch sw)
        {
            HttpClient httpClient = new HttpClient();
            var response = httpClient.GetAsync(_url).Result;
            var obj = JsonConvert.DeserializeObject<T>(
                response.Content.ReadAsStringAsync().Result);
            sw.Stop();

            Trace.WriteLine("HttpClient Sync " + sw.ElapsedMilliseconds);
        }

        public static void GetDataFromHttpClientAsync<T>(Stopwatch sw)
        {
           HttpClient httpClient = new HttpClient();
           var response = httpClient.GetAsync(_url).ContinueWith(
              (a) => {
                 JsonConvert.DeserializeObject<T>(
                    a.Result.Content.ReadAsStringAsync().Result);
                 sw.Stop();
                 Trace.WriteLine("HttpClient Async " + sw.ElapsedMilliseconds);
              }, TaskContinuationOptions.None);
        }
    }
}

Мои вопросы

  1. REST-вызовы возвращаются через 3-4 секунды, что приемлемо. Вызовы службы REST инициируются в методах контроллера, которые вызываются из вызовов ajax. Начнем с того, что вызовы выполняются в другом потоке и не блокируют пользовательский интерфейс. Итак, я могу просто придерживаться синхронизации вызовов?
  2. Приведенный выше код был запущен в моем localbox. В настройке prod будут задействованы DNS и прокси. Есть ли преимущество использования HttpClientболее WebClient?
  3. Является ли HttpClientпараллелизм лучше WebClient? Из результатов теста я вижу, что WebClientсинхронизация вызовов работает лучше.
  4. Будет HttpClientли лучший выбор дизайна, если мы перейдем на .Net 4.5? Производительность является ключевым фактором дизайна.

5
Ваш тест несправедлив, GetDataFromHttpClientAsyncпотому что он выполняется первым, другие вызовы получают выгоду от потенциальной возможности кэширования данных (будь то на локальном компьютере или на любом прозрачном прокси между вами и пунктом назначения) и будут быстрее. Кроме того, при правильных условиях var response = httpClient.GetAsync("http://localhost:9000/api/values/").Result;может привести к тупиковой ситуации из-за истощения потоков резьбы пула. Никогда не следует блокировать действие, которое зависит от пула потоков в потоках ThreadPool, awaitвместо этого следует возвращать поток обратно в пул.
Скотт Чемберлен

1
HttpClient с клиентом Web API отлично подходит для клиента JSON / XML REST.
Кори Нельсон

@ Скотт Чемберлен - Спасибо за ваш ответ. Поскольку все тестовые вызовы выполняются в Parallel.Foreach, нет гарантии, какой из них будет запущен первым. Кроме того, если бы первый вызов службы был из GetDataFromHttpClientAsync, все последующие вызовы из GetDataFromHttpClientAsync должны были получить выгоду от кэша и работать быстрее. Я не видел этого в результате. Rgd жду, мы все еще используем 4.0. Я согласен с вами, что синхронизация HttpClient приведет к тупику, и я выбрал этот вариант из соображений дизайна.
user3092913

@CoryNelson Не могли бы вы пояснить, почему HttpClient с клиентом Web API идеально подходит для REST-клиента JSON / XML?
user3092913

2
Вот несколько слов о разнице между HttpClient и WebClient: blogs.msdn.com/b/henrikn/archive/2012/02/11/…
JustAndrei

Ответы:


243

Я живу в мире F # и Web API.

С Web API происходит много хорошего, особенно в виде обработчиков сообщений для безопасности и т. Д.

Я знаю, что мое мнение только одно, но я бы порекомендовал использовать его HttpClientдля любой будущей работы . Возможно, есть какой-то способ использовать некоторые другие компоненты System.Net.Httpбез непосредственного использования этой сборки, но я не представляю, как это будет работать в настоящее время.

Говоря о сравнении этих двух

  • HttpClient ближе к HTTP, чем WebClient.
  • HttpClient не предназначался для полной замены веб-клиента, поскольку существуют такие вещи, как ход выполнения отчета, настраиваемая схема URI и выполнение FTP-вызовов, которые предоставляет WebClient, но HttpClient этого не делает.
+--------------------------------------------+--------------------------------------------+
|               WebClient                    |               HttpClient                   |
+--------------------------------------------+--------------------------------------------+
| Available in older versions of .NET        | .NET 4.5 only.  Created to support the     |
|                                            | growing need of the Web API REST calls     |
+--------------------------------------------+--------------------------------------------+
| WinRT applications cannot use WebClient    | HTTPClient can be used with WinRT          |
+--------------------------------------------+--------------------------------------------+
| Provides progress reporting for downloads  | No progress reporting for downloads        |
+--------------------------------------------+--------------------------------------------+
| Does not reuse resolved DNS,               | Can reuse resolved DNS, cookie             |
| configured cookies                         | configuration and other authentication     |
+--------------------------------------------+--------------------------------------------+
| You need to new up a WebClient to          | Single HttpClient can make concurrent      |
| make concurrent requests.                  | requests                                   |
+--------------------------------------------+--------------------------------------------+
| Thin layer over WebRequest and             | Thin layer of HttpWebRequest and           |
| WebResponse                                | HttpWebResponse                            |
+--------------------------------------------+--------------------------------------------+
| Mocking and testing WebClient is difficult | Mocking and testing HttpClient is easy     |
+--------------------------------------------+--------------------------------------------+
| Supports FTP                               | No support for FTP                         |
+--------------------------------------------+--------------------------------------------+
| Both Synchronous and Asynchronous methods  | All IO bound methods in                    |
| are available for IO bound requests        | HTTPClient are asynchronous                |
+--------------------------------------------+--------------------------------------------+

Если вы используете .NET 4.5, пожалуйста, используйте асинхронные свойства HttpClient, которые Microsoft предоставляет разработчикам. HttpClient очень симметричен по отношению к братьям на стороне сервера HTTP, это HttpRequest и HttpResponse.

Обновление: 5 причин использования нового API HttpClient:

  • Сильно типизированные заголовки.
  • Общие кэши, файлы cookie и учетные данные
  • Доступ к файлам cookie и общим файлам cookie
  • Контроль за кэшированием и общим кэшем.
  • Вставьте свой модуль кода в конвейер ASP.NET. Более чистый и модульный код.

Ссылка

C # 5.0 Джозеф Албахари

(Channel9 - Video Build 2013)

Пять великих причин использовать новый HttpClient API для подключения к веб-сервисам

WebClient против HttpClient против HttpWebRequest


4
Следует отметить, что HttpClient доступен и для .NET 4.0 .
Тодд Менье

2
Это не объясняет, почему WebClient, кажется, на величины быстрее, чем HttpClient. Также, WebClientпохоже, теперь есть асинхронные методы.
раздавить

8
@crush это потому, что OP создает новый экземпляр HttpClient для каждого отдельного запроса. Вместо этого вы должны использовать один экземпляр HttpClient на весь срок службы вашего приложения. См. Stackoverflow.com/a/22561368/57369
Габриэль

6
Стоит отметить, WebClientчто нет в наличии, .Net Coreно HttpClientесть.
Пранав Сингх

3
С тех пор .Net Core 2.0 WebClient (среди тысяч других API) вернулся и доступен.
CoderBang

56

HttpClient является более новым из API, и он имеет преимущества

  • имеет хорошую модель асинхронного программирования
  • над ним работает Генрик Ф. Нильсон, который в основном является одним из изобретателей HTTP, и он разработал API, чтобы вам было легко следовать стандарту HTTP, например, при создании заголовков, соответствующих стандартам.
  • находится в .Net Framework 4.5, поэтому он имеет некоторый гарантированный уровень поддержки на обозримое будущее
  • также имеет версию библиотеки xcopyable / portable-framework, если вы хотите использовать ее на других платформах - .Net 4.0, Windows Phone и т. д.

Если вы пишете веб-сервис, который выполняет вызовы REST для других веб-сервисов, вам следует использовать модель асинхронного программирования для всех ваших вызовов REST, чтобы вы не столкнулись с истощением потоков. Возможно, вы также захотите использовать новейший компилятор C #, который поддерживает async / await.

Примечание: это не более производительный AFAIK. Вероятно, это будет примерно так же эффективно, если вы создадите честный тест.


Если бы у него был способ переключить прокси, это было бы безумием
ed22

4

HttpClientFactory

Важно оценить различные способы создания HttpClient, и частью этого является понимание HttpClientFactory.

https://docs.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests

Это не прямой ответ, я знаю - но лучше начинать здесь, чем заканчивать new HttpClient(...)везде.


3

Во-первых, я не являюсь авторитетом в WebClient против HttpClient, в частности. Во-вторых, из ваших комментариев выше, кажется, можно предположить, что WebClient синхронизирован ТОЛЬКО, тогда как HttpClient - оба.

Я сделал быстрый тест производительности, чтобы выяснить, как работают WebClient (синхронизация вызовов), HttpClient (синхронизация и асинхронизация). и вот результаты.

Я вижу в этом огромную разницу, думая о будущем, то есть о длительно запущенных процессах, адаптивном графическом интерфейсе и т. Д. (Добавьте к преимуществу, которое вы предлагаете с помощью платформы 4.5, которая в моем реальном опыте значительно быстрее в IIS)


4
WebClientпохоже, что в последних версиях .NET есть асинхронные возможности. Я хотел бы знать, почему он выглядит лучше, чем HttpClient в таких масштабных масштабах.
раздавить

1
Согласно stackoverflow.com/a/4988325/1662973 , кажется, что это то же самое, за исключением того факта, что одно является абстракцией другого. Возможно, это зависит от того, как объекты используются / загружаются. Минимальное время поддерживает утверждение о том, что webclient на самом деле является абстракцией HttpClient, поэтому накладные расходы составляют миллисекунды. Фреймворк может быть «скрытным» в том, как он действительно объединяет или избавляется от веб-клиента.
Энтони Хорн

2

У меня есть тест между HttpClient, WebClient, HttpWebResponse, а затем вызвать Rest Web Api

и результат Call Rest Web Api Benchmark

--------------------- Этап 1 ---- 10 Запрос

{00: 00: 17.2232544} ====> HttpClinet

{00: 00: 04.3108986} ====> WebRequest

{00: 00: 04.5436889} ====> WebClient

--------------------- Этап 1 ---- 10 Запрос - Малый размер

{00: 00: 17,2232544} ====> HttpClinet

{00: 00: 04,3108986} ====> WebRequest

{00: 00: 04,5436889} ====> WebClient

--------------------- Этап 3 ---- 10 запросов синхронизации - Малый размер

{00: 00: 15,3047502} ====> HttpClinet

{00: 00: 03,5505249} ====> WebRequest

{00: 00: 04,0761359} ====> WebClient

--------------------- Этап 4 ---- Запрос синхронизации 100 - Малый размер

{00: 03: 23,6268086} ====> HttpClinet

{00: 00: 47,1406632} ====> WebRequest

{00: 01: 01,2319499} ====> WebClient

--------------------- Этап 5 ---- 10 Запрос синхронизации - Максимальный размер

{00: 00: 58,1804677} ====> HttpClinet

{00: 00: 58,0710444} ====> WebRequest

{00: 00: 38,4170938} ====> WebClient

--------------------- Этап 6 ---- 10 Запрос синхронизации - Максимальный размер

{00: 01: 04,9964278} ====> HttpClinet

{00: 00: 59,1429764} ====> WebRequest

{00: 00: 32,0584836} ====> WebClient

_____ веб-клиент быстрее ()

var stopWatch = new Stopwatch();
        stopWatch.Start();
        for (var i = 0; i < 10; ++i)
        {
            CallGetHttpClient();
            CallPostHttpClient();
        }

        stopWatch.Stop();

        var httpClientValue = stopWatch.Elapsed;

        stopWatch = new Stopwatch();

        stopWatch.Start();
        for (var i = 0; i < 10; ++i)
        {
            CallGetWebRequest();
            CallPostWebRequest();
        }

        stopWatch.Stop();

        var webRequesttValue = stopWatch.Elapsed;


        stopWatch = new Stopwatch();

        stopWatch.Start();
        for (var i = 0; i < 10; ++i)
        {

            CallGetWebClient();
            CallPostWebClient();

        }

        stopWatch.Stop();

        var webClientValue = stopWatch.Elapsed;

// ------------------------- функции

private void CallPostHttpClient()
    {
        var httpClient = new HttpClient();
        httpClient.BaseAddress = new Uri("https://localhost:44354/api/test/");
        var responseTask = httpClient.PostAsync("PostJson", null);
        responseTask.Wait();

        var result = responseTask.Result;
        var readTask = result.Content.ReadAsStringAsync().Result;

    }
    private void CallGetHttpClient()
    {
        var httpClient = new HttpClient();
        httpClient.BaseAddress = new Uri("https://localhost:44354/api/test/");
        var responseTask = httpClient.GetAsync("getjson");
        responseTask.Wait();

        var result = responseTask.Result;
        var readTask = result.Content.ReadAsStringAsync().Result;

    }
    private string CallGetWebRequest()
    {
        var request = (HttpWebRequest)WebRequest.Create("https://localhost:44354/api/test/getjson");

        request.Method = "GET";
        request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;

        var content = string.Empty;

        using (var response = (HttpWebResponse)request.GetResponse())
        {
            using (var stream = response.GetResponseStream())
            {
                using (var sr = new StreamReader(stream))
                {
                    content = sr.ReadToEnd();
                }
            }
        }

        return content;
    }
    private string CallPostWebRequest()
    {

        var apiUrl = "https://localhost:44354/api/test/PostJson";


        HttpWebRequest httpRequest = (HttpWebRequest)WebRequest.Create(new Uri(apiUrl));
        httpRequest.ContentType = "application/json";
        httpRequest.Method = "POST";
        httpRequest.ContentLength = 0;

        using (var httpResponse = (HttpWebResponse)httpRequest.GetResponse())
        {
            using (Stream stream = httpResponse.GetResponseStream())
            {
                var json = new StreamReader(stream).ReadToEnd();
                return json;
            }
        }

        return "";
    }

    private string CallGetWebClient()
    {
        string apiUrl = "https://localhost:44354/api/test/getjson";


        var client = new WebClient();

        client.Headers["Content-type"] = "application/json";

        client.Encoding = Encoding.UTF8;

        var json = client.DownloadString(apiUrl);


        return json;
    }

    private string CallPostWebClient()
    {
        string apiUrl = "https://localhost:44354/api/test/PostJson";


        var client = new WebClient();

        client.Headers["Content-type"] = "application/json";

        client.Encoding = Encoding.UTF8;

        var json = client.UploadString(apiUrl, "");


        return json;
    }

1
Смотрите комментарий Габриэля выше. Короче говоря, HttpClient намного быстрее, если вы создаете один экземпляр HttpClient и используете его повторно.
LT Dan

1

Возможно, вы могли бы думать о проблеме по-другому. WebClientи HttpClientпо существу разные реализации одной и той же вещи. Что я рекомендую, так это внедрение шаблона внедрения зависимостей. с контейнером IoC во всем приложении. Вы должны создать клиентский интерфейс с более высоким уровнем абстракции, чем низкий уровень HTTP-передачи. Вы можете написать конкретные классы, которые используют оба WebClientи HttpClient, а затем использовать контейнер IoC для внедрения реализации через config.

То , что это позволит вам сделать будет переключаться между HttpClientи WebClientлегко , так что вы можете объективно тест в производственной среде.

Итак, такие вопросы, как:

Будет ли HttpClient лучшим выбором при переходе на .Net 4.5?

На самом деле можно получить объективный ответ путем переключения между двумя реализациями клиента с использованием контейнера IoC. Вот пример интерфейса, от которого вы можете зависеть, который не содержит никаких подробностей о HttpClientили WebClient.

/// <summary>
/// Dependency Injection abstraction for rest clients. 
/// </summary>
public interface IClient
{
    /// <summary>
    /// Adapter for serialization/deserialization of http body data
    /// </summary>
    ISerializationAdapter SerializationAdapter { get; }

    /// <summary>
    /// Sends a strongly typed request to the server and waits for a strongly typed response
    /// </summary>
    /// <typeparam name="TResponseBody">The expected type of the response body</typeparam>
    /// <typeparam name="TRequestBody">The type of the request body if specified</typeparam>
    /// <param name="request">The request that will be translated to a http request</param>
    /// <returns></returns>
    Task<Response<TResponseBody>> SendAsync<TResponseBody, TRequestBody>(Request<TRequestBody> request);

    /// <summary>
    /// Default headers to be sent with http requests
    /// </summary>
    IHeadersCollection DefaultRequestHeaders { get; }

    /// <summary>
    /// Default timeout for http requests
    /// </summary>
    TimeSpan Timeout { get; set; }

    /// <summary>
    /// Base Uri for the client. Any resources specified on requests will be relative to this.
    /// </summary>
    Uri BaseUri { get; set; }

    /// <summary>
    /// Name of the client
    /// </summary>
    string Name { get; }
}

public class Request<TRequestBody>
{
    #region Public Properties
    public IHeadersCollection Headers { get; }
    public Uri Resource { get; set; }
    public HttpRequestMethod HttpRequestMethod { get; set; }
    public TRequestBody Body { get; set; }
    public CancellationToken CancellationToken { get; set; }
    public string CustomHttpRequestMethod { get; set; }
    #endregion

    public Request(Uri resource,
        TRequestBody body,
        IHeadersCollection headers,
        HttpRequestMethod httpRequestMethod,
        IClient client,
        CancellationToken cancellationToken)
    {
        Body = body;
        Headers = headers;
        Resource = resource;
        HttpRequestMethod = httpRequestMethod;
        CancellationToken = cancellationToken;

        if (Headers == null) Headers = new RequestHeadersCollection();

        var defaultRequestHeaders = client?.DefaultRequestHeaders;
        if (defaultRequestHeaders == null) return;

        foreach (var kvp in defaultRequestHeaders)
        {
            Headers.Add(kvp);
        }
    }
}

public abstract class Response<TResponseBody> : Response
{
    #region Public Properties
    public virtual TResponseBody Body { get; }

    #endregion

    #region Constructors
    /// <summary>
    /// Only used for mocking or other inheritance
    /// </summary>
    protected Response() : base()
    {
    }

    protected Response(
    IHeadersCollection headersCollection,
    int statusCode,
    HttpRequestMethod httpRequestMethod,
    byte[] responseData,
    TResponseBody body,
    Uri requestUri
    ) : base(
        headersCollection,
        statusCode,
        httpRequestMethod,
        responseData,
        requestUri)
    {
        Body = body;
    }

    public static implicit operator TResponseBody(Response<TResponseBody> readResult)
    {
        return readResult.Body;
    }
    #endregion
}

public abstract class Response
{
    #region Fields
    private readonly byte[] _responseData;
    #endregion

    #region Public Properties
    public virtual int StatusCode { get; }
    public virtual IHeadersCollection Headers { get; }
    public virtual HttpRequestMethod HttpRequestMethod { get; }
    public abstract bool IsSuccess { get; }
    public virtual Uri RequestUri { get; }
    #endregion

    #region Constructor
    /// <summary>
    /// Only used for mocking or other inheritance
    /// </summary>
    protected Response()
    {
    }

    protected Response
    (
    IHeadersCollection headersCollection,
    int statusCode,
    HttpRequestMethod httpRequestMethod,
    byte[] responseData,
    Uri requestUri
    )
    {
        StatusCode = statusCode;
        Headers = headersCollection;
        HttpRequestMethod = httpRequestMethod;
        RequestUri = requestUri;
        _responseData = responseData;
    }
    #endregion

    #region Public Methods
    public virtual byte[] GetResponseData()
    {
        return _responseData;
    }
    #endregion
}

Полный код

Реализация HttpClient

Вы можете использовать, Task.Runчтобы сделатьWebClient асинхронного запуска в своей реализации.

Внедрение зависимостей, если все сделано правильно, помогает облегчить необходимость принятия решений низкого уровня заранее. В конечном счете, единственный способ узнать верный ответ - попробовать как в реальной среде, так и посмотреть, какой из них работает лучше всего. Вполне возможно, что это WebClientможет работать лучше для некоторых клиентов, и HttpClientможет работать лучше для других. Вот почему абстракция важна. Это означает, что код можно быстро поменять местами или изменить с помощью конфигурации, не меняя основной дизайн приложения.


1

Непопулярное мнение с 2020 года:

Когда дело доходит до ASP.NET приложений я предпочитаю WebClientболее , HttpClientпотому что:

  1. Современная реализация поставляется с асинхронными / ожидаемыми методами, основанными на задачах
  2. Имеет меньший объем памяти и в 2-5 раз быстрее (другие ответы уже упоминали об этом)
  3. Предлагается " повторно использовать один экземпляр HttpClient в течение срока службы вашего приложения ». Но у ASP.NET нет «времени жизни приложения», только время жизни запроса.
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.