🔷 C#

Работа с сетью и HTTP API в C# (.NET): подробное руководство

Что это такое и зачем? (аналогия из реальной жизни)

Представьте, что вы сидите в кафе и хотите заказать пиццу. Вы не идёте на кухню сами, не лезете в печь и не трогаете тесто. Вместо этого вы подзываете официанта, говорите ему свой заказ, он уходит на кухню, а через некоторое время возвращается с готовой пиццей. Вы даже не знаете, как именно её готовили — вам важен только результат.

Работа с сетью и HTTP API в программировании работает точно так же. Ваша программа (клиент) отправляет запрос на удалённый сервер (как заказ официанту), сервер обрабатывает этот запрос и возвращает ответ (как готовое блюдо). Всё это происходит по правилам, которые называются HTTP (HyperText Transfer Protocol — протокол передачи гипертекста).

Зачем это нужно? Представьте, что вы пишете приложение для прогноза погоды. Вы же не будете сами устанавливать метеостанции по всему миру? Вместо этого вы обращаетесь к сервису погоды (например, OpenWeatherMap), отправляете запрос: «Какая погода в Москве?», и получаете готовый ответ с температурой, влажностью и ветром. Или, например, вы пишете интернет-магазин — вам нужно получать список товаров, добавлять новые заказы, проверять статус доставки. Всё это делается через HTTP API.

Важно: API (Application Programming Interface) — это просто набор правил, по которым программы общаются друг с другом. HTTP API — это API, работающий поверх протокола HTTP. Когда вы слышите «REST API» или «Web API» — это всё разновидности HTTP API.

В современном мире практически ни одно серьёзное приложение не обходится без работы с сетью. Будь то мобильное приложение, веб-сайт, десктопная программа или игра — все они постоянно обмениваются данными с серверами. И C# (.NET) предоставляет для этого мощные и удобные инструменты.

Теория: как это работает изнутри

Давайте заглянем «под капот» и разберёмся, что происходит, когда ваша программа отправляет HTTP-запрос.

Основные понятия HTTP

HTTP — это текстовый протокол. Это значит, что запросы и ответы — это просто строки текста, которые передаются по сети. У каждого HTTP-сообщения есть три основные части:

  1. Стартовая строка — содержит метод запроса (GET, POST, PUT, DELETE и др.), путь к ресурсу и версию протокола.
  2. Заголовки (Headers) — мета-информация о запросе или ответе (тип данных, длина сообщения, куки, авторизация и т.д.).
  3. Тело (Body) — сами данные (необязательно, в GET-запросах тела нет).

Методы HTTP (глаголы)

Метод HTTP показывает, какое действие мы хотим выполнить. Самые популярные:

Метод Что делает Аналогия из жизни
GET Получить данные Посмотреть меню в кафе
POST Создать новый ресурс Сделать заказ
PUT Полностью заменить ресурс Заменить всё блюдо на новое
PATCH Частично изменить ресурс Попросить добавить сыра в пиццу
DELETE Удалить ресурс Отменить заказ

Коды ответа (Status Codes)

Сервер всегда отвечает числовым кодом, который говорит о результате операции. Коды сгруппированы по сотням:

Диапазон Категория Примеры
1xx Информационные 100 Continue (продолжай)
2xx Успех 200 OK, 201 Created (создано)
3xx Перенаправление 301 Moved Permanently (переехало)
4xx Ошибка клиента 400 Bad Request (плохой запрос), 404 Not Found (не найдено), 401 Unauthorized (не авторизован)
5xx Ошибка сервера 500 Internal Server Error (внутренняя ошибка сервера), 503 Service Unavailable (сервис недоступен)

URL — адрес ресурса

URL (Uniform Resource Locator) — это адрес, по которому мы обращаемся к ресурсу. Он состоит из нескольких частей:

https://api.weather.com/v2/weather?city=Moscow&units=metric
|______|  |______________| |___| |________________________|
протокол      домен        путь        параметры запроса
  • Протокол — обычно http или https (защищённый).
  • Домен — адрес сервера (может быть IP-адресом).
  • Путь — указывает на конкретный ресурс на сервере.
  • Параметры запроса (Query String) — начинаются с ?, разделяются &. Используются для передачи дополнительных данных в GET-запросах.

Форматы данных: JSON и XML

Когда сервер возвращает данные, они должны быть в каком-то формате, который понятен и человеку, и программе. Самый популярный сегодня — JSON (JavaScript Object Notation). Выглядит он так:

{
  "city": "Moscow",
  "temperature": 22.5,
  "humidity": 65,
  "wind": {
    "speed": 3.5,
    "direction": "NW"
  }
}

JSON очень похож на объекты в C#: есть ключи (строки) и значения (строки, числа, булевы значения, массивы или другие объекты). Именно поэтому JSON так удобен — его легко преобразовать в объекты C# и обратно. Этот процесс называется сериализация (из объекта в JSON) и десериализация (из JSON в объект).

Важно: В современных .NET приложениях для работы с JSON используется библиотека System.Text.Json (встроена в .NET) или Newtonsoft.Json (Json.NET) — более старая, но всё ещё популярная сторонняя библиотека.

Синтаксис в C# (.NET): все формы и варианты

В C# есть несколько способов отправлять HTTP-запросы. Мы рассмотрим основные, от простых к более продвинутым.

1. HttpClient — главный инструмент

HttpClient — это основной класс в .NET для отправки HTTP-запросов и получения ответов. Он находится в пространстве имён System.Net.Http.

Важное правило: HttpClient предназначен для многократного использования. Не создавайте новый экземпляр на каждый запрос! Создайте один раз и используйте его на протяжении всей работы программы. Для этого в современных приложениях используется IHttpClientFactory (фабрика HttpClient), но для простых примеров мы будем создавать один статический экземпляр.

2. Основные методы HttpClient

Метод Описание HTTP метод
GetAsync(url) Отправляет GET-запрос GET
PostAsync(url, content) Отправляет POST-запрос с телом POST
PutAsync(url, content) Отправляет PUT-запрос с телом PUT
PatchAsync(url, content) Отправляет PATCH-запрос с телом PATCH
DeleteAsync(url) Отправляет DELETE-запрос DELETE
SendAsync(request) Универсальный метод для любого запроса Любой

3. Классы для работы с содержимым

Для передачи данных в теле запроса используются классы, наследующие HttpContent:

Класс Для чего
StringContent Для передачи строки (например, JSON)
ByteArrayContent Для передачи массива байт
FormUrlEncodedContent Для передачи данных формы (key=value)
MultipartFormDataContent Для передачи файлов и сложных форм

4. Обработка ответа

Методы HttpClient возвращают Task<HttpResponseMessage>. HttpResponseMessage содержит:

  • StatusCode — код ответа (например, 200, 404).
  • IsSuccessStatusCode — true, если код в диапазоне 200-299.
  • Content — тело ответа (тип HttpContent).
  • Headers — заголовки ответа.

Чтобы прочитать тело ответа как строку, используется метод ReadAsStringAsync(). Чтобы прочитать как массив байт — ReadAsByteArrayAsync(). Чтобы прочитать как поток — ReadAsStreamAsync().

5. Асинхронность — обязательное условие

Все сетевые операции в .NET — асинхронные. Это значит, что пока ваш запрос выполняется, программа не «зависает», а может делать другие дела. Методы возвращают Task или Task<T>, и их нужно вызывать с ключевым словом await.

Важно: Если вы забудете написать await, программа не дождётся ответа и пойдёт дальше, что приведёт к ошибкам. Компилятор предупредит вас об этом жёлтым подчёркиванием.

Примеры кода — от простого к сложному

Теперь перейдём к практике. Все примеры рабочие, вы можете скопировать их и запустить в своём проекте (консольное приложение .NET).

Пример 1: Простой GET-запрос (получение данных)

using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program
{
    // Создаём ОДИН экземпляр HttpClient на всю программу
    private static readonly HttpClient client = new HttpClient();

    static async Task Main(string[] args)
    {
        // Отправляем GET-запрос на сайт, который возвращает случайные факты
        string url = "https://catfact.ninja/fact";
        
        // Отправляем запрос и ждём ответ
        HttpResponseMessage response = await client.GetAsync(url);
        
        // Проверяем, успешен ли запрос
        if (response.IsSuccessStatusCode)
        {
            // Читаем тело ответа как строку
            string json = await response.Content.ReadAsStringAsync();
            Console.WriteLine("Ответ от сервера:");
            Console.WriteLine(json);
        }
        else
        {
            Console.WriteLine($"Ошибка! Код: {response.StatusCode}");
        }
    }
}

Объяснение:

  • HttpClient client = new HttpClient() — создаём клиента.
  • await client.GetAsync(url) — отправляем GET-запрос и ждём ответа. await заставляет программу ждать, но не блокирует её.
  • response.IsSuccessStatusCode — проверяем, что сервер ответил успехом (код 200-299).
  • await response.Content.ReadAsStringAsync() — читаем тело ответа как строку.

Пример 2: GET-запрос с параметрами (Query String)

using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program
{
    private static readonly HttpClient client = new HttpClient();

    static async Task Main(string[] args)
    {
        // Сайт, который возвращает данные о пользователях
        // Мы передаём параметры: количество пользователей и национальность
        string baseUrl = "https://randomuser.me/api/";
        string parameters = "?results=2&nat=us";
        string fullUrl = baseUrl + parameters;
        
        HttpResponseMessage response = await client.GetAsync(fullUrl);
        
        if (response.IsSuccessStatusCode)
        {
            string json = await response.Content.ReadAsStringAsync();
            Console.WriteLine(json);
        }
    }
}

Объяснение: Мы просто добавляем параметры к URL. В реальных проектах для построения URL с параметрами лучше использовать класс UriBuilder или библиотеку Flurl, но для простоты можно и так.

Пример 3: POST-запрос с JSON (отправка данных)

using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

class Program
{
    private static readonly HttpClient client = new HttpClient();

    static async Task Main(string[] args)
    {
        // Создаём объект с данными, которые хотим отправить
        var newUser = new
        {
            name = "Иван Иванов",
            email = "ivan@example.com",
            age = 25
        };
        
        // Сериализуем объект в JSON-строку
        string jsonContent = JsonSerializer.Serialize(newUser);
        Console.WriteLine($"Отправляем JSON: {jsonContent}");
        
        // Создаём содержимое запроса (StringContent)
        // Указываем, что это JSON и кодировка UTF-8
        var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
        
        // Отправляем POST-запрос на тестовый сервер
        string url = "https://jsonplaceholder.typicode.com/users";
        HttpResponseMessage response = await client.PostAsync(url, content);
        
        if (response.IsSuccessStatusCode)
        {
            string responseBody = await response.Content.ReadAsStringAsync();
            Console.WriteLine($"Сервер ответил: {responseBody}");
            Console.WriteLine($"Код статуса: {response.StatusCode}");
        }
    }
}

Объяснение:

  • JsonSerializer.Serialize(newUser) — превращаем анонимный объект в JSON.
  • new StringContent(...) — создаём содержимое запроса. Третий параметр — MIME-тип (application/json).
  • client.PostAsync(url, content) — отправляем POST-запрос с телом.

Пример 4: Десериализация JSON в объект C#

using System;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;

// Создаём класс, соответствующий структуре JSON-ответа
public class CatFact
{
    public string Fact { get; set; }  // Свойство "fact" из JSON
    public int Length { get; set; }   // Свойство "length" из JSON
}

class Program
{
    private static readonly HttpClient client = new HttpClient();

    static async Task Main(string[] args)
    {
        string url = "https://catfact.ninja/fact";
        HttpResponseMessage response = await client.GetAsync(url);
        
        if (response.IsSuccessStatusCode)
        {
            // Читаем JSON как строку
            string json = await response.Content.ReadAsStringAsync();
            
            // Десериализуем JSON в объект CatFact
            CatFact catFact = JsonSerializer.Deserialize(json);
            
            // Теперь можно работать с объектом!
            Console.WriteLine($"Факт о котах: {catFact.Fact}");
            Console.WriteLine($"Длина факта: {catFact.Length} символов");
        }
    }
}

Объяснение: Мы создали класс CatFact, поля которого соответствуют ключам в JSON. JsonSerializer.Deserialize<CatFact>(json) автоматически создаёт объект и заполняет его свойства. Обратите внимание: имена свойств в C# могут отличаться от ключей в JSON (регистр не важен по умолчанию).

Пример 5: Обработка ошибок и исключений

using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program
{
    private static readonly HttpClient client = new HttpClient();

    static async Task Main(string[] args)
    {
        // Попробуем обратиться к несуществующему адресу
        string url = "https://this-site-does-not-exist-12345.com/api";
        
        try
        {
            HttpResponseMessage response = await client.GetAsync(url);
            
            // Явно выбрасываем исключение, если код не успешный
            response.EnsureSuccessStatusCode();
            
            string content = await response.Content.ReadAsStringAsync();
            Console.WriteLine(content);
        }
        catch (HttpRequestException ex)
        {
            // Ошибка на уровне HTTP (нет сети, DNS не найден, таймаут и т.д.)
            Console.WriteLine($"Ошибка HTTP: {ex.Message}");
        }
        catch (TaskCanceledException ex)
        {
            // Запрос был отменён (например, превышен таймаут)
            Console.WriteLine($"Запрос отменён (таймаут): {ex.Message}");
        }
        catch (Exception ex)
        {
            // Любая другая ошибка
            Console.WriteLine($"Неизвестная ошибка: {ex.Message}");
        }
    }
}

Объяснение:

  • response.EnsureSuccessStatusCode() — если код ответа не 2xx, выбрасывает исключение HttpRequestException.
  • Мы ловим разные типы исключений, чтобы обработать их по-разному.
  • TaskCanceledException возникает, если запрос длится дольше, чем установленный таймаут (по умолчанию 100 секунд).

Пример 6: Отправка заголовков (Headers) — авторизация

using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;

class Program
{
    private static readonly HttpClient client = new HttpClient();

    static async Task Main(string[] args)
    {
        // Многие API требуют авторизации через заголовок Authorization
        // Обычно это токен вида "Bearer ваш_токен"
        string apiKey = "ваш_секретный_токен";
        
        // Добавляем заголовок Authorization ко всем запросам этого клиента
        client.DefaultRequestHeaders.Authorization = 
            new AuthenticationHeaderValue("Bearer", apiKey);
        
        // Также можно добавить пользовательский заголовок
        client.DefaultRequestHeaders.Add("X-Custom-Header", "MyValue");
        
        string url = "https://api.example.com/protected-data";
        HttpResponseMessage response = await client.GetAsync(url);
        
        if (response.IsSuccessStatusCode)
        {
            string data = await response.Content.ReadAsStringAsync();
            Console.WriteLine(data);
        }
        else
        {
            Console.WriteLine($"Ошибка: {response.StatusCode}");
            // Если 401 — значит, токен недействителен
        }
    }
}

Объяснение: Заголовки можно добавлять глобально (через DefaultRequestHeaders) — они будут отправляться с каждым запросом. Или можно добавлять их к конкретному запросу, используя класс HttpRequestMessage.

Пример 7: Использование HttpRequestMessage для полного контроля

using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

class Program
{
    private static readonly HttpClient client = new HttpClient();

    static async Task Main(string[] args)
    {
        // Создаём объект запроса вручную
        var request = new HttpRequestMessage(HttpMethod.Post, "https://jsonplaceholder.typicode.com/posts");
        
        // Добавляем заголовки к этому конкретному запросу
        request.Headers.Add("User-Agent", "MyApp/1.0");
        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        
        // Создаём тело запроса
        var postData = new { title = "Заголовок", body = "Текст поста", userId = 1 };
        string json = JsonSerializer.Serialize(postData);
        request.Content = new StringContent(json, Encoding.UTF8, "application/json");
        
        // Отправляем запрос
        HttpResponseMessage response = await client.SendAsync(request);
        
        if (response.IsSuccessStatusCode)
        {
            string responseBody = await response.Content.ReadAsStringAsync();
            Console.WriteLine($"Создан новый пост: {responseBody}");
        }
    }
}

Объяснение: HttpRequestMessage даёт полный контроль над запросом. Вы можете задать метод, URL, заголовки, тело — всё что угодно. Затем передаёте его в client.SendAsync(). Это самый гибкий способ.

Пример 8: Скачивание файла

using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;

class Program
{
    private static readonly HttpClient client = new HttpClient();

    static async Task Main(string[] args)
    {
        string fileUrl = "https://www.example.com/image.jpg";
        string savePath = "downloaded_image.jpg";
        
        try
        {
            // Отправляем GET-запрос и получаем поток с данными
            HttpResponseMessage response = await client.GetAsync(fileUrl);
            response.EnsureSuccessStatusCode();
            
            // Читаем содержимое как поток
            using (Stream contentStream = await response.Content.ReadAsStreamAsync())
            {
                // Создаём файл и копируем в него данные из потока
                using (FileStream fileStream = new FileStream(savePath, FileMode.Create, FileAccess.Write))
                {
                    await contentStream.CopyToAsync(fileStream);
                }
            }
            
            Console.WriteLine($"Файл скачан и сохранён как {savePath}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Ошибка при скачивании: {ex.Message}");
        }
    }
}

Объяснение: Для скачивания больших файлов не нужно читать всё в память — используем потоки. ReadAsStreamAsync() возвращает поток, который мы копируем в файл. using гарантирует, что потоки будут закрыты.

Сравнение с Python: чем похоже и чем отличается

Если вы уже знакомы с Python, вам будет легче понять C#. Давайте сравним основные моменты.

Аспект C# (.NET) Python
Основная библиотека System.Net.Http.HttpClient (встроенная) requests (сторонняя, но стандарт де-факто)
Создание клиента new HttpClient() (один на всю программу) requests.Session() (или просто requests.get())
GET-запрос await client.GetAsync(url) requests.get(url)
POST с JSON await client.PostAsync(url, content) requests.post(url, json=data)
Чтение ответа await response.Content.ReadAsStringAsync() response.text или response.json()
Параметры URL Вручную или через UriBuilder requests.get(url, params=params)
Заголовки client.DefaultRequestHeaders.Add() или через HttpRequestMessage requests.get(url, headers=headers)
Обработка ошибок try-catch + EnsureSuccessStatusCode() response.raise_for_status() в блоке try-except
Асинхронность Обязательная (async/await) Опциональная (через aiohttp или httpx)
Типизация Строгая (нужно указывать типы) Динамическая (типы не обязательны)

Главные отличия:

  1. Асинхронность: В C# работа с сетью всегда асинхронна. В Python вы можете использовать синхронный requests и не думать об этом. Но асинхронный код в C# — это не сложно, просто нужно привыкнуть к async и await.
  2. Типизация: В C# вы чаще работаете с классами и строгими типами. JSON десериализуется в объекты конкретных классов. В Python вы можете работать с JSON как со словарём напрямую.
  3. Управление ресурсами: В C# нужно быть аккуратным с HttpClient (не создавать на каждый запрос). В Python с requests этой проблемы нет, но для производительности тоже используют Session.

Типичные ошибки и подводные камни

Даже опытные разработчики иногда допускают эти ошибки. Вот список самых распространённых:

1. Создание нового HttpClient на каждый запрос

Проблема: Каждый новый экземпляр HttpClient открывает новые сокеты (сетевые соединения). Если вы создаёте их в цикле, вы можете исчерпать лимит сокетов в операционной системе, что приведёт к ошибке SocketException.

Решение: Создавайте один статический экземпляр HttpClient и используйте его повторно. Или используйте IHttpClientFactory (рекомендуется в ASP.NET Core).

2. Забытый await

Проблема: Если вы напишете client.GetAsync(url) без await, программа не дождётся ответа и продолжит выполнение. Вы получите не ответ, а Task (объект, представляющий асинхронную операцию).

Решение: Всегда используйте await при вызове асинхронных методов. Компилятор предупредит вас, если вы забудете.

3. Игнорирование кодов ошибок

Проблема: Не все ответы с кодом 200 — успешны. Некоторые API могут вернуть 200 с сообщением об ошибке в теле. И наоборот, код 404 может быть нормальным ответом (ресурс не найден, но это ожидаемо).

Решение: Всегда проверяйте IsSuccessStatusCode или используйте EnsureSuccessStatusCode(). Анализируйте тело ответа, даже если код успешный.

4. Неправильная обработка JSON

Проблема: Имена свойств в JSON и C# не совпадают. Например, в JSON ключ "first_name", а в C# свойство <