Перейти к основному содержимому

Сетевое взаимодействие в C#

Разработчику Архитектору

Работа с сетью

Протоколы TCP и UDP

TCP — надёжный поток байтов с установкой соединения. В .NET: TcpClient / TcpListener (System.Net.Sockets).

UDP — датаграммы без гарантии доставки. В .NET: UdpClient.

Для прикладных API почти всегда достаточно HTTP поверх TCP; сырые сокеты — игры, протоколы устройств, высоконагруженные шлюзы.

using var client = new TcpClient();
await client.ConnectAsync("example.com", 80);
using NetworkStream stream = client.GetStream();

Разбор:

  • TcpClient создаёт клиентский TCP-сокет для исходящего подключения.
  • ConnectAsync("example.com", 80) асинхронно устанавливает соединение с хостом и портом HTTP.
  • await освобождает поток во время сетевого рукопожатия.
  • GetStream() возвращает NetworkStream для чтения и записи байтов по установленному соединению.
  • using гарантирует закрытие сокета и потока после завершения работы.

HTTP и HTTPS

ТемаВ .NET
КлиентHttpClient (System.Net.Http)
УстарелоHttpWebRequest, WebClient
Жизненный циклIHttpClientFactory в ASP.NET Core
МетодыGET, POST, PUT, PATCH, DELETE
ТелоStringContent, ByteArrayContent, MultipartFormDataContent
Коды ответа2xx успех, 4xx клиент, 5xx сервер — свойство StatusCode

HTTP (HyperText Transfer Protocol) — протокол прикладного уровня для передачи данных. Мы его изучали и в первом томе, и будем возвращаться в третьем. И C#, как серверный язык, неизбежно связан с HTTP в том числе - на практике придётся сталкиваться с различными задачами, особенно при интеграциях - получение данных (например, список пользователей), отправлять данные, загружать файлы, авторизовываться.

В ранних версиях .NET Framework использовались первые низкоуровневые классы для HTTP-запросов - HttpWebRequest и HttpWebResponse. Сейчас в .NET уже используется - HttpClient, из пространства имён System.Net.Http.

В C# с HTTP можно делать всё что позволяет этот протокол.

Краткий пример GET (для учебного скрипта):

using var client = new HttpClient();
string response = await client.GetStringAsync("https://api.example.com/data");
Console.WriteLine(response);

Разбор:

  • HttpClient предоставляет высокоуровневый API поверх HTTP.
  • GetStringAsync(...) отправляет GET-запрос и возвращает тело ответа строкой.
  • await делает вызов неблокирующим для текущего потока.
  • response содержит полезную нагрузку, обычно JSON или текст.
  • Console.WriteLine(response) выводит полученный контент для быстрой диагностики.

В веб-приложении и долгоживущих сервисах регистрируйте клиент через фабрику — иначе частое new HttpClient() может исчерпать сокеты:

// Program.cs (ASP.NET Core)
builder.Services.AddHttpClient("MyApi", c =>
{
c.BaseAddress = new Uri("https://api.example.com/");
c.Timeout = TimeSpan.FromSeconds(30);
});

Разбор:

  • AddHttpClient("MyApi", ...) регистрирует именованный HTTP-клиент в DI-контейнере.
  • BaseAddress задаёт базовый URI, чтобы в запросах использовать относительные пути.
  • Timeout ограничивает максимальное время ожидания ответа.
  • Конфигурация централизует сетевые настройки и упрощает сопровождение интеграций.
  • IHttpClientFactory переиспользует внутренние хендлеры и снижает риск socket exhaustion.

При работе с интеграциями важно предварительно изучить API, типы данных, примеры ответов и запросов, чтобы не допускать ошибок. А если всё подробно описано, то, как и в других языках, возможностей здесь куча.

Можно добавить обработку статус-кода, проверив, успешен ли запрос - для этого используется HttpResponseMessage:

HttpResponseMessage response = await client.GetAsync("https://api.example.com/data");
if (response.IsSuccessStatusCode)
{
string content = await response.Content.ReadAsStringAsync();
var data = JsonSerializer.Deserialize<DataModel>(content);
}

Разбор:

  • GetAsync(...) возвращает HttpResponseMessage с кодом, заголовками и телом.
  • IsSuccessStatusCode проверяет, попал ли статус в диапазон 2xx.
  • ReadAsStringAsync() асинхронно читает тело ответа в строку.
  • JsonSerializer.Deserialize<DataModel>(content) преобразует JSON в типизированную модель.
  • Проверка статуса до десериализации защищает от попытки распарсить ошибочный ответ как валидные данные.

В данном случае IsSuccessStatusCode - true, если код 2xx (например, 200 OK), а Content - тело ответа, из которого можно прочитать строку, поток, байты и т.д.

Можно отправлять POST-запросы, к примеру, JSON - для этого нужно сериализовать объект в JSON, указать тип контента (application/json), и отправить через PostAsync:

var data = new { Name = "Alice", Age = 25 };
string json = JsonSerializer.Serialize(data);
var content = new StringContent(json, Encoding.UTF8, "application/json");
HttpResponseMessage response = await client.PostAsync("https://api.example.com/users", content);

Разбор:

  • Анонимный объект new { Name = ..., Age = ... } формирует payload для отправки.
  • JsonSerializer.Serialize(data) превращает объект в JSON-строку.
  • StringContent(..., "application/json") задаёт тело запроса и корректный Content-Type.
  • PostAsync(...) отправляет POST-запрос с телом на сервер.
  • Ответ приходит как HttpResponseMessage, который затем проверяют по статусу и содержимому.

Поддерживается и работа с заголовками (headers). Это метаинформация запроса, их можно добавлять глобально или для каждого запроса. К примеру:

client.DefaultRequestHeaders.Add("Authorization", "Bearer token123");
client.DefaultRequestHeaders.UserAgent.ParseAdd("MyApp/1.0");

Разбор:

  • DefaultRequestHeaders задаёт заголовки, автоматически добавляемые ко всем запросам клиента.
  • Authorization: Bearer ... передаёт токен доступа для защищённых API.
  • UserAgent.ParseAdd("MyApp/1.0") указывает идентификатор клиента для логов и аналитики сервера.
  • Такой подход удобен для общих заголовков, которые повторяются в каждом вызове.
  • Для разовых заголовков лучше использовать HttpRequestMessage и устанавливать их локально.

Чтение заголовков ответа:

var response = await client.GetAsync("https://api.example.com");
foreach (var header in response.Headers)
{
Console.WriteLine($"{header.Key}: {string.Join(", ", header.Value)}");
}

Разбор:

  • response.Headers содержит коллекцию HTTP-заголовков ответа.
  • foreach перебирает пары "имя заголовка -> список значений".
  • string.Join(", ", header.Value) объединяет множественные значения одного заголовка в строку.
  • Console.WriteLine(...) помогает быстро отладить поведение API и прокси.
  • Такой вывод полезен для проверки кэширования, авторизации и ограничений по rate-limit.

Query Parameters (параметры запроса) позволяют вручную формировать URL, передавая параметры в URL (?page=1&limit=10). Тут два варианта. Вручную:

string url = "https://api.example.com/users?page=1&limit=10";

Разбор:

  • Строка вручную формирует URL с query-параметрами page и limit.
  • Такой способ простой, но менее надёжный при динамических значениях и кодировании спецсимволов.
  • При усложнении параметров повышается риск ошибок в конкатенации строки.
  • Подходит для статических учебных примеров и быстрых прототипов.
  • В production-коде чаще используют утилиты сборки query-строки.

В ASP.NET Core удобнее QueryHelpers из Microsoft.AspNetCore.WebUtilities:

var query = new Dictionary<string, string?> { ["page"] = "1", ["limit"] = "10" };
string url = QueryHelpers.AddQueryString("https://api.example.com/users", query);

Разбор:

  • Словарь query хранит параметры запроса в структурированном виде.
  • QueryHelpers.AddQueryString(...) корректно добавляет параметры к базовому URL.
  • Метод автоматически экранирует значения и снижает риск ошибок ручной сборки строки.
  • Подход удобен для динамических фильтров и пагинации.
  • Код легче расширять: новые параметры добавляются в словарь без переписывания URL.

Для загрузки файлов используется multipart/form-data. Пример:

using var content = new MultipartFormDataContent();
content.Add(new StreamContent(File.OpenRead("photo.jpg")), "file", "photo.jpg");
content.Add(new StringContent("123"), "userId");
var response = await client.PostAsync("https://api.example.com/upload", content);

Разбор:

  • MultipartFormDataContent формирует тело multipart/form-data для смешанных полей и файлов.
  • StreamContent(File.OpenRead("photo.jpg")) добавляет файловый поток без полной загрузки в память.
  • Аргументы "file" и "photo.jpg" задают имя поля формы и имя файла в multipart-части.
  • StringContent("123") добавляет обычное текстовое поле userId.
  • PostAsync(..., content) отправляет весь multipart-пакет на endpoint загрузки.

Если нужно отправить PDF, изображение и т.п., используется отправка бинарных данных. ByteArrayContent — для любых бинарных данных:

byte[] fileBytes = File.ReadAllBytes("document.pdf");
var byteContent = new ByteArrayContent(fileBytes);
byteContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/pdf");
await client.PostAsync("https://api.example.com/upload", byteContent);

Разбор:

  • File.ReadAllBytes(...) загружает файл целиком в массив байтов.
  • ByteArrayContent оборачивает байты в HTTP-тело запроса.
  • ContentType = "application/pdf" явно сообщает серверу MIME-тип отправляемых данных.
  • PostAsync(...) отправляет бинарное тело как обычный POST-запрос.
  • Для очень больших файлов чаще выбирают StreamContent, чтобы не держать всё в памяти.

Для авторизации можно использовать несколько вариантов:

  1. Basic Auth:
var authToken = Convert.ToBase64String(Encoding.UTF8.GetBytes("user:password"));
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authToken);

Разбор:

  • Encoding.UTF8.GetBytes("user:password") преобразует пару логин/пароль в байты.
  • Convert.ToBase64String(...) кодирует байты в Base64-строку для заголовка Basic Auth.
  • AuthenticationHeaderValue("Basic", authToken) формирует корректный заголовок Authorization.
  • Заголовок устанавливается в DefaultRequestHeaders и применяется ко всем запросам клиента.
  • Basic Auth обязательно используют только поверх HTTPS, чтобы не раскрывать учётные данные.
  1. Bearer Token (OAuth 2.0):
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "token123");

Разбор:

  • Схема Bearer передаёт токен доступа, выданный системой аутентификации.
  • Токен добавляется в заголовок Authorization и проверяется сервером на каждом запросе.
  • Такой механизм стандартен для OAuth 2.0 и JWT-интеграций.
  • В реальных проектах токен обновляют по сроку жизни и не хранят в коде.
  • Установка в DefaultRequestHeaders удобна для клиента, работающего с одним защищённым API.
  1. OAuth 2.0 с HttpClientFactory:
services.AddHttpClient("SecureApi")
.AddHttpMessageHandler(() => new BearerTokenHandler("token123"));

Разбор:

  • AddHttpClient("SecureApi") создаёт именованный клиент с отдельной конфигурацией.
  • AddHttpMessageHandler(...) подключает кастомный delegating handler в конвейер отправки.
  • BearerTokenHandler централизованно подставляет токен перед каждым запросом.
  • Такой подход избавляет бизнес-код от ручной работы с заголовками авторизации.
  • Через handler удобно добавить автообновление токена и единое логирование.

В C# для работы с HTTP-запросами используется набор классов из пространства имен System.Net.Http. Они предоставляют гибкий и мощный API для выполнения HTTP-запросов, обработки ответов и управления соединениями.

HttpClient для отправки запросов и получения ответов. Имеет:

  • GetAsync, PostAsync, PutAsync, DeleteAsync — асинхронные методы для разных HTTP-методов;
  • SendAsync — универсальный метод для отправки кастомных запросов (HttpRequestMessage);
  • GetStringAsync, GetStreamAsync, GetByteArrayAsync — упрощённые методы для получения данных.

Важные свойства:

  • BaseAddress — базовый URL для относительных путей.
  • DefaultRequestHeaders — заголовки, отправляемые с каждым запросом.
  • Timeout — максимальное время ожидания ответа.

HttpClientHandler используется для настройки поведения HTTP-клиента, контролирует низкоуровневые аспекты HTTP-соединений (прокси, SSL, куки и прочее).

HttpMethod — представление HTTP-метода (GET, POST и др.), описывает тип запроса:

  • HttpMethod.Get;
  • HttpMethod.Post;
  • HttpMethod.Put;
  • HttpMethod.Delete;
  • HttpMethod.Patch (появился в .NET 5+).

HttpContent — абстрактный класс для тела запроса/ответа. Представляет содержимое HTTP-сообщения (тело запроса или ответа). У него есть наследники:

  • StringContent — текст (JSON, XML, plain text);
  • ByteArrayContent — бинарные данные (файлы, изображения);
  • MultipartFormDataContent — отправка форм с файлами;
  • StreamContent — потоковые данные.

HttpRequestMessage — настройка запроса. Полная кастомизация HTTP-запроса (метод, URL, заголовки, тело).

HttpResponseMessage — обработка ответа. Содержит данные HTTP-ответа (статус, заголовки, тело).

Основные свойства:

  • StatusCode — код ответа (200 OK, 404 Not Found и т. д.).
  • IsSuccessStatusCode — true, если статус 2xx.
  • Headers — заголовки ответа.
  • Content — тело ответа (HttpContent).

HttpMessageHandler и цепочки обработчиков позволяют встраивать middleware-логику (логирование, аутентификацию).

IHttpClientFactory — фабрика для управления HttpClient. HttpClient реализует IDisposable, но его не следует создавать и уничтожать часто (риск исчерпания сокетов). Это управление жизненным циклом HttpClient.


Отправка email

SmtpClient в BCL помечен как устаревший для новых приложений. На практике используют:

  • транзакционные API (SendGrid, Mailgun, Amazon SES) через HTTP;
  • корпоративный SMTP с TLS (порт 587) через специализированные библиотеки.

Минимальная схема — хост, порт, TLS, учётные данные, From / To, тело (текст или HTML).


Прокси-сервер

Прокси перенаправляет исходящий HTTP-трафик (корпоративная сеть, отладка). В HttpClientHandler:

var handler = new HttpClientHandler
{
Proxy = new WebProxy("http://proxy.company:8080"),
UseProxy = true
};
using var client = new HttpClient(handler);

Разбор:

  • HttpClientHandler настраивает низкоуровневое поведение HTTP-клиента.
  • Proxy = new WebProxy(...) задаёт адрес прокси-сервера для исходящего трафика.
  • UseProxy = true явно включает работу через прокси в этом клиенте.
  • new HttpClient(handler) связывает клиент с настроенным обработчиком.
  • Конфигурация полезна в корпоративных сетях и при отладке через proxy tools.

Переменные окружения HTTP_PROXY / HTTPS_PROXY также учитываются средой выполнения.


Практический шаблон устойчивого HTTP-клиента

Для реальных интеграций удобно использовать один рабочий шаблон:

  1. Именованный или типизированный клиент через IHttpClientFactory.
  2. Таймауты и повторные попытки только для идемпотентных операций.
  3. Явная обработка StatusCode и полезные логи для диагностики.
  4. Отдельные DTO для запроса и ответа, без анонимных структур в бизнес-слое.

Такой каркас упрощает сопровождение и снижает риск хаотичных HttpClient-вызовов по всему проекту.


Что часто ломает сетевой слой

  • Создание new HttpClient() на каждый запрос.
  • Отсутствие CancellationToken в долгих вызовах.
  • Слепой EnsureSuccessStatusCode() без анализа тела ошибки.
  • Смешивание бизнес-логики и HTTP-логики в одном методе.

Куда перейти дальше


Основа по протоколу

Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.