Лекция 13. Сетевое взаимодействие. Часть 2: надёжность, оптимизация, безопасность
Введение
В предыдущей лекции мы разобрали основы сетевого взаимодействия: протоколы,
форматы данных, работу с fetch и REST API в Expo/React Native. Но в реальном
приложении одного «умения отправить запрос» недостаточно. Мобильная сеть
нестабильна: пользователь заходит в метро, переключается с Wi-Fi на сотовую
сеть, теряет сигнал. Сервер может временно не отвечать, а злоумышленник в
публичной сети — попытаться перехватить трафик. Эта лекция о том, что отличает
учебный код от рабочего приложения:
- как классифицировать сетевые ошибки и правильно на них реагировать;
- как грамотно повторять запросы (retry с экспоненциальной задержкой);
- как кэшировать данные, чтобы приложение оставалось отзывчивым;
- как оптимизировать сетевой трафик (батчинг, дедупликация, пагинация, сжатие, отмена запросов);
- как защитить передаваемые данные и токены;
- какие современные технологии (Edge Computing, GraphQL, gRPC) меняют подход к сетевому слою.
1. Классификация сетевых ошибок
Прежде чем реагировать на ошибку, её нужно правильно квалифицировать. Реакция на разные классы ошибок принципиально различается: одни имеет смысл повторять, другие — нет.
1.1. Три класса ошибок
| Класс | Примеры | Природа | Правильная реакция |
|---|---|---|---|
| Временные (transient) | Таймаут, потеря сети, 503 Service Unavailable, 429 Too Many Requests | Состояние временное, скоро пройдёт | Повторить запрос с задержкой (retry + backoff) |
| Постоянные (permanent) | 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 422 | Ошибка в самом запросе или правах | НЕ повторять; исправить запрос, обновить токен или показать сообщение |
| Системные (fatal/client) | 500 Internal Server Error, краш парсинга JSON, ошибка в коде клиента | Сбой на стороне сервера или баг клиента | Залогировать, уведомить пользователя, при 500 возможен ограниченный retry |
Ключевая идея: повторять имеет смысл только временные ошибки. Повтор
постоянной ошибки (например, 400) бесполезен — запрос так и останется
некорректным, мы лишь зря потратим трафик и батарею.
1.2. Как различать на практике
Правило простое: отсутствие ответа (нет сети, таймаут) и коды 408, 429,
5xx считаем временными (transient) и пробуем повторить; остальные 4xx
(400, 401, 403, 404, 422) — постоянные (permanent), повторять
бессмысленно.
Особый случай — 401 Unauthorized: формально это постоянная ошибка, но
зачастую её можно «вылечить» один раз, обновив access-токен через
refresh-токен, после чего повторить исходный запрос.
2. Стратегия повторных попыток (retry)
2.1. Зачем нужна экспоненциальная задержка
Наивный подход «повторять сразу же, пока не получится» вреден: если сервер перегружен, сотни клиентов будут долбить его одновременно, усугубляя проблему. Это явление называют thundering herd («стадо громовержцев»).
Решение — экспоненциальная задержка (exponential backoff): каждая следующая попытка ждёт вдвое дольше предыдущей. К ней добавляют jitter (случайный разброс), чтобы клиенты не синхронизировались и не приходили на сервер ровно в один и тот же момент.
Попытка 1 → ждём ~1 cПопытка 2 → ждём ~2 cПопытка 3 → ждём ~4 cПопытка 4 → ждём ~8 c (но не больше maxDelay)2.2. Алгоритм retry + backoff + jitter (псевдокод)
ФУНКЦИЯ retry(operation, maxRetries, baseDelay, maxDelay): attempt ← 0 ПОВТОРЯТЬ: ПОПРОБОВАТЬ: результат ← ВЫПОЛНИТЬ operation() ВЕРНУТЬ результат // успех ПЕРЕХВАТИТЬ error: ЕСЛИ classify(error) ≠ "transient": // постоянная/системная — ВОЗБУДИТЬ error // повторять нельзя ЕСЛИ attempt ≥ maxRetries: ВОЗБУДИТЬ error // попытки кончились // экспоненциальная задержка delay ← MIN(baseDelay × 2^attempt, maxDelay) // jitter: случайный множитель 0.5..1.0 («full jitter») delay ← delay × СЛУЧАЙНОЕ(0.5, 1.0) ЖДАТЬ delay attempt ← attempt + 1Реализация на JavaScript для React Native:
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function retry(operation, { maxRetries = 3, baseDelay = 1000, maxDelay = 16000 } = {}) { for (let attempt = 0; ; attempt++) { try { return await operation(); } catch (error) { if (!error.isTransient || attempt >= maxRetries) throw error; const backoff = Math.min(baseDelay * 2 ** attempt, maxDelay); await sleep(backoff * (0.5 + Math.random() * 0.5)); // full jitter } }}2.3. Когда НЕ нужно повторять: идемпотентность
Идемпотентная операция — это операция, многократное выполнение которой даёт тот же результат, что и однократное. Это центральное понятие для безопасного retry.
| HTTP-метод | Идемпотентен? | Можно повторять? |
|---|---|---|
GET | Да | Да, безопасно |
PUT | Да | Да |
DELETE | Да | Да |
HEAD | Да | Да |
POST | Нет | Осторожно — может создать дубликат |
Опасность: если мы отправили POST /orders (создать заказ), не дождались
ответа из-за таймаута и повторили запрос — заказ может создаться дважды.
Поэтому:
- Не повторяйте
POSTвслепую. - Для критичных операций используйте ключ идемпотентности —
уникальный идентификатор операции в заголовке (
Idempotency-Key), по которому сервер распознаёт и отбрасывает дубликаты. - Повторяйте только при сетевых ошибках/
5xx, но не при4xx.
2.4. Circuit Breaker («предохранитель»)
Если сервер падает надолго, продолжать слать к нему запросы вредно. Паттерн Circuit Breaker работает как электрический предохранитель и имеет три состояния:
- Closed (замкнут) — запросы проходят нормально; считаем число ошибок.
- Open (разомкнут) — после серии ошибок запросы мгновенно отклоняются без обращения к сети (экономим ресурсы, даём серверу прийти в себя).
- Half-Open (полуоткрыт) — через паузу пропускаем один пробный запрос: успех → возвращаемся в Closed, неудача → снова Open.
3. Кэширование данных
Кэш — это локальная копия данных, позволяющая ответить пользователю быстро и
работать при слабой сети или офлайн. Различают уровни кэша: память
(быстро, но теряется при закрытии), персистентное хранилище (AsyncStorage,
SQLite) и HTTP-кэш на уровне заголовков (Cache-Control, ETag).
3.1. Три стратегии (с акцентом на сеть)
| Стратегия | Как работает | Когда применять | Минусы |
|---|---|---|---|
| Cache-First | Сначала кэш; сеть — только если в кэше пусто | Редко меняющиеся данные: справочники, профиль, картинки | Можно показать устаревшие данные |
| Network-First | Сначала сеть; при ошибке — fallback на кэш | Часто меняющиеся данные, где важна свежесть: лента, баланс | Медленнее при плохой сети |
| Stale-While-Revalidate | Сразу отдаём кэш («stale»), параллельно идём в сеть и обновляем | Универсальный баланс скорости и свежести (ленты, списки) | Интерфейс «дёргается» при обновлении |
3.2. Stale-While-Revalidate в коде
async function swr(key, fetcher, onUpdate) { const cached = await cache.get(key); if (cached) onUpdate(cached); // 1. мгновенно показали кэш
try { const fresh = await fetcher(); // 2. пошли в сеть await cache.set(key, fresh); onUpdate(fresh); // 3. обновили UI свежими данными } catch (e) { if (!cached) throw e; // нет ни кэша, ни сети — ошибка }}Именно по принципу SWR работают популярные библиотеки серверного состояния (React Query, SWR): они кэшируют ответы по ключу запроса и фоном их обновляют.
3.3. Инвалидация кэша
«В компьютерных науках есть две сложные задачи: инвалидация кэша и именование переменных». Основные подходы к устареванию кэша:
- TTL (Time To Live) — у записи есть срок жизни; по истечении считается устаревшей.
- ETag / условные запросы — сервер присылает «отпечаток» данных; клиент
при следующем запросе шлёт
If-None-Match, и если данные не менялись, сервер отвечает304 Not Modifiedбез тела (экономия трафика). - Событийная инвалидация — после мутации (
POST/PUT/DELETE) принудительно сбрасываем связанные ключи кэша.
4. Оптимизация производительности сети
Мобильный трафик стоит денег и батареи, а каждый лишний запрос увеличивает задержку. Рассмотрим основные приёмы оптимизации.
4.1. Батчинг запросов
Батчинг — объединение нескольких логических запросов в один сетевой
вызов. Вместо трёх отдельных обращений (user, posts, stats) отправляем
один пакет с массивом операций, а сервер возвращает массив ответов.
Меньше запросов — меньше накладных расходов на установление соединения,
заголовки и «пробуждение» радиомодуля (что важно для энергопотребления).
4.2. Дедупликация запросов
Если несколько компонентов экрана одновременно запросили одни и те же данные,
нет смысла слать несколько одинаковых запросов. Дедупликация означает:
для одинакового ключа держим один «летящий» запрос (in-flight), а остальным
отдаём тот же Promise.
const inFlight = new Map();
function dedupedFetch(key, fetcher) { if (inFlight.has(key)) return inFlight.get(key); // уже летит — переиспользуем const promise = fetcher().finally(() => inFlight.delete(key)); inFlight.set(key, promise); return promise;}4.3. Пагинация
Никогда не загружайте «всё сразу». Пагинация разбивает большой набор данных на страницы.
- Offset-based (
?page=2&limit=20) — просто, но «съезжает» при вставке новых элементов и неэффективно на больших offset. - Cursor-based (
?after=<курсор>) — сервер возвращает указатель на следующую порцию; устойчиво к вставкам, оптимально для бесконечных лент иFlatListсonEndReached.
4.4. Сжатие
- Включайте сжатие тела ответа: заголовок
Accept-Encoding: gzip, br(Brotli плотнее gzip). Текст и JSON сжимаются в разы. - Для изображений используйте современные форматы (WebP, AVIF) и отдавайте размер под экран устройства, а не оригинал.
- Не передавайте лишние поля — запрашивайте только нужное (этому помогает GraphQL, см. ниже).
4.5. Отмена запросов: AbortController
Если пользователь ушёл с экрана или быстро поменял поисковый запрос, старый ответ уже не нужен — а он продолжает грузиться и может «перезаписать» актуальные данные (состояние гонки, race condition). Запрос нужно отменять.
В React Native для этого используется стандартный AbortController:
useEffect(() => { const controller = new AbortController();
fetch('https://api.example.com/search?q=' + query, { signal: controller.signal, }) .then((r) => r.json()) .then(setResults) .catch((e) => { if (e.name !== 'AbortError') console.error(e); // отмену игнорируем });
return () => controller.abort(); // отменяем при размонтировании / смене query}, [query]);Отмена особенно полезна для «живого поиска» (search-as-you-type): на каждый новый символ предыдущий запрос отменяется, в сети остаётся только актуальный.
5. Безопасность сетевого взаимодействия
Мобильное приложение часто работает в недоверенной сети (публичный Wi-Fi), поэтому безопасность канала и данных критична.
5.1. Транспортное шифрование (TLS)
- Всегда используйте HTTPS (HTTP поверх TLS). На iOS это требование обеспечивается App Transport Security, открытый HTTP по умолчанию запрещён.
- Современный стандарт — TLS 1.3: меньше задержек на рукопожатие, убраны устаревшие слабые алгоритмы.
- Perfect Forward Secrecy (PFS) гарантирует, что компрометация долговременного ключа сервера не раскроет ранее перехваченный трафик.
TLS защищает от прослушивания и подмены данных «на проводе» (атака man-in-the-middle, MITM).
5.2. Certificate Pinning
Обычный TLS доверяет любому сертификату, подписанному доверенным центром (CA). Если злоумышленник скомпрометирует CA или подсунет собственный корневой сертификат на устройство, MITM снова возможен.
Certificate pinning («закрепление сертификата») — это «зашивание» в приложение ожидаемого сертификата сервера или его публичного ключа. При рукопожатии приложение сверяет сертификат сервера с закреплённым и рвёт соединение при несовпадении.
- Плюс: сильная защита от MITM даже при скомпрометированном CA.
- Минус: при смене сертификата на сервере нужно вовремя обновить приложение, иначе оно перестанет работать (поэтому закрепляют несколько ключей и предусматривают резервный).
5.3. Защита токенов
Токены (JWT access/refresh, API-ключи) — самые ценные данные приложения.
- Хранение: не используйте обычный
AsyncStorageдля токенов — он не шифрован. Применяйте защищённое хранилище ОС:expo-secure-store(Keychain на iOS, Keystore/EncryptedSharedPreferences на Android). - Срок жизни: короткоживущий access-токен + долгоживущий refresh-токен. Утечка короткого токена менее опасна.
- Передача: только по HTTPS, в заголовке
Authorization: Bearer <token>, не в URL (URL попадает в логи и историю). - Отзыв: предусмотрите логаут/ротацию refresh-токенов на сервере.
5.4. Валидация на клиенте
Клиентская валидация — это удобство и быстрая обратная связь, но не замена серверной проверки. Главное правило безопасности: никогда не доверяйте клиенту — любой запрос можно подделать в обход приложения.
- Проверяйте и нормализуйте ввод перед отправкой (email, длина, типы) — это экономит трафик и улучшает UX, но не считается мерой безопасности.
- Авторитетная проверка всегда на сервере.
- Валидируйте и ответы сервера: проверяйте структуру JSON перед использованием.
Полезно держать в голове типичные угрозы из OWASP Mobile Top 10: небезопасное хранение данных, небезопасная передача, слабая аутентификация и авторизация.
6. Современные технологии и тренды
6.1. Edge Computing
Edge Computing («граничные вычисления») — это перенос части вычислений и хранения данных ближе к пользователю, на «край» сети, вместо обращения к далёкому центральному дата-центру.
- Идея: запрос обрабатывается на ближайшем к пользователю узле (точке присутствия CDN), что резко снижает задержку (latency).
- Технологии: Cloudflare Workers, AWS Lambda@Edge, Vercel Edge Functions — код выполняется в сотнях точек по всему миру.
- Применение в мобильных приложениях: кэширование и персонализация ответов у границы, проверка авторизации, лёгкая обработка/агрегация данных, A/B-тесты — без обращения к основному серверу. Для мобильного пользователя в другой стране это разница между откликом в 30 мс и 300 мс.
6.2. GraphQL (обзорно)
GraphQL — язык запросов к API, где клиент сам описывает, какие именно
поля ему нужны, через единую конечную точку (/graphql).
- Решает проблемы REST: over-fetching (сервер прислал лишние поля) и under-fetching (пришлось делать несколько запросов).
- Один запрос может собрать данные из связанных сущностей (пользователь + его посты + комментарии) за один сетевой вызов.
- Строгая типизация схемы и поддержка подписок (real-time).
- Для мобильных приложений это меньше трафика и меньше запросов; популярный клиент — Apollo Client.
6.3. gRPC (обзорно)
gRPC — высокопроизводительный RPC-фреймворк от Google поверх HTTP/2.
- Данные сериализуются в бинарный формат Protocol Buffers — компактнее и быстрее JSON.
- Строгий контракт через
.proto-файлы, кодогенерация клиента и сервера. - Поддержка стриминга в обе стороны.
- Применяется чаще для связи между микросервисами; на мобильных клиентах — там, где критичны производительность и низкая задержка, хотя интеграция сложнее, чем у REST/GraphQL.
Краткие итоги
- Ошибки делятся на временные (повторять с задержкой), постоянные
(
4xx— не повторять) и системные (логировать, ограниченно повторять5xx). - Retry строят на экспоненциальной задержке с jitter (против thundering
herd). Повторять можно только идемпотентные операции; для
POST— ключ идемпотентности. Circuit Breaker защищает падающий сервер. - Кэширование ускоряет работу и спасает в офлайне: Cache-First, Network-First, Stale-While-Revalidate; кэш инвалидируют (TTL, ETag, событийно).
- Оптимизация: батчинг, дедупликация, пагинация (cursor-based),
сжатие (gzip/Brotli, WebP/AVIF), отмена через
AbortController. - Безопасность: обязательный TLS (HTTPS), при необходимости — certificate pinning, токены в secure storage, принцип «не доверяй клиенту» (валидация на сервере).
- Тренды: Edge Computing снижает задержку у границы сети; GraphQL борется с over-/under-fetching; gRPC даёт бинарную производительность.
Вопросы для самопроверки
-
На какие три класса делятся сетевые ошибки? Приведите примеры и объясните, почему повторять имеет смысл только временные ошибки.
-
Что такое экспоненциальная задержка и jitter? От какой проблемы защищает jitter и как выглядит алгоритм retry с backoff?
-
Что такое идемпотентность операции? Почему
POST-запросы нельзя повторять вслепую и как это решается с помощью ключа идемпотентности? -
Сравните стратегии кэширования Cache-First, Network-First и Stale-While-Revalidate: как они работают и для каких данных подходят?
-
Перечислите приёмы оптимизации сетевого трафика (батчинг, дедупликация, пагинация, сжатие, отмена). Для чего нужен
AbortControllerи в каком сценарии он особенно полезен? -
Какими средствами обеспечивается безопасность сетевого взаимодействия? Объясните роль TLS, certificate pinning, защищённого хранения токенов и серверной валидации. Что такое Edge Computing и чем GraphQL отличается от REST?