Сетевое взаимодействие в 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, чтобы не держать всё в памяти.
Для авторизации можно использовать несколько вариантов:
- 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, чтобы не раскрывать учётные данные.
- Bearer Token (OAuth 2.0):
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "token123");
Разбор:
- Схема
Bearerпередаёт токен доступа, выданный системой аутентификации. - Токен добавляется в заголовок
Authorizationи проверяется сервером на каждом запросе. - Такой механизм стандартен для OAuth 2.0 и JWT-интеграций.
- В реальных проектах токен обновляют по сроку жизни и не хранят в коде.
- Установка в
DefaultRequestHeadersудобна для клиента, работающего с одним защищённым API.
- 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-клиента
Для реальных интеграций удобно использовать один рабочий шаблон:
- Именованный или типизированный клиент через
IHttpClientFactory. - Таймауты и повторные попытки только для идемпотентных операций.
- Явная обработка
StatusCodeи полезные логи для диагностики. - Отдельные DTO для запроса и ответа, без анонимных структур в бизнес-слое.
Такой каркас упрощает сопровождение и снижает риск хаотичных HttpClient-вызовов по всему проекту.
Что часто ломает сетевой слой
- Создание
new HttpClient()на каждый запрос. - Отсутствие
CancellationTokenв долгих вызовах. - Слепой
EnsureSuccessStatusCode()без анализа тела ошибки. - Смешивание бизнес-логики и HTTP-логики в одном методе.
Куда перейти дальше
Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.